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
91 changes: 50 additions & 41 deletions admin/create-new-changelog.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,81 +9,90 @@ function replace_file_content(string $path, string $pattern, string $replace): v
file_put_contents($path, $output);
}

// Main.
chdir(__DIR__ . '/..');

if ($argc !== 3) {
echo "Usage: php {$argv[0]} <current_version> <new_version>" . PHP_EOL;
echo "E.g.,: php {$argv[0]} 4.4.3 4.4.4" . PHP_EOL;
if (! isset($argv[1]) || ! isset($argv[2])) {
echo "Usage: php {$argv[0]} <current_version> <new_version> [--dry-run]\n";
echo "E.g. : php {$argv[0]} 4.4.3 4.4.4 --dry-run\n";

exit(1);
}

// Gets version number from argument.
$versionCurrent = $argv[1]; // e.g., '4.4.3'
$versionCurrentParts = explode('.', $versionCurrent);
$minorCurrent = $versionCurrentParts[0] . '.' . $versionCurrentParts[1];
$version = $argv[2]; // e.g., '4.4.4'
$versionParts = explode('.', $version);
$minor = $versionParts[0] . '.' . $versionParts[1];
$isMinorUpdate = ($minorCurrent !== $minor);

// Creates a branch for release.
if (! $isMinorUpdate) {
system('git switch develop');
$currentVersion = $argv[1]; // e.g., '4.4.3'
$currentVersionParts = explode('.', $currentVersion, 3);
$currentMinorVersion = $currentVersionParts[0] . '.' . $currentVersionParts[1];
$newVersion = $argv[2]; // e.g., '4.4.4'
$newVersionParts = explode('.', $newVersion, 3);
$newMinorVersion = $newVersionParts[0] . '.' . $newVersionParts[1];
$isMinorUpdate = $currentMinorVersion !== $newMinorVersion;

// Creates a branch for release
if (! in_array('--dry-run', $argv, true)) {
if (! $isMinorUpdate) {
system('git switch develop');
}

system("git switch -c docs-changelog-{$newVersion}");
system("git switch docs-changelog-{$newVersion}");
}
system('git switch -c docs-changelog-' . $version);
system('git switch docs-changelog-' . $version);

// Copy changelog
$changelog = "./user_guide_src/source/changelogs/v{$version}.rst";
$newChangelog = "./user_guide_src/source/changelogs/v{$newVersion}.rst";
$changelogIndex = './user_guide_src/source/changelogs/index.rst';

if ($isMinorUpdate) {
copy('./admin/next-changelog-minor.rst', $changelog);
copy('./admin/next-changelog-minor.rst', $newChangelog);
} else {
copy('./admin/next-changelog-patch.rst', $changelog);
copy('./admin/next-changelog-patch.rst', $newChangelog);
}

// Replace version in CodeIgniter.php to {version}-dev.
replace_file_content(
'./system/CodeIgniter.php',
'/public const CI_VERSION = \'.*?\';/u',
"public const CI_VERSION = '{$newVersion}-dev';",
);

// Add changelog to index.rst.
replace_file_content(
$changelogIndex,
'/\.\. toctree::\n :titlesonly:\n/u',
".. toctree::\n :titlesonly:\n\n v{$version}",
".. toctree::\n :titlesonly:\n\n v{$newVersion}",
);

// Replace {version}
$length = mb_strlen("Version {$version}");
$underline = str_repeat('#', $length);
$underline = str_repeat('#', mb_strlen("Version {$newVersion}"));
replace_file_content(
$changelog,
$newChangelog,
'/#################\nVersion {version}\n#################/u',
"{$underline}\nVersion {$version}\n{$underline}",
);
replace_file_content(
$changelog,
'/{version}/u',
"{$version}",
"{$underline}\nVersion {$newVersion}\n{$underline}",
);
replace_file_content($newChangelog, '/{version}/u', $newVersion);

// Copy upgrading
$versionWithoutDots = str_replace('.', '', $version);
$upgrading = "./user_guide_src/source/installation/upgrade_{$versionWithoutDots}.rst";
$versionWithoutDots = str_replace('.', '', $newVersion);
$newUpgrading = "./user_guide_src/source/installation/upgrade_{$versionWithoutDots}.rst";
$upgradingIndex = './user_guide_src/source/installation/upgrading.rst';
copy('./admin/next-upgrading-guide.rst', $upgrading);
copy('./admin/next-upgrading-guide.rst', $newUpgrading);

// Add upgrading to upgrading.rst.
replace_file_content(
$upgradingIndex,
'/ backward_compatibility_notes\n/u',
" backward_compatibility_notes\n\n upgrade_{$versionWithoutDots}",
);

// Replace {version}
$length = mb_strlen("Upgrading from {$versionCurrent} to {$version}");
$underline = str_repeat('#', $length);
$underline = str_repeat('#', mb_strlen("Upgrading from {$currentVersion} to {$newVersion}"));
replace_file_content(
$upgrading,
$newUpgrading,
'/##############################\nUpgrading from {version} to {version}\n##############################/u',
"{$underline}\nUpgrading from {$versionCurrent} to {$version}\n{$underline}",
"{$underline}\nUpgrading from {$currentVersion} to {$newVersion}\n{$underline}",
);

// Commits
system("git add {$changelog} {$changelogIndex}");
system("git add {$upgrading} {$upgradingIndex}");
system('git commit -m "docs: add changelog and upgrade for v' . $version . '"');
if (! in_array('--dry-run', $argv, true)) {
system("git add {$newChangelog} {$changelogIndex}");
system("git add {$newUpgrading} {$upgradingIndex}");
system("git commit -m \"docs: add changelog and upgrade for v{$newVersion}\"");
}
2 changes: 1 addition & 1 deletion system/CodeIgniter.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class CodeIgniter
/**
* The current version of CodeIgniter Framework
*/
public const CI_VERSION = '4.7.0';
public const CI_VERSION = '4.7.1-dev';

/**
* App startup time.
Expand Down
148 changes: 148 additions & 0 deletions tests/system/AutoReview/CreateNewChangelogTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\AutoReview;

use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;

/**
* @internal
*/
#[CoversNothing]
#[Group('AutoReview')]
final class CreateNewChangelogTest extends TestCase
{
private string $currentVersion;

public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();

if (getenv('GITHUB_ACTIONS') !== false) {
exec('git fetch --unshallow 2>&1', $output, $exitCode);
exec('git fetch --tags 2>&1', $output, $exitCode);

if ($exitCode !== 0) {
self::fail(sprintf(
"Failed to fetch git history and tags.\nOutput: %s",
implode("\n", $output),
));
}
}
}

protected function setUp(): void
{
parent::setUp();

exec('git describe --tags --abbrev=0 2>&1', $output, $exitCode);

if ($exitCode !== 0) {
$this->markTestSkipped(sprintf(
"Unable to get the latest git tag.\nOutput: %s",
implode("\n", $output),
));
}

// Current tag should already have the next patch docs done, so for testing purposes,
// we will treat the next patch version as the current version.
$this->currentVersion = $this->incrementVersion(trim($output[0], 'v'));
}

#[DataProvider('provideCreateNewChangelog')]
public function testCreateNewChangelog(string $mode): void
{
$currentVersion = $this->currentVersion;
$newVersion = $this->incrementVersion($currentVersion, $mode);

exec(
sprintf('php ./admin/create-new-changelog.php %s %s --dry-run', $currentVersion, $newVersion),
$output,
$exitCode,
);

$this->assertSame(0, $exitCode, "Script exited with code {$exitCode}. Output: " . implode("\n", $output));

$this->assertStringContainsString(
"public const CI_VERSION = '{$newVersion}-dev';",
$this->getContents('./system/CodeIgniter.php'),
);

$this->assertFileExists("./user_guide_src/source/changelogs/v{$newVersion}.rst");
$this->assertStringContainsString(
"Version {$newVersion}",
$this->getContents("./user_guide_src/source/changelogs/v{$newVersion}.rst"),
);
$this->assertStringContainsString(
"**{$newVersion} release of CodeIgniter4**",
$this->getContents("./user_guide_src/source/changelogs/v{$newVersion}.rst"),
);
$this->assertStringContainsString(
$newVersion,
$this->getContents('./user_guide_src/source/changelogs/index.rst'),
);

$versionWithoutDots = str_replace('.', '', $newVersion);
$this->assertFileExists("./user_guide_src/source/installation/upgrade_{$versionWithoutDots}.rst");
$this->assertStringContainsString(
"Upgrading from {$currentVersion} to {$newVersion}",
$this->getContents("./user_guide_src/source/installation/upgrade_{$versionWithoutDots}.rst"),
);
$this->assertStringContainsString(
"upgrade_{$versionWithoutDots}",
$this->getContents('./user_guide_src/source/installation/upgrading.rst'),
);

// cleanup added and modified files
exec('git restore .');
Copy link
Contributor

@neznaika0 neznaika0 Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paulbalandan You forgot about the developers.
The command deletes uncommited changes in the local environment. I cleared 10 files after the test 😢

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry about this. I was also stuck at why my files gets deleted when running auto review tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I won't be able to come to my laptop until Friday. Will you be able to effect the changes? Sorry again.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not know how to change this. What if I edited the changelog and the test got lost? Cancellation is not possible.

I restored the files via the IDE timeline.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I need to issue a note about uncommitted changes or create a temporary commit and return after the test?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exec('git clean -fd');
}

/**
* @return iterable<string, array{0: string}>
*/
public static function provideCreateNewChangelog(): iterable
{
yield 'patch update' => ['patch'];

yield 'minor update' => ['minor'];

yield 'major update' => ['major'];
}

private function incrementVersion(string $version, string $mode = 'patch'): string
{
$parts = explode('.', $version);

return match ($mode) {
'major' => sprintf('%d.0.0', ++$parts[0]),
'minor' => sprintf('%d.%d.0', $parts[0], ++$parts[1]),
'patch' => sprintf('%d.%d.%d', $parts[0], $parts[1], ++$parts[2]),
default => $this->fail('Invalid version increment mode. Use "major", "minor", or "patch".'),
};
}

private function getContents(string $path): string
{
$contents = @file_get_contents($path);

if ($contents === false) {
$this->fail("Failed to read file contents from {$path}.");
}

return $contents;
}
}
Loading