Skip to content

Commit 92dfd05

Browse files
authored
Add a command adding a changelog entry in all modified packages (#2015)
This command is useful when doing updates in the code generator or the php-cs-fixer configuration, where we know that all changes will have the same cause.
1 parent 1bb452f commit 92dfd05

File tree

2 files changed

+183
-0
lines changed

2 files changed

+183
-0
lines changed

ChangeLogUpdater.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace AsyncAws\CodeGenerator;
4+
5+
/**
6+
* @internal
7+
*/
8+
final class ChangeLogUpdater
9+
{
10+
/**
11+
* @param string[] $newLines
12+
* @param callable(string): void $warningReporter
13+
*/
14+
public function addNewChangeLogLines(string $changeLogPath, string $service, string $sectionLabel, array $newLines, callable $warningReporter): void
15+
{
16+
$changeLog = explode("\n", file_get_contents($changeLogPath));
17+
$nrSection = false;
18+
$fixSection = false;
19+
$fixSectionOrder = [
20+
'### BC-BREAK', '### Removed', // Major
21+
'### Added', '### Deprecated', '### Dependency bumped', // Minor
22+
'### Changed', '### Fixed', '### Security', // Patch
23+
];
24+
$fixSectionIndex = array_search($sectionLabel, $fixSectionOrder);
25+
foreach ($changeLog as $index => $line) {
26+
if ('## NOT RELEASED' === $line) {
27+
$nrSection = true;
28+
29+
continue;
30+
}
31+
if (!$nrSection) {
32+
continue;
33+
}
34+
if (str_starts_with($line, '## ')) {
35+
break;
36+
}
37+
if (str_starts_with($line, '### ') && array_search($line, $fixSectionOrder) > $fixSectionIndex) {
38+
break;
39+
}
40+
41+
if ($line === $sectionLabel) {
42+
$fixSection = true;
43+
44+
continue;
45+
}
46+
if (!$fixSection) {
47+
continue;
48+
}
49+
50+
if ('' !== $line && false !== $index = array_search($line, $newLines, true)) {
51+
array_splice($newLines, $index, 1);
52+
}
53+
}
54+
55+
if (empty($newLines)) {
56+
$warningReporter('duplicate entry in CHANGELOG ' . $service);
57+
58+
return;
59+
}
60+
61+
if (!$nrSection) {
62+
array_splice($changeLog, 2, 0, array_merge([
63+
'## NOT RELEASED',
64+
'',
65+
$sectionLabel,
66+
'',
67+
], $newLines, ['']));
68+
} elseif (!$fixSection) {
69+
array_splice($changeLog, $index, 0, array_merge([
70+
$sectionLabel,
71+
'',
72+
], $newLines, ['']));
73+
} else {
74+
array_splice($changeLog, $index - 1, 0, $newLines);
75+
}
76+
file_put_contents($changeLogPath, implode("\n", $changeLog));
77+
}
78+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AsyncAws\CodeGenerator\Command;
6+
7+
use AsyncAws\CodeGenerator\ChangeLogUpdater;
8+
use Symfony\Component\Console\Attribute\AsCommand;
9+
use Symfony\Component\Console\Command\Command;
10+
use Symfony\Component\Console\Input\InputArgument;
11+
use Symfony\Component\Console\Input\InputInterface;
12+
use Symfony\Component\Console\Output\ConsoleOutput;
13+
use Symfony\Component\Console\Output\OutputInterface;
14+
use Symfony\Component\Process\Process;
15+
16+
/**
17+
* @internal
18+
*/
19+
#[AsCommand(name: 'add-change-log', description: 'Add a change log entry in all packages containing modified files.')]
20+
class AddChangeLogCommand extends Command
21+
{
22+
private const CHANGELOG_LABELS = ['BC-BREAK', 'Removed', 'Added', 'Deprecated', 'Dependency bumped', 'Changed', 'Fixed', 'Security'];
23+
24+
private readonly ChangeLogUpdater $changeLogUpdater;
25+
26+
public function __construct()
27+
{
28+
$this->changeLogUpdater = new ChangeLogUpdater();
29+
parent::__construct();
30+
}
31+
32+
protected function configure(): void
33+
{
34+
$this->addArgument('type', InputArgument::REQUIRED, 'The type of change. Supported values: ' . implode(', ', self::CHANGELOG_LABELS), null, self::CHANGELOG_LABELS);
35+
$this->addArgument('message', InputArgument::REQUIRED);
36+
}
37+
38+
protected function execute(InputInterface $input, OutputInterface $output): int
39+
{
40+
$errorOutput = $output instanceof ConsoleOutput ? $output->getErrorOutput() : $output;
41+
42+
if (!\in_array($input->getArgument('type'), self::CHANGELOG_LABELS, true)) {
43+
$errorOutput->writeln('Invalid type provided. It must be one of: ' . implode(', ', self::CHANGELOG_LABELS) . '.');
44+
45+
return 1;
46+
}
47+
48+
$changedFiles = explode("\n", (new Process(['git', 'ls-files', '--modified']))->mustRun()->getOutput());
49+
50+
$changedServices = [];
51+
foreach ($changedFiles as $file) {
52+
$parts = explode('/', $file);
53+
if ('src' !== $parts[0]) {
54+
continue;
55+
}
56+
if (!isset($parts[1])) {
57+
continue;
58+
}
59+
60+
if ('Service' === $parts[1]) {
61+
$service = $parts[2];
62+
$base = 'src/Service/' . $service;
63+
64+
if ('.template' === $service) {
65+
continue; // The service template does not have an actual changelog
66+
}
67+
} elseif ('Integration' === $parts[1]) {
68+
$service = $parts[2] . '/' . $parts[3];
69+
$base = 'src/Integration/' . $service;
70+
} elseif ('CodeGenerator' === $parts[1]) {
71+
continue; // The code generator does not have a changelog as it has no releases
72+
} elseif ('Core' === $parts[1]) {
73+
$service = $parts[1];
74+
$base = 'src/' . $service;
75+
} else {
76+
continue;
77+
}
78+
79+
$subPath = substr($file, \strlen($base));
80+
$normalizedSubPath = str_replace(\DIRECTORY_SEPARATOR, '/', $subPath);
81+
82+
if (\in_array($normalizedSubPath, ['/README.md', '/Makefile', '/.gitattributes', '/.gitignore', '/LICENSE', '/phpunit.xml.dist', '/roave-bc-check.yaml'], true) || str_starts_with($normalizedSubPath, '/.github/') || str_starts_with($normalizedSubPath, '/tests/')) {
83+
// Scaffolding files don't require a changelog entry when modified as they don't impact the usage of the packages
84+
continue;
85+
}
86+
87+
if (!isset($changedServices[$service])) {
88+
$changedServices[$service] = ['base' => $base, 'files' => []];
89+
}
90+
$changedServices[$service]['files'][] = $subPath;
91+
}
92+
93+
$fixSectionLabel = '### ' . $input->getArgument('type');
94+
$message = $input->getArgument('message');
95+
96+
foreach ($changedServices as $service => $info) {
97+
$newLines = ['- ' . $message];
98+
99+
$changeLogPath = $info['base'] . '/CHANGELOG.md';
100+
$this->changeLogUpdater->addNewChangeLogLines($changeLogPath, $service, $fixSectionLabel, $newLines, $errorOutput->writeln(...));
101+
}
102+
103+
return 0;
104+
}
105+
}

0 commit comments

Comments
 (0)