diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index 16ce6d86a1ed8..535df0c0897b4 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +5.4 +--- + +* Add `github` format & autodetection to render errors as annotations when + running the Twig linter command in a Github Actions environment. + 5.3 --- diff --git a/src/Symfony/Bridge/Twig/Command/LintCommand.php b/src/Symfony/Bridge/Twig/Command/LintCommand.php index a16c771d6b6ae..53e1d0bb583c4 100644 --- a/src/Symfony/Bridge/Twig/Command/LintCommand.php +++ b/src/Symfony/Bridge/Twig/Command/LintCommand.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\Twig\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; @@ -39,6 +40,11 @@ class LintCommand extends Command private $twig; + /** + * @var string|null + */ + private $format; + public function __construct(Environment $twig) { parent::__construct(); @@ -50,7 +56,7 @@ protected function configure() { $this ->setDescription(self::$defaultDescription) - ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') + ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format') ->addOption('show-deprecations', null, InputOption::VALUE_NONE, 'Show deprecations as errors') ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN') ->setHelp(<<<'EOF' @@ -80,6 +86,11 @@ protected function execute(InputInterface $input, OutputInterface $output) $io = new SymfonyStyle($input, $output); $filenames = $input->getArgument('filename'); $showDeprecations = $input->getOption('show-deprecations'); + $this->format = $input->getOption('format'); + + if (null === $this->format) { + $this->format = GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt'; + } if (['-'] === $filenames) { return $this->display($input, $output, $io, [$this->validate(file_get_contents('php://stdin'), uniqid('sf_', true))]); @@ -169,26 +180,29 @@ private function validate(string $template, string $file): array private function display(InputInterface $input, OutputInterface $output, SymfonyStyle $io, array $files) { - switch ($input->getOption('format')) { + switch ($this->format) { case 'txt': return $this->displayTxt($output, $io, $files); case 'json': return $this->displayJson($output, $files); + case 'github': + return $this->displayTxt($output, $io, $files, true); default: throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $input->getOption('format'))); } } - private function displayTxt(OutputInterface $output, SymfonyStyle $io, array $filesInfo) + private function displayTxt(OutputInterface $output, SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false) { $errors = 0; + $githubReporter = $errorAsGithubAnnotations ? new GithubActionReporter($output) : null; foreach ($filesInfo as $info) { if ($info['valid'] && $output->isVerbose()) { $io->comment('OK'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); } elseif (!$info['valid']) { ++$errors; - $this->renderException($io, $info['template'], $info['exception'], $info['file']); + $this->renderException($io, $info['template'], $info['exception'], $info['file'], $githubReporter); } } @@ -220,10 +234,14 @@ private function displayJson(OutputInterface $output, array $filesInfo) return min($errors, 1); } - private function renderException(SymfonyStyle $output, string $template, Error $exception, string $file = null) + private function renderException(SymfonyStyle $output, string $template, Error $exception, string $file = null, ?GithubActionReporter $githubReporter = null) { $line = $exception->getTemplateLine(); + if ($githubReporter) { + $githubReporter->error($exception->getRawMessage(), $file, $line <= 0 ? null : $line); + } + if ($file) { $output->text(sprintf(' ERROR in %s (line %s)', $file, $line)); } else { diff --git a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php index 9bb9a9867c745..c26dabd6df9b0 100644 --- a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php @@ -107,6 +107,33 @@ public function testLintDefaultPaths() self::assertStringContainsString('OK in', trim($tester->getDisplay())); } + public function testLintIncorrectFileWithGithubFormat() + { + $filename = $this->createFile('{{ foo'); + $tester = $this->createCommandTester(); + $tester->execute(['filename' => [$filename], '--format' => 'github'], ['decorated' => false]); + self::assertEquals(1, $tester->getStatusCode(), 'Returns 1 in case of error'); + self::assertStringMatchesFormat('%A::error file=%s,line=1,col=0::Unexpected token "end of template" ("end of print statement" expected).%A', trim($tester->getDisplay())); + } + + public function testLintAutodetectsGithubActionEnvironment() + { + $prev = getenv('GITHUB_ACTIONS'); + putenv('GITHUB_ACTIONS'); + + try { + putenv('GITHUB_ACTIONS=1'); + + $filename = $this->createFile('{{ foo'); + $tester = $this->createCommandTester(); + + $tester->execute(['filename' => [$filename]], ['decorated' => false]); + self::assertStringMatchesFormat('%A::error file=%s,line=1,col=0::Unexpected token "end of template" ("end of print statement" expected).%A', trim($tester->getDisplay())); + } finally { + putenv('GITHUB_ACTIONS'.($prev ? "=$prev" : '')); + } + } + private function createCommandTester(): CommandTester { $environment = new Environment(new FilesystemLoader(\dirname(__DIR__).'/Fixtures/templates/')); diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 5a0735e250df2..88f5f7896b18b 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -44,7 +44,7 @@ "symfony/security-http": "^4.4|^5.0|^6.0", "symfony/serializer": "^5.2|^6.0", "symfony/stopwatch": "^4.4|^5.0|^6.0", - "symfony/console": "^4.4|^5.0|^6.0", + "symfony/console": "^5.3|^6.0", "symfony/expression-language": "^4.4|^5.0|^6.0", "symfony/web-link": "^4.4|^5.0|^6.0", "symfony/workflow": "^5.2|^6.0", @@ -55,7 +55,7 @@ "conflict": { "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "symfony/console": "<4.4", + "symfony/console": "<5.3", "symfony/form": "<5.3", "symfony/http-foundation": "<5.3", "symfony/http-kernel": "<4.4",