Skip to content

Latest commit

 

History

History
448 lines (321 loc) · 36.9 KB

File metadata and controls

448 lines (321 loc) · 36.9 KB
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

Release operator guide

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, run gh locally, and trigger workflow_dispatch in the GitHub Actions UI. It does not assume you wrote the v0.5 plan.

Authorization boundary. Cutting and merging the release/vX.Y.Z PR 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 explicit confirm input that matches the requested version (SPEC-V05-002, NFR-V05-001).

1. Before you start

You should already have:

  1. A merged release/vX.Y.Z PR on main per ADR-0020:
    • package.json#version equals X.Y.Z,
    • CHANGELOG.md has an unreleased-promoted heading for [vX.Y.Z],
    • specs/version-X-Y-plan/release-notes.md is finalised.
  2. The canonical tag vX.Y.Z cut on main after the merge (never on the release branch). The workflow uses gh release create … --verify-tag and refuses to fall back to auto-tagging.
  3. Green v0.4 quality signals available to the readiness check, surfaced through the RELEASE_* repository variables (or an explicit operator waiver via RELEASE_QUALITY_WAIVER). See scripts/lib/release-readiness.ts (QualitySignals) for the contract.
  4. npmjs.com Trusted Publisher configured for the specorator package against this repository's release.yml workflow on the release deployment environment (ADR-0044, restoring ADR-0040 after the ADR-0041 NPM_TOKEN fallback used for v0.7.0 / v0.7.1; tracking #411 closed). The id-token: write workflow permission mints the OIDC token consumed by npm publish --provenance; no long-lived NPM_TOKEN secret is required. The actions/attest-build-provenance step still uses the same OIDC permission to sign the GitHub Release tarball asset.
  5. 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 release to 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 in specs/version-0-5-plan/retrospective.md §Incident). Verify with gh 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's enforced_by_owner field 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.

2. Workflow inputs

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).

3. Pre-flight — Layer 1 readiness, locally

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 -- --json

A 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.

4. Dry run path

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:

  1. Go to Actions → Release → Run workflow.
  2. Inputs:
    • version: X.Y.Z
    • dry_run: true (default)
    • prerelease, draft, publish_package: leave defaults
    • confirm: leave empty
  3. 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.

5. Stable publish path

Only after at least one fully green dry run, request a stable publish.

  1. Trigger Actions → Release → Run workflow with:

    • version: X.Y.Z
    • dry_run: false
    • prerelease: false
    • draft: false
    • confirm: type the literal X.Y.Z (the confirm gate compares it to version and fails if they differ — SPEC-V05-002).
    • publish_package: true if you intend to publish the npm package to npmjs.com on this run; false if you only want to publish the Release this run.
  2. 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 runs gh 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 new vX.Y.(Z+1) instead.
    • npm publish --provenance — only when publish_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 --provenance flag mints a sigstore provenance statement that ships with the tarball and is visible on the npmjs.com package page under Provenance. The GitHub Release tarball asset also carries its own sigstore attestation from actions/attest-build-provenance above.
  3. 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.Z with a provenance badge linking to the GitHub Actions run (only if publish_package: true):

      npm view specorator@X.Y.Z --registry https://registry.npmjs.org
  4. 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 .npmrc configuration, 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.

5.1 Release provenance posture

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.

5.2 SBOM posture

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.

6. Rollback

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.

7. Failed publish recovery

Recoverability differs per step:

  • npm publish (the workflow's idempotency guard wraps npm view so a successful publish is detected on a rerun) — idempotent.
  • gh release upload --clobber — idempotent.
  • gh release createnot idempotent on its own, but as of #233 prevention C the workflow's "Create or promote GitHub Release" step now detects an existing Release via gh release view and switches to gh release edit + gh release upload --clobber for 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.

7.1 npm publish failed after gh release create succeeded

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.

7.2 gh release create failed before tag verification

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 HEADmain 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.Z

If 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 argumentgit tag defaults to HEAD and will silently mis-tag if main moved.

Then rerun the workflow.

7.3 Layer 1 readiness diagnostic

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.

7.4 Layer 2 fresh-surface diagnostic

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#files and 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/, or sales/ shipped non-empty — strip per-engagement state from the codebase before cutting, or update the manual stub-form step (OQ-V05-003 in package-contract.md is the open automation gap).
  • A docs/ page is not in stub form — restore the stub shape from templates/release-package-stub.md.

7.5 Confirm gate refused

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.

7.6 Operator waiver path (last resort)

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_QUALITY diagnostic; never Version, TagMissing, TagNotAtMain, ChangelogMissing, fresh-surface, or workflow-permissions diagnostics.
  • Must be recorded verbatim in specs/version-X-Y-plan/release-notes.md and the implementation log entry for the publish (REQ-V05-010 acceptance — explicit waiver).

A waiver with no recorded justification is a release defect.

7.7 Immutable-release failures (create-time tag burn vs upload-time asset refusal)

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 burngh release create itself fails with HTTP 422 tag_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 refusalgh release create succeeded but gh release upload (or the workflow's "Attach release asset" step) fails with Cannot 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:

  1. Accept an asset-less Release (only viable when the npm package was already published this dispatch — i.e. publish_package: true and 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 in specs/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 — because publish_package defaults to false and a draft / prerelease run typically skips it.
  2. 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.

8. Post-release cleanup

After a successful stable publish:

  1. Delete the release branch.

    git push origin --delete release/vX.Y.Z
    git branch -D release/vX.Y.Z   # or in the worktree

    Release branches are not reused per docs/branching.md.

  2. Close v0.4 quality RELEASE_* waivers if any were used. Reset the variables to their post-release state (green for pass-through, unset waiver).

  3. Record the publish in the implementation log. Append a Release published entry to specs/version-X-Y-plan/implementation-log.md naming: tag SHA, GitHub Release URL, package version URL, any rollback or waiver, the operator who triggered the run.

  4. Update the changelog. If CHANGELOG.md still has [Unreleased] content for items that shipped in vX.Y.Z, fold them into the released heading.

  5. Trigger Stage 11 (Retrospective) for the release feature folder via /spec:retro so the loop closes.

8.1 Stage 10 state while the tag is pending

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-progress until the irreversible action finishes,
  • add a dated ## Hand-off notes entry that starts with release-tag hold and 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.

9. Quick-reference command bundle

# 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)

10. Diagnostic codes — quick reference

The release-readiness scripts emit machine-stable codes. Treat them as the contract.

Layer 1 — scripts/lib/release-readiness.ts

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#namespecorator.
RELEASE_READINESS_PKG_REGISTRY publishConfig.registryhttps://registry.npmjs.org.
RELEASE_READINESS_PKG_REPOSITORY package.json#repositoryhttps://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.

Layer 1 warnings (informational, do not fail the gate)

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.

Layer 2 — scripts/lib/release-package-contract.ts

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.

Operator-facing GitHub API errors

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.

11. References