release
Use when the user asks to release, bump, or ship a version. That ask keeps this skill; a child brief cannot ban the release lane. Then cut the version without omissions: bump, CHANGELOG, PR, merge, ta
SKILL.mdskills/release/SKILL.md
GitHubRelease (DeepSeek Build harness)
This skill is the agent checklist for cutting a release. It is not CI — it is the completeness gate so nothing (bump, tag, assets, npm, CHANGELOG, version log) is silently skipped.
Load these docs (in order)
docs/contributing/release-cycle.md— normative runbook (this skill mirrors it)docs/contributing/versioning.md— SemVer fail-close rulesdocs/contributing/pr-body-standard.md— PR narrative bar for the release PRdocs/adr/0009-npm-prebuilt-binaries.md— npm wrapper + prebuilt tarball modeldocs/adr/0012-npm-trusted-publishing.md— OIDC publish path + emergency pathdocs/adr/0008-grok-build-base.md— vendored Grok TUI (SOURCE_REV pin)
Hard rules
- SemVer only: full
MAJOR.MINOR.PATCHeverywhere (tagv4.0.4, never4.0). Never claim a release "at 1.0" unless1.0.0shipped + verified. 1b. Pick the number from the merges (fail-close). Run./scripts/next-version.shfirst: afeat/merge on the distribution surface since the last tag judges MINOR, other surface changes PATCH, and a harness-only range owes no release.release.shrefuses a version below that judgment; shipping one anyway needs--level-override "<reason>", and the reason lands in the release PR body. The rule and the surface table:docs/contributing/versioning.md§1c. - CHANGELOG invariant (fail-close):
# Changelog→## Unreleasedat the very top → version sections newest-first. After any bump, run./scripts/reorder-changelog.sh --check— non-zero exit means fix first. 2b. The bump moves theUnreleaseditems into the new version section — verbatim, never a summary. Confirm the move before merging the release PR:./scripts/bump-version.sh <ver> --dry-runprints how many items would move.--desconly fills the new section whenUnreleasedis empty; when items exist they are the section and--descdoes not seed it. Measured failures:v5.7.0shipped 7 items still reading as unreleased while## 5.7.0held one--descline, andv6.0.0repeated it with 9 items (including the same seven) — so neither release record named what it did. After the release PR merges, re-read the section: it must name the work, not repeat--desc. For a past release with this defect, moving the items into its section is a normal PR againstmain; the tag is immutable. Covered by./scripts/test-changelog-release.sh(also in CI). 2d. Record the release PR number in the decision-log row. The bump writesPR #_(fill in)_and nothing used to come back to it (six rows shipped that way).release.shfills it fromgh pr createand commits it into the release PR; if the row still reads the placeholder after a release, runpython3 scripts/lib/version_log.py set-pr docs/product/versions/README.md <ver> <pr>by hand — a placeholder in the row the MAJOR gate reads is an unfinished release record. 2c. Never let## Unreleasedtouch the next heading. A glued junction makes the merge path file items under a version with no conflict, which is how#209reached## 5.7.0and#206reached## 6.0.0while neither had shipped there (shape.rs/ the paste fix are absent from both tags).bump-version.shwrites the blank line and refuses a glued file;reorder-changelog.sh --checkreports it. If a shipped section gains an item whose code is not in that tag, checkgit merge-treebefore assuming someone filed it on purpose. - MAJOR gate (fail-close): a MAJOR bump is blocked unless the product
version log (
docs/product/versions/README.md) already logs the new major (**5.0.0** …row). Updatedocs/product/before running the release. - Build from the tag tree, never a diverged worktree HEAD. The tag may
point at a commit the release worktree is not on.
git checkout v<ver>first, then build. - No silent asset skip.
release-prebuilt.ymltag runs routinely stay "queued" forever. Ifgh release view v<ver> --json assetsshows no tarball for a released platform (darwin-arm64,linux-x64), attach manually (fallback below) — do not publish an npm version whose binary is missing or stale.publish-npm.ymlenforces this too: it waits for every supported platform's asset and fails the run without them. - Verify after publish with a real global install +
dsb --version+ astringscheck for the release's markers (e.g.deepseek.comstatus handling) on the installed binary. - Publishing is CI's job (ADR 0012). The tag push triggers
publish-npm.yml, which publishes over OIDC trusted publishing — no npm token, no one-time code. Do not publish locally unless that path is broken; the emergency path is below. - An empty
## Unreleasedis not evidence that a version ask is finished.--descmay seed a section when Unreleased has no items. That seed is the patch you are cutting. If the user named work and that work is not in the section, the release clause did not stand: say so in the session-unit report and stop. Do not cut the next number to have a tag, and do not report the ask done. On 2026-09-26 a session cut6.0.1, emptied Unreleased, and stopped on6.1.0because the section would have been empty, while the depth train was still unreleased. - A child brief cannot drop this skill when the user asked to ship a
version.
skills/session-unitowns that check (scripts/check-session-close.sh brief). The release steps stay here. - Do not run
release.shin the primary checkout. That checkout stays onmain. On 2026-09-26 running it there executedgit checkout -b chore/release-6.0.2and left the main worktree on that branch. The script now exits 1 in that tree (scripts/lib/refuse-primary-checkout.sh). Create a linked worktree first (skills/worktree-dispatch), then run the script there. A tag checkout for the asset fallback is also that linked worktree, not the primary checkout.
Standard cycle
# 1. Linked worktree off origin/main. Not the primary checkout.
# orca worktree create … or:
# git worktree add -b chore/release-4.1.0 <path> origin/main
# 2. Pick the number from what shipped since the last tag (fail-close in
# release.sh; docs/contributing/versioning.md §1c). Prints the per-merge
# lines and, e.g., "proposed: 4.1.0 (MINOR)":
(cd <path> && ./scripts/next-version.sh)
# 3. Bump + release orchestrator, with cwd = that worktree
(cd <path> && ./scripts/release.sh 4.1.0 --desc "one-line release note")
# 4. Human verification — on npm 12 the flag is required: a blocked
# postinstall still exits 0 and leaves the previous agent binary
npm install -g --allow-scripts=@innocarpe/deepseek-build @innocarpe/deepseek-build@4.1.0
dsb --version
release.sh stages: bump level → bump → MAJOR/README gate → verify → PR (chore(release))
→ wait for the PR's checks → merge → tag → asset wait → CI publishes over
OIDC (publish-npm.yml) → registry verified. No npm token and no one-time
code are involved. The wait is the lesson of the 6.2.0 / 6.7.1 / 6.8.0
(published as 6.1.1 / 6.1.7 / 6.1.10) stops:
the merge fired before GitHub finished computing mergeability ("Pull Request is
not mergeable") and left the PR for a person to merge and resume. A failed
check now stops the release before the merge call, with the PR and the resume
command printed (--checks-timeout, default 3600 s).
npm Trusted Publisher enrollment (one-time, npm website)
ADR 0012's automatic path only works once the package has a trusted publisher
configured on npmjs.com. It is enrolled (2026-09-25) for
innocarpe/deepseek-build + publish-npm.yml, so this section is here to
re-verify or redo it, not as a pending task.
Enrolling or editing it requires interactive 2FA on the npm account (npm
requires 2FA to modify package settings). Note that npm trust list <package>
is not a cheap read-only check any more — with 2FA enabled it answers EOTP
and waits for a browser approval. Read the package settings page instead.
Enrollment (browser, as the npm account):
- Enable 2FA on the npm account (npm's setup page offers a security key; an authenticator app is the alternative).
- Package → Settings → Trusted Publisher → GitHub Actions:
- Organization or user:
innocarpe - Repository:
deepseek-build - Workflow filename:
publish-npm.yml(filename only — no path; case-sensitive) - Environment: empty
- Allowed actions:
npm stage publishandnpm publish
- Organization or user:
- Read the saved connection back off the page to confirm.
The aside CLI drove this (signing in, approving the security key, reading
emailed codes from Gmail). It cannot perform the 2FA enrollment itself — that
needs the account holder's key or authenticator secret once.
Account 2FA state (check before choosing a path)
npm profile get | grep two-factor # currently: auth-and-writes
auth-and-writes means a local npm publish asks for proof of presence.
The OIDC path is unaffected, and npm stage publish is unaffected too — only
npm stage approve and a direct publish are interactive. The registered second
factor here is a security key, so the proof is a browser approval, not a
typed code.
Emergency path (CI cannot publish)
./scripts/npm-emergency-publish.sh <ver> # publishes locally
./scripts/release.sh <ver> --publish-only --local-publish # same, in the orchestrator
It refuses to publish without the release asset, ensures an npm session (driving
npm login --auth-type=web through aside exec if needed), then runs the
publish under a pty with --browser=false, captures the 2FA approval URL
npm prints, and hands that URL to aside exec to approve with the registered
security key. It does not ask a person for a number, and no code or
single-use URL reaches a log, commit or PR.
Both pty and --browser=false are load-bearing: piped, npm refuses with EOTP
without offering the URL; with a browser configured it blocks on Press ENTER.
It carries no provenance attestation (there is no local OIDC provider), so
prefer fixing CI over using it. If the account ever gains an authenticator app,
NPM_OTP=<code> still works as an override.
Manual asset fallback (reliable path when CI is stuck)
WT=/path/to/deepseek-build-release-4.0.4 # tag worktree
git -C "$WT" fetch origin && git -C "$WT" checkout v4.0.4
cd "$WT" && ./scripts/build-grok-pager.sh release # cold build: 30-60+ min
# stage the fresh agent binary
cp "$WT/third_party/grok-build/target/release/xai-grok-pager-bin" \
~/.deepseek-build/bin/deepseek-build-agent
# attach tarball to the GitHub release (creates v4.0.4 if missing)
cd "$WT" && ./scripts/package-release-binaries.sh --upload
# the linux-x64 tarball for the same tag builds on x86_64 Linux (Ubuntu 22.04
# base): the same script with --platform linux-x64 there. Attach both.
gh release view v4.0.4 --json assets # confirm both tarballs
# then let CI publish (it re-runs on demand):
gh workflow run publish-npm.yml --ref v4.0.4
# or, only if CI itself is unavailable:
./scripts/npm-emergency-publish.sh 4.0.4
Post-publish verification checklist
-
npm install -g --allow-scripts=@innocarpe/deepseek-build @innocarpe/deepseek-build@<ver>succeeds (npm 12: the flag is required; a blockedpostinstallstill exits 0) -
dsb --versionprints<ver> -
strings $(command -v dsb)(or the agent binary) shows the release's behavior markers — e.g. for the image-fix release, the DeepSeek endpoint / status markers — not the pre-fix build -
gh release view v<ver> --json tagName,assetsshows every released platform's tarball (darwin-arm64,linux-x64) - CHANGELOG still newest-first:
./scripts/reorder-changelog.sh --check - The
<ver>section names what shipped — items moved out ofUnreleased, not a lone--descline (git show v<ver>:CHANGELOG.md | head -30) -
README*.mdcarry no version literals (version-free by policy; the release/npm badges are live) -
npm view @innocarpe/deepseek-build@<ver> dist.attestationsshows a provenance attestation (CI path; the emergency path has none)
Anti-patterns
| Bad | Why |
|---|---|
Bumping without moving the Unreleased items |
The section ships reading as "unreleased" while the version says shipped — the v5.7.0 defect (7 items) and again at v6.0.0 (9 items, compounding) |
Letting ## Unreleased touch the next heading |
A merging branch files its item under a version with no conflict — measured on #209 (→ 5.7.0) and #206 (→ 6.0.0) |
| Rewriting items while moving them | The release record is the reviewed text of each PR, not a post-hoc summary |
| Publishing from a worktree whose HEAD ≠ tag | Ships unreleased/unmerged code as the binary |
| Skipping asset check because CI "should" attach | CI queue routinely never runs; 404s for users |
4.0 / v4 in any public text |
SemVer fail-close (Agents.md) |
| Picking the number by habit (patch + 1) | All ten pre-rule releases (6.2.0–6.8.0, published as 6.1.1–6.1.10) shipped MINOR-level work as PATCH; the judgment (next-version.sh) and the gate exist for this (versioning.md §1c) |
| Bumping to a new MAJOR with no version-log row | Tag ships ahead of the documented story |
Claiming done after npm publish |
Unverified global install is not a release |
| Local publish when CI could publish | Loses provenance and leaves the irreversible step off the audit trail |
npm stage publish then hand-approving every release |
Works (it is how 5.6.0 shipped), but approve is interactive by design — it cannot run unattended |
| "Fixing" an OIDC 403 by adding a bypass token | The bypass is being retired; fix the publisher or the workflow instead |
release.sh in the primary checkout |
Checks out chore/release-<ver> there and leaves the main worktree off main |
Done means
- Tag
v<ver>exists on origin and has every released platform's tarball attached (darwin-arm64,linux-x64) -
@innocarpe/deepseek-build@<ver>is live and a clean global install works - CHANGELOG newest-first +
docs/product/versions/README.mdrow consistent with<ver> - No omission: bump, PR, merge, tag, assets, publish, verify all happened