Skip to content

Commit

Permalink
ci: improve release script
Browse files Browse the repository at this point in the history
  • Loading branch information
innocenzi committed Dec 16, 2024
1 parent 49f019c commit 30d74a0
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 29 deletions.
43 changes: 43 additions & 0 deletions .github/workflows/publish-javascript-packages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Release JavaScript packages

on:
push:
tags:
- 'v*'

jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2

- name: Setup Node for npm publishing
uses: actions/setup-node@v4
with:
node-version: current

- name: Install dependencies
run: bun install

- name: Run tests
run: bun run test

- name: Build packages
run: bun run build

- name: Publish packages
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
for package in packages/*; do
if [ -d "$package" ] && [ -f "$package/package.json" ]; then
cd "$package"
echo "Publishing package in $package"
bun publish --access public
cd ../..
fi
done
204 changes: 178 additions & 26 deletions bin/release
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@

declare(strict_types=1);

/**
* This script:
* - Prompts for the next version
* - Bumps the Kernel version
* - Bumps the versions of PHP packages
* - Bumps the versions of JavaScript packages
* - Updates the changelog
* - Commits and push a new tag, which triggers GitHub Actions
* - Cleans up release artefacts
*/

use Composer\Semver\VersionParser;
use Tempest\Console\Console;
use Tempest\Console\ConsoleApplication;
Expand All @@ -13,27 +24,104 @@ use function Tempest\Support\str;

require_once getcwd() . '/vendor/autoload.php';

function ensureAccess(string $remote, string $branch): void
/**
* Bumps the constant version in the Kernel class.
*/
function bumpKernelVersion(string $version): void
{
$kernel = __DIR__ . '/../src/Tempest/Core/src/Kernel.php';
$content = preg_replace(
pattern: '/public const VERSION = \'.*\';/',
replacement: "public const VERSION = '{$version}';",
subject: file_get_contents($kernel),
);

file_put_contents($kernel, $content);
}

// TODO: I don't know the exact release process for PHP packages.
function bumpPhpPackages(string $version, bool $isMajor): void
{
if ($isMajor) {
executeCommands([
"./vendor/bin/monorepo-builder bump-interdependency {$version}",
'./vendor/bin/monorepo-builder validate',
]);
}
}

// TODO: I don't know the exact release process for PHP packages.
function cleanUpAfterRelease(): void
{
}

/**
* Bumps versions in all `package.json`.
*/
function bumpJavaScriptPackages(string $version): void
{
$rootPackageJson = json_decode(file_get_contents(__DIR__ . '/../package.json'), associative: true);
$packages = [];

foreach ($rootPackageJson['workspaces'] as $pattern) {
foreach (glob($pattern, GLOB_ONLYDIR) as $dir) {
$packageJsonPath = __DIR__ . '/../' . $dir . '/package.json';

if (file_exists($packageJsonPath)) {
$packages[] = realpath($packageJsonPath);
}
}
}

foreach ($packages as $package) {
updateJsonFile(
path: $package,
callback: function (array $content) use ($version) {
$content['version'] = $version;

return $content;
},
);
}
}

/**
* Updates the `CHANGELOG.md` file.
*/
function updateChangelog(): void
{
executeCommands('bunx git-cliff > CHANGELOG.md');
}

/**
* Ensure the release script can run.
*/
function performPreReleaseChecks(string $remote, string $branch): void
{
if (empty(shell_exec('which bun'))) {
throw new Exception('This script requires `bun` to be installed.');
}

if (! empty(shell_exec('git status --porcelain 2>&1'))) {
throw new Exception('Repository must be in a clean state to release.');
}

if (! str_starts_with(shell_exec('git rev-parse --abbrev-ref --symbolic-full-name @{u}'), "$remote/$branch")) {
throw new Exception("You must be on the $remote/$branch branch to release.");
if (! str_starts_with(shell_exec('git rev-parse --abbrev-ref --symbolic-full-name @{u}'), "{$remote}/{$branch}")) {
throw new Exception("You must be on the {$remote}/{$branch} branch to release.");
}
}

/**
* Gets the current version.
*/
function getCurrentVersion(): string
{
return exec('git describe --tags --abbrev=0');
}

function normalizeVersion(string $version): string
{
return preg_replace('/^(\d+\.\d+\.\d+)\.0(-|$|\+)/', '$1$2', (new VersionParser())->normalize($version));
}

/**
* Suggests a semver-valid version.
*/
function suggestNextVersions(string $current): array
{
$version = normalizeVersion($current);
Expand Down Expand Up @@ -69,12 +157,17 @@ function suggestNextVersions(string $current): array
]);
}

function releaseTag(string $tag): void
function normalizeVersion(string $version): string
{
return preg_replace('/^(\d+\.\d+\.\d+)\.0(-|$|\+)/', '$1$2', (new VersionParser())->normalize($version));
}

/**
* Executes the given shell commands.
*/
function executeCommands(string|array $commands): void
{
$commands = [
"git tag {$tag}",
"git push origin tag {$tag}",
];
$commands = is_array($commands) ? $commands : [$commands];

foreach ($commands as $command) {
exec($command, result_code: $code);
Expand All @@ -83,7 +176,44 @@ function releaseTag(string $tag): void
continue;
}

throw new Exception("Pushing of git tag failed.");
throw new Exception("Command failed: {$command}");
}
}

/**
* Creates a commit with the current changes.
*/
function commit(string $message): void
{
executeCommands("git commit -am '{$message}'");
}

/**
* Updates a JSON file, preserving indentation.
*/
function updateJsonFile(string $path, Closure $callback): void
{
$content = file_get_contents($path);
$indent = detectIndent($content);
$content = $callback(json_decode($content, associative: true), $indent);
$content = preg_replace_callback(
'/^ +/m',
fn ($m) => str_repeat($indent, \strlen($m[0]) / 4),
json_encode($content, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES),
);

file_put_contents($path, $content . "\n");
}

/**
* Detects the indentation of a raw JSON file.
*/
function detectIndent(string $raw, string $default = ' '): string
{
try {
return explode('"', explode("\n", $raw)[1])[0] ?? $default;
} catch (Throwable) {
return $default;
}
}

Expand All @@ -101,36 +231,58 @@ try {
$console = get(Console::class);

$console->writeln();
$console->info(sprintf("Current version is <u><strong>%s</strong></u>", $current = getCurrentVersion()));
$console->info(sprintf('Current version is <em>%s</em>.', $current = getCurrentVersion()));

$console->writeln();
$new = $console->ask(
question: "What should the new version be?",
question: 'What should the new version be?',
options: arr(suggestNextVersions($current))
->map(fn (string $version, string $type) => (string) str($type)->replace('_', ' ')->append(': ', $version))
->values()
->toArray(),
);

$tag = (string) str($new)->afterLast(': ')->prepend('v');
$isMajor = str_contains($new, 'major');
$version = (string) str($new)->afterLast(': ');
$tag = "v{$version}";

$console->writeln();
if (! $console->confirm("The next tag will be <u><strong>{$tag}</strong></u><question>. Release?")) {
if (! $console->confirm("The next tag will be <em>{$tag}</em>. Release?")) {
$console->error('Cancelled.');
exit;
}

$console->writeln();
$console->writeln();
$console->writeln();
// TODO: use `$console->task` here

// Bump PHP packages
$console->info('Bumping PHP packages...');
bumpKernelVersion($version);
bumpPhpPackages($version, $isMajor);

// Bump JavaScript packages
$console->info('Bumping JavaScript packages...');
bumpJavaScriptPackages($version);

// Update changelog
updateChangelog();

// Push tags
$console->info('Releasing...');
// commit("chore: release {$tag}");
// executeCommands([
// "git tag {$tag}",
// "git push origin tag {$tag}",
// ]);

releaseTag($tag);
// Clean up
$console->info('Cleaning up...');
cleanUpAfterRelease();
// commit('chore: post-release clean up');

$console->writeln();
$console->success("Released {$tag}");
$console->success(sprintf(
'Released <em>%1$s</em>. The <href="https://github.com/tempestphp/tempest-framework/releases/tag/%1$s">GitHub release</href> will be created automatically in a few seconds.',
$tag,
));

exit;

} catch (InterruptException) {
}
2 changes: 1 addition & 1 deletion cliff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ body = """
"""

[git]
skip_tags = "(0\\.0\\.\\d)|(1\\.0-alpha1)"
skip_tags = "(0\\.0\\.\\d)|1\\.0\\.0-alpha\\.1"
commit_parsers = [
{ field = "breaking", pattern = "true", group = "<!-- -1 -->🚨 Breaking changes" },
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
Expand Down
4 changes: 4 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@
"phpstan": "vendor/bin/phpstan analyse src tests --memory-limit=1G",
"rector": "vendor/bin/rector process --no-ansi",
"merge": "vendor/bin/monorepo-builder merge",
"release": [
"composer qa",
"./bin/release"
],
"qa": [
"composer merge",
"./bin/validate-packages",
Expand Down
18 changes: 16 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
import defineEslintConfig from '@innocenzi/eslint-config'
import defineEslintConfig, { jsonc } from '@innocenzi/eslint-config'

export default defineEslintConfig()
export default defineEslintConfig(
{ jsonc: false },
jsonc({
files: ['package.json', '**/package.json'],
overrides: {
'jsonc/indent': ['error', 2],
},
}),
jsonc({
files: ['composer.json', '**/composer.json'],
overrides: {
'jsonc/indent': ['error', 4],
},
}),
)
2 changes: 2 additions & 0 deletions src/Tempest/Core/src/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

final class Kernel
{
public const VERSION = '1.0.0-alpha.3';

public readonly Container $container;

public bool $discoveryCache;
Expand Down

0 comments on commit 30d74a0

Please sign in to comment.