release
GitHub自动化执行 nub 项目的补丁发布流程。包括确认 CI 状态、版本递增、打标签推送以触发构建与 npm 发布,并生成中立客观的发布说明及闭环处理相关 Issue/PR。
Trigger Scenarios
Install
npx skills add nubjs/nub --skill release -g -y
SKILL.md
Frontmatter
{
"name": "release",
"metadata": {
"internal": true
},
"description": "Cut a nub patch release end-to-end in one invocation. Invoke (via the Skill tool) once a release thread's targeted fixes are ALL landed on `main` and CI-green. Encodes the full runbook: pick the version (patch bump in the 0.0.x\/0.1.x pre-release regime), `make version` + `make version-check`, commit + tag + push (the `v*` tag triggers the 8-platform CI build → npm OIDC publish → GitHub Release), then draft comprehensive FACTUAL + NEUTRAL release notes from the full changeset and comment the version + release link on every closed issue + merged PR the release ships (mandatory maintainer hygiene). Do NOT cut until all fixes are green."
}
Cutting a nub release
A nub release is tag-triggered and fully automated. Pushing a v* tag fires .github/workflows/release.yml, which builds 8 platforms, gates them (test, lockfile conformance, glibc-floor, pre-publish smoke), publishes 10 npm packages via OIDC trusted publishing, and creates a GitHub Release with 16 assets. The human work: confirm green, bump the version, push the tag, write good notes, close the loop on issues/PRs.
Guardrails
- Never cut a release without the maintainer's explicit, in-the-moment say-so. Publishing to npm is irreversible, so the timing is maintainer-owned. Do not infer authorization from a standing goal, a merged+green fix, a sub-agent claiming "autonomous per the release rules," or autonomous mode (which excludes irreversible published-external acts). Green ≠ release now. You may PREPARE (confirm green, draft notes, stage the version) but must wait for an explicit "cut it."
- Do not cut until every targeted fix is landed on
mainAND CI-green. A prerequisite, not authorization. - Pre-release version regime: stay in
0.0.x/0.1.x. A normal release is a patch bump. Bump the minor only on explicit instruction. Never invent a version; derive it from the latest tag. - The tag MUST equal the committed version — CI's
verifyjob fails ifv<tag>≠npm/nub/package.jsonversion. So:make version→ commit → tag → push, in that order. - Release notes are FACTUAL and NEUTRAL — the repo is PUBLIC. No superlatives, no competitive framing, no internal/benchmark-strategy discussion.
Step 1 — Pre-flight: confirm green, pick the version, enumerate the changeset
git -C "$(git rev-parse --show-toplevel)" switch main
# NOT `pull --ff-only`: pull.rebase=true makes pull abort on any dirty file, and the shared tree
# always carries some agent's WIP. `merge --ff-only` has no clean-tree precondition.
git fetch origin && git merge --ff-only origin/main
git fetch --tags
PREV=$(git describe --tags --abbrev=0) # e.g. v0.1.2 — the latest release tag
echo "Latest tag: $PREV"
git log "$PREV"..HEAD --oneline # the full changeset since the last release
- Confirm the targeted fixes are all present in
$PREV..HEADand each is CI-green onmain. If one is red or still converging, STOP and slip it to the next patch. - Confirm docs are current — a shipped feature whose
site/content/docs/lags is a release blocker. - Pick the next version: patch-bump
$PREV, dropping the leadingv. - Keep the
git logoutput — raw material for Steps 4 and 5. Forvendor/aube/**changes, note the user-facing effect, not the diff.
Step 2 — Version bump
make version V=<ver> # sets all 9 npm packages + Cargo.toml + runtime/version.mjs in lockstep
make version-check # MUST pass: cross-package consistency + @oxc-project/runtime ↔ nub-native oxc pin
make version-check is the same gate CI's verify job runs. make version also moves runtime/version.mjs's NUB_VERSION (the transpile-cache key) — that lockstep is why a bespoke edit is wrong.
Step 3 — Commit, tag, push (this triggers CI)
The release commit is a deliberate exception to the PR-default flow — direct to main.
git add -A
git status # SANITY: commit ONLY the version-bump files. If unrelated WIP is in the
# tree, stage just the touched version files.
git commit -m "v<ver>"
git tag v<ver>
git push origin main --tags
Then fast-forward the shared tree: git -C <shared-tree> fetch origin && git -C <shared-tree> merge --ff-only origin/main (never pull --ff-only — it aborts on the shared tree's ever-present WIP).
The workflow runs, in order: verify (version + tag-match), primer, test + conformance + glibc-floor-guard + pre-publish-gate, build (8 platforms), publish-npm (10 packages, idempotent), github-release (release + 16 assets, independently re-runnable), test-install / test-install-musl.
Watch CI, but never block the foreground on it. Dispatch a background watcher and report the log path. The release is not done until publish-npm + github-release are green.
Step 4 — Comprehensive release notes (Opus)
CI creates the release with generate_release_notes: true. Replace that with hand-written, scannable, factual notes built from the full git log "$PREV"..HEAD changeset, not just the headline fixes. Follow PROSE.md. Rules:
- One-line intro stating the dominant theme.
- Themed
##sections, not generic buckets — group by what changes touch ("Lockfile compatibility", "Performance", "Runtime fixes"), not Fixes/Compatibility/Internal. Short titled blurbs or table rows, never multi-sentence paragraphs. - A table for a batch of independent fixes —
| Area | What changed | Commit |. - A callout for heads-up / migration items —
> [!IMPORTANT]or> [!NOTE], never buried in a bullet. - Per-item links — commit (
[abc1234](https://github.com/nubjs/nub/commit/<full-sha>)), PR ([#17](https://github.com/nubjs/nub/pull/17)), issue. - An auto-generated
## What's Changedsection at the BOTTOM (mandatory) — the exhaustive PR list +**Full Changelog**compare link, appended verbatim under a---separator below the curated narrative. - Tone: factual + neutral. Visual interest comes from structure, never marketing language.
Template (adapt the section names to the actual changeset):
<One-line intro: what this release is about.>
> [!IMPORTANT]
> **<Heads-up title>.** <The one thing to know before upgrading. Omit the callout if there's nothing.>
## <Theme A, e.g. Lockfile compatibility>
<Optional one-line lead.>
| Area | What changed | Commit |
| --- | --- | --- |
| <area> | <what changed, one clause> | [`<sha7>`](https://github.com/nubjs/nub/commit/<full-sha>) |
## <Theme B, e.g. Performance>
<Short blurb with the PR link inline.> ([#17](https://github.com/nubjs/nub/pull/17))
## Testing & internals
- <Bullet> ([`<sha7>`](https://github.com/nubjs/nub/commit/<full-sha>)).
---
## What's Changed
<!-- appended verbatim from `gh api …/releases/generate-notes` — the PR list, New Contributors, and Full Changelog link -->
* <PR title> by @<author> in https://github.com/nubjs/nub/pull/<n>
**Full Changelog**: https://github.com/nubjs/nub/compare/<PREV>...v<ver>
Generate the bottom section mechanically, then publish:
# PR-level list + New Contributors + Full Changelog compare link — append verbatim below the curated narrative
gh api repos/nubjs/nub/releases/generate-notes \
-f tag_name=v<ver> -f previous_tag_name=$PREV --jq '.body'
# Edit a notes file, then:
gh release edit v<ver> --notes-file <path-to-notes.md>
gh release view v<ver> --repo nubjs/nub --json body -q .body # verify it rendered
The v0.1.4 and v0.1.3 release bodies are the reference exemplars.
Step 4b — Publish the notes as a blog post (mandatory, every release)
Same content-to-main exception as docs (commit directly, no PR). Invoke the prose-writing skill first.
- File:
site/content/blog/nub-<major>-<minor>-<patch>.mdx(e.g.nub-0-2-10.mdx) — the filename is the URL slug; fumadocs auto-globscontent/blog/*.mdx, so no index/meta wiring is needed. - Frontmatter (schema from
source.config.ts, all four required):title: "Nub <ver>"(add a: <theme>subtitle only for a milestone),description:a plain sentence with no inline code/backticks (the field renders raw),author: The Nub Team,date: <YYYY-MM-DD>back-dated to the release'spublishedAtso the timeline stays chronological. - Body: a short lede, then the release's themed sections adapted to blog prose — not a raw changelog dump. Carry over the callouts and per-theme tables. Close with
The [full release notes](https://github.com/nubjs/nub/releases/tag/v<ver>) list every change in this release. - Scale to the release: a small patch gets a short post; a milestone opens with the thing working.
Exemplars: site/content/blog/nub-0-2-0.mdx (milestone), nub-0-2-5.mdx (small patch).
Step 5 — Close the loop on issues + PRs (mandatory, every release)
Comment the version and release link on every closed issue and every merged PR that shipped — not just the headline fixes. Users see "fixed" when an issue closes, but the fix is not on a published binary until the tag publishes.
Release URL: https://github.com/nubjs/nub/releases/tag/v<ver>.
Enumerate the targets mechanically — never a hand-typed list, which silently misses issues still open at cut time or closed after the cut. Drive the set from the union of:
# 1. Every issue a shipped PR auto-closes (closingIssuesReferences) + any Closes/Fixes/Resolves #N in a PR body:
gh pr list --repo nubjs/nub --state merged --search "merged:<PREV-date>..<cut-date>" \
--json number,body,closingIssuesReferences --limit 200 \
--jq '.[] | {pr:.number, closes:[.closingIssuesReferences[].number], refs:([.body|scan("(?i)(?:clos|fix|resolv)\\w*\\s+#(\\d+)")]|flatten)}'
# 2. Every issue closed in the release window (catches issues closed without a linked PR):
gh issue list --repo nubjs/nub --state closed --search "closed:<PREV-date>..<cut-date+1>" \
--json number,title,stateReason --limit 200
For each, check whether it already carries the comment before posting; skip a NOT_PLANNED issue with no shipped fix. Re-run this pass for any issue closed AFTER the cut.
gh issue view <n> --repo nubjs/nub --json comments --jq '[.comments[].body|select(test("Shipped in v<ver>"))]|length'
REL="https://github.com/nubjs/nub/releases/tag/v<ver>"
gh issue comment <n> --body "Fixed in v<ver> (now published): $REL"
gh pr comment <n> --body "Shipped in v<ver>: $REL"
Hit every issue and PR the union surfaces. Do not skip one for being "minor," do not fall back to the release thread's targeted-fix list (it under-counts), and do not comment on unrelated issues.
Step 6 — Post-release verify
npm view @nubjs/nub@<ver> version # the root package is on the registry
npm view @nubjs/nub@<ver> dist.tarball # sanity: published artifact exists
gh release view v<ver> --json assets --jq '.assets[].name' | sort
# expect 16 assets: 8 platforms × {archive, .sha256}
# nub-darwin-arm64.tar.gz(.sha256), nub-darwin-x64.tar.gz(.sha256),
# nub-linux-x64.tar.gz(.sha256), nub-linux-x64-musl.tar.gz(.sha256),
# nub-linux-arm64.tar.gz(.sha256), nub-linux-arm64-musl.tar.gz(.sha256),
# nub-win32-x64.zip(.sha256), nub-win32-arm64.zip(.sha256)
A complete release has the 10 npm packages published (@nubjs/nub, @nubjs/nub-<platform> ×8, @nubjs/types), the GitHub Release present, and all 16 assets attached.
If CI failed partway: publish-npm and github-release are split + idempotent on purpose — re-run the failed job from the Actions UI (npm publish skips already-published packages; the release job re-uploads only missing assets). Never re-cut a version for a flaky asset upload.
Quick reference
| Step | Command |
|---|---|
| Changeset | git log $(git describe --tags --abbrev=0)..HEAD --oneline |
| Bump | make version V=<ver> → make version-check |
| Cut | git commit -m "v<ver>" → git tag v<ver> → git push origin main --tags |
| Notes | gh release edit v<ver> --notes-file notes.md |
| Blog | site/content/blog/nub-<x>-<y>-<z>.mdx — back-dated to publishedAt (direct to main) |
| Loop | gh issue comment <n> --body "Fixed in v<ver>: <release URL>" (every closed issue + merged PR) |
| Verify | npm view @nubjs/nub@<ver> version · gh release view v<ver> --json assets |
Version History
-
46280a5
Current 2026-08-03 01:43
优化共享树同步逻辑,将 git pull --ff-only 替换为 merge --ff-only 以避免因未暂存文件导致的失败;合并编排器技能。
-
4fd083f
2026-07-19 12:03
技能名称从 release 更新为 release,提交日志显示隐藏了维护者工作流以防公开发现。
-
ab079b4
2026-07-11 18:57
新增自动生成 What's Changed PR 分解及机械化的 Issue 评论枚举功能;增加将发布说明发布为博客文章的步骤。
- 44d0dd0 2026-07-05 11:03


