8000 [Console][Yaml] Linter: add Github annotations format for errors by ogizanagi · Pull Request #38982 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Console][Yaml] Linter: add Github annotations format for errors #38982

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Symfony/Component/Console/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

5.3.0
-----

* Added `GithubActionReporter` to render annotations in a Github Action

5.2.0
-----

Expand Down
99 changes: 99 additions & 0 deletions src/Symfony/Component/Console/CI/GithubActionReporter.php
8000
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Console\CI;

use Symfony\Component\Console\Output\OutputInterface;

/**
* Utility class for Github actions.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class GithubActionReporter
{
private $output;

/**
* @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L80-L85
*/
private const ESCAPED_DATA = [
'%' => '%25',
"\r" => '%0D',
"\n" => '%0A',
];

/**
* @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L87-L94
*/
private const ESCAPED_PROPERTIES = [
'%' => '%25',
"\r" => '%0D',
"\n" => '%0A',
':' => '%3A',
',' => '%2C',
];

public function __construct(OutputInterface $output)
{
$this->output = $output;
}

public static function isGithubActionEnvironment(): bool
{
return false !== getenv('GITHUB_ACTIONS');
}

/**
* Output an error using the Github annotations format.
*
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
*/
public function error(string $message, string $file = null, int $line = null, int $col = null): void
{
$this->log('error', $message, $file, $line, $col);
}

/**
* Output a warning using the Github annotations format.
*
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message
*/
public function warning(string $message, string $file = null, int $line = null, int $col = null): void
{
$this->log('warning', $message, $file, $line, $col);
}

/**
* Output a debug log using the Github annotations format.
*
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-debug-message
*/
public function debug(string $message, string $file = null, int $line = null, int $col = null): void
{
$this->log('debug', $message, $file, $line, $col);
}

private function log(string $type, string $message, string $file = null, int $line = null, int $col = null): void
{
// Some values must be encoded.
$message = strtr($message, self::ESCAPED_DATA);

if (!$file) {
// No file provided, output the message solely:
$this->output->writeln(sprintf('::%s::%s', $type, $message));

return;
}

$this->output->writeln(sprintf('::%s file=%s, line=%s, col=%s::%s', $type, strtr($file, self::ESCAPED_PROPERTIES), strtr($line ?? 1, self::ESCAPED_PROPERTIES), strtr($col ?? 0, self::ESCAPED_PROPERTIES), $message));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Console\Tests\CI;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\CI\GithubActionReporter;
use Symfony\Component\Console\Output\BufferedOutput;

class GithubActionReporterTest extends TestCase
{
public function testIsGithubActionEnvironment()
{
$prev = getenv('GITHUB_ACTIONS');
putenv('GITHUB_ACTIONS');

try {
self::assertFalse(GithubActionReporter::isGithubActionEnvironment());
putenv('GITHUB_ACTIONS=1');
self::assertTrue(GithubActionReporter::isGithubActionEnvironment());
} finally {
putenv('GITHUB_ACTIONS'.($prev ? "=$prev" : ''));
}
}

/**
* @dataProvider annotationsFormatProvider
*/
public function testAnnotationsFormat(string $type, string $message, string $file = null, int $line = null, int $col = null, string $expected)
{
$reporter = new GithubActionReporter($buffer = new BufferedOutput());

$reporter->{$type}($message, $file, $line, $col);

self::assertSame($expected.\PHP_EOL, $buffer->fetch());
}

public function annotationsFormatProvider(): iterable
{
yield 'warning' => ['warning', 'A warning', null, null, null, '::warning::A warning'];
yield 'error' => ['error', 'An error', null, null, null, '::error::An error'];
yield 'debug' => ['debug', 'A debug log', null, null, null, '::debug::A debug log'];

yield 'with message to escape' => [
'debug',
"There are 100% chances\nfor this to be escaped properly\rRight?",
null,
null,
null,
'::debug::There are 100%25 chances%0Afor this to be escaped properly%0DRight?',
];

yield 'with meta' => [
'warning',
'A warning',
'foo/bar.php',
2,
4,
'::warning file=foo/bar.php, line=2, col=4::A warning',
];

yield 'with file property to escape' => [
'warning',
'A warning',
'foo,bar:baz%quz.php',
2,
4,
'::warning file=foo%2Cbar%3Abaz%25quz.php, line=2, col=4::A warning',
];

yield 'without file ignores col & line' => ['warning', 'A warning', null, 2, 4, '::warning::A warning'];
}
}
6 changes: 6 additions & 0 deletions src/Symfony/Component/Yaml/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
CHANGELOG
=========

5.3.0
-----

* Added `github` format support & autodetection to render errors as annotations
when running the YAML linter command in a Github Action environment.

5.1.0
-----

Expand Down
25 changes: 23 additions & 2 deletions src/Symfony/Component/Yaml/Command/LintCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Yaml\Command;

use Symfony\Component\Console\CI\GithubActionReporter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
Expand Down Expand Up @@ -55,7 +56,7 @@ protected function configure()
$this
->setDescription('Lints a file and outputs encountered errors')
->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN')
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt')
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format')
->addOption('parse-tags', null, InputOption::VALUE_NONE, 'Parse custom tags')
->setHelp(<<<EOF
The <info>%command.name%</info> command lints a YAML file and outputs to STDOUT
Expand Down Expand Up @@ -84,6 +85,16 @@ protected function execute(InputInterface $input, OutputInterface $output)
$io = new SymfonyStyle($input, $output);
$filenames = (array) $input->getArgument('filename');
$this->format = $input->getOption('format');

if ('github' === $this->format && !class_exists(GithubActionReporter::class)) {
throw new \InvalidArgumentException('The "github" format is only available since "symfony/console" >= 5.3.');
}

if (null === $this->format) {
// Autodetect format according to CI environment
$this->format = class_exists(GithubActionReporter::class) && GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt';
}

$this->displayCorrectFiles = $output->isVerbose();
$flags = $input->getOption('parse-tags') ? Yaml::PARSE_CUSTOM_TAGS : 0;

Expand Down Expand Up @@ -137,17 +148,23 @@ private function display(SymfonyStyle $io, array $files): int
return $this->displayTxt($io, $files);
case 'json':
return $this->displayJson($io, $files);
case 'github':
return $this->displayTxt($io, $files, true);
default:
throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $this->format));
}
}

private function displayTxt(SymfonyStyle $io, array $filesInfo): int
private function displayTxt(SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false): int
{
$countFiles = \count($filesInfo);
$erroredFiles = 0;
$suggestTagOption = false;

if ($errorAsGithubAnnotations) {
$githubReporter = new GithubActionReporter($io);
}

foreach ($filesInfo as $info) {
if ($info['valid'] && $this->displayCorrectFiles) {
$io->comment('<info>OK</info>'.($info['file'] ? sprintf(' in %s', $info['file']) : ''));
Expand All @@ -159,6 +176,10 @@ private function displayTxt(SymfonyStyle $io, array $filesInfo): int
if (false !== strpos($info['message'], 'PARSE_CUSTOM_TAGS')) {
$suggestTagOption = true;
}

if ($errorAsGithubAnnotations) {
$githubReporter->error($info['message'], $info['file'] ?? 'php://stdin', $info['line']);
}
}
}

Expand Down
52 changes: 52 additions & 0 deletions src/Symfony/Component/Yaml/Tests/Command/LintCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\CI\GithubActionReporter;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Yaml\Command\LintCommand;
Expand Down Expand Up @@ -63,6 +64,57 @@ public function testLintIncorrectFile()
$this->assertStringContainsString('Unable to parse at line 3 (near "bar").', trim($tester->getDisplay()));
}

public function testLintIncorrectFileWithGithubFormat()
{
if (!class_exists(GithubActionReporter::class)) {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The "github" format is only available since "symfony/console" >= 5.3.');
}

$incorrectContent = <<<YAML
foo:
bar
YAML;
$tester = $this->createCommandTester();
$filename = $this->createFile($incorrectContent);

$tester->execute(['filename' => $filename, '--format' => 'github'], ['decorated' => false]);

if (!class_exists(GithubActionReporter::class)) {
return;
}

self::assertEquals(1, $tester->getStatusCode(), 'Returns 1 in case of error');
self::assertStringMatchesFormat('%A::error file=%s, line=2, col=0::Unable to parse at line 2 (near "bar")%A', trim($tester->getDisplay()));
}

public function testLintAutodetectsGithubActionEnvironment()
{
if (!class_exists(GithubActionReporter::class)) {
$this->markTestSkipped('The "github" format is only available since "symfony/console" >= 5.3.');
}

$prev = getenv('GITHUB_ACTIONS');
putenv('GITHUB_ACTIONS');

try {
putenv('GITHUB_ACTIONS=1');

$incorrectContent = <<<YAML
foo:
bar
YAML;
$tester = $this->createCommandTester();
$filename = $this->createFile($incorrectContent);

$tester->execute(['filename' => $filename], ['decorated' => false]);

self::assertStringMatchesFormat('%A::error file=%s, line=2, col=0::Unable to parse at line 2 (near "bar")%A', trim($tester->getDisplay()));
} finally {
putenv('GITHUB_ACTIONS'.($prev ? "=$prev" : ''));
}
}

public function testConstantAsKey()
{
$yaml = <<<YAML
Expand Down
0