From 9c09edec8860ba5f4a4cdfb61998ca4ef7534bc5 Mon Sep 17 00:00:00 2001 From: dev-milos <271274416+dev-milos@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:40:36 +0200 Subject: [PATCH 1/3] Publish bobbit to npm on tag push via trusted publishing (OIDC) Adds .github/workflows/release-publish.yml: on a v* tag push it builds and publishes the root bobbit package to npm using trusted publishing (OIDC) with automatic provenance -- no NPM_TOKEN, no OTP. Mirrors the validated gr-oss-developers/trusted-publishing-demo workflow (top-level id-token: write, Node 24 for npm >= 11.5.1, npm publish --provenance), plus the two things bobbit needs that the demo doesn't: npm ci (bobbit has deps; the build runs via the existing prepublishOnly hook) and a tag-vs-package.json-version guard. package.json gains repository/homepage/bugs. repository is required for provenance -- its URL must resolve to the building repo -- and was absent, which is why the runbook's manual `npm publish --provenance` never actually produced an attestation. Runbook (SKILL.md + docs/releasing.md) now delegates the root publish to CI: the human bumps the version, writes notes, signs the tag, and pushes it; the tag push triggers the publish. npm login/OTP is scoped to the binary sub-package republish case (still manual). Also fixes a stale SuuBro/bobbit reference to G-Research/bobbit. The root publish is not exercisable from a fork -- the npm trusted-publisher is bound to the G-Research/bobbit repo identity. Validated here: actionlint clean, npm publish --dry-run packs on Node 24, guard logic, and the JSON/YAML parse. Manual prerequisites (npm admin, not code): configure the trusted publisher on npmjs.com for bobbit -> G-Research/bobbit -> release-publish.yml. bobbit already exists on npm (0.14.2), so no manual first publish is needed. --- .claude/skills/release/SKILL.md | 76 +++++++++++++++------------ .github/workflows/release-publish.yml | 39 ++++++++++++++ docs/releasing.md | 11 ++-- package.json | 8 +++ 4 files changed, 96 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/release-publish.yml diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index 7e41c4edb..db505e7dc 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -1,13 +1,15 @@ --- name: release -description: Cut a Bobbit release — preflight checks, version bump, signed tag, npm publish (with optional binary sub-packages), GitHub release with generated notes. +description: Cut a Bobbit release — preflight checks, version bump, signed tag (CI publishes the root to npm via OIDC on tag push), optional binary sub-packages, GitHub release with generated notes. argument-hint: [major|minor|patch|] --- Drive an end-to-end release of Bobbit. The maintainer (human) must be at the -keyboard for `npm login` / OTP prompts and to sign the tag — pause and ask -when the flow needs them. **Never** run `npm publish` non-interactively or -behind their back. +keyboard to sign the tag, and for `npm login` / OTP prompts **only when +republishing binary sub-packages** — pause and ask when the flow needs them. +The root `bobbit` package publishes automatically via CI (npm trusted +publishing / OIDC) when the signed tag is pushed; **never** run a manual root +`npm publish`. Single source of truth for release mechanics: [`docs/releasing.md`](../../../docs/releasing.md). This skill orchestrates that doc + version bump + notes + GitHub release. @@ -37,8 +39,8 @@ git rev-parse origin/master # sha we'll release from git tag --sort=-v:refname | head -5 # find previous tag git log --oneline ..origin/master | head # must be non-empty (something to release) node -v # must satisfy engines.node (>=22.19.0) -npm whoami # must succeed; if not -> ask user to `npm login` -gh auth status # must be authed for SuuBro/bobbit +npm whoami # only needed if republishing binary sub-packages (§3); root ships via CI +gh auth status # must be authed for G-Research/bobbit git config --get user.signingkey || echo "NO_SIGNING_KEY" git config --get commit.gpgsign || echo "commit.gpgsign=unset" ``` @@ -50,7 +52,7 @@ so the session/primary worktree state is irrelevant. What matters is that **Stop and ask the user** if any of: - `origin/master` has nothing new since the previous tag (nothing to release), or it isn't the commit they expect to ship. -- `npm whoami` fails — ask them to run `npm login` (and enable 2FA if not already; npm requires OTP for publishes on this scope). +- `npm whoami` fails **and** step 3 will republish binary sub-packages — ask them to run `npm login` (and enable 2FA if not already; npm requires OTP for those sub-package publishes). The root `bobbit` publish needs no npm login — it goes through CI/OIDC. - `gh auth status` not logged in — ask them to run `gh auth login`. - No GPG/SSH signing key configured — confirm whether to proceed with **unsigned** tag or wait until they set one up. Default to waiting. @@ -202,15 +204,19 @@ the last irreversible step before the source and tag become public. Do not create the tag before the squash merge because the pre-merge commit will not be the commit that lands on `master`. -## 7. Publish to npm +## 7. Publish binary sub-packages (only if step 3 bumped binaries) -Only enter this step after the release PR is fully green and mergeable. -**Pause and confirm with the user** before publishing — npm publishes are -irreversible (you can `unpublish` for 72h but the version number is burned -either way). Use `ask_user_choices` with the exact `npm publish` commands -about to run. +**The root `bobbit` package is NOT published here.** It publishes automatically +via the `.github/workflows/release-publish.yml` workflow (npm trusted publishing +/ OIDC, with provenance) when the signed tag is pushed in §8 — there is no manual +root `npm publish`. Skip straight to §8 unless step 3 bumped the binary +sub-packages. -If step 3 bumped binaries: +If step 3 bumped binaries, publish the sub-packages now — **before** the tag push +in §8, so they are on npm before the root that pins them. **Pause and confirm with +the user** first; npm publishes are irreversible (you can `unpublish` for 72h but +the version number is burned either way). Use `ask_user_choices` with the exact +commands: ```bash npm publish ./binaries/binaries-darwin-arm64 @@ -220,21 +226,14 @@ npm publish ./binaries/binaries-linux-arm64 npm publish ./binaries/binaries-win32-x64 ``` -Then the root: - -```bash -npm publish --provenance -``` - Notes: -- `--provenance` attaches a signed npm provenance attestation (sigstore) so users can verify the package came from this repo. Requires the maintainer to be running npm ≥9.5 from a machine where the npm CLI can reach Sigstore. If the env doesn't support it (rare; older corp networks), drop the flag and tell the user the publish is unattested. - `publishConfig.access: "public"` is baked into each sub-package, so `--access public` is not needed. - npm will prompt for OTP — that's the maintainer's job; just wait. - If publish fails after some sub-packages went through, **do not** try to bump+republish under a new version. Re-run `npm publish` on the remaining packages with the same version once the issue is fixed. -## 8. Squash-merge the release PR, then tag it +## 8. Squash-merge the release PR, then tag it (this triggers the npm publish) -Immediately after npm succeeds, squash-merge the already-green release PR. +Once the release PR is fully green and mergeable, squash-merge it. Pin the subject and co-author trailer explicitly: ```bash @@ -252,20 +251,32 @@ git diff --exit-code HEAD "$MERGE_SHA" -- \ ``` Tag the PR's exact squash commit, not the current `origin/master` tip (another -PR may have merged immediately afterward): +PR may have merged immediately afterward). **Pushing the tag triggers the root +npm publish** (`.github/workflows/release-publish.yml`) and is irreversible — +pause and confirm with the user before the `git push`: ```bash git tag -s v "$MERGE_SHA" -m "Bobbit v" git tag -v v -git push origin v +git push origin v # -> release-publish.yml publishes bobbit to npm (OIDC + provenance) ``` If the user explicitly opted out of signing in step 0, use `git tag -a` instead and inspect it with `git show --no-patch v`. -If merging fails after npm publish, stop and repair the same release PR. Do -not change the version, create a second release commit, force-push `master`, -or tag the detached pre-merge commit. +Then watch the publish workflow and confirm it succeeds before moving on: + +```bash +gh run watch "$(gh run list --workflow release-publish.yml --branch v --limit 1 --json databaseId -q '.[0].databaseId')" --exit-status +``` + +If the publish job fails (e.g. transient registry error), **re-run the same +workflow run** for the same version — do not bump the version or re-tag; the +tag is already public and the version number is immutable. + +If merging fails, stop and repair the same release PR. Do not change the +version, create a second release commit, force-push `master`, or tag the +detached pre-merge commit. **Refresh the running dev server** so it picks up the release commit (its local `master` is now behind remote): @@ -319,8 +330,7 @@ the same checkout. Report to the user: - Version + tag + GitHub release URL (`gh release view v --json url -q .url`) -- npm package URL (`https://www.npmjs.com/package/bobbit/v/`) -- Whether provenance was attached +- npm package URL (`https://www.npmjs.com/package/bobbit/v/`) — provenance is attached automatically by CI - Whether binaries were republished, and which versions - Root and packed-consumer audit results (both must be clean) @@ -328,9 +338,9 @@ Report to the user: - **Signed tag, always.** Use `git tag -s`. Only fall back to `-a` if the maintainer explicitly opted out in step 0. - **Signed commit if a signing key is configured.** Add `-S` to `git commit`. Never override `user.name` / `user.email`; never silently disable signing. -- **`--provenance` on the root publish** whenever the environment supports it. -- **OTP is the human's job.** Pause and let them type it; don't try to read it from anywhere. -- **Never `npm publish --force`.** If a republish is genuinely needed, bump the patch version and republish cleanly. +- **The root publish is CI-only.** It runs via `release-publish.yml` (OIDC trusted publishing) on the tag push, with provenance automatic. Never run a manual root `npm publish`. +- **OTP is the human's job** — but only for the binary sub-package publishes. Pause and let them type it; don't try to read it from anywhere. The root publish uses no OTP. +- **Never `npm publish --force`.** If a sub-package republish is genuinely needed, bump the patch version and republish cleanly. If the CI root publish fails, re-run the same workflow run for the same version. - **Never delete a tag that's been pushed.** If you tagged wrong, bump the version and tag again — published version numbers are immutable. - **Use the required squash-merge PR flow.** Never tag the detached release commit; tag the PR's exact `mergeCommit` SHA after verifying its package version and release files. - **One release at a time.** Don't start a second version bump while the previous tag/publish is in flight. diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml new file mode 100644 index 000000000..eea23df8a --- /dev/null +++ b/.github/workflows/release-publish.yml @@ -0,0 +1,39 @@ +name: Publish to npm + +on: + push: + tags: + - 'v*' + +permissions: + contents: read + id-token: write # required for npm trusted publishing (OIDC) + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + persist-credentials: false + + - name: Guard tag matches package.json version + run: | + TAG="${GITHUB_REF_NAME#v}" + PKG="$(node -p "require('./package.json').version")" + [ "$TAG" = "$PKG" ] || { echo "tag v$TAG != package.json $PKG" >&2; exit 1; } + + - name: Set up Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' # bundles npm >= 11.5.1, required for OIDC + registry-url: 'https://registry.npmjs.org' + + - name: Install + run: npm ci + + - name: Publish (OIDC trusted publishing) + run: | + echo "node: $(node -v) npm: $(npm -v)" + npm publish --provenance --loglevel verbose diff --git a/docs/releasing.md b/docs/releasing.md index 1f2ad0abc..9f914bc81 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,6 +1,6 @@ # Releasing Bobbit -This doc covers the Bobbit release checks that cannot be inferred from a normal development checkout: the installed consumer's security report and the bundled `fd`/`rg` binaries. +This doc covers the Bobbit release checks that cannot be inferred from a normal development checkout: the installed consumer's security report and the bundled `fd`/`rg` binaries. The root `bobbit` package itself is published automatically by [`.github/workflows/release-publish.yml`](../.github/workflows/release-publish.yml) (npm trusted publishing / OIDC, with provenance) when a `v*` tag is pushed; only the binary sub-packages below are published manually. ## Required packed-consumer audit @@ -82,17 +82,18 @@ changes. (the build script no longer auto-bumps these). Update the matching pin in the root `package.json` `optionalDependencies` block to the new version. -7. Publish each sub-package, then the root: +7. Publish each sub-package (the root is not published here — it ships via + CI on the tag push): ```bash npm publish ./binaries/binaries-darwin-arm64 npm publish ./binaries/binaries-darwin-x64 npm publish ./binaries/binaries-linux-x64 npm publish ./binaries/binaries-linux-arm64 npm publish ./binaries/binaries-win32-x64 - npm publish ``` - `publishConfig.access: "public"` is baked into each sub-package, so - `--access public` is no longer needed on the CLI. + Do this **before** pushing the release tag, so the sub-packages are on npm + before the root that pins them. `publishConfig.access: "public"` is baked + into each sub-package, so `--access public` is not needed on the CLI. ### Decoupled versioning diff --git a/package.json b/package.json index 2cdcd8eac..06e62f18e 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,14 @@ "name": "bobbit", "version": "0.14.2", "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/G-Research/bobbit.git" + }, + "homepage": "https://github.com/G-Research/bobbit#readme", + "bugs": { + "url": "https://github.com/G-Research/bobbit/issues" + }, "engines": { "node": ">=22.19.0" }, From 1860429794416f3c489a1c41ee141e49ad4aca3e Mon Sep 17 00:00:00 2001 From: dev-milos <271274416+dev-milos@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:58:43 +0200 Subject: [PATCH 2/3] Route pre-release tags to the next dist-tag A v* tag with a semver pre-release suffix (v1.0.0-rc.1, v0.15.0-beta) now publishes to the `next` dist-tag instead of `latest`, so a pre-release cannot become the version `npm install bobbit` resolves. Stable tags still go to `latest`. --- .github/workflows/release-publish.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index eea23df8a..277a6fbaf 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -7,7 +7,7 @@ on: permissions: contents: read - id-token: write # required for npm trusted publishing (OIDC) + id-token: write jobs: publish: @@ -27,7 +27,7 @@ jobs: - name: Set up Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: '24' # bundles npm >= 11.5.1, required for OIDC + node-version: '24' registry-url: 'https://registry.npmjs.org' - name: Install @@ -36,4 +36,9 @@ jobs: - name: Publish (OIDC trusted publishing) run: | echo "node: $(node -v) npm: $(npm -v)" - npm publish --provenance --loglevel verbose + case "${GITHUB_REF_NAME#v}" in + *-*) DIST_TAG=next ;; + *) DIST_TAG=latest ;; + esac + echo "publishing bobbit to dist-tag: $DIST_TAG" + npm publish --provenance --tag "$DIST_TAG" --loglevel verbose From 15bf8535a9b81d1b4fe6385d64715033cdade7b9 Mon Sep 17 00:00:00 2001 From: dev-milos <271274416+dev-milos@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:31:49 +0200 Subject: [PATCH 3/3] Scope the package as @gresearch/bobbit Publish under the @gresearch npm org: sets name to @gresearch/bobbit and adds publishConfig.access public (scoped packages default to restricted). Updates the package-lock.json root name to match. The bin command stays `bobbit`. This is a new scoped package on npm, so its first publish is a manual bootstrap before trusted publishing takes over. --- package-lock.json | 4 ++-- package.json | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c6e016381..72da1148d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "bobbit", + "name": "@gresearch/bobbit", "version": "0.14.2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "bobbit", + "name": "@gresearch/bobbit", "version": "0.14.2", "dependencies": { "@earendil-works/pi-agent-core": "0.81.1", diff --git a/package.json b/package.json index 06e62f18e..195f9b4a8 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "bobbit", + "name": "@gresearch/bobbit", "version": "0.14.2", "type": "module", "repository": { @@ -10,6 +10,9 @@ "bugs": { "url": "https://github.com/G-Research/bobbit/issues" }, + "publishConfig": { + "access": "public" + }, "engines": { "node": ">=22.19.0" },