Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 45 additions & 35 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
@@ -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|<explicit-version>]
---

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 `@gresearch/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.
Expand Down Expand Up @@ -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 <prev-tag>..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"
```
Expand All @@ -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 `@gresearch/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.

Expand Down Expand Up @@ -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 `@gresearch/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
Expand All @@ -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
Expand All @@ -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<new-version> "$MERGE_SHA" -m "Bobbit v<new-version>"
git tag -v v<new-version>
git push origin v<new-version>
git push origin v<new-version> # -> release-publish.yml publishes @gresearch/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<new-version>`.

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<new-version> --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):
Expand Down Expand Up @@ -294,9 +305,9 @@ In a scratch directory, prove the published artefact actually installs and resol

```bash
cd $(mktemp -d) && npm init -y >/dev/null
npm install bobbit@<new-version>
npm install @gresearch/bobbit@<new-version>
ls node_modules/@bobbit/binaries-*/bin/
node -e "import('bobbit/dist/server/binaries.js').then(m => console.log(m.getFdPath(), m.getRgPath()))"
node -e "import('@gresearch/bobbit/dist/server/binaries.js').then(m => console.log(m.getFdPath(), m.getRgPath()))"
```

Both paths should print a real file. If they're `undefined`, the platform sub-package didn't install — investigate before announcing the release.
Expand All @@ -319,18 +330,17 @@ the same checkout.

Report to the user:
- Version + tag + GitHub release URL (`gh release view v<new-version> --json url -q .url`)
- npm package URL (`https://www.npmjs.com/package/bobbit/v/<new-version>`)
- Whether provenance was attached
- npm package URL (`https://www.npmjs.com/package/@gresearch/bobbit/v/<new-version>`) — provenance is attached automatically by CI
- Whether binaries were republished, and which versions
- Root and packed-consumer audit results (both must be clean)

## Rules / best practices

- **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.
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/release-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Publish to npm

on:
push:
tags:
- 'v*'
Comment thread
greptile-apps[bot] marked this conversation as resolved.

permissions:
contents: read
id-token: write

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'
registry-url: 'https://registry.npmjs.org'

- name: Install
run: npm ci

- name: Publish (OIDC trusted publishing)
run: |
echo "node: $(node -v) npm: $(npm -v)"
case "${GITHUB_REF_NAME#v}" in
*-*) DIST_TAG=next ;;
*) DIST_TAG=latest ;;
esac
echo "publishing @gresearch/bobbit to dist-tag: $DIST_TAG"
npm publish --provenance --tag "$DIST_TAG" --loglevel verbose
13 changes: 7 additions & 6 deletions docs/releasing.md
Original file line number Diff line number Diff line change
@@ -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 `@gresearch/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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -145,7 +146,7 @@ npm pack ./binaries/binaries-$(node -e 'console.log(process.platform+"-"+process
mkdir /tmp/bobbit-smoke && cd /tmp/bobbit-smoke && npm init -y
npm install /path/to/bobbit-*.tgz
ls node_modules/@bobbit/binaries-*/bin/
node -e "import('bobbit/dist/server/binaries.js').then(m => console.log(m.getFdPath(), m.getRgPath()))"
node -e "import('@gresearch/bobbit/dist/server/binaries.js').then(m => console.log(m.getFdPath(), m.getRgPath()))"
```

### Docker sandbox
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
{
"name": "bobbit",
"name": "@gresearch/bobbit",
Comment thread
greptile-apps[bot] marked this conversation as resolved.
"version": "0.15.0",
"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"
},
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=22.19.0"
},
Expand Down
16 changes: 16 additions & 0 deletions tests2/core/release-skill-preflight-order.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

const skill = readFileSync(resolve(process.cwd(), ".claude/skills/release/SKILL.md"), "utf8");
const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf8")) as {
name?: string;
scripts?: Record<string, string>;
};
const preflight = skill.match(/## 2\. Pre-flight quality gates[\s\S]*?```bash\n([\s\S]*?)\n```/)?.[1];
Expand All @@ -33,6 +34,21 @@
return index;
}

describe("release skill scoped package identity", () => {
it("verifies and reports the package published by CI", () => {
assert.equal(packageJson.name, "@gresearch/bobbit");
assert.match(skill, /npm install @gresearch\/bobbit@<new-version>/);
assert.match(skill, /import\('@gresearch\/bobbit\/dist\/server\/binaries\.js'\)/);
assert.match(
skill,
/https:\/\/www\.npmjs\.com\/package\/@gresearch\/bobbit\/v\/<new-version>/,
);
assert.doesNotMatch(skill, /npm install bobbit@/);
assert.doesNotMatch(skill, /import\('bobbit\//);
assert.doesNotMatch(skill, /npmjs\.com\/package\/bobbit\//);

Check failure

Code scanning / CodeQL

Missing regular expression anchor High test

When this is used as a regular expression on a URL, it may match anywhere, and arbitrary hosts may come before or after it.
});
});

describe("release skill pre-flight order", () => {
it("audits the built tarball consumer before type-checking and tests", () => {
assert.equal(
Expand Down
2 changes: 1 addition & 1 deletion tests2/tests-map.json
Original file line number Diff line number Diff line change
Expand Up @@ -940,7 +940,7 @@
},
{
"path": "tests2/core/release-skill-preflight-order.test.ts",
"reason": "Pins the clean-release pre-flight order so the build emits dist/server declarations before tsconfig.tests2 follows intentional runtime imports of those artifacts.",
"reason": "Pins the scoped npm identity used by release verification/reporting and the clean-release pre-flight order required by emitted test declarations.",
"execution": {
"runner": "vitest",
"tier": "unit",
Expand Down