chore(release): run the full CI suite before bumping a version

A local release now has to pass the same install, tests, catalog,
typecheck, build, and lint steps as GitHub CI, so a broken tree
cannot be tagged.
This commit is contained in:
DrSmoothl
2026-09-13 20:46:43 +08:00
parent 4d6011ab04
commit 26fe4e5fb5
6 changed files with 108 additions and 21 deletions
+2 -8
View File
@@ -1,4 +1,5 @@
# Run the root validation commands on every pull request.
# Command list is duplicated in tools/ci-check.mjs — keep them identical.
name: CI
on:
@@ -38,14 +39,7 @@ jobs:
# catalog.json the docs site consumes). Fail if the committed snapshot
# drifted from the live specs — keeps the API docs from going stale.
- name: Verify generated action catalog is up to date
run: |
catalog_status="$(git status --porcelain=v1 --untracked-files=all -- packages/mcp/src/generated)"
if [ -n "$catalog_status" ]; then
git diff -- packages/mcp/src/generated
printf '%s\n' "$catalog_status"
echo '::error::Generated action catalog is stale. Run `pnpm --filter @snowluma/mcp gen` and commit packages/mcp/src/generated/.' >&2
exit 1
fi
run: node tools/check-action-catalog.mjs
typecheck:
runs-on: ubuntu-latest
+1 -8
View File
@@ -37,14 +37,7 @@ jobs:
run: pnpm typecheck
- name: Verify generated action catalog is up to date
run: |
catalog_status="$(git status --porcelain=v1 --untracked-files=all -- packages/mcp/src/generated)"
if [ -n "$catalog_status" ]; then
git diff -- packages/mcp/src/generated
printf '%s\n' "$catalog_status"
echo '::error::Generated action catalog is stale. Run `pnpm --filter @snowluma/mcp gen` and commit packages/mcp/src/generated/.' >&2
exit 1
fi
run: node tools/check-action-catalog.mjs
build:
needs: preflight
+1
View File
@@ -25,6 +25,7 @@
"typecheck": "pnpm -r --if-present typecheck",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"ci": "node tools/ci-check.mjs",
"start": "node dist/index.mjs",
"bump": "node tools/bump-version.mjs",
"release": "node tools/release.mjs"
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env node
// Same catalog gate as the CI `test` job. Keep the git invocation and
// failure text in lockstep with `.github/workflows/ci.yml`.
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
function runGit(args, { inherit = false } = {}) {
return spawnSync('git', args, {
cwd: repoRoot,
encoding: 'utf-8',
stdio: inherit ? 'inherit' : ['ignore', 'pipe', 'pipe'],
});
}
const status = runGit([
'status',
'--porcelain=v1',
'--untracked-files=all',
'--',
'packages/mcp/src/generated',
]);
if (status.status !== 0) {
process.stderr.write(status.stderr || 'git status failed\n');
process.exit(status.status === null ? 1 : status.status);
}
const catalogStatus = (status.stdout || '').trim();
if (!catalogStatus) process.exit(0);
runGit(['diff', '--', 'packages/mcp/src/generated'], { inherit: true });
process.stdout.write(`${catalogStatus}\n`);
process.stderr.write(
'::error::Generated action catalog is stale. Run `pnpm --filter @snowluma/mcp gen` and commit packages/mcp/src/generated/.\n',
);
process.exit(1);
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env node
// Local replica of `.github/workflows/ci.yml`.
// Every command and step title below must stay identical to that workflow.
// CI jobs run these in parallel after a frozen install; this script runs the
// same commands once each, in that job's internal order, and still executes
// later steps if an earlier one fails — same as four GitHub jobs reporting
// independently.
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const checks = [
['Install dependencies', 'pnpm install --frozen-lockfile'],
['Run all package tests', 'pnpm test'],
['Verify generated action catalog is up to date', 'node tools/check-action-catalog.mjs'],
['Type check all packages', 'pnpm typecheck'],
['Build release inputs', 'pnpm run build:all'],
['Lint managed source files', 'pnpm lint'],
];
const failed = [];
for (const [title, command] of checks) {
console.log(`\n▸ ${title}\n$ ${command}\n`);
const result = spawnSync(command, {
cwd: repoRoot,
shell: true,
stdio: 'inherit',
env: process.env,
});
const code = result.status === 0 ? 0 : (result.status ?? 1);
if (code !== 0) {
failed.push({ title, command, code });
console.error(`\n✗ ${title} failed (exit ${code})`);
} else {
console.log(`\n✓ ${title}`);
}
}
if (failed.length > 0) {
console.error('\nCI suite failed:');
for (const item of failed) {
console.error(` - ${item.title}: \`${item.command}\` (exit ${item.code})`);
}
process.exit(1);
}
console.log('\nCI suite passed (same commands as .github/workflows/ci.yml).');
+14 -5
View File
@@ -9,17 +9,19 @@
// 1. Sanity-check: on `dev`, clean tree. Must not be behind origin/dev.
// Ahead-by-one is allowed when HEAD is already this version's
// `chore(release):` commit (resume after a dropped push).
// 2. `pnpm bump <version>` (writes every package.json).
// 3. Commit `chore(release): vX.Y.Z` — that prefix triggers the
// 2. Full CI suite (`node tools/ci-check.mjs`) — the same commands as
// `.github/workflows/ci.yml`. Fail here instead of after the bump.
// 3. `pnpm bump <version>` (writes every package.json).
// 4. Commit `chore(release): vX.Y.Z` — that prefix triggers the
// Promote workflow, which opens (or updates) the dev→main PR and
// auto-merges if the GitHub setting allows. Skipped when HEAD is
// already that commit.
// 4. Push to origin/dev (retries transient HTTPS / TLS failures).
// 5. (Unless `--no-wait`) poll the Promote PR via `gh` until it
// 5. Push to origin/dev (retries transient HTTPS / TLS failures).
// 6. (Unless `--no-wait`) poll the Promote PR via `gh` until it
// merges, then fetch and fast-forward `main` to `origin/main`,
// create `vX.Y.Z` tag, push it — that triggers `release.yml`.
// Fetch / tag push also retry.
// 6. Switch back to `dev` so the next `git log` view is back where
// 7. Switch back to `dev` so the next `git log` view is back where
// you were.
//
// Re-run the same `pnpm release <version>` after a dropped push; do
@@ -201,6 +203,12 @@ function preflight() {
if (!dryRun) ok(`on dev, clean, ready to release ${tag}`);
}
function runCiSuite() {
info('running the full CI suite (same commands as .github/workflows/ci.yml)');
sh('node tools/ci-check.mjs');
if (!dryRun) ok('CI suite passed');
}
// ───────────── step 2-4: bump + commit + push dev ─────────────
function bumpAndPushDev() {
@@ -320,6 +328,7 @@ async function waitAndTag() {
(async () => {
console.log(`Release pipeline → ${tag}${dryRun ? ' (dry-run)' : ''}\n`);
preflight();
runCiSuite();
bumpAndPushDev();
if (noWait) {