Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 04eec8b

Browse filesBrowse files
committed
feature #38982 [Console][Yaml] Linter: add Github annotations format for errors (ogizanagi)
This PR was squashed before being merged into the 5.3-dev branch. Discussion ---------- [Console][Yaml] Linter: add Github annotations format for errors | Q | A | ------------- | --- | Branch? | 5.x <!-- see below --> | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Tickets | N/A <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead --> | License | MIT | Doc PR | TODO Github actions [can write errors and warning](https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-error-message) directly in their output, which result into annotations into the Github checks. It can even provide a filename, line & col number, allowing to display the annnotations inside the PR diff directly, at the right place. More advanced usage of annotations can be made using the [API](https://docs.github.com/en/free-pro-team@latest/rest/reference/checks#list-check-run-annotations), but regarding the linters provided in Symfony components, it seems the shortcut using output is a great way to enhance the integration with Github Actions. This PR starts by proposing these changes in the yaml linter: - add the `github` format, which is the same as the `txt` one, except for errors and warning, for which we'll adapt the output to the Github annotations format. - remove the `txt` format as default, and autodetect if the script is running in a Github action context, then use `github` format. If it's not, we fallback to `txt` as before. Once we agree on the details, we could perform the same for other linters (xliff, twig, ...) Here is a PR using it: ogizanagi/symfony-lint-gha-demo#2 and some screenshots: | PR checks run | PR checks annotations | PR diff | | -- | -- | -- | | ![Capture d’écran 2020-11-04 à 09 37 07](https://user-images.githubusercontent.com/2211145/98089377-ed416600-1e82-11eb-8b10-40602b45efb1.png) | ![Capture d’écran 2020-11-04 à 09 37 28](https://user-images.githubusercontent.com/2211145/98089379-edd9fc80-1e82-11eb-8302-4e104abaeb2c.png) | ![Capture d’écran 2020-11-04 à 09 38 28](https://user-images.githubusercontent.com/2211145/98089381-edd9fc80-1e82-11eb-982a-9e4413ec30ba.png) | ~~(tests to add)~~ --- This was inspired by [PHPStan](https://github.com/phpstan/phpstan-src/blob/d77bd87da9f2fad0440fc1614158cdfc1b7cc88a/src/Command/ErrorFormatter/GithubErrorFormatter.php) which is already auto-adapting the output according to the CI, using https://github.com/OndraM/ci-detector Commits ------- f0bbdc8 [Console][Yaml] Linter: add Github annotations format for errors
2 parents 3c0cfbc + f0bbdc8 commit 04eec8b
Copy full SHA for 04eec8b

File tree

Expand file treeCollapse file tree

6 files changed

+266
-2
lines changed
Filter options
Expand file treeCollapse file tree

6 files changed

+266
-2
lines changed

‎src/Symfony/Component/Console/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/CHANGELOG.md
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
5.3.0
5+
-----
6+
7+
* Added `GithubActionReporter` to render annotations in a Github Action
8+
49
5.2.0
510
-----
611

+99Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\CI;
13+
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
16+
/**
17+
* Utility class for Github actions.
18+
*
19+
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
20+
*/
21+
class GithubActionReporter
22+
{
23+
private $output;
24+
25+
/**
26+
* @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L80-L85
27+
*/
28+
private const ESCAPED_DATA = [
29+
'%' => '%25',
30+
"\r" => '%0D',
31+
"\n" => '%0A',
32+
];
33+
34+
/**
35+
* @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L87-L94
36+
*/
37+
private const ESCAPED_PROPERTIES = [
38+
'%' => '%25',
39+
"\r" => '%0D',
40+
"\n" => '%0A',
41+
':' => '%3A',
42+
',' => '%2C',
43+
];
44+
45+
public function __construct(OutputInterface $output)
46+
{
47+
$this->output = $output;
48+
}
49+
50+
public static function isGithubActionEnvironment(): bool
51+
{
52+
return false !== getenv('GITHUB_ACTIONS');
53+
}
54+
55+
/**
56+
* Output an error using the Github annotations format.
57+
*
58+
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
59+
*/
60+
public function error(string $message, string $file = null, int $line = null, int $col = null): void
61+
{
62+
$this->log('error', $message, $file, $line, $col);
63+
}
64+
65+
/**
66+
* Output a warning using the Github annotations format.
67+
*
68+
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message
69+
*/
70+
public function warning(string $message, string $file = null, int $line = null, int $col = null): void
71+
{
72+
$this->log('warning', $message, $file, $line, $col);
73+
}
74+
75+
/**
76+
* Output a debug log using the Github annotations format.
77+
*
78+
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-debug-message
79+
*/
80+
public function debug(string $message, string $file = null, int $line = null, int $col = null): void
81+
{
82+
$this->log('debug', $message, $file, $line, $col);
83+
}
84+
85+
private function log(string $type, string $message, string $file = null, int $line = null, int $col = null): void
86+
{
87+
// Some values must be encoded.
88+
$message = strtr($message, self::ESCAPED_DATA);
89+
90+
if (!$file) {
91+
// No file provided, output the message solely:
92+
$this->output->writeln(sprintf('::%s::%s', $type, $message));
93+
94+
return;
95+
}
96+
97+
$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));
98+
}
99+
}
+81Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Tests\CI;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Console\CI\GithubActionReporter;
16+
use Symfony\Component\Console\Output\BufferedOutput;
17+
18+
class GithubActionReporterTest extends TestCase
19+
{
20+
public function testIsGithubActionEnvironment()
21+
{
22+
$prev = getenv('GITHUB_ACTIONS');
23+
putenv('GITHUB_ACTIONS');
24+
25+
try {
26+
self::assertFalse(GithubActionReporter::isGithubActionEnvironment());
27+
putenv('GITHUB_ACTIONS=1');
28+
self::assertTrue(GithubActionReporter::isGithubActionEnvironment());
29+
} finally {
30+
putenv('GITHUB_ACTIONS'.($prev ? "=$prev" : ''));
31+
}
32+
}
33+
34+
/**
35+
* @dataProvider annotationsFormatProvider
36+
*/
37+
public function testAnnotationsFormat(string $type, string $message, string $file = null, int $line = null, int $col = null, string $expected)
38+
{
39+
$reporter = new GithubActionReporter($buffer = new BufferedOutput());
40+
41+
$reporter->{$type}($message, $file, $line, $col);
42+
43+
self::assertSame($expected.\PHP_EOL, $buffer->fetch());
44+
}
45+
46+
public function annotationsFormatProvider(): iterable
47+
{
48+
yield 'warning' => ['warning', 'A warning', null, null, null, '::warning::A warning'];
49+
yield 'error' => ['error', 'An error', null, null, null, '::error::An error'];
50+
yield 'debug' => ['debug', 'A debug log', null, null, null, '::debug::A debug log'];
51+
52+
yield 'with message to escape' => [
53+
'debug',
54+
"There are 100% chances\nfor this to be escaped properly\rRight?",
55+
null,
56+
null,
57+
null,
58+
'::debug::There are 100%25 chances%0Afor this to be escaped properly%0DRight?',
59+
];
60+
61+
yield 'with meta' => [
62+
'warning',
63+
'A warning',
64+
'foo/bar.php',
65+
2,
66+
4,
67+
'::warning file=foo/bar.php, line=2, col=4::A warning',
68+
];
69+
70+
yield 'with file property to escape' => [
71+
'warning',
72+
'A warning',
73+
'foo,bar:baz%quz.php',
74+
2,
75+
4,
76+
'::warning file=foo%2Cbar%3Abaz%25quz.php, line=2, col=4::A warning',
77+
];
78+
79+
yield 'without file ignores col & line' => ['warning', 'A warning', null, 2, 4, '::warning::A warning'];
80+
}
81+
}

‎src/Symfony/Component/Yaml/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Yaml/CHANGELOG.md
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
CHANGELOG
22
=========
33

4+
5.3.0
5+
-----
6+
7+
* Added `github` format support & autodetection to render errors as annotations
8+
when running the YAML linter command in a Github Action environment.
9+
410
5.1.0
511
-----
612

‎src/Symfony/Component/Yaml/Command/LintCommand.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Yaml/Command/LintCommand.php
+23-2Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Yaml\Command;
1313

14+
use Symfony\Component\Console\CI\GithubActionReporter;
1415
use Symfony\Component\Console\Command\Command;
1516
use Symfony\Component\Console\Exception\InvalidArgumentException;
1617
use Symfony\Component\Console\Exception\RuntimeException;
@@ -55,7 +56,7 @@ protected function configure()
5556
$this
5657
->setDescription('Lints a file and outputs encountered errors')
5758
->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN')
58-
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt')
59+
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format')
5960
->addOption('parse-tags', null, InputOption::VALUE_NONE, 'Parse custom tags')
6061
->setHelp(<<<EOF
6162
The <info>%command.name%</info> command lints a YAML file and outputs to STDOUT
@@ -84,6 +85,16 @@ protected function execute(InputInterface $input, OutputInterface $output)
8485
$io = new SymfonyStyle($input, $output);
8586
$filenames = (array) $input->getArgument('filename');
8687
$this->format = $input->getOption('format');
88+
89+
if ('github' === $this->format && !class_exists(GithubActionReporter::class)) {
90+
throw new \InvalidArgumentException('The "github" format is only available since "symfony/console" >= 5.3.');
91+
}
92+
93+
if (null === $this->format) {
94+
// Autodetect format according to CI environment
95+
$this->format = class_exists(GithubActionReporter::class) && GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt';
96+
}
97+
8798
$this->displayCorrectFiles = $output->isVerbose();
8899
$flags = $input->getOption('parse-tags') ? Yaml::PARSE_CUSTOM_TAGS : 0;
89100

@@ -137,17 +148,23 @@ private function display(SymfonyStyle $io, array $files): int
137148
return $this->displayTxt($io, $files);
138149
case 'json':
139150
return $this->displayJson($io, $files);
151+
case 'github':
152+
return $this->displayTxt($io, $files, true);
140153
default:
141154
throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $this->format));
142155
}
143156
}
144157

145-
private function displayTxt(SymfonyStyle $io, array $filesInfo): int
158+
private function displayTxt(SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false): int
146159
{
147160
$countFiles = \count($filesInfo);
148161
$erroredFiles = 0;
149162
$suggestTagOption = false;
150163

164+
if ($errorAsGithubAnnotations) {
165+
$githubReporter = new GithubActionReporter($io);
166+
}
167+
151168
foreach ($filesInfo as $info) {
152169
if ($info['valid'] && $this->displayCorrectFiles) {
153170
$io->comment('<info>OK</info>'.($info['file'] ? sprintf(' in %s', $info['file']) : ''));
@@ -159,6 +176,10 @@ private function displayTxt(SymfonyStyle $io, array $filesInfo): int
159176
if (false !== strpos($info['message'], 'PARSE_CUSTOM_TAGS')) {
160177
$suggestTagOption = true;
161178
}
179+
180+
if ($errorAsGithubAnnotations) {
181+
$githubReporter->error($info['message'], $info['file'] ?? 'php://stdin', $info['line']);
182+
}
162183
}
163184
}
164185

‎src/Symfony/Component/Yaml/Tests/Command/LintCommandTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Yaml/Tests/Command/LintCommandTest.php
+52Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

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

67+
public function testLintIncorrectFileWithGithubFormat()
68+
{
69+
if (!class_exists(GithubActionReporter::class)) {
70+
$this->expectException(\InvalidArgumentException::class);
71+
$this->expectExceptionMessage('The "github" format is only available since "symfony/console" >= 5.3.');
72+
}
73+
74+
$incorrectContent = <<<YAML
75+
foo:
76+
bar
77+
YAML;
78+
$tester = $this->createCommandTester();
79+
$filename = $this->createFile($incorrectContent);
80+
81+
$tester->execute(['filename' => $filename, '--format' => 'github'], ['decorated' => false]);
82+
83+
if (!class_exists(GithubActionReporter::class)) {
84+
return;
85+
}
86+
87+
self::assertEquals(1, $tester->getStatusCode(), 'Returns 1 in case of error');
88+
self::assertStringMatchesFormat('%A::error file=%s, line=2, col=0::Unable to parse at line 2 (near "bar")%A', trim($tester->getDisplay()));
89+
}
90+
91+
public function testLintAutodetectsGithubActionEnvironment()
92+
{
93+
if (!class_exists(GithubActionReporter::class)) {
94+
$this->markTestSkipped('The "github" format is only available since "symfony/console" >= 5.3.');
95+
}
96+
97+
$prev = getenv('GITHUB_ACTIONS');
98+
putenv('GITHUB_ACTIONS');
99+
100+
try {
101+
putenv('GITHUB_ACTIONS=1');
102+
103+
$incorrectContent = <<<YAML
104+
foo:
105+
bar
106+
YAML;
107+
$tester = $this->createCommandTester();
108+
$filename = $this->createFile($incorrectContent);
109+
110+
$tester->execute(['filename' => $filename], ['decorated' => false]);
111+
112+
self::assertStringMatchesFormat('%A::error file=%s, line=2, col=0::Unable to parse at line 2 (near "bar")%A', trim($tester->getDisplay()));
113+
} finally {
114+
putenv('GITHUB_ACTIONS'.($prev ? "=$prev" : ''));
115+
}
116+
}
117+
66118
public function testConstantAsKey()
67119
{
68120
$yaml = <<<YAML

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.