CHANGELOG
=========
+5.3.0
+-----
+
+ * Added `GithubActionReporter` to render annotations in a Github Action
+
5.2.0
-----
--- /dev/null
+<?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));
+ }
+}
--- /dev/null
+<?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'];
+ }
+}
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
-----
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;
$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
$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;
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']) : ''));
if (false !== strpos($info['message'], 'PARSE_CUSTOM_TAGS')) {
$suggestTagOption = true;
}
+
+ if ($errorAsGithubAnnotations) {
+ $githubReporter->error($info['message'], $info['file'] ?? 'php://stdin', $info['line']);
+ }
}
}
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;
$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