Skip to content

Commit 26961d9

Browse files
lidge-junclaude
andcommitted
ci(release): adopt jawcode-style npm release (workflow_dispatch + dry-run + provenance)
Replace the tag-push publish workflow with jawcode's pattern: a `workflow_dispatch` "Release" job with version / dist-tag (latest|preview) / dry-run (default true) inputs that verifies the input matches package.json, then `npm publish --provenance` (prepublishOnly builds the GUI first), then a post-publish `npm view` smoke. Add scripts/release.ts (`bun run release <version> [--publish]`) — opencodex's single-package version of jawcode's release helper: clean-main+typecheck preflight → bump → commit → push → dispatch → watch. release.ts is not shipped in the npm tarball; only scripts/postinstall.mjs is. Runbook updated in devlog/65. Verified: release.ts transpiles, tsc clean, workflow YAML valid, npm pack excludes release.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8e199dd commit 26961d9

5 files changed

Lines changed: 186 additions & 91 deletions

File tree

.github/workflows/publish-npm.yml

Lines changed: 0 additions & 58 deletions
This file was deleted.

.github/workflows/release.yml

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
name: Release
2+
3+
# Publish opencodex to npm — jawcode-style: triggered from the Actions tab with an explicit
4+
# version, dist-tag, and a dry-run-first default. Bump package.json on main BEFORE dispatching
5+
# (or use `bun run release <version>`, which does the bump+commit+push+dispatch for you); the
6+
# workflow verifies the version matches before publishing.
7+
on:
8+
workflow_dispatch:
9+
inputs:
10+
version:
11+
description: "Version to publish — must equal package.json (e.g. 0.1.0)"
12+
required: true
13+
type: string
14+
tag:
15+
description: "npm dist-tag"
16+
required: true
17+
type: choice
18+
options:
19+
- latest
20+
- preview
21+
default: latest
22+
dry-run:
23+
description: "Dry run (build + pack, no actual publish)"
24+
required: false
25+
type: boolean
26+
default: true
27+
28+
permissions:
29+
contents: read
30+
id-token: write # npm provenance attestation
31+
32+
concurrency:
33+
group: release
34+
cancel-in-progress: false
35+
36+
jobs:
37+
publish:
38+
runs-on: ubuntu-latest
39+
timeout-minutes: 15
40+
steps:
41+
- name: Checkout
42+
uses: actions/checkout@v4
43+
44+
# opencodex is bun-native (the prepublishOnly GUI build + typecheck run under bun).
45+
- name: Setup Bun
46+
uses: oven-sh/setup-bun@v2
47+
with:
48+
bun-version: latest
49+
50+
# node + npm perform the actual publish (with provenance); registry-url wires NODE_AUTH_TOKEN.
51+
- name: Setup Node
52+
uses: actions/setup-node@v4
53+
with:
54+
node-version: 24
55+
registry-url: "https://registry.npmjs.org"
56+
57+
- name: Install dependencies
58+
run: bun install
59+
60+
- name: Verify version matches package.json
61+
run: |
62+
PKG=$(node -p "require('./package.json').version")
63+
echo "package.json=$PKG input=${{ inputs.version }}"
64+
test "$PKG" = "${{ inputs.version }}" || {
65+
echo "::error::package.json ($PKG) != requested (${{ inputs.version }}) — bump package.json on main first";
66+
exit 1;
67+
}
68+
69+
# `npm publish` runs prepublishOnly (typecheck + build the GUI into gui/dist) first, so even a
70+
# dry-run fully verifies the build. --provenance attests the tarball was built in this CI run.
71+
- name: Publish (or dry-run)
72+
run: |
73+
if [ "${{ inputs.dry-run }}" = "true" ]; then
74+
echo "::notice::DRY RUN — building + packing, not publishing"
75+
npm publish --dry-run --tag "${{ inputs.tag }}" --access public --provenance
76+
else
77+
npm publish --tag "${{ inputs.tag }}" --access public --provenance
78+
fi
79+
env:
80+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
81+
82+
# Confirm the registry actually has the new version (real publishes only).
83+
- name: Post-publish registry smoke
84+
if: ${{ inputs.dry-run != true }}
85+
run: |
86+
sleep 10
87+
npm view "opencodex@${{ inputs.version }}" version dist-tags
Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,38 @@
1-
# npm publish CI + release runbook
1+
# npm release CI + runbook (jawcode-style)
22

33
## Why
4-
`bun install -g opencodex` / `npm install -g opencodex` install from the **npm registry**, NOT from
5-
GitHub. Pushing to GitHub does not make those commands work — opencodex must be **published to npm**
6-
first. This adds a GitHub Actions workflow that publishes on a version tag.
4+
`bun/npm install -g opencodex` install from the **npm registry**, not GitHub — so opencodex must be
5+
**published to npm**. Modeled on jawcode's release flow (minus its monorepo/native-binary bits).
76

8-
## What shipped
9-
- `.github/workflows/publish-npm.yml` — on a pushed tag `vX.Y.Z` (or manual dispatch):
10-
setup bun (for the `prepublishOnly` GUI build) + node (for `npm publish`), `bun install`, verify the
11-
tag matches `package.json` version, then `npm publish --provenance --access public`.
12-
- Package was already made publish-ready (earlier `63_release-prep`):
13-
`files` allowlist (src + gui/dist + scripts/postinstall.mjs + README + LICENSE), `bin` (opencodex/ocx,
14-
`#!/usr/bin/env bun`), `engines.bun`, `prepublishOnly` (typecheck + build the GUI into gui/dist so the
15-
published tarball always carries a fresh dashboard). `npm pack --dry-run`: 54 files / 634 KB, ships
16-
gui/dist + the bin, excludes devlog/docs-site/gui-src.
7+
## How jawcode does it (reference)
8+
`jawcode/.github/workflows/release.yml` is a **`workflow_dispatch`** job with `version` + `tag`
9+
(latest/preview) + `dry-run` (default **true**) inputs: it verifies package.json matches the input,
10+
then `npm publish --tag … --access public --provenance`, then a post-publish registry smoke. A local
11+
`bun scripts/release.ts <version>` does preflight → bump → commit → push → dispatch → watch.
12+
13+
## What shipped (opencodex)
14+
- **`.github/workflows/release.yml`**`workflow_dispatch` with `version` / `tag` (latest|preview) /
15+
`dry-run` (default true). Verifies the tag input == package.json, then `npm publish` (prepublishOnly
16+
builds the GUI into gui/dist first) with `--provenance`, then `npm view` smoke on real publishes.
17+
Replaces the old tag-push `publish-npm.yml`.
18+
- **`scripts/release.ts`** (+ `bun run release` / `release:watch`) — single-package version of
19+
jawcode's helper: clean-main + typecheck preflight → `npm version --no-git-tag-version` → commit →
20+
push → `gh workflow run release.yml` (dry-run unless `--publish`) → watch. Not shipped in the tarball.
21+
- Package already publish-ready (`files`, bin `#!/usr/bin/env bun`, `engines.bun`, `prepublishOnly`).
1722

1823
## One-time setup (owner)
19-
1. Create an npm **Automation** access token: npmjs.com → Access Tokens → Generate → *Automation*.
20-
2. Add it to the repo as a secret named **`NPM_TOKEN`**: GitHub → Settings → Secrets and variables →
21-
Actions → New repository secret.
22-
3. (First publish only) the npm name `opencodex` is currently unclaimed, so the first tagged release
23-
claims it under your npm account.
24+
1. npm **Automation** (or Granular all-packages Read+Write) token: npmjs.com → Access Tokens.
25+
2. Add it as the repo secret **`NPM_TOKEN`**: GitHub → Settings → Secrets and variables → Actions.
2426

25-
## Releasing (each version)
26-
```bash
27-
# from a clean main
28-
npm version patch # or minor/major — bumps package.json + creates the vX.Y.Z git tag
29-
git push --follow-tags # pushes the commit AND the tag → CI publishes to npm (with provenance)
30-
```
31-
After the workflow goes green:
27+
## Releasing
28+
**Easiest (local helper):**
3229
```bash
33-
bun install -g opencodex # or: npm install -g opencodex
34-
ocx init && ocx start
30+
bun run release 0.1.0 # preflight → bump → commit → push → DRY-RUN dispatch → watch
31+
bun run release 0.1.0 --publish # …and actually publish (after the dry-run looks good)
3532
```
33+
**Manual (GitHub UI):** bump `version` in package.json on main, commit/push, then Actions → **Release**
34+
→ Run workflow → enter the version, pick the dist-tag, leave **dry-run on** first, re-run with dry-run
35+
off to publish.
3636

37-
## Notes
38-
- opencodex is **bun-native** (the `ocx` bin is `#!/usr/bin/env bun` and the server uses `Bun.serve`),
39-
so installers still need **bun** on PATH even via `npm install -g`. `engines.bun` documents this.
40-
- The consumer-side `postinstall` (interactive [Y/n] GitHub-star prompt) is TTY-gated → it no-ops in
41-
CI / piped installs, so it never blocks an automated install.
37+
After a real publish: `bun install -g opencodex` / `npm install -g opencodex` work (package page:
38+
https://www.npmjs.com/package/opencodex). opencodex is bun-native, so installers still need **bun** on PATH.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
"typecheck": "bun x tsc --noEmit",
2525
"build:gui": "cd gui && bun install && bun run build",
2626
"postinstall": "node scripts/postinstall.mjs",
27-
"prepublishOnly": "bun run typecheck && bun run build:gui"
27+
"prepublishOnly": "bun run typecheck && bun run build:gui",
28+
"release": "bun scripts/release.ts",
29+
"release:watch": "bun scripts/release.ts watch"
2830
},
2931
"dependencies": {
3032
"zod": "^4.0.0"

scripts/release.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* Release helper (jawcode-style, single package). Not shipped in the npm tarball.
4+
*
5+
* Usage:
6+
* bun scripts/release.ts <version> [--tag latest|preview] [--publish]
7+
* Preflight (clean tree on main + typecheck) → bump package.json → commit → push →
8+
* dispatch the Release workflow → watch it. Dry-run by default; pass --publish to publish.
9+
* bun scripts/release.ts watch
10+
* Watch the most recent Release run.
11+
*
12+
* Example: bun scripts/release.ts 0.1.0 # dry-run release of 0.1.0
13+
* bun scripts/release.ts 0.1.0 --publish # actually publish 0.1.0
14+
*
15+
* Requires: gh CLI (authed) + an NPM_TOKEN repo secret for the actual publish.
16+
*/
17+
import { $ } from "bun";
18+
19+
const args = process.argv.slice(2);
20+
21+
async function watchLatest(): Promise<void> {
22+
const id = (await $`gh run list --workflow release.yml --limit 1 --json databaseId -q '.[0].databaseId'`.text()).trim();
23+
if (!id) { console.error("No Release runs found yet."); process.exit(1); }
24+
console.log(`→ watching Release run ${id}`);
25+
await $`gh run watch ${id} --exit-status --interval 10`;
26+
}
27+
28+
if (args[0] === "watch") {
29+
await watchLatest();
30+
process.exit(0);
31+
}
32+
33+
const version = args[0];
34+
if (!version || !/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(version)) {
35+
console.error("Usage: bun scripts/release.ts <version> [--tag latest|preview] [--publish]\n bun scripts/release.ts watch");
36+
process.exit(1);
37+
}
38+
const tag = args.includes("--tag") ? (args[args.indexOf("--tag") + 1] ?? "latest") : "latest";
39+
const dryRun = !args.includes("--publish");
40+
41+
// 1. Preflight — must be on a clean main, and typecheck must pass.
42+
const branch = (await $`git rev-parse --abbrev-ref HEAD`.text()).trim();
43+
if (branch !== "main") { console.error(`✗ must be on main (currently ${branch}).`); process.exit(1); }
44+
if ((await $`git status --porcelain`.text()).trim()) { console.error("✗ working tree not clean — commit or stash first."); process.exit(1); }
45+
console.log("→ typecheck");
46+
await $`bun x tsc --noEmit`;
47+
48+
// 2. Bump package.json (no git tag — the workflow verifies against this version).
49+
console.log(`→ bump package.json → ${version}`);
50+
await $`npm version ${version} --no-git-tag-version`;
51+
52+
// 3. Commit + push the version bump.
53+
await $`git add package.json`;
54+
await $`git commit -m ${`release: v${version}`}`;
55+
console.log("→ push origin main");
56+
await $`git push origin main`;
57+
58+
// 4. Dispatch the Release workflow.
59+
console.log(`→ dispatch Release (tag=${tag}, dry-run=${dryRun})`);
60+
await $`gh workflow run release.yml -f version=${version} -f tag=${tag} -f dry-run=${String(dryRun)}`;
61+
await Bun.sleep(4000);
62+
63+
// 5. Watch it.
64+
await watchLatest();
65+
console.log(dryRun
66+
? "\n✓ Dry run complete. Re-run with --publish to publish for real."
67+
: "\n✓ Published. Try: bun install -g opencodex");

0 commit comments

Comments
 (0)