From 26fe4e5fb50c98c1595ebd1f4ff81579ddbece2e Mon Sep 17 00:00:00 2001 From: DrSmoothl <1787882683@qq.com> Date: Sun, 13 Sep 2026 20:46:43 +0800 Subject: [PATCH] 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. --- .github/workflows/ci.yml | 10 ++----- .github/workflows/release.yml | 9 +----- package.json | 1 + tools/check-action-catalog.mjs | 39 ++++++++++++++++++++++++++ tools/ci-check.mjs | 51 ++++++++++++++++++++++++++++++++++ tools/release.mjs | 19 +++++++++---- 6 files changed, 108 insertions(+), 21 deletions(-) create mode 100644 tools/check-action-catalog.mjs create mode 100644 tools/ci-check.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b47e7006..9ab52886 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0b931d7e..e8553b00 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/package.json b/package.json index a535d944..c49c6a1b 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/tools/check-action-catalog.mjs b/tools/check-action-catalog.mjs new file mode 100644 index 00000000..663256d5 --- /dev/null +++ b/tools/check-action-catalog.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); diff --git a/tools/ci-check.mjs b/tools/ci-check.mjs new file mode 100644 index 00000000..5410813a --- /dev/null +++ b/tools/ci-check.mjs @@ -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).'); diff --git a/tools/release.mjs b/tools/release.mjs index e0c83904..dae61ebb 100644 --- a/tools/release.mjs +++ b/tools/release.mjs @@ -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 ` (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 ` (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 ` 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) {