Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ changelog:
exclude:
authors:
- dependabot
- dependabot[bot]
categories:
- title: Breaking Changes
labels:
Expand Down
4 changes: 4 additions & 0 deletions admin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ This folder contains tools or docs useful for project maintainers.
The release process is detailed in [RELEASE.md](./RELEASE.md). These scripts
are used as part of that process:

- **generate-changelog.php** prepends the CHANGELOG.md entry for a new
release, built from GitHub's auto-generated release notes and the
SECURITY section of the version's detailed changelog.
Usage: `php admin/generate-changelog.php 4.x.x [--dry-run]`
- **prepare-release.php** creates the `release-4.x.x` branch and updates
version references in the framework source, the user guide, and the
distribution build script.
Expand Down
37 changes: 21 additions & 16 deletions admin/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,26 @@ PRs with breaking changes must have the following additional label:

### Generate Changelog

To auto-generate the changelog, navigate to the
[Releases](https://github.com/codeigniter4/CodeIgniter4/releases) page,
click the "Draft a new release" button.

* Choose a tag: `v4.x.x` (Create new tag: v4.x.x on publish)
* Target: `develop`

Click the "Generate release notes" button.

Check the resulting content. If there are items in the *Others* section which
should be included in the changelog, add a label to the PR and regenerate
the changelog.

Copy the resulting contents into **CHANGELOG.md** and adjust the format to match
the existing content.
The changelog is generated from GitHub's auto-generated release notes with a
script. It requires the authenticated [GitHub CLI](https://cli.github.com/).

The script also copies the security fixes from the SECURITY section of
**user_guide_src/source/changelogs/v4.x.x.rst**, so any security advisory PRs
must be merged and documented there before generating the changelog.

* [ ] Run `php admin/generate-changelog.php 4.x.x --dry-run` and check the output.
* The script lists the PRs that have none of the changelog labels. If any of
them should be included in the changelog, add a label to the PR and run
the script again.
* [ ] Run `php admin/generate-changelog.php 4.x.x` to prepend the new entry to
**CHANGELOG.md**.

If the script cannot be used, the same notes can be generated from the
[Releases](https://github.com/codeigniter4/CodeIgniter4/releases) page:
click "Draft a new release", choose the tag `v4.x.x` (create new tag on
publish) with target `develop`, and click "Generate release notes". Copy the
resulting contents into **CHANGELOG.md** and adjust the format to match the
existing content.

## Process

Expand All @@ -100,7 +105,7 @@ the existing content.
> generating much new content.

* [ ] Merge any security advisory PRs in private forks.
* [ ] Add the current version to **CHANGELOG.md** with the contents generated above.
* [ ] Check that **CHANGELOG.md** contains the entry for the new version, generated above.
* [ ] Update **user_guide_src/source/changelogs/v4.x.x.rst**
* Remove the section titles that have no items
* [ ] Update **user_guide_src/source/installation/upgrade_4xx.rst**
Expand Down
153 changes: 153 additions & 0 deletions admin/generate-changelog.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

declare(strict_types=1);

/**
* Generates the CHANGELOG.md entry for a new release from GitHub's
* auto-generated release notes and prepends it to CHANGELOG.md.
*
* Requires the authenticated GitHub CLI (`gh`).
*
* Usage: php admin/generate-changelog.php <version> [--dry-run]
*/
chdir(__DIR__ . '/..');

$args = array_slice($argv, 1);
$options = array_values(array_filter($args, static fn (string $arg): bool => str_starts_with($arg, '--')));
$params = array_values(array_diff($args, $options));

if (count($params) !== 1 || preg_match('/\A\d+\.\d+\.\d+\z/', $params[0]) !== 1) {
echo "Usage: php {$argv[0]} <version> [--dry-run]\n";
echo "E.g.,: php {$argv[0]} 4.7.5\n";

exit(1);
}

$version = $params[0];
$dryRun = in_array('--dry-run', $options, true);
$repo = 'codeigniter4/CodeIgniter4';

$changelogPath = './CHANGELOG.md';
$changelog = file_get_contents($changelogPath);

if (! $dryRun && str_contains($changelog, "## [v{$version}]")) {
echo "CHANGELOG.md already contains an entry for v{$version}.\n";

exit(1);
}

// Fetches the auto-generated release notes.
$command = sprintf(
'gh api repos/%s/releases/generate-notes -f tag_name=v%s -f target_commitish=develop 2>&1',
$repo,
$version,
);
exec($command, $output, $exitCode);

if ($exitCode !== 0) {
echo "Failed to fetch the release notes from GitHub:\n";
echo implode("\n", $output) . "\n";

exit(1);
}

$response = json_decode(implode("\n", $output), true);
$body = $response['body'] ?? '';

// Parses the release notes into categories. See ".github/release.yml".
$sections = [];
$current = null;
$previousTag = null;

foreach (preg_split('/\R/', $body) as $line) {
if (preg_match('/^### (.+)$/', $line, $matches) === 1) {
$current = $matches[1];
$sections[$current] = [];

continue;
}

if (str_starts_with($line, '## ')) {
$current = null;

continue;
}

if (preg_match('~^\*\*Full Changelog\*\*: https://github\.com/[^/]+/[^/]+/compare/(.+?)\.\.\.~', $line, $matches) === 1) {
$previousTag = $matches[1];

continue;
}

if ($current !== null && str_starts_with($line, '* ')) {
$sections[$current][] = $line;
}
}

if ($previousTag === null) {
echo "Could not determine the previous tag from the release notes.\n";

exit(1);
}

// PRs in the catch-all category have none of the changelog labels. They are
// listed for checking only and are not part of the generated entry.
$othersTitle = 'Others (Only for checking. Remove this category)';
$others = $sections[$othersTitle] ?? [];
unset($sections[$othersTitle]);

// Extracts the security fixes from the SECURITY section of the detailed
// changelog, if any. These come from security advisories, not from PRs, so
// they are not part of the generated release notes.
$rstPath = "./user_guide_src/source/changelogs/v{$version}.rst";
$security = '';

if (is_file($rstPath)) {
$rst = file_get_contents($rstPath);

if (preg_match('/^\*+\nSECURITY\n\*+\n(.*?)(?=^\*+$|\z)/msu', $rst, $matches) === 1) {
$security = trim($matches[1], "\n");
$security = preg_replace('/^- /mu', '* ', $security);
}
}

$date = date('Y-m-d');
$entry = "## [v{$version}](https://github.com/{$repo}/tree/v{$version}) ({$date})\n";
$entry .= "[Full Changelog](https://github.com/{$repo}/compare/{$previousTag}...v{$version})\n";

if ($security !== '') {
$entry .= "\n### Security\n\n{$security}\n";
}

foreach ($sections as $title => $items) {
if ($items === []) {
continue;
}

$entry .= "\n### {$title}\n\n";
$entry .= implode("\n", $items) . "\n";
}

if ($others !== []) {
echo "The following PRs have no changelog label and were NOT included.\n";
echo "If any of them belong in the changelog, label the PR and run this script again:\n";
echo implode("\n", $others) . "\n\n";
}

if ($dryRun) {
echo $entry;

exit(0);
}

$updated = preg_replace('/\A# Changelog\n\n/', "# Changelog\n\n{$entry}\n", $changelog, 1, $count);

if ($count !== 1) {
echo 'Could not find the "# Changelog" header in CHANGELOG.md.' . "\n";

exit(1);
}

file_put_contents($changelogPath, $updated);

echo "Added the v{$version} entry to CHANGELOG.md.\n";
Loading
Loading