| title | Release operator guide |
|---|---|
| folder | docs |
| description | Step-by-step path for the human release operator to publish a Specorator GitHub Release and (when enabled) an npmjs.com package — covers dry run, authorization, publish, rollback, failed publish recovery, and post-release cleanup. |
| entry_point | false |
This guide is the runnable, version-by-version operator path for publishing a Specorator release through the manual workflow at .github/workflows/release.yml. It satisfies SPEC-V05-006 and is consumed before every publish from v0.5 onward.
Audience. A maintainer who has not authored the release. The guide assumes you can read
package.json, runghlocally, and triggerworkflow_dispatchin the GitHub Actions UI. It does not assume you wrote the v0.5 plan.
Authorization boundary. Cutting and merging the
release/vX.Y.ZPR is ordinary topic-branch work (docs/branching.md). Publishing the GitHub Release and (when enabled) the GitHub Package is a separate, manually authorized action. The workflow refuses to publish without an explicitconfirminput that matches the requestedversion(SPEC-V05-002, NFR-V05-001).
You should already have:
- A merged
release/vX.Y.ZPR onmainper ADR-0020:package.json#versionequalsX.Y.Z,CHANGELOG.mdhas an unreleased-promoted heading for[vX.Y.Z],specs/version-X-Y-plan/release-notes.mdis finalised.
- The canonical tag
vX.Y.Zcut onmainafter the merge (never on the release branch). The workflow usesgh release create … --verify-tagand refuses to fall back to auto-tagging. - Green v0.4 quality signals available to the readiness check, surfaced through the
RELEASE_*repository variables (or an explicit operator waiver viaRELEASE_QUALITY_WAIVER). Seescripts/lib/release-readiness.ts(QualitySignals) for the contract. - npmjs.com Trusted Publisher configured for the
specoratorpackage against this repository'srelease.ymlworkflow on thereleasedeployment environment (ADR-0044, restoring ADR-0040 after the ADR-0041 NPM_TOKEN fallback used for v0.7.0 / v0.7.1; tracking #411 closed). Theid-token: writeworkflow permission mints the OIDC token consumed bynpm publish --provenance; no long-livedNPM_TOKENsecret is required. Theactions/attest-build-provenancestep still uses the same OIDC permission to sign the GitHub Release tarball asset. - Repo Settings → General → Releases → "Immutable releases" is DISABLED. When the setting is on, GitHub auto-flags every new Release immutable. If asset upload then fails — or the operator deletes the Release — the tag is permanently burned: the GitHub API returns HTTP 422
tag_name was used by an immutable releaseto every later attempt to host a Release on that tag, including a fresh draft. The v0.5.0 publish dispatch hit exactly this and forced the v0.5.1 recovery release (#233; incident timeline inspecs/version-0-5-plan/retrospective.md§Incident). Verify withgh api repos/{owner}/{repo}/immutable-releases. Per the GitHub REST contract the endpoint returns HTTP 404 (Not Found) when the setting is disabled — that is the safe state. HTTP 200 means the setting is enabled; the JSON body'senforced_by_ownerfield tells you whether the toggle came from this repo or an org-level default. Disable before dispatching, or accept the failure mode knowingly.
If any of items 1–4 is missing, stop. The readiness check fails closed on those (preferred). Item 5 is owned by the operator: the v0.5.0 retrospective showed the setting is not always operator-controlled (org-level defaults can propagate via enforced_by_owner: true), so the readiness check cannot always fail closed on it without blocking legitimate dispatches against repos the operator does not own. Verify by hand before every dispatch.
The six workflow_dispatch inputs of release.yml are the authoritative control surface:
| Input | Type | Default | Meaning |
|---|---|---|---|
version |
string | (required) | X.Y.Z. Must equal package.json#version and the existing vX.Y.Z tag on main. |
dry_run |
boolean | true |
true runs readiness + lifecycle steps without creating a Release (SPEC-V05-009). false enters the publish path. |
prerelease |
boolean | false |
Marks the published Release as a pre-release. |
draft |
boolean | false |
Creates the Release in draft state; operator finalises before publish. |
confirm |
string | "" |
When dry_run == false, must equal version literally. Mismatch fails the workflow before any irreversible step (SPEC-V05-002). |
publish_package |
boolean | false |
When true and dry_run == false, publishes the package to npmjs.com. Default false so a draft or prerelease candidate run does not push an irreversible npm publish. |
Inputs flow through env: mappings — never directly into a run: shell string — so no operator value is interpolated into shell text (zizmor template-injection guard).
Run the readiness check on your laptop before triggering the workflow. Same script the workflow runs:
RELEASE_VERSION=X.Y.Z \
RELEASE_CI_STATUS=green \
RELEASE_VALIDATION_STATUS=pass \
npm run check:release-readiness -- --jsonA green run prints {"diagnostics": []}; a failed run prints one or more diagnostics with the codes listed in §10. Resolve every diagnostic before triggering the workflow — the workflow runs the same check and will fail closed.
Use this for every release until you are convinced the artifact is correct. Dry run is non-destructive: it runs the full readiness pipeline, builds a candidate archive with npm pack, runs the Layer 2 fresh-surface assertions (SPEC-V05-010), and prints a generated-notes preview without creating a public Release or publishing the package.
Trigger:
- Go to Actions → Release → Run workflow.
- Inputs:
version:X.Y.Zdry_run:true(default)prerelease,draft,publish_package: leave defaultsconfirm: leave empty
- Run.
Inspect the run log:
- Step "Readiness — Layer 1" → green.
- Step "Build candidate archive" → tarball name + extracted dir.
- Step "Readiness — Layer 2 (fresh-surface)" → green.
- Step "Log dry-run candidate (no Release created)" → printed candidate tag and generated release-notes body.
If any step fails, fix the underlying cause (do not rerun without a fix) and trigger another dry run.
Only after at least one fully green dry run, request a stable publish.
-
Trigger Actions → Release → Run workflow with:
version:X.Y.Zdry_run:falseprerelease:falsedraft:falseconfirm: type the literalX.Y.Z(the confirm gate compares it toversionand fails if they differ — SPEC-V05-002).publish_package:trueif you intend to publish the npm package to npmjs.com on this run;falseif you only want to publish the Release this run.
-
The workflow executes, in order:
- Layer 1 readiness — release metadata.
npm pack— candidate archive built and extracted.- Layer 2 readiness — fresh-surface contract (ADR-0021).
- Confirm gate — refuses to continue unless
confirm == version. actions/attest-build-provenance— emits a GitHub artifact attestation for the workflow-built release tarball. This happens before the Release is created or the npmjs.com publish step runs, and it does not change the npm registry path.gh release create vX.Y.Z --target main --verify-tag --generate-notes ${TARBALL}— creates the GitHub Release with the candidate tarball attached in one call when no Release for the tag exists. When a Release already exists (the two-step CLAR-V05-003 path), the workflow runsgh release edit … --draft=<bool> --prerelease=<bool>to flip flags in place and uploads the asset only if it is not already attached, so a single Release per tag is preserved (#233 prevention B + C). The promote branch refuses to demote an already-published stable Release back to draft or prerelease — that flip would unpublish a consumer-visible release; cut a newvX.Y.(Z+1)instead.npm publish --provenance— only whenpublish_package: true; authenticates via npmjs.com Trusted Publishing (ADR-0044, restoring ADR-0040 after the ADR-0041 NPM_TOKEN fallback used for v0.7.0 / v0.7.1; #411 closed); idempotent (see §7.1). The--provenanceflag mints a sigstore provenance statement that ships with the tarball and is visible on the npmjs.com package page underProvenance. The GitHub Release tarball asset also carries its own sigstore attestation fromactions/attest-build-provenanceabove.
-
Verify on
https://github.com/Luis85/agentic-workflow/releases/tag/vX.Y.Z:-
Release notes body matches the dry-run preview.
-
Tarball asset is attached.
-
Tarball provenance verifies with
gh attestation verify:gh release download "vX.Y.Z" \ --repo Luis85/agentic-workflow \ --pattern "*.tgz" \ --dir /tmp/specorator-release gh attestation verify /tmp/specorator-release/*.tgz \ --repo Luis85/agentic-workflow
-
npmjs.com page shows
specorator@X.Y.Zwith a provenance badge linking to the GitHub Actions run (only ifpublish_package: true):npm view specorator@X.Y.Z --registry https://registry.npmjs.org
-
-
Smoke-test the consumer install path against the published package:
( cd "$(mktemp -d)" npm install -g specorator specorator --version specorator init --dry-run --target ./fake-target )
No
.npmrcconfiguration, no PAT — npmjs.com is public and unauthenticated for read.Failure here is a release-quality bug, not a publish bug — capture it and decide whether to deprecate or supersede before announcing.
Release provenance has two surfaces, both produced by the release workflow:
| Surface | Posture | Mechanism |
|---|---|---|
| GitHub Release tarball | Required for non-dry-run releases. | actions/attest-build-provenance runs against the packed .tgz after the fresh-surface check and confirm gate, before the GitHub Release is created. Verify with gh attestation verify. |
| npmjs.com package | Required for non-dry-run releases that publish the package (ADR-0044, restoring ADR-0040 after the ADR-0041 NPM_TOKEN fallback used for v0.7.0 / v0.7.1; #411 closed). | The publish path runs npm publish --provenance authenticated via npmjs.com Trusted Publishing (OIDC, no long-lived token). The sigstore provenance statement is visible on the npmjs.com package page under Provenance, and verifiable with npm view specorator@X.Y.Z --registry https://registry.npmjs.org --json (look at dist.attestations). |
Both surfaces bind the artifact to this repository's .github/workflows/release.yml workflow run via OIDC. Consumers can verify either chain without trusting maintainer signatures.
The npmjs.com Trusted Publisher configuration is: Repo Luis85/agentic-workflow, Workflow release.yml (bare filename — entering .github/workflows/release.yml would cause OIDC authentication to fail), Environment release. The deployment environment on the workflow's release job must match the Trusted Publisher's Environment field. If the npmjs.com configuration ever drifts (workflow rename, repo rename, environment removed), the publish step fails closed; repair on the npmjs.com side and re-dispatch.
SBOM generation is internal-only and deferred for the next release line. A release does not need an SBOM file in the GitHub Release assets, the npmjs.com package tarball, or the fresh-surface starter package.
Current target surface:
| Surface | Current posture | Rationale |
|---|---|---|
| Release package / fresh-surface starter | Do not ship an SBOM. | The released package is a starter template, and docs/release-package-contents.md keeps consumer-facing releases free of accumulated maintainer state. Shipping an SBOM before the generated surface is precise would imply a consumer-facing dependency inventory that may not match a downstream adopter's first project. |
| GitHub Release assets | Do not attach an SBOM yet. | A standalone SBOM asset should be paired with a repeatable generation command and, ideally, an SBOM attestation. That implementation is not part of the current release posture. |
| Internal release review | Allowed, not required. | Maintainers may generate SBOM evidence while reviewing a release candidate, but missing SBOM evidence is not a release-blocking diagnostic today. |
For a lightweight first pass, prefer built-in npm sbom because it is available with the npm CLI and can emit SPDX or CycloneDX from the current project. Use a richer CycloneDX npm generator only if the review needs fields or artifact-shape precision that npm sbom does not provide. In either case, record the source surface explicitly: package lock, installed tree, staged release archive, or extracted candidate.
Generator adoption is tracked separately in #390. Keep SBOM scope separate from release provenance: provenance answers where and how an artifact was built; an SBOM answers what components the chosen artifact surface contains. If SBOM attestations are later adopted, they should build on both #387 and #390 rather than changing this posture silently.
Tag creation, GitHub Release publication, and npmjs.com package publication are all irreversible under the rules in Article IX of the constitution. Rollback is therefore forward-only: you supersede, you do not undo.
| Symptom | Action | Why this and not "delete" |
|---|---|---|
| Release notes are wrong but artifact is correct. | Edit the Release in the GitHub UI ("Edit release"), regenerate body if needed. The tag and assets stay. | Reversible content fix; no new version needed. |
| Artifact is wrong (bad tarball, wrong fresh-surface) but the package is not published. | Edit the Release to draft in the UI to remove it from the public list, then supersede via vX.Y.(Z+1) with the corrected source. Update the broken Release's notes to point at the superseding tag. Do not delete or move the vX.Y.Z tag and do not rerun the workflow on the same vX.Y.Z. |
The workflow's RELEASE_READINESS_TAG_NOT_AT_MAIN and gh release create (HTTP 422 on existing Release) both refuse a same-tag rerun once a fix lands on main; tag move / deletion are denied by .claude/settings.json. Same-version supersession is the only forward-only path the gates permit. NFR-V05-005. |
| Artifact and package are published, and consumers will hit an issue. | Cut vX.Y.(Z+1) with a fix, deprecate the broken version on npmjs.com (npm deprecate specorator@X.Y.Z "<reason>"), and update the broken Release notes to point to the superseding version. Do not force-push or rewrite tags. |
Force-push to main and tag deletion are denied by .claude/settings.json and break consumer caches; supersession preserves history. NFR-V05-005. |
| Catastrophic problem (license violation, secret leak). | Yank the package version (npm unpublish specorator@X.Y.Z within the registry's allowed window), make the Release a draft, file an incident, and supersede with vX.Y.(Z+1). |
Documented escape hatch; do not use for ordinary mistakes. |
| Asset upload failed against a Release that GitHub auto-flagged immutable (Repo Setting → "Immutable releases" was on). | Do NOT delete the Release. GitHub blocks asset modification on a published immutable Release, so the existing tag cannot recover its asset. Either accept an asset-less Release and rely on npm install specorator as the consumer-facing artifact, or supersede with vX.Y.(Z+1). Then disable the Immutable Releases setting before the next dispatch. See §7.7. |
Deleting an immutable Release permanently burns the tag: every later attempt to host a Release on that tag returns HTTP 422 tag_name was used by an immutable release. The v0.5.0 incident burned v0.5.0 and forced the v0.5.1 recovery release (#233). |
In every case, append an entry to specs/version-X-Y-plan/release-notes.md and the implementation log naming the rollback action and the superseding version.
Recoverability differs per step:
npm publish(the workflow's idempotency guard wrapsnpm viewso a successful publish is detected on a rerun) — idempotent.gh release upload --clobber— idempotent.gh release create— not idempotent on its own, but as of #233 prevention C the workflow's "Create or promote GitHub Release" step now detects an existing Release viagh release viewand switches togh release edit+gh release upload --clobberfor the promote-in-place path. So a rerun against an existing draft is now safe and will flip the draft flag and replace the asset rather than failing closed with HTTP 422 ("release already exists"). A rerun against an existing stable Release (draft=false, asset already published) still flips no flags meaningfully and is wasted work — but it no longer fails the workflow.
So the rule is: the workflow is now safely rerunnable across the two-step CLAR-V05-003 path (draft+prerelease → stable+publish). It is still not the right tool for surgical recovery — use the targeted manual commands below for npm publish failures or asset-upload-only retries. The recovery scenarios are numbered by the failing step.
Symptom: the GitHub Release exists (the tarball may or may not be attached), but npm view specorator@X.Y.Z --registry https://registry.npmjs.org reports 404. Cause: network blip, registry hiccup, OIDC token mint failure, or EPUBLISHCONFLICT from a prior partial run.
Recovery — rerun the release workflow with the same inputs. As of ADR-0040 the workflow's publish step is idempotent (NFR-V05-005): it queries npm view first and skips publish when the version already exists. Trusted publishing handles auth automatically.
If the rerun fails again, fall back to a manual publish from a local checkout of vX.Y.Z. ADR-0044 removed the long-lived NPM_TOKEN repo secret, so this path requires minting a fresh classic Automation token on the npmjs.com web UI just for the recovery, and revoking it immediately after the publish completes:
# Run the whole block as a paste-once unit.
(
set -e
trap 'rm -f .npmrc' EXIT
# 0. From a clean clone, check out the exact tagged commit.
git fetch --tags origin
git checkout vX.Y.Z
# 1. Authenticate npm against npmjs.com using a freshly-minted classic
# Automation token. Generate at npmjs.com → Account → Access Tokens →
# Generate New Token → Classic Token → Automation. Revoke after the
# publish completes.
export NPM_TOKEN=<freshly-minted-automation-token>
cat > .npmrc <<'EOF'
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
EOF
# 2. Build the publication-canonical archive.
npm ci
cp package-lock.json npm-shrinkwrap.json
npm run build:release-archive -- --out .release-staging
TARBALL="$(npm pack --silent ./.release-staging)"
# 3. Idempotent publish (no provenance — manual recovery cannot mint OIDC).
set +e
view_output="$(npm view "specorator@X.Y.Z" version --json 2>&1)"
view_exit=$?
set -e
if [ "$view_exit" -eq 0 ] && echo "$view_output" | grep -q '"X.Y.Z"'; then
echo "Already published — skipping npm publish"
elif echo "$view_output" | grep -qE '"code": *"E404"|E404|code E404|404 Not Found'; then
npm publish --provenance=false "${TARBALL}"
else
echo "npm view failed with a non-404 error — refusing to publish" >&2
echo "$view_output" >&2
exit 1
fi
# 4. Replace any partial release asset.
gh release upload "vX.Y.Z" "${TARBALL}" --clobber
)Note: a manual recovery publish via NPM_TOKEN does not produce a provenance statement — only the OIDC trusted-publishing path emits provenance. If provenance must be present, prefer the workflow rerun path.
This satisfies NFR-V05-005 recoverability without force-pushing protected branches and without depending on a workflow rerun the gates would refuse.
Symptom: workflow stops at "Create GitHub Release" with tag vX.Y.Z does not exist (--verify-tag).
Recovery: cut the missing tag on the exact intended release commit (do not let gh release create auto-create it, and do not rely on HEAD — main may have advanced past the release commit while the missing-tag issue was being handled, and a bare git tag vX.Y.Z would then point at the wrong commit and publish unintended changes under that tag).
Identify the canonical release commit on main — the merge commit of the release/vX.Y.Z PR — and tag that SHA explicitly:
git fetch origin
# Find the merge commit of the release/vX.Y.Z PR (replace <pr-num>):
RELEASE_SHA="$(gh pr view <pr-num> --json mergeCommit --jq .mergeCommit.oid)"
# Sanity-check before tagging — should print the release-prep PR's merge commit subject.
git log -1 --pretty=%s "${RELEASE_SHA}"
# Tag that exact SHA, not HEAD.
git tag vX.Y.Z "${RELEASE_SHA}"
git push origin vX.Y.ZIf the release PR pre-dates gh access, resolve RELEASE_SHA by hand from git log --first-parent main and pin the SHA in the git tag invocation. Either way, never run git tag vX.Y.Z without an explicit object argument — git tag defaults to HEAD and will silently mis-tag if main moved.
Then rerun the workflow.
Symptom: workflow stops at "Readiness — Layer 1" with one or more RELEASE_READINESS_* codes (§10).
Recovery: read the JSON diagnostics, fix the underlying source (version mismatch, missing CHANGELOG entry, missing tag, drifted package metadata, widened workflow permissions, missing quality signal), and rerun. Do not rerun until the cause is fixed.
Symptom: workflow stops at "Readiness — Layer 2 (fresh-surface)" with one of RELEASE_PKG_ADR, RELEASE_PKG_INTAKE, RELEASE_PKG_DOC_STUB, RELEASE_PKG_STUB_TEMPLATE_MISSING.
Recovery: the candidate archive violates the fresh-surface contract from ADR-0021 / SPEC-V05-010. Either:
- An ADR file leaked into the archive — confirm
package.json#filesand codebase ADR placement; the contract says no numbered ADR files ship. - An intake folder under
inputs/,specs/,discovery/,projects/,portfolio/,roadmaps/,quality/,scaffolding/,stock-taking/, orsales/shipped non-empty — strip per-engagement state from the codebase before cutting, or update the manual stub-form step (OQ-V05-003 inpackage-contract.mdis the open automation gap). - A
docs/page is not in stub form — restore the stub shape fromtemplates/release-package-stub.md.
Symptom: workflow stops at "Confirm gate" with confirm input does not match version — refusing to publish (SPEC-V05-002).
Recovery: trigger a new run with confirm set to the literal X.Y.Z. Do not paste a different version into confirm; the gate is the explicit-authorisation boundary Article IX of the constitution requires.
If a Layer 1 quality signal is genuinely not available (e.g. v0.4 maturity evidence cannot be regenerated in time), the human release operator may set RELEASE_QUALITY_WAIVER for the workflow run with a free-text justification. The waiver:
- Suppresses only the
RELEASE_READINESS_QUALITYdiagnostic; neverVersion,TagMissing,TagNotAtMain,ChangelogMissing, fresh-surface, or workflow-permissions diagnostics. - Must be recorded verbatim in
specs/version-X-Y-plan/release-notes.mdand the implementation log entry for the publish (REQ-V05-010 acceptance — explicit waiver).
A waiver with no recorded justification is a release defect.
Two distinct failure points share root cause "Repo Setting → 'Immutable releases' was on at dispatch time and §1 pre-condition 5 was not honoured":
- Create-time tag burn —
gh release createitself fails with HTTP 422tag_name was used by an immutable release. The tag was already used by a prior immutable Release on this repo (and possibly deleted), and GitHub permanently refuses any later Release on it. Recovery cannot run on the original tag — only path (2) below. - Upload-time asset refusal —
gh release createsucceeded butgh release upload(or the workflow's "Attach release asset" step) fails withCannot upload assets to an immutable release. The Release exists; only the asset is missing.
Do not delete the Release in either case. Deleting an immutable Release permanently burns the tag — see §6 rollback table for why and what that costs.
GitHub's immutable-release contract blocks asset modification once a Release is published, and gh release create only attaches assets while the Release is still a draft. So for a published immutable Release with a missing asset — the v0.5.0 case — you cannot recover the asset on the existing tag. The two paths:
- Accept an asset-less Release (only viable when the npm package was already published this dispatch — i.e.
publish_package: trueand the workflow's package step ran cleanly). The npm package on npmjs.com becomes the consumer-facing artifact (npm install specorator); document the missing asset inspecs/version-X-Y-plan/release-notes.md§Known limitations and move on. The Release page still exists, the tag is still cut, the package is still installable. Verify the package version actually published before choosing this path —npm view specorator versions --registry https://registry.npmjs.org— becausepublish_packagedefaults tofalseand a draft / prerelease run typically skips it. - Ship a recovery release. If (1) is not viable — package was never published, the Release page is the consumer-facing artifact, or the original tag is burned at create-time — bump to
vX.Y.(Z+1), restate the v0.5 incident pattern in the new release notes, disable the Immutable Releases setting before the new dispatch, and run the standard publish path. The original burned tag stays burned forever; only the new tag carries a stable Release. This is the path the v0.5.1 recovery release took, and the only path for a create-time tag burn.
After either (1) or (2): disable the Immutable Releases setting if it is still on, update §1 pre-conditions in your operator runbook, and file the incident in the project's retrospective and #233 punch list.
After a successful stable publish:
-
Delete the release branch.
git push origin --delete release/vX.Y.Z git branch -D release/vX.Y.Z # or in the worktreeRelease branches are not reused per
docs/branching.md. -
Close v0.4 quality
RELEASE_*waivers if any were used. Reset the variables to their post-release state (greenfor pass-through, unset waiver). -
Record the publish in the implementation log. Append a
Release publishedentry tospecs/version-X-Y-plan/implementation-log.mdnaming: tag SHA, GitHub Release URL, package version URL, any rollback or waiver, the operator who triggered the run. -
Update the changelog. If
CHANGELOG.mdstill has[Unreleased]content for items that shipped invX.Y.Z, fold them into the released heading. -
Trigger Stage 11 (Retrospective) for the release feature folder via
/spec:retroso the loop closes.
When the readiness check is green but the tag, GitHub Release, package publish, or stable promotion still needs explicit human authorization, keep the feature in Stage 10 rather than advancing to Learning.
In specs/version-X-Y-plan/workflow-state.md:
- keep
current_stage: release, - keep
status: active, - keep
artifacts.release-notes.md: in-progressuntil the irreversible action finishes, - add a dated
## Hand-off notesentry that starts withrelease-tag holdand names the readiness verdict, verification command, pending irreversible action, required human authorization, and owning issue / PR.
Use this hold for the period between release-readiness completion and the authorized release action. Once the tag / publish / promotion is complete, update the release notes, mark Stage 10 complete, then run the retrospective.
# Pre-flight — Layer 1 readiness, locally
RELEASE_VERSION=X.Y.Z RELEASE_CI_STATUS=green RELEASE_VALIDATION_STATUS=pass \
npm run check:release-readiness -- --json
# Pre-flight — Layer 2 fresh-surface, locally. The check walks an extracted
# archive directory, not the codebase, so build the staged tree, pack it,
# extract, and run the assertions. `npm run build:release-archive` applies
# the build-time transform (T-V05-013); bare `npm pack` from the repo root
# is blocked by the prepack guard because it would ship the codebase form.
npm run build:release-archive -- --out .release-staging
TARBALL="$(npm pack --silent ./.release-staging)"
mkdir -p release-extracted
tar -xzf "${TARBALL}" -C release-extracted --strip-components=1
RELEASE_PACKAGE_ARCHIVE=./release-extracted \
npm run check:release-package-contents -- --json
rm -rf release-extracted .release-staging "${TARBALL}"
# Cut canonical tag on main (after release branch is merged). Use the merge
# commit of the release PR explicitly — never let `git tag` default to HEAD
# (main may have advanced past the release commit).
RELEASE_SHA="$(gh pr view <pr-num> --json mergeCommit --jq .mergeCommit.oid)"
git tag vX.Y.Z "${RELEASE_SHA}" && git push origin vX.Y.Z
# Trigger the workflow — UI: Actions → Release → Run workflow
# Inputs: version=X.Y.Z, dry_run=true|false, confirm=X.Y.Z (publish only),
# publish_package=true (only when pushing the GitHub Package)The release-readiness scripts emit machine-stable codes. Treat them as the contract.
| Code | Meaning |
|---|---|
RELEASE_READINESS_VERSION_MISMATCH |
package.json#version ≠ requested version. |
RELEASE_READINESS_TAG_MISSING |
vX.Y.Z tag does not exist. |
RELEASE_READINESS_TAG_NOT_AT_MAIN |
Tag does not point to a commit on main. |
RELEASE_READINESS_CHANGELOG_MISSING |
CHANGELOG.md missing or has no [vX.Y.Z] heading. |
RELEASE_READINESS_RELEASE_YML_MISSING |
.github/release.yml (PR-categorisation config) missing. |
RELEASE_READINESS_RELEASE_YML_SHAPE |
.github/release.yml shape is invalid. |
RELEASE_READINESS_PKG_NAME |
package.json#name ≠ specorator. |
RELEASE_READINESS_PKG_REGISTRY |
publishConfig.registry ≠ https://registry.npmjs.org. |
RELEASE_READINESS_PKG_REPOSITORY |
package.json#repository ≠ https://github.com/Luis85/agentic-workflow. |
RELEASE_READINESS_PKG_FILES |
package.json#files is missing required entries. |
RELEASE_READINESS_PACKAGE_JSON_MISSING |
package.json not found or unreadable. |
RELEASE_READINESS_WORKFLOW_MISSING |
.github/workflows/release.yml not found. |
RELEASE_READINESS_WORKFLOW_PERMISSIONS |
Top-level permissions: block ≠ { contents: write, attestations: write, id-token: write }. |
RELEASE_READINESS_QUALITY |
A v0.4 quality signal is missing or not green and no waiver is recorded. |
| Code | Meaning |
|---|---|
RELEASE_READINESS_IMMUTABLE_REPO |
Repo Setting "Immutable releases" is confirmed ENABLED (GET /repos/{owner}/{repo}/immutable-releases returned enabled=true). Every new Release is auto-flagged immutable; a failed asset upload or operator deletion permanently burns the tag (#233 prevention E). Surfaces as a ::warning:: annotation; does not block dispatch. Disable the setting or accept the failure mode knowingly before triggering. |
RELEASE_READINESS_IMMUTABLE_PROBE_DENIED |
The probe could not verify the "Immutable releases" setting — endpoint returned 401/403/Bad credentials (workflow token lacks scope or repo access is restricted). The setting may or may not be on; this is not a confirmation. Verify manually in Repo Settings → General → Releases before dispatching, or grant the workflow token sufficient scope. Surfaces as a ::warning:: annotation; does not block dispatch. |
| Code | Meaning |
|---|---|
RELEASE_PKG_ADR |
A numbered ADR file matched docs/adr/[0-9][0-9][0-9][0-9]-*.md in the candidate archive. |
RELEASE_PKG_INTAKE |
An enumerated intake folder shipped with state beyond a top-level README.md. |
RELEASE_PKG_DOC_STUB |
A docs/ page in the candidate archive is not in stub form. |
RELEASE_PKG_STUB_TEMPLATE_MISSING |
templates/release-package-stub.md is missing — Layer 2 cannot validate. |
These are not readiness diagnostics but gh API errors a release operator may see during dispatch or recovery; they map to documented incidents.
| Error | Likely cause | Recovery |
|---|---|---|
tag_name was used by an immutable release (HTTP 422) |
Repo Setting → "Immutable releases" was on; a prior Release on this tag was created (and possibly deleted) under that setting and burned the tag. | §7.7. The tag cannot host a new Release; ship a recovery release on vX.Y.(Z+1). |
Cannot upload assets to an immutable release (HTTP 422) |
The current Release was auto-flagged immutable; asset upload via the API is refused. | §7.7. For a published immutable Release, accept an asset-less Release or supersede on vX.Y.(Z+1) — GitHub blocks asset modification post-publish, so neither API nor UI upload can recover. Do not delete the Release. |
release already exists (HTTP 422 from gh release create) |
A Release for this tag exists from a prior dispatch (often a draft from the two-step CLAR-V05-003 path). | §7. Resume from the existing Release rather than rerunning the workflow as a recovery primitive — gh release create is not idempotent. |
- Workflow file:
.github/workflows/release.yml. - Release readiness library:
scripts/lib/release-readiness.ts— diagnostic codes are the contract. - Fresh-surface library:
scripts/lib/release-package-contract.ts. - Release branch + tag rules: ADR-0020,
docs/branching.md. - Released package shape: ADR-0021,
docs/release-package-contents.md. - Package contract:
specs/version-0-5-plan/package-contract.md. - Stage 10 companion artifact (optional):
docs/release-readiness-guide.md. - Requirements and spec:
specs/version-0-5-plan/requirements.md,specs/version-0-5-plan/spec.md. REQ-V05-008, REQ-V05-010, REQ-V05-011, NFR-V05-004, NFR-V05-005, SPEC-V05-006, SPEC-V05-008, SPEC-V05-009. - GitHub artifact attestations: https://docs.github.com/en/actions/concepts/security/artifact-attestations and https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations.
- npm provenance and trusted publishing: https://docs.npmjs.com/generating-provenance-statements/ and https://docs.npmjs.com/trusted-publishers/.
- npm trusted publishing setup: https://docs.npmjs.com/trusted-publishers/.
- ADR-0040 — Migrate npm publication from GitHub Packages to npmjs.com:
docs/adr/0040-migrate-npm-publication-to-npmjs-com.md. - npm SBOM command: https://docs.npmjs.com/cli/v10/commands/npm-sbom/.
- CycloneDX SBOM guidance: https://cyclonedx.org/guides/sbom/.