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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user