Skip to content

Commit efc4f3c

Browse files
AceGreenmanclaude
andcommitted
Add iso-route release workflow + check-source guard
Mirrors iso-harness: tag iso-route-vX.Y.Z → GitHub release fires the workflow, which verifies the build-test-lint CI check-run succeeded for that commit and that package.json version matches the tag before running npm publish --provenance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 289e325 commit efc4f3c

3 files changed

Lines changed: 109 additions & 0 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
name: iso-route Release to npm
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
defaults:
8+
run:
9+
working-directory: packages/iso-route
10+
11+
jobs:
12+
publish:
13+
if: startsWith(github.ref_name, 'iso-route-v')
14+
runs-on: ubuntu-latest
15+
permissions:
16+
contents: read
17+
id-token: write
18+
steps:
19+
- uses: actions/checkout@v6
20+
21+
- uses: actions/setup-node@v6
22+
with:
23+
node-version: '22'
24+
registry-url: 'https://registry.npmjs.org'
25+
cache: 'npm'
26+
27+
- name: Set version from release tag
28+
run: |
29+
VERSION="${GITHUB_REF_NAME#iso-route-v}"
30+
echo "VERSION=$VERSION" >> $GITHUB_ENV
31+
32+
- name: Verify CI build-test-lint passed for release commit
33+
env:
34+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35+
working-directory: .
36+
run: |
37+
SHA=$(git rev-parse HEAD)
38+
echo "Verifying CI build-test-lint check-run for $SHA"
39+
DEADLINE=$(( $(date +%s) + 1800 ))
40+
STATUS=""
41+
CONCLUSION=""
42+
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
43+
read -r STATUS CONCLUSION <<<"$(gh api "repos/${{ github.repository }}/commits/$SHA/check-runs" \
44+
--jq '[.check_runs[] | select(.name == "build-test-lint")] | sort_by(.started_at) | last | "\(.status // "missing") \(.conclusion // "null")"')"
45+
echo "build-test-lint check-run: status=$STATUS conclusion=$CONCLUSION"
46+
if [ "$STATUS" = "completed" ]; then
47+
break
48+
fi
49+
sleep 15
50+
done
51+
if [ "$STATUS" != "completed" ]; then
52+
echo "::error::CI build-test-lint for $SHA never completed within 30min (last status: $STATUS)."
53+
exit 1
54+
fi
55+
if [ "$CONCLUSION" != "success" ]; then
56+
echo "::error::CI build-test-lint for $SHA did not succeed (conclusion: $CONCLUSION). Fix before re-releasing."
57+
exit 1
58+
fi
59+
echo "CI build-test-lint succeeded for $SHA"
60+
61+
- name: Verify package.json version matches release tag
62+
run: npm run release:check-source -- "$VERSION"
63+
64+
- name: Install (workspace root)
65+
run: npm ci
66+
working-directory: .
67+
68+
- name: Publish to npm (with provenance)
69+
run: npm publish --provenance --access public
70+
env:
71+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

packages/iso-route/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"typecheck": "tsc -p tsconfig.json --noEmit",
2626
"example": "tsx src/cli.ts build examples/models.yaml --out examples/out --dry-run",
2727
"ci": "npm run typecheck && npm test",
28+
"release:check-source": "node ./scripts/release/check-source.mjs",
2829
"prepublishOnly": "npm run clean && npm run build && npm test"
2930
},
3031
"dependencies": {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { readFile } from 'node:fs/promises';
2+
import { fileURLToPath } from 'node:url';
3+
import path from 'node:path';
4+
5+
const version = process.argv[2];
6+
7+
if (!version) {
8+
console.error('Usage: node scripts/release/check-source.mjs <version>');
9+
process.exit(1);
10+
}
11+
12+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
13+
const pkgPath = path.join(root, 'package.json');
14+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
15+
16+
if (pkg.version !== version) {
17+
console.error(
18+
`package.json version ${pkg.version ?? 'unknown'} does not match release tag ${version}. ` +
19+
`Bump "version" in package.json, commit, and retag before publishing.`,
20+
);
21+
process.exit(1);
22+
}
23+
24+
for (const section of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
25+
const deps = pkg[section];
26+
if (!deps) continue;
27+
for (const [name, spec] of Object.entries(deps)) {
28+
if (typeof spec === 'string' && spec.startsWith('file:')) {
29+
console.error(
30+
`${section}["${name}"] is "${spec}" — file: deps are published verbatim and break consumers.`,
31+
);
32+
process.exit(1);
33+
}
34+
}
35+
}
36+
37+
console.log(`${pkg.name}: ${pkg.version}`);

0 commit comments

Comments
 (0)