diff --git a/admin/README.md b/admin/README.md index 958004b41967..a4048bf59152 100644 --- a/admin/README.md +++ b/admin/README.md @@ -65,6 +65,10 @@ are used as part of that process: of the version's upgrade guide with the project space files changed since the last release. Usage: `php admin/update-upgrade-guide.php [--dry-run]` +- **sync-release-branches.php** checks out the target branch and merges + upstream and the source branch into it, as done at several points of the + release process. Without `--push`, the result is left unpushed for review. + Usage: `php admin/sync-release-branches.php [--push]` ## Other Stuff diff --git a/admin/RELEASE.md b/admin/RELEASE.md index 8fa117e4f8a4..75f2a0fbf35a 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -1,11 +1,10 @@ # Release Process -> Documentation guide based on the releases of `4.0.5` and `4.1.0` on January 31, 2021. +> Documentation guide based on the releases of `4.0.5` and `4.1.0` on January 31, 2021 (MGatner) > -> Updated for `4.5.0` on April 7, 2024. -> Updated for `4.6.0` on January 19, 2025. -> -> -MGatner, kenjis +> Updated for `4.5.0` on April 7, 2024 (kenjis) +> Updated for `4.6.0` on January 19, 2025 (paulbalandan) +> Updated for `4.7.5` on July 8, 2026 (paulbalandan) ## Notation @@ -24,11 +23,7 @@ merging will take time. * [ ] Merge `develop` into `4.y`: ```console - git fetch upstream - git switch 4.y - git merge upstream/4.y - git merge upstream/develop - git push upstream HEAD + php admin/sync-release-branches.php 4.y develop --push ``` ## [Minor version only] Merge minor version branch into `develop` @@ -183,19 +178,11 @@ existing content. * If it fails, fix the cause and re-run it via "Run workflow" with the tag `v4.x.x`. * [ ] Fast-forward `develop` branch to catch the merge commit from `master` ```console - git fetch upstream - git checkout develop - git merge upstream/develop - git merge upstream/master - git push upstream HEAD + php admin/sync-release-branches.php develop master --push ``` * [ ] Update the next minor version branch `4.y`: ```console - git fetch upstream - git switch 4.y - git merge upstream/4.y - git merge upstream/develop - git push upstream HEAD + php admin/sync-release-branches.php 4.y develop --push ``` * [ ] [Minor version only] Create the new next minor version branch `4.z`: ```console diff --git a/admin/sync-release-branches.php b/admin/sync-release-branches.php new file mode 100644 index 000000000000..4f82a424bc65 --- /dev/null +++ b/admin/sync-release-branches.php @@ -0,0 +1,96 @@ + [--push] + */ +function run_command(string $command): void +{ + echo sprintf("$ %s\n", $command); + system($command, $exitCode); + + if ($exitCode !== 0) { + exit($exitCode); + } +} + +function merge_branch(string $ref): void +{ + echo sprintf("$ git merge %s\n", $ref); + system(sprintf('git merge %s', escapeshellarg($ref)), $exitCode); + + if ($exitCode !== 0) { + echo sprintf("\nMerging %s failed. Resolve the conflicts and run \"git merge --continue\",\n", $ref); + echo "then push with \"git push upstream HEAD\".\n"; + + exit(1); + } +} + +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)); + +$isBranch = static fn (string $name): bool => preg_match('/\A(?:develop|master|\d+\.\d+)\z/', $name) === 1; + +if ( + count($params) !== 2 + || ! $isBranch($params[0]) + || ! $isBranch($params[1]) + || $params[0] === $params[1] + || array_diff($options, ['--push']) !== [] +) { + echo sprintf("Usage: php %s [--push]\n", $argv[0]); + echo sprintf("E.g.,: php %s 4.8 develop --push # merges develop into 4.8\n", $argv[0]); + echo sprintf(" php %s develop master --push # merges master into develop\n", $argv[0]); + echo "Checks out , merges upstream/ and then upstream/ into it.\n"; + echo "Without --push, the result is left unpushed for review.\n"; + + exit(1); +} + +[$target, $source] = $params; +$push = in_array('--push', $options, true); + +exec('git remote get-url upstream 2>&1', $remoteOutput, $exitCode); + +if ($exitCode !== 0) { + echo "The \"upstream\" remote is not configured.\n"; + + exit(1); +} + +exec('git status --porcelain 2>&1', $statusOutput, $exitCode); + +if ($exitCode !== 0 || $statusOutput !== []) { + echo "The working tree is not clean. Commit or stash your changes first.\n"; + + exit(1); +} + +run_command('git fetch upstream'); + +exec(sprintf('git rev-parse --verify --quiet refs/heads/%s 2>&1', escapeshellarg($target)), $verifyOutput, $exitCode); + +if ($exitCode === 0) { + run_command(sprintf('git switch %s', escapeshellarg($target))); +} else { + run_command(sprintf('git switch -c %s upstream/%s', escapeshellarg($target), escapeshellarg($target))); +} + +merge_branch("upstream/{$target}"); +merge_branch("upstream/{$source}"); + +if ($push) { + run_command('git push upstream HEAD'); + + echo sprintf("Merged upstream/%s into %s and pushed to upstream.\n", $source, $target); +} else { + echo sprintf("Merged upstream/%s into %s. Review the result, then run \"git push upstream HEAD\".\n", $source, $target); +} diff --git a/tests/system/AutoReview/SyncReleaseBranchesTest.php b/tests/system/AutoReview/SyncReleaseBranchesTest.php new file mode 100644 index 000000000000..26d90c4b1592 --- /dev/null +++ b/tests/system/AutoReview/SyncReleaseBranchesTest.php @@ -0,0 +1,57 @@ + + * + * 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; + +/** + * The successful path switches branches and merges, so only the argument + * validation is exercised here. + * + * @internal + */ +#[CoversNothing] +#[Group('AutoReview')] +final class SyncReleaseBranchesTest extends TestCase +{ + #[DataProvider('provideUsageErrors')] + public function testUsageErrors(string $arguments): void + { + exec(sprintf('php ./admin/sync-release-branches.php %s 2>&1', $arguments), $output, $exitCode); + + $this->assertSame(1, $exitCode); + $this->assertStringContainsString('Usage:', implode("\n", $output)); + } + + /** + * @return iterable + */ + public static function provideUsageErrors(): iterable + { + yield 'no arguments' => ['']; + + yield 'missing source' => ['4.8']; + + yield 'invalid branch' => ['release-4.8.0 develop']; + + yield 'invalid source' => ['4.8 upstream/develop']; + + yield 'same branch and source' => ['develop develop']; + + yield 'unknown option' => ['4.8 develop --force']; + } +}