Releasing Workcell
August 11, 2026 ยท View on GitHub
This runbook defines the repeatable process for cutting a new Workcell release.
Current assurance model
Workcell currently operates in single-maintainer release mode.
That means:
- one maintainer may open, merge, tag, approve the release environment, and verify publication
- asynchronous review from humans and configured async reviewers is still expected and must be swept before merge
- asynchronous review is advisory input, not equivalent to an independent human approval
- signed history, strict CI, reproducibility checks, provenance, SBOMs, attestations, immutable releases, and public review artifacts are the primary compensating controls
This is lower assurance than true separation of duties and should be described honestly in docs, status reports, and release commentary.
Principles
- Treat a release request as implicitly including peer review unless the maintainer explicitly narrows that scope.
- For release work, peer review means continuing through review findings, validation failures, documentation drift, and hosted workflow failures until no actionable findings remain or a concrete blocker is reported.
- Review and finish open pull requests before cutting a release.
- Address actionable PR comments and review feedback as part of release work.
- Use signed commits and signed tags.
- Before tagging a release, make sure shipped features are documented and do not remain on the roadmap. Remove roadmap items only after the code and focused validation confirm they are fully implemented.
- Before tagging a release, sweep outstanding security findings and verify each claimed fix with a one-off proof-of-closure command or repro, even when the repo already has permanent regression tests.
- Before tagging a release, verify that release-facing documentation examples are still covered by existing tests or scenario lanes.
- Before publishing or merging a release PR, verify that
policy/operator-contract.toml,policy/requirements.toml,workcell --help,man/workcell.1, and any curatedREADME.mdworkflow claims still agree. - When a release changes a user-visible workflow, run the repo-local
workcell-contract-parityskill sweep and treat any parity failure as a release blocker. - Review any intentional upstream holdbacks or exceptions before refreshing pins, and document them in policy or release notes rather than carrying unexplained drift.
- Publish
main-based release PRs from the host with./scripts/repo-publish-pr.shafter fresh localpr-parityevidence exists. - For agentic release PR publication and follow-up, use the repo-local
workcell-pr-lifecycleskill in addition to this release runbook. - Wait for
mainto be green before pushing the release tag. - Follow the tag-triggered
Releaseworkflow through completion. - Before pushing the tag, verify the hosted-controls audit confirms that the
releaseenvironment permits onlyv*deployment tags, with no deployment branches, variables, secrets, or administrator bypass. - Approve the
releaseenvironment only after release preflight and install verification are green. - Verify that the repository-level immutable-release control is enabled before pushing a release tag.
- Publish release assets through a draft GitHub release first, then publish the final release record only after the asset set is complete. The release workflow stages the exact asset inventory into private, unlinked, read-only handles before its first GitHub mutation and uploads only those staged bytes. The operator verifies the finished published release state.
- Verify the published GitHub release, attached assets, and immutable-release state before concluding.
- Do not rewrite or delete a failed release tag. Recover by patching
mainand cutting the next patch release. - In single-maintainer mode, leave an explicit public release-PR comment before
merge that records the version, exact head SHA, timestamp, and the exact
single-maintainer path used, including maintainer self-review for the
releaseenvironment and any explicit branch-protection bypass actually used for the PR merge because no second approver was configured. - If the repository-level immutable-release control was disabled, enable it
before tagging. A release that was already published mutable cannot be made
immutable in place; fix the control, patch
main, and cut the next patch release instead.
Inputs
Set these values before starting:
export REPO="omkhar/workcell"
export VERSION="vX.Y.Z"
export RELEASE_BRANCH="codex/release-${VERSION}"
export RELEASE_TITLE="Release ${VERSION}"
If a previously pushed release tag already failed, do not reuse it. Bump the patch version instead.
Release mode
Use one of these modes explicitly:
review-gated: default for interactive release work with the maintainer in the thread. Stop before each irreversible release action, present a short review packet, and wait for maintainer feedback or approval.autonomous: only when the maintainer explicitly asks for an end-to-end release run without pauses between release gates.
If the operator has not clearly opted into autonomous, use review-gated.
Maintainer checkpoints
In review-gated mode, stop before each of these actions:
- publishing the release PR
- marking the release PR ready or merging it
- pushing the signed release tag
- approving the
releaseenvironment - declaring the release complete and cleaning up lingering release branches or temporary workspaces
Each checkpoint packet should stay short and include:
- the exact next action
- the exact branch, PR, tag, or commit SHA involved
- current CI and comment-sweep state
- documentation and roadmap status
- any open risks, tradeoffs, or deviations from the normal path
Release-path pull requests should stay human-reviewable. Split opportunistic cleanup, unrelated fixes, or separate reviewer-sized concerns into other PRs before publishing the release path.
If the maintainer gives feedback, incorporate it and refresh the packet before continuing.
PR comment sweep
Every PR involved in the release path must go through a comment sweep.
Configured async reviewer identities live in
policy/reviewer-identities.toml.
A PR is not ready to merge until all of the following are true:
- top-level PR comments have been reviewed
- inline review comments and review threads have been reviewed
- unresolved review threads are resolved or explicitly closed with rationale
- actionable comments from human reviewers and configured async reviewers are fixed or answered
- the comment sweep has been repeated after CI turned green
- the comment sweep has been repeated immediately before merge
If no async reviewer identities are configured, still sweep all PR comments, review comments, and unresolved review threads before merge.
The required sweep points are:
- after the PR is published
- after required CI turns green
- immediately before merge
Useful commands:
gh pr view <pr-number> --repo "${REPO}" --comments
gh pr checks <pr-number> --repo "${REPO}"
Use the GitHub API or GraphQL as needed to inspect unresolved review threads.
Documentation review gate
Every release branch must also go through an explicit documentation review.
A release is not ready to merge unless all of the following are true:
CHANGELOG.mdaccurately summarizes the release contents and dateREADME.mdanddocs/getting-started.mdstill describe the current support boundary, install path, and tested release-install matrix honestly- provider and rollout docs that affect the release, such as
docs/injection-policy.md,docs/provider-matrix.md, and relevant quickstarts or setup guides, match the current implementation and auth maturity policy/operator-contract.tomlstill points each public workflow at current docs and automated evidence, and compatibility aliases still have working alias probesROADMAP.mdand nearby planning or design docs do not describe shipped work as future work and do not remove partially shipped work from the roadmap- release-sensitive runbooks such as
docs/releasing.md,docs/provenance.md, anddocs/github-workflows.mdstill describe the current release process accurately - release-facing documentation claims are backed by code, CI, or focused manual validation rather than assumption
The required documentation review points are:
- while preparing the release branch
- again after the release PR checks turn green
- immediately before merge if the release diff changed after the second review
1. Start from a clean main worktree
Use a dedicated release worktree or an otherwise clean checkout rooted at
main.
git fetch origin --tags
git checkout main
git pull --ff-only origin main
Confirm the working tree is clean before making release changes.
2. Review open pull requests first
List open PRs:
gh pr list --repo "${REPO}" --state open
For each open PR that is part of the release path:
- inspect the PR, changed files, checks, reviews, and comments
- perform the PR comment sweep
- address actionable review feedback and comment threads
- re-run or fix CI until the PR is green
- perform the PR comment sweep again after CI is green
- merge only after the final pre-merge comment sweep succeeds
Useful commands:
gh pr view <pr-number> --repo "${REPO}" --comments --web
gh pr checks <pr-number> --repo "${REPO}" --watch
gh pr diff <pr-number> --repo "${REPO}"
3. Confirm the next version
Check the latest existing tags before choosing the next release:
git tag --sort=-v:refname | head
Rules:
- normal case: cut the next patch release
- recovery case: if a release tag already exists but its release workflow
failed, do not reuse that tag; patch
mainand cut the next patch release
4. Prepare the release branch
Create the release branch from up-to-date main:
git checkout -b "${RELEASE_BRANCH}"
Update the changelog for the new version and date.
Before opening the release PR, sweep the roadmap and nearby planning docs:
- remove features from
ROADMAP.mdonly when the current code and tests prove they are shipped - keep any partially implemented work on the roadmap
- update user or design docs when shipped features would otherwise still appear as future work
Before opening the release PR, complete the release-readiness sweeps that tend to rot between releases:
- review any open external or offline security finding queue for the repo
- run a one-off proof-of-closure for each security finding being marked fixed
- verify release-facing documentation examples still map to requirements, tests, or scenario coverage
- review
policy/provider-bumps.tomland any temporary holdbacks before refreshing upstream pins
Useful commands:
./scripts/verify-requirements-coverage.sh
./scripts/run-scenario-tests.sh --repo-required
./scripts/verify-scenario-coverage.sh
Release preflight also runs ./scripts/verify-mutation-score.sh, which executes
the mutation harness and fails when the mutation score drops below the reviewed
baseline in policy/mutation-score-policy.json
(currently 100%: every mutant is caught). The policy also pins the reviewed
mutant count (expected_mutants), so the score cannot be met by shrinking the
mutant set. A drop in either is a diff a reviewer must approve, so a release
cannot silently regress the mutation safety net.
When a release touches the local runtime boundary or launch path, also run the local certification lane on a machine that has the live runtime prerequisites:
./scripts/run-scenario-tests.sh --secretless-only --certification-only
Perform the release documentation review on the exact branch diff:
git diff --stat main...HEAD -- CHANGELOG.md README.md ROADMAP.md docs
git diff main...HEAD -- CHANGELOG.md README.md ROADMAP.md docs
At minimum, review:
- the changelog entry for the release version and date
README.md,docs/getting-started.md, and changed quickstarts or setup docsROADMAP.mdplus any changed planning or system-design docs- changed rollout, auth-maturity, provenance, workflow, or release-runbook docs
policy/operator-contract.tomlandpolicy/requirements.toml- any doc statement about supported hosts, CI coverage, session surfaces, auth maturity, or release posture that could overstate what the current code and validation actually prove Before opening the release PR, verify release inputs that commonly drift:
./scripts/update-upstream-pins.sh --check
If the check fails because reviewed upstream pins or Debian snapshots drifted, refresh them:
./scripts/update-upstream-pins.sh --apply
Then update the changelog entry to describe the refresh and continue with the new patch version if needed.
Run the focused validation needed to justify roadmap and documentation updates before committing:
./scripts/verify-operator-contract.sh
./tests/scenarios/shared/test-auth-commands.sh
./tests/scenarios/shared/test-auth-status.sh
./tests/scenarios/shared/test-session-commands.sh
Then run basic validation before committing:
git diff --check
Before committing, run a short live sanity sweep for interactive flows that CI does not exercise well enough:
- from a clean throwaway checkout or other clean scratch workspace, launch an attached interactive session and resize the terminal window
- from a clean throwaway checkout or other clean scratch workspace, start, attach to, send to, stop, and delete a detached session
- confirm session cleanup removes the expected runtime artifacts only
Create a signed release commit:
git add -A
git commit -S -m "^D Prepare ${VERSION} release (local release checks pass; user-visible release record)"
5. Publish the release PR from the host
Prepare and publish the release PR with the host-side helper after running the local parity gate:
./scripts/pre-merge.sh --profile pr-parity
./scripts/repo-publish-pr.sh \
--branch "${RELEASE_BRANCH}" \
--title "${RELEASE_TITLE}" \
--body "Prepare ${VERSION} release." \
--commit-message "^D Prepare ${VERSION} release (local release checks pass; user-visible release record)"
In review-gated mode, stop with the first checkpoint packet before running
the publish step.
As soon as the PR exists, perform the first PR comment sweep.
Before merge in the current single-maintainer path, leave a public PR comment using the exact reviewed release head SHA. A literal template is below so the public artifact stays consistent from release to release:
Single-maintainer release approval for vX.Y.Z on <timestamp>.
Reviewed head SHA: <release-pr-head-sha>
Path used: signed commits, required CI, comment sweeps after publish/green/pre-merge,
and single-maintainer merge because no second approver was configured.
PR merge path: <normal merge | explicit admin or branch-protection bypass, with reason>
If the release workflow later waits on the release environment, I will record
that self-review publicly with the workflow run URL and timestamp.
6. Follow release PR checks and comments until green
Stay on the release PR until all required checks succeed and all actionable comments are addressed.
Useful commands:
gh pr checks <pr-number> --repo "${REPO}" --required --watch
gh pr checks <pr-number> --repo "${REPO}"
gh pr view <pr-number> --repo "${REPO}" --comments
If CI fails:
- inspect the failing workflow or job logs
- fix the issue on the same release branch
- commit with a signed follow-up commit
- push and re-run checks
- repeat until green
When required checks turn green, perform the second PR comment sweep.
Then repeat the documentation review on the exact release PR diff before deciding the branch is ready to merge.
In review-gated mode, stop with the second checkpoint packet before marking
the release PR ready or merging it.
Do not merge while required checks are failing or while comment sweeps are incomplete.
7. Merge the release PR to main
In the current single-maintainer operating model, merge after all of the following are true:
- required checks are green
- the PR comment sweep has been completed after CI turned green
- the final pre-merge comment sweep has been completed
- the release documentation review has been completed after CI turned green
- release-facing docs accurately describe the exact merge diff
- changelog and release notes are correct
- the public single-maintainer release comment records the version, exact head SHA, timestamp, and the exact single-maintainer path used, including any release-environment self-review and any explicit PR-merge bypass when applicable
After merge, record the resulting main commit SHA. This is the commit that
will be tagged.
8. Wait for post-merge main CI
Do not tag immediately after merging. First wait for the workflows on the
merged main commit to finish green.
Example:
gh run list --repo "${REPO}" --commit <main-commit-sha>
./scripts/check-repo-readiness.sh --repo "${REPO}" --base main --watch
Proceed only when every required workflow on the merge commit has completed
successfully and the repository readiness gate reports repo_readiness=ready.
Refresh the local repository so the merged main commit is present locally
before tagging:
git fetch origin main
git checkout main
git pull --ff-only origin main
Before pushing the tag, verify the repository-level immutable-release control:
gh api repos/"${REPO}"/immutable-releases
If it reports "enabled": false, enable it before tagging:
gh api -X PUT repos/"${REPO}"/immutable-releases
9. Create and push the signed tag
In review-gated mode, stop with the third checkpoint packet before pushing
the signed tag.
Create a signed tag on the merged main commit:
git tag -s "${VERSION}" -m "${VERSION}" <main-commit-sha>
git tag -v "${VERSION}"
git push origin "refs/tags/${VERSION}"
Never move or rewrite an existing release tag.
10. Follow the tag-triggered Release workflow
Watch the Release workflow for the tagged commit until it completes:
gh run list --repo "${REPO}" --workflow Release --limit 10
gh run watch <release-run-id> --repo "${REPO}"
If the workflow enters a waiting state for the release environment:
- verify that preflight and install verification jobs are green
- approve the environment in the standard single-maintainer path
- continue watching until publication finishes
In review-gated mode, stop with the fourth checkpoint packet before approving
the environment.
The workflow starts with a read-only tag-policy gate. The gate and host-side publisher use the same Go tag-classification policy, and every later release job depends on the gate directly. The publisher also classifies the tag before its first GitHub release-API request. Unsupported tags therefore fail before a write-capable job or API mutation can run. Release candidates are published as prereleases and never become latest; final tags are non-prereleases and become latest only when their populated draft is published.
In immutable-release mode, the release publisher must create or reuse a draft release, stage and validate the full artifact set before the first GitHub mutation, upload only from the sealed staging handles, revalidate the exact uploaded inventory, and only then publish the final release record. Source-path changes after staging cannot change the uploaded bytes. The entrypoint rejects extended ACLs on source and staging handles. Before publication, require the native Darwin ACL lane. Require the validator Linux ACL fixture to pass. The entrypoint also binds publication to the locally checked-out annotated tag object and its peeled commit.
The Go publisher verifies that exact binding against GitHub before and after
publication. Release preflight verifies repository release immutability
with the environment-scoped WORKCELL_HOSTED_CONTROLS_TOKEN. After the
release-approved job seals and uploads its workflow artifact, a minimal final
job in hosted-controls-audit refreshes that check immediately before
publication and removes the admin-metadata credential before invoking the
publisher. The publisher still attempts the direct check with its default
Actions token and accepts only GitHub's exact
Resource not accessible by integration denial when the fresh preverification
is present. A disabled control or any other response still fails closed.
If publication instead tries to upload assets into an
already-published immutable release, treat that as a release-process bug, patch
main, and cut the next patch release rather than rewriting the failed tag.
If a create, delete, upload, or publish request fails after it starts, treat the
hosted mutation outcome as ambiguous and inspect the exact-tag draft and asset
inventory before recovery. Never retry a tag whose publication might have
completed; recover with the next patch release.
Sealed publisher activation certification
The sealed publisher was live-certified on 2026-08-02 in a maintainer-controlled
private fixture repository with repository release immutability enabled. One
run invoked the Go publisher directly and a second used the production
scripts/publish-github-release.sh entrypoint. Each run used a distinct signed
annotated release-candidate tag, verified the exact tag-object and peeled-commit
binding, and published the exact 18-asset manifest. GitHub reported every asset
as uploaded with a sha256: digest, both releases as immutable prereleases, and
both tag signatures as verified with reason valid.
The hosted-controls handoff was live-certified on 2026-08-04 with signed tag
v0.0.0-rc.3 in the same fixture. The fixture workflow first proved that its
default Actions token received HTTP 403 with GitHub's exact
Resource not accessible by integration response from the immutable-release
administration endpoint. It then invoked the patched production Go publisher
with the explicit preverification handoff and published release id 365146990.
Workflow run 30949637336
completed successfully; GitHub reported the resulting prerelease as immutable
with the exact 18-asset inventory and a valid signed tag.
The fresh final-gate design was live-certified on 2026-08-04 with signed tag
v0.0.0-rc.4. In one hosted-controls job and one shell step, the fixture used
its environment-scoped admin-metadata token to observe enabled = true, unset
that token, proved the default Actions token still received the exact HTTP 403
integration denial, and invoked the production publisher. Workflow run
30955963114
published release id 365188077; GitHub reports it as an immutable prerelease
with the exact 18 digest-bearing assets and a valid signed tag. The temporary
fixture environment secret was deleted immediately after the run.
Release asset ACL rejection was live-certified on 2026-08-11 with signed tag
v0.0.0-rc.5 in the same fixture. Before tag creation, an ACL-bearing source
failed during local asset inspection. The negative run created no tag or
release. The corrected run published release id
368796851
with 18 digest-bearing assets. GitHub reports the prerelease as immutable and
the signed tag as verified with reason valid.
The certified non-document candidate fingerprint is
sha256:078903a66b1e3fc30e06a9c2d82152a999aac34f0ab71bc470d06ad4c77486c3.
The fingerprint covers the 19 changed non-document paths.
After approving the environment in single-maintainer mode, leave a public PR follow-up comment so the self-review is visible in the same release thread. Use this template:
Single-maintainer release-environment self-review recorded for vX.Y.Z.
Workflow run: <actions-run-url>
Approved at: <timestamp>
Reason: release preflight and install verification were green, and no second
release approver was configured for this repository.
If the release workflow fails:
- inspect the failing job and step
- determine whether the issue can be fixed on
main - patch
mainthrough a normal PR - cut the next patch release
- do not rewrite or delete the failed tag
11. Verify the GitHub release
Confirm the GitHub release exists and assets are uploaded:
gh release view "${VERSION}" --repo "${REPO}" \
--json name,tagName,isDraft,isPrerelease,isImmutable,url,assets
At minimum, verify:
- the release record exists
- the release is not a draft
- the release is not a prerelease unless intentionally marked that way
- the release is immutable
- expected assets are present, including the release tarball, Homebrew formula, checksums, signed metadata, manifests, and SBOMs
If isImmutable is false, treat that as a hosted-control regression to fix.
12. Final closeout
In review-gated mode, stop with the fifth checkpoint packet before declaring
the release complete.
At the end of the release, confirm all of the following:
- no open PRs remain that should have been part of the release
- actionable PR comments were addressed or dispositioned
- external or offline security findings for the release scope were reviewed, and any claimed closures were validated with one-off PoCs
- release-facing docs and examples match the shipped behavior
mainis green- the signed release tag exists on GitHub
- the
Releaseworkflow completed successfully - the GitHub release exists with uploaded assets
- the GitHub release is immutable
- stale remote release-path branches were reviewed for overlap and either merged intentionally or deleted as superseded work
- temporary release worktrees or clones were removed if the requested outcome was clean local state
Recovery notes
Failed release tag already exists
If a release tag was already pushed and its release workflow failed:
- do not delete the tag
- do not force-move the tag
- patch
main - cut the next patch release instead
Upstream drift during release
A common release-preflight failure is drift in reviewed upstream pins or the Debian snapshot. Check with:
./scripts/update-upstream-pins.sh --check
If drift is reported, apply the refresh:
./scripts/update-upstream-pins.sh --apply
Then update the changelog, merge the fix to main, and cut the next patch
release rather than reusing the failed tag.
Published release is mutable
If the release workflow succeeded but gh release view reports
"isImmutable": false:
- do not rewrite, delete, or recreate the published release
- verify the repository-level immutable-release control with
gh api repos/"${REPO}"/immutable-releases - enable it with
gh api -X PUT repos/"${REPO}"/immutable-releasesif needed - patch
mainso the release publisher uses a draft-first upload flow before the immutable release record is published - patch
mainso the docs and changelog describe the gap honestly - cut the next patch release under the enabled immutable-release control