9.4 KiB
Releasing OpenWork
Releases are pure GitHub Actions + GitHub Releases: versions live in git
tags, every committed package.json holds the permanent 0.0.0-dev
placeholder, and CI stamps the tag-derived version into the workspace at build
time. Cutting a release makes zero commits to this repo. A release is
done when the Release App run is green and Publish GitHub Release has
flipped the release public — not when the tag is created.
pnpm release:cut # dispatch Release App with bump=patch
pnpm release:cut minor # or major
pnpm release:cut --version 0.19.0
pnpm release:cut:watch # dispatch, then tail the run
pnpm release:review # sanity: placeholders intact, opencode pin present
How a release works
release:cutdispatchesRelease App(or run it from the Actions tab).- The
resolve-releasejob computes the next version from the highest stablev*tag (scripts/release/versions.mjs), creates the tag onorigin/devHEAD, and creates it via REST as the diff-warden app. The app's tag retriggers the workflow, but that duplicate run is skipped by an actor guard — the dispatch run is the release run. verify-releasechecks the tag (scripts/release/verify-tag.mjs): strict stablevX.Y.Zformat, and fresh tags must be strictly greater than every other stable tag.release:review --strictguards that the committed placeholders were not replaced with real versions.- Build jobs check out the tag and run
scripts/release/stamp-version.mjs, which writes the version intoapps/{app,desktop,server}/package.jsonin the CI workspace only. That is what electron-builder,app.getVersion(), the Vite renderer bundle, and theopenwork-servernpm publish all read. publish-releaseflips the draft public once required assets exist. From that moment den-api serves the new version to orgs: it reads published releases from the GitHub Releases API at runtime (ee/apps/den-api/src/desktop-releases.ts) — prereleases and drafts are excluded, so a rollback demotion removes a version immediately.
Prerequisites
ghauthenticated with permission to dispatch workflows.- One-time repo setting (done): the org-owned diff-warden GitHub App
is a bypass actor on the
v*tag ruleset;resolve-releasemints its token (WARDEN_APP_ID+WARDEN_PRIVATE_KEYin thewarden-clearanceenvironment) to create the tag ref via REST. The built-in GitHub Actions app cannot be a bypass actor — GitHub rejects it. Without the app token the run falls back to GITHUB_TOKEN and fails with instructions. - Manual tag pushes (expedited path below) additionally require repo/org admin, as before.
How protection actually works here
devis protected for everyone, including admins: PR required, one approval, approval from someone other than the last pusher, signed commits, linear history. The release never touchesdev— nothing to backfill.v*tags are creation-restricted: admins and the GitHub Actions principal (for workflow-created tags) can create them.
Two entry paths:
Dispatch (default) — pnpm release:cut. The tag is created on
origin/dev HEAD, so the released code is always reviewed code.
Tag-first (expedited, admins only) — when a release must go out now from
a commit not yet on dev:
git tag vX.Y.Z <sha>
git push origin vX.Y.Z
The tag names exactly the code that ships (there is no bump commit). The
Expedited Release Audit workflow opens a post-hoc review issue whenever a
released tag's commit is not on dev; land the same changes on dev through
a normal reviewed PR and close the loop there.
Recovery and reruns
-
Rerun an existing tag (infra failure, replay AUR/Daytona):
gh workflow run "Release App" --repo different-ai/openwork -f tag=vX.Y.ZRecovery runs skip tag creation and monotonicity. Sources are pinned to the tag; workflow-file fixes are picked up from
devautomatically because the workflow definition runs from the dispatched ref. -
A tagged version turned out defective before publish: leave the release as a draft or delete it (
gh release delete vX.Y.Z), fix forward ondev, and cut the next patch. If the bad version reached npm, deprecate it:npm deprecate openwork-server@X.Y.Z "<reason — use X.Y.Z+1>".
Rolling back a published release
Use this flow when a bad version is published and marked Latest.
1. Stop the bleed
Dry-run first, then execute:
pnpm release:rollback
pnpm release:rollback --bad vX.Y.Z --execute
Always pin --bad when executing: after a successful rollback the good
release is Latest, so a bare re-run would select it as bad. The script
re-points Latest, demotes the bad release to prerelease, and prepends a
warning to its notes. Demotion also removes the version from den-api's
published list at runtime (it excludes prereleases), so org installs and the
update gate stop offering it within the cache window.
2. Reissue for updated clients
Updaters refuse downgrades. Tag the last-good commit with a version higher than the bad release — no commits, no worktree:
git tag vX.Y.Z+1 <last-good-tag>
git push origin vX.Y.Z+1
3. Deprecate npm
npm deprecate openwork-server@<bad-version> "rolled back — use <next>"
| Client state | Recovery |
|---|---|
| Not yet updated | Fixed by step 1 |
| Already updated | Fixed only by step 2 |
Revert the offending PR on dev through the normal reviewed-PR flow. That
cleanup is not in the critical path of user recovery.
What blocks publishing (and what doesn't)
Publish GitHub Release requires the electron matrix, electron assets, and
npm publish. It does not require:
Publish AUR(continue-on-error) — aur.archlinux.org outages are not release failures. AUR publishing renders the committedpackaging/aurtemplate (pkgver=0.0.0) with the real version + checksums in the CI workspace and pushes only to the AUR remote.Build + Push Daytona Snapshot— snapshots are rebuildable afterwards by re-running the workflow with the same tag.
Verification checklist
gh run list --repo different-ai/openwork --workflow "Release App" --limit 3
gh release view vX.Y.Z --repo different-ai/openwork # published, not draft
- Release is not a draft and marked Latest
- Asset count looks right (macOS + Linux + Windows + updater
latest*.ymlmanifests — the desktop updater 404s until the manifests are published) npm view openwork-server versionshows the new versioncurl -s https://api.openworklabs.com/v1/app-versionlists the new version once den-api's cache refreshes (≤5 minutes)
Where versions live now
| Question | Answer |
|---|---|
| What versions exist? | git tag --list 'v*' / GitHub Releases |
| What's the latest? | gh release view --json tagName |
| What commit is vX.Y.Z? | git rev-parse vX.Y.Z |
| What version is this checkout? | git describe --tags (package.json says 0.0.0-dev on purpose) |
| Oldest supported desktop version? | MIN_SUPPORTED_DESKTOP_VERSION — committed policy in scripts/release/generate-desktop-versions.mjs |
ee/apps/den-api/src/generated/desktop-versions.ts is a cold-start/offline
fallback snapshot only; refresh it occasionally with
node scripts/release/generate-desktop-versions.mjs --version <latest>.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Run fails pushing the tag | diff-warden app missing from the v* ruleset bypass list, or its secrets unset |
re-add the app (Settings → Rules) / restore WARDEN_APP_ID+WARDEN_PRIVATE_KEY, or push the tag manually as an admin and rerun |
Tag vX.Y.Z already exists on a fresh cut |
version already released | rerun with -f tag=vX.Y.Z (recovery) or pick a higher version |
verify-release fails monotonicity |
manual tag lower than an existing release | choose a version above the current highest stable tag |
release:review fails placeholder check |
someone committed a real version into package.json | restore 0.0.0-dev — CI stamps versions from the tag |
| Desktop app shows updater 404 for the new version | tag exists but the release is still a draft mid-run | wait for Publish GitHub Release; self-heals |
| Release run red only on AUR / Daytona | external channel failure | release still publishes; rerun with the same tag when the channel recovers |
All electron-linux-* fail compiling a native module |
a raw-V8 native addon meeting new Electron headers under GCC | keep native deps converged on one N-API-based major across the whole workspace (see #3561/#3563) |
Windows legs fail in afterPack with Missing staged MCP runtime package |
asar path-separator mismatch | fixed — normalizeAsarEntryPath in electron-after-pack.cjs |
History
- 2026-08: releases became commit-free (tags are the only version source; CI stamps the workspace; den-api reads published releases at runtime; AUR renders a committed template). Previously every release required a version bump commit, a dev backfill PR, and an AUR packaging PR.
- 2026-08-05 (v0.18.15/v0.18.16): three releases red since the Electron
35→43 upgrade left
apps/serveron better-sqlite3 v12 while desktop moved to v13 — electron-builder rebuilds every copy of a native module it finds. Fixed by converging the workspace on v13 (N-API) and teachingopencode-dbto usebun:sqliteunder Bun. Full context: #3561, #3563, #3564.