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",