Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci: improve release script #862

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
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
Loading