cockpit-release
SkillWeb & browsingCut 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.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the cockpit-release skill
What this skill tells your AI
The instructions your AI receives, as published by surething-io/cockpit in docs/skills/cockpit-release/SKILL.md and read by ahel’s review.
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, ormajor. 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 fromwebsite/ - Workflows:
Publish to npm— triggered bypush tag v*, runsnpm publishand 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 bypushtouchingwebsite/**ORworkflow_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-changelogskill). Auto-generated--generate-notescontent (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 --porcelainis empty (working tree clean) -
git rev-parse --abbrev-ref HEADismain -
git fetch && git rev-list HEAD..origin/main --countis0(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 -
Lock smoke — Step 2 installs a packed tarball and never runs the repo's
npm ci, so a corruptpackage-lock.jsonpasses every smoke test and then kills the publish workflow at its first step. Costs ~1s here, versus a tag to unwind if you find it after the bump:rm -rf /tmp/lock-smoke && git clone -q . /tmp/lock-smoke (cd /tmp/lock-smoke && npm ci --dry-run) || { echo "❌ npm ci broken at HEAD"; exit 1; } rm -rf /tmp/lock-smokenpm error Invalid Version:means some entry lost itsversion—npm installtolerates that,npm cidoes not (v1.0.256: pinningnextleft asharp-freebsd-wasm32stub). Fix by deleting that entry, thennpm install --package-lock-only— that order, since it reads the broken lock first. -
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> # creates commit "1.0.x" and annotated tag v1.0.x
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?
# Build the artifact CI will publish. TURBOPACK= overrides any shell-leaked
# TURBOPACK env (e.g. from a running dev cockpit), which would conflict with
# `next build --webpack` in package.json. `env -u NODE_ENV` strips a leaked
# NODE_ENV=development (same source), which otherwise makes `next build`
# mix React dev bundles into the prod build and crash prerender with
# "Cannot read properties of null (reading 'useContext')" (seen in v1.0.220).
env -u NODE_ENV TURBOPACK= npm run build && env -u NODE_ENV npm run build:server
# `npm pack` does NOT trigger `prepublishOnly`, so the webpack cache that
# `prepublishOnly` cleans is still present and would bloat the tarball from
# 7 MB to ~90 MB. Clean it manually.
rm -rf .next-prod/cache
# Pack into a tarball locally
npm pack # produces surething-cockpit-1.0.x.tgz
# Install into a clean, isolated directory — POSTINSTALL must succeed
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 # must report the new version
# Keep $SMOKE_DIR around for step 2b; clean up at the end.
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
# fix the bug, recommit, npm version again
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.
# Boot the JUST-PACKED tarball, not the globally-linked cock.
# Re-install into a temp dir if you don't already have one from step 2a.
SMOKE_PORT=3458
SMOKE_HOME=$(mktemp -d) # isolated data dir — avoids the "already running cockpit" guard
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 # cold-start of next-server + WS + share server takes ~8-12s
FAIL=0
# Probe a few endpoints — any non-2xx is a hard fail.
# /api/version must also report the new version string (not empty) — the
# handler used to read process.cwd()/package.json which silently returned
# empty when the prod-install was running from outside the repo.
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
# First-run path. $SMOKE_HOME is a fresh dir, so this instance has no projects —
# exactly the state a new user installs into, and the only state in which the
# empty screen renders. It backs the project browser, which is now the sole way
# in from a clean install.
curl -fsS "http://localhost:$SMOKE_PORT/api/sessions/projects" >/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 probes exercise four 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)/api/sessions/projects— backs the project browser, i.e. the first-run path
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.
Also open http://localhost:$SMOKE_PORT in a browser and confirm the empty
screen offers a way in — a folder icon, "select a project", and an Open
Project button that opens the browser modal.
This one cannot be curl'd: the workspace tree is client-rendered, so the
server-side HTML contains none of it. It is worth the ten seconds anyway,
because $SMOKE_HOME is the only time this screen is ever seen — it renders
solely when no project is open, so a developer's own cockpit never shows it and
no amount of daily use will surface a regression here. Two shipped bugs lived
in exactly that blind spot: the screen had no way to open a folder at all (a
fresh install was a dead end, reachable only by knowing the cockpit <path>
CLI form), and its project list grew past the viewport with no scroll.
2b-bis. HTML app runtime probes
The three probes above never touch /apps, so a release that breaks the HTML
app runtime sails straight through them (v1.0.246 was almost entirely an
/apps rewrite and would have passed 2b with the runtime completely broken).
Run these against the same booted instance — $B is http://localhost:$SMOKE_PORT:
# Injection: every .html served by /apps gets the SDK, no marker involved.
# Assert on `window.cockpit` — that string is really in the injected SDK source;
# `cockpit.bash` is NOT (it appears only in jsdoc), so grepping for it silently
# passes forever. Check both address spaces: builtin and local are separate
# branches of the route and have regressed independently before.
A=$(curl -fsS "$B/apps/builtin/file-viewer/index.html") || { echo "builtin app route failed"; FAIL=1; }
echo "$A" | grep -qi "<html" || FAIL=1
echo "$A" | grep -q "window.cockpit" || { echo "SDK missing on builtin doc"; FAIL=1; }
PLAIN=$(mktemp -d)/plain.html
printf '<html><head><title>plain</title></head><body>hi</body></html>' > "$PLAIN"
L=$(curl -fsS "$B/apps/local$PLAIN") || { echo "local html route failed"; FAIL=1; }
echo "$L" | grep -q "window.cockpit" || { echo "SDK missing on local .html"; FAIL=1; }
rm -rf "$(dirname "$PLAIN")"
# A non-HTML local file must stay untouched (the injection path is html-only).
R=$(curl -fsS "$B/apps/local$PWD/README.md") || { echo "local md route failed"; FAIL=1; }
echo "$R" | grep -q "window.cockpit" && { echo "SDK leaked into non-html"; FAIL=1; }
# Hosted frontend libs — regenerated by scripts/build-html-lib.mjs each build,
# so a broken build stage shows up as a 404 here rather than at build time.
for f in theme.css react.production.min.js markdown.js json-viewer.js pdf-viewer.js; do
[ "$(curl -s -o /dev/null -w '%{http_code}' "$B/html-lib/$f")" = "200" ] || FAIL=1
done
# Regression guard for the css-accumulation bug fixed in v1.0.246: the Tailwind
# block used to be appended once per build (pdf-viewer.css reached 27 copies).
[ "$(curl -fsS "$B/html-lib/theme.css" | grep -c '^:root')" -le 2 ] || FAIL=1
# /apps/local serves arbitrary local files...
[ "$(curl -s -o /dev/null -w '%{http_code}' "$B/apps/local$PWD/README.md")" = "200" ] || FAIL=1
# ...but root-relative paths outside /html-lib and /apps must NOT resolve.
[ "$(curl -s -o /dev/null -w '%{http_code}' "$B/package.json")" = "200" ] && {
echo "repo file exposed at root"; FAIL=1; }
Assert window.cockpit (not the bare string cockpit) for the positive case —
cockpit appears incidentally in app markup, so a loose grep passes even when
injection is broken.
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:
# Kill the node child + the wrapper, then verify the port is free.
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
}
# Drop the install dir, the isolated data dir, and the local tarball — the
# first two are ~39 MB combined, and the tarball name collides with the next
# bump if left behind.
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/ripgrep403 (GitHub releases rate limit),npm ciECONNRESET, 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 publishmanually (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 docs/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 — Rebuild the /try demo sandbox template
The "Try Online" demo at opencockpit.dev/try boots an E2B sandbox whose image
pins @surething/cockpit@latest at image build time. latest is resolved
once and frozen, so publishing to npm does nothing to the demo — it keeps
serving whatever version was current when the template was last built.
Nothing reports this. The sandbox boots, the demo works, it is just an old release. It is only caught by someone noticing the version pill.
cd e2b
set -a && . ./.env && set +a # or: export E2B_API_KEY=$(node -e "console.log(require('$HOME/.e2b/config.json').teamApiKey)")
npm install # first time only
npm run build-template 2>&1 | tee /tmp/e2b-build.log # ~2m30s
grep INSTALLED /tmp/e2b-build.log # must show the version you just published
cd ..
The install layer echoes the version it resolved, so the log says it outright:
[builder 4/8] [stdout]: INSTALLED @surething/cockpit@1.0.263
A successful build showing the version you just published is enough. If it shows the previous one, npm's registry hadn't caught up — wait a minute and rebuild.
try.ts refers to the template by name (cockpit-demo), not by template or
build id, so no code change and no website redeploy are needed — the next
POST /sandboxes picks up the rebuilt image.
Step 9 — Verify everything is live
# 1. npm
npm view @surething/cockpit version
# 2. GitHub Release
gh release view v1.0.x --repo Surething-io/cockpit --json name,publishedAt,url
# 3. Website changelog has the new entry at the top
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
# 4. Sanity: no "Full Changelog: …compare…" tail in the release body
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 publishmanually unless the CI workflow has demonstrably failed and the human has explicitly asked. - Never
git tag -dorgit resetafter 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.
- Never skip Step 8 after a publish. The demo image pins
latestat build time, so without a rebuildopencockpit.dev/trysilently serves the old release — nothing anywhere reports the drift.
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 versionagain. 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. -
Publish workflow failed BEFORE the
npm publishstep (npm ci/ build red): nothing consumed this version, so the tag is a dud and moving it beats burning a version number. Prove it first —npm view @surething/cockpit versionstill shows the PREVIOUS version andgh release view v1.0.xsays "release not found" — then, with human sign-off (this is thegit tag -fthe Refusals cover), commit the fix on top and:git tag -f -a v1.0.x -m "v1.0.x" <fix-sha> git push origin main && git push --force origin v1.0.x # re-triggers Publish to npmIf either check comes back dirty, do not move the tag — bump to the next patch.
-
npm publish failed mid-CI:
- Transient (
@vscode/ripgrep403,npm ciECONNRESET, GH API 504):gh run rerun <id>is fine. - Real:
gh run view <id> --log-failed. Common causes:COCKPIT_NPM_TOKENrotated,npm run buildflake,package-lock.jsondrift.
- Transient (
-
GH Release
Create GitHub Releasestep inside the publish workflow 504'd: workflow finishes "failed" but npm IS published. Step 6'sgh release create v1.0.x --notes-file ...(instead ofgh 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 /trydemo sandbox template:e2b/README.md- Sister skill:
docs/skills/cockpit-changelog/SKILL.md(release notes voice + structure)
Signals
- GitHub stars
- 36
- Forks
- 8
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
cockpit-release- Source
- github.com/surething-io/cockpit