| name | cockpit-release |
| description | Cut a new Cockpit release end-to-end — bump version, tag, publish to npm, write user-facing release notes, refresh the website, and verify everything went live. |
| argument-hint | ["patch|minor|major"] |
| icon | 🚀 |
You are the release captain for Cockpit (@surething/cockpit). Your job is to run the full release pipeline in order and stop at every irreversible step to confirm with the human first.
Inputs
$ARGUMENTS — patch (default), minor, or major. If empty, ask first which one.
Project facts (assume these unless overridden)
- Repo:
Surething-io/cockpit (origin/main is the release branch)
- npm package:
@surething/cockpit (public, scoped, provenance-enabled)
- Website:
opencockpit.dev, deployed via Cloudflare Pages from website/
- Workflows:
Publish to npm — triggered by push tag v*, runs npm publish and creates a GitHub Release with an empty body (intentionally — see .github/workflows/publish.yml). Step 6 of this skill is the one that fills the body.
Deploy Website — triggered by push touching website/** OR workflow_dispatch. Builds at the time of run, so re-running it picks up any new GitHub Release notes.
- Convention: every release ships with hand-authored user-facing release notes in the project's voice (see
/cockpit-changelog skill). Auto-generated --generate-notes content (in particular the **Full Changelog**: …compare/… tail) is not part of the convention — that's why the workflow no longer uses --generate-notes.
Pre-flight (before touching anything)
Run these checks. If any fails, stop and report — do not "fix" them silently.
git status --porcelain is empty (working tree clean)
git rev-parse --abbrev-ref HEAD is main
git fetch && git rev-list HEAD..origin/main --count is 0 (local is up to date, nothing remote ahead)
- Show the commits since the last tag so the human can sanity-check the bump scope:
PREV=$(git describe --tags --abbrev=0)
git log "$PREV..HEAD" --oneline
- Read the bump type from
$ARGUMENTS. If absent or unclear, ask: "Bump from $PREV → patch / minor / major?"
The release pipeline
Execute step-by-step. Print each command before running it. Wait for confirmation only at the marked points.
Step 1 — Bump version (local, reversible)
npm version <patch|minor|major>
Show the resulting git log -1 --oneline and the new tag. Reversible with git tag -d <tag> && git reset --hard HEAD~1 — say so explicitly to the human.
Step 2 — Smoke test the local tarball BEFORE push 🆕
This step exists because three out of three 1.0.198 → 199 → 200 → 201 releases shipped DOA: install path, browser bundle, server bundle. CI green ≠ users can run it. Do NOT skip — you will spend 30 minutes shipping hot-fixes if you do.
2a. Install smoke — does npm install succeed at all?
env -u NODE_ENV TURBOPACK= npm run build && env -u NODE_ENV npm run build:server
rm -rf .next-prod/cache
npm pack
TARBALL=$(ls -t surething-cockpit-*.tgz | head -1)
SMOKE_DIR=$(mktemp -d)
(cd "$SMOKE_DIR" && npm install -g --prefix . "$OLDPWD/$TARBALL") || {
echo "❌ Install failed — DO NOT push"
exit 1
}
"$SMOKE_DIR/bin/cock" --version
If npm install errors with ERR_MODULE_NOT_FOUND / postinstall failures / missing files / 404 on workspace @cockpit/* packages, the published package.json dependencies list is broken (e.g. listing private workspace packages npm can't fetch). Reset and fix:
git tag -d v1.0.x
git reset --hard HEAD~1
2b. Prod-runtime smoke — does the prod build actually run?
CI compiles fine but runtime errors only show when the app boots in prod mode. The webpack browser/server split has bitten us multiple times (see v1.0.200 / 201 commit messages — createRequire is not a function was a runtime, not build, error).
Use port 3458 for smoke — it avoids conflicts with both:
- the local dev cockpit (port 3456) you usually have running
- any prod cockpit (port 3457) you may also have installed via
npm link
The cock binary picks up PORT=3458 from env and binds there. env -i strips inherited env (notably any COCKPIT_PORT leaked from a running dev cockpit) so the smoke can't accidentally probe the wrong instance.
Port isolation is NOT enough — you MUST also isolate the data dir with COCKPIT_HOME. Cockpit guards on its data dir (~/.cockpit/server.json), not on the port: if any cockpit (the dev one on 3456, a prod one on 3457) is already running off the default ~/.cockpit, the smoke instance refuses to boot with:
✗ This data dir already has a running cockpit (pid …, port …).
Data dir: /Users/<you>/.cockpit
…and exits before binding 3458, so all three probes fail with Couldn't connect to server — a false negative that looks like a broken tarball (cost ~5 min of confused debugging in v1.0.230). Point COCKPIT_HOME at a throwaway temp dir so the smoke gets its own data dir and can't collide with a cockpit the human is actively using.
SMOKE_PORT=3458
SMOKE_HOME=$(mktemp -d)
env -i PATH="$PATH" HOME="$HOME" COCKPIT_HOME="$SMOKE_HOME" PORT=$SMOKE_PORT COCKPIT_ENV=prod COCKPIT_NO_OPEN=1 \
"$SMOKE_DIR/bin/cock" > /tmp/cock-smoke.log 2>&1 &
COCK_PID=$!
sleep 12
FAIL=0
VERSION_BODY=$(curl -fsS "http://localhost:$SMOKE_PORT/api/version") || FAIL=1
echo "$VERSION_BODY" | grep -q "1\.0\." || { echo "version body unexpected: $VERSION_BODY"; FAIL=1; }
curl -fsS "http://localhost:$SMOKE_PORT/api/commands?cwd=$PWD" >/dev/null || FAIL=1
curl -fsS "http://localhost:$SMOKE_PORT/api/projectGraph/file-functions?cwd=$PWD&path=packages/feature/explorer/src/server/codeMap/types.ts" >/dev/null || FAIL=1
kill $COCK_PID 2>/dev/null
wait $COCK_PID 2>/dev/null
if [ "$FAIL" = "1" ]; then
echo "❌ Prod runtime smoke failed — DO NOT push"
echo "--- log ---"; tail -25 /tmp/cock-smoke.log
exit 1
fi
The three probes exercise three different code paths:
/api/version — minimal handler, confirms boot + Next.js routing works
/api/commands — exercises @cockpit/feature-agent (slash-command discovery)
/api/projectGraph/file-functions — exercises @cockpit/feature-explorer (code-map, tree-sitter, the heaviest workspace package)
If any returns a non-2xx, the published tarball is broken — typically because a workspace package's source isn't being inlined into dist/ or .next-prod/ and the runtime hits an unresolved @cockpit/* import.
If your release introduces a new feature, manually exercise it in the running prod cockpit too. The probes above only cover the existing known paths.
2c. Cleanup — kill the smoke process and remove the tarball/install dir
The single kill $COCK_PID above only signals the wrapper process; the underlying node server (the actual port listener) often survives and keeps listening on $SMOKE_PORT. A leftover smoke instance on 3458 will silently absorb the next release's probes and make /api/version look broken (the stale instance is on old code where /api/version returns {"version":""} because it reads process.cwd()/package.json from outside the repo) — costing 10+ minutes of confused debugging. Kill the whole process tree:
pkill -P $COCK_PID 2>/dev/null
kill $COCK_PID 2>/dev/null
sleep 2
lsof -i :$SMOKE_PORT -sTCP:LISTEN -P -n >/dev/null && {
echo "❌ $SMOKE_PORT still bound — leftover smoke process. Investigate before next release."
lsof -i :$SMOKE_PORT -sTCP:LISTEN -P -n
exit 1
}
rm -rf "$SMOKE_DIR" "$SMOKE_HOME" surething-cockpit-*.tgz
If $SMOKE_PORT is unexpectedly already bound when you START step 2b, that's almost always a leftover from a previous release that wasn't cleaned up. Check ps -o pid,lstart,command -p <pid> to confirm it's a smoke leftover (started today, command is cockpit, /api/version returns {"version":""}) before killing — don't blow away an unrelated cockpit the human is actively using.
🛑 Confirm with human before pushing.
Step 3 — Push commit + tag (triggers npm publish, IRREVERSIBLE)
git push --follow-tags
Pushing the v* tag triggers Publish to npm. After this point, npm publish is on its way and a published version cannot be retracted (npm's 72h unpublish window exists but should be avoided).
Step 4 — Watch Publish to npm workflow
Find the run id and watch it:
gh run list --repo Surething-io/cockpit --workflow "Publish to npm" --limit 1 --json databaseId,status
gh run watch <id> --repo Surething-io/cockpit --exit-status
If it fails:
- Transient infra:
@vscode/ripgrep 403 (GitHub releases rate limit), npm ci ECONNRESET, GH API 504 — gh run rerun <id> is fine.
- Real failure in npm publish or build: stop, report, do not retry blindly. Possible recovery is
npm publish manually (see .github/RELEASING.md "Manual Publishing"), but only after diagnosing.
Step 5 — Verify npm published
npm view @surething/cockpit version dist-tags
npm view @surething/cockpit bin
Expected: version matches new tag, dist-tags.latest matches, bin includes both cockpit and cock.
Step 6 — Replace the auto-generated release body with hand-authored notes
The publish workflow creates the GitHub Release with empty body (the workflow used to use --generate-notes, which leaked **Full Changelog**: ...compare/... tails — see commit history). The body MUST be filled in by you.
Invoke the /cockpit-changelog skill (or if running standalone, draft notes using the same conventions: see skills/cockpit-changelog/SKILL.md).
Save the markdown to a temp file, then:
gh release edit v1.0.x --repo Surething-io/cockpit --notes-file /tmp/release-notes.md
Hard verify the body is what you intended (and that no rogue compare/ link leaked back in):
gh release view v1.0.x --repo Surething-io/cockpit --json body --jq .body | tail -c 200
gh release view v1.0.x --repo Surething-io/cockpit --json body --jq .body | grep -q '/compare/' && {
echo "❌ Full Changelog tail leaked — re-run gh release edit"
exit 1
}
🛑 Show the human the rendered notes (gh release view v1.0.x --repo Surething-io/cockpit) before moving on. Hand-authored notes are the public face of the release; one round of human review is worth it.
Step 7 — Trigger website redeploy (so /changelog page picks up the new notes)
The npm publish workflow does not trigger a website rebuild. The website's /changelog page reads data/changelog.json, generated at build time by scripts/fetch-changelog.mjs from GitHub Releases. New release → new notes → must rebuild.
gh workflow run "Deploy Website" --repo Surething-io/cockpit --ref main
sleep 8
id=$(gh run list --repo Surething-io/cockpit --workflow "Deploy Website" --limit 1 --json databaseId --jq '.[0].databaseId')
gh run watch "$id" --repo Surething-io/cockpit --exit-status
Step 8 — Verify everything is live
npm view @surething/cockpit version
gh release view v1.0.x --repo Surething-io/cockpit --json name,publishedAt,url
curl -s --http1.1 "https://opencockpit.dev/en/changelog/" | grep -oE 'v1\.0\.[0-9]+' | head -3
curl -s --http1.1 "https://opencockpit.dev/zh/changelog/" | grep -oE 'v1\.0\.[0-9]+' | head -3
gh release view v1.0.x --repo Surething-io/cockpit --json body --jq .body | tail -c 200
Expected: new tag at top of changelog (en + zh), no compare/v1.0.x-1...v1.0.x URL in the body.
Refusals
- Never push tags during the pre-flight without confirmation.
- Never skip Step 2 (smoke tests). Three out of three releases between v1.0.198 and v1.0.201 shipped DOA because earlier versions of this skill let CI green stand in for "users can install + run it"; the resulting hot-fix chain ate an evening. Smoke before push, every time.
- Never include a
**Full Changelog**: https://github.com/.../compare/... tail in the release body — historical convention is hand-written prose, no auto-tail.
- Never
npm publish manually unless the CI workflow has demonstrably failed and the human has explicitly asked.
- Never
git tag -d or git reset after Step 3 (push) without explicit human instruction — the tag is now visible to npm and Cloudflare and others.
- Never skip Step 6 (hand-authored notes). The publish workflow creates the release with an empty body specifically so this step is unavoidable; if you find yourself looking at an empty release page on opencockpit.dev/changelog, you forgot.
Failure recovery cheats (only on instruction)
- Step 2 install/runtime smoke failed:
git tag -d v1.0.x && git reset --hard HEAD~1, fix the bug, npm version again. Catching this here saves shipping a hot-fix release.
- Wrong release notes published:
gh release edit v1.0.x --notes-file … rewrites them. Then re-trigger Deploy Website.
- Website didn't pick up new notes: re-run
gh workflow run "Deploy Website". Build is idempotent.
- npm publish failed mid-CI:
- Transient (
@vscode/ripgrep 403, npm ci ECONNRESET, GH API 504): gh run rerun <id> is fine.
- Real:
gh run view <id> --log-failed. Common causes: COCKPIT_NPM_TOKEN rotated, npm run build flake, package-lock.json drift.
- GH Release
Create GitHub Release step inside the publish workflow 504'd: workflow finishes "failed" but npm IS published. Step 6's gh release create v1.0.x --notes-file ... (instead of gh release edit) covers this — the release didn't exist yet so create-with-notes is the right call.
Reference
- Full release docs:
.github/RELEASING.md
- npm publish workflow:
.github/workflows/publish.yml
- Website deploy workflow:
.github/workflows/website-deploy.yml
- Sister skill:
skills/cockpit-changelog/SKILL.md (release notes voice + structure)