Release-cycle runbook
Complete release workflow from local gates through the signed GitHub Release to the npm publish.
This runbook drives a vX.Y.Z release of apothem from a clean
working tree to verified distribution surfaces. The procedure is written
for the maintainer wielding gh, git, and the release-tier shell; it
terminates with a verified Release page, a filed CHANGELOG, and an
install-path smoke confirming each user-facing surface answers the
canonical install command.
The release model is additive. Each release adds one commit on main and
one signed, annotated vX.Y.Z tag; nothing is force-pushed and no earlier
tag, GitHub Release or npm version is removed. Every release keeps its tag,
its GitHub Release and its npm version, and all stay published: npm versions
are immutable (a published name@version can never be replaced), and pinned
installs depend on the tags.
The tag push starts three workflows. .github/workflows/release.yml builds,
signs, attests, and publishes the GitHub Release with its full artifact set:
sdist, wheel, CycloneDX SBOM, Sigstore cosign bundles, SLSA-3 provenance,
and the platform runtime archives. publish-npm.yml publishes
@ahmed-g-gad/apothem to npmjs.com through npm trusted publishing, and
publish-vscode.yml packages the VS Code extension and publishes it to the
Visual Studio Marketplace. The
versioned CHANGELOG section lands in the release commit; the operator runs
the install-path smoke after both workflows finish.
1. Prerequisites
The maintainer confirms the following before tagging:
-
Working tree. Clean (
git statusshows nothing pending). All feature work for the release is onmain. -
CI green. Latest
maingh run list --branch=main --limit=1 --json conclusion --jq '.[0].conclusion'returnssuccess. -
Tooling.
gh(GitHub CLI) version 2.40 or later authenticated againstahmed-g-gad;git2.40 or later with the GPG signing subkey loaded; the npm trusted-publisher binding for@ahmed-g-gad/apothemis live (verified per the publisher-account-setup runbook). -
Version anchors aligned. The version string declared at
pyproject.tomlmatches the planned tag without thevprefix (§2.1 sets it, with every other anchor). Verify:grep '^version' pyproject.tomlExpected output (for a
vX.Y.Zrelease; placeholders stand for the in-flight release tag):version = "X.Y.Z" -
CHANGELOG entries staged. The release bullets sit under
## [Unreleased]in Keep-a-Changelog headings; §2.1 turns them into the dated version section in the commit that lands the tag.
2. Tag and trigger
2.1 Author the release commit
The version lives in more than a dozen files, so do not edit them by hand. One command moves all of them:
git checkout main
git pull --ff-only origin main
python scripts/release/bump_version.py X.Y.Zbump_version.py updates every version anchor together:
-
Every packaging, extension, plugin and marketplace manifest, plus the citation file (version and release date). The full list:
pyproject.toml package.json site/package.json site/package-lock.json (the project entries only) vscode-extension/package.json gemini-extension.json qwen-extension.json .claude-plugin/marketplace.json (top level and metadata) plugins/apothem/plugin.json plugins/apothem/.codex-plugin/plugin.json CITATION.cff -
CHANGELOG.md: the[Unreleased]entries become a dated## [X.Y.Z]section,[Unreleased]compares fromvX.Y.Z, and a[X.Y.Z]link is added. -
SECURITY.md: a new minor gets a supportedX.Y.xrow. Whether older minors stay listed is your decision; the script prints a reminder.
It then regenerates the derived trees: the Claude Code plugin package, the
behavior-diff goldens, and the docs reference and changelog pages. Pass
--dry-run to list the changes first, --date YYYY-MM-DD to set the release date (default: today in UTC), or --no-regen
to skip the regeneration. The script refuses a malformed version or a
downgrade, and re-running it with the current version only re-aligns stray
anchors.
Review the diff, then commit:
git add -A
git commit -S -m "chore(release): cut vX.Y.Z"
git push origin mainThe -S flag signs the commit with the GPG key. The push triggers
the standard CI matrix (lint, test, build); wait for green before
tagging.
The readiness check in §2.4 also needs CodeQL and Conformity green on the
release commit. Both run only when their own paths change, and a release
commit usually changes none of them, so start them once the release commit is
the head of main, then confirm each run names that commit:
gh workflow run codeql.yml --ref main
gh workflow run conformity.yml --ref main
gh run list --workflow codeql.yml --commit "$(git rev-parse HEAD)" --limit 1
gh run list --workflow conformity.yml --commit "$(git rev-parse HEAD)" --limit 1Wait for both to succeed before tagging, and push nothing else to main until
the release is out: a later push moves main away from the release commit, and
CI cancels a run still in progress on the same branch.
2.2 Sign and push the tag
git tag -a -s vX.Y.Z -m "vX.Y.Z"
git push origin vX.Y.ZThe -a -s combination produces an annotated, GPG-signed tag. The
push triggers .github/workflows/release.yml, which builds, signs,
attests, and publishes.
2.3 Confirm the release workflow completed
gh run watch $(gh run list --workflow=release.yml --limit=1 --json databaseId --jq '.[0].databaseId')The workflow runs build (sdist + wheel, then a CycloneDX SBOM of those
distributions written beside them), keyless cosign signing, SLSA-3 provenance generation and
verification, platform archive builds (darwin / linux / windows), archive
signing with an aggregated SHA256SUMS manifest, and the final GitHub
Release publish. The maintainer scans the publish GitHub Release job
log for the asset-upload confirmations.
The publish GitHub Release job runs in the release environment, which
requires the maintainer's approval, so the run waits there once the earlier
jobs pass. Approve it on the run's page (Review deployments), or from the
shell:
run_id=$(gh run list --workflow=release.yml --limit=1 --json databaseId --jq '.[0].databaseId')
env_id=$(gh api repos/ahmed-g-gad/apothem/environments/release --jq .id)
gh api -X POST "repos/ahmed-g-gad/apothem/actions/runs/${run_id}/pending_deployments" \
-F "environment_ids[]=${env_id}" -f state=approved -f comment="vX.Y.Z"The tag push also starts the npm and VS Code extension workflows. Their publish jobs wait for the same approval until §2.4.
2.4 Confirm the npm publish
The tag push also runs publish-npm.yml beside release.yml. The npm
workflow does not wait for the release workflow; the release environment
approval is the gate. Once the release workflow from §2.3 has finished,
confirm readiness. The check reads releases, workflow runs and the published
SBOM, so set up two inputs first:
GH_TOKEN. The script refuses to run without it, even whenghis already logged in. Use a fine-grained personal access token limited toahmed-g-gad/apothem, with Actions: Read (workflow runs) and Contents: Read (releases). The check writes nothing, so no write permission is needed. A token fromgh auth tokencarries every scope of the login, so prefer the fine-grained token.- The published SBOM. The check reads
dist/release-assets/sbom.cdx.json, the CycloneDX file the release attaches. Download the copy the release published rather than a local build.
read -rs GH_TOKEN && export GH_TOKEN
mkdir -p dist/release-assets
gh release download vX.Y.Z --repo ahmed-g-gad/apothem \
--pattern sbom.cdx.json --dir dist/release-assets --clobber
bash scripts/release/check-release-ready.sh vX.Y.ZThe check fails when the release is a draft or a prerelease, when a required workflow for the tag's commit is not green, when the SBOM is missing, when it describes another version, or when it omits a vendored package. The failure message names the cause.
When the check passes, approve the waiting Publish npm run the same way as
in §2.3 (its run ID comes from gh run list --workflow=publish-npm.yml). The
workflow publishes @ahmed-g-gad/[email protected] to npmjs.com through npm
trusted publishing (OIDC); no registry token is stored in the repository.
Approve the waiting Publish VS Code Extension run as well. It packages
apothem.vsix and publishes it to the Visual Studio Marketplace as
ahmed-g-gad.apothem-vscode. The publish step runs only while the repository
variable VSCODE_MARKETPLACE_READY is true and one credential is
configured: the variable VSCE_AUTH=oidc (Marketplace Trusted Publishing, no
stored secret), the variables VSCE_AUTH=entra, AZURE_CLIENT_ID and
AZURE_TENANT_ID (a short-lived Microsoft Entra ID token, no stored secret),
or the VSCE_PAT secret. With none, the run packages the extension and skips
publication. The credential is a switch, not a retry: a failed publish on one
route does not fall back to another by itself. The setup for each route is in
Marketplace publishing with Trusted Publishing
(preferred) and
Marketplace publishing with Microsoft Entra ID
below.
To publish again after a failed run, dispatch the workflow for the same tag
(gh workflow run publish-npm.yml --field tag=vX.Y.Z). It skips publication
when the version is already on the registry, because npm versions are
immutable.
Marketplace publishing with Trusted Publishing
Trusted Publishing lets the Marketplace accept this repository's GitHub OIDC
token directly: vsce publish --oidc exchanges the publish job's token for a
short-lived Marketplace credential. There is no stored secret and no Azure
resource, so this is the preferred route. The Entra route below and the
VSCE_PAT secret stay as alternatives that the operator selects.
-
Use a vsce release that supports it. On this route the publish job checks the vsce release locked in
tools/vscebefore it publishes, and stops with an error that names the minimum release when the lock is older. Dependabot proposes vsce updates for that lockfile; merge the one that reaches the minimum before you switch. -
Create the policy. Sign in as an Owner of the publisher, open
https://marketplace.visualstudio.com/manage/publishers/ahmed-g-gad, and add a policy under Trusted Publishing:- Policy name: any name, such as
apothem-release - Repository owner:
Ahmed-G-Gad - Repository:
apothem - Workflow:
publish-vscode.yml - Environment:
release
The field names are the ones the VS Code publishing guide lists, in its section on publishing with GitHub Actions and Trusted Publishing (
https://code.visualstudio.com/api/working-with-extensions/publishing-extension). Unconfirmed: whether every publisher sees the Trusted Publishing tab yet. If yours does not, use the Entra route. - Policy name: any name, such as
-
Switch the route. Keep
VSCODE_MARKETPLACE_READYset totrueand set the selector. Set it when no tag is waiting, because the next tag push publishes through this route.gh variable set VSCE_AUTH --body oidc -
Publish. The next tag push, or a dispatch for an existing tag, publishes through Trusted Publishing. This route has no dry run: the first publish is the check, and a dispatch with
dry_runonly warns that nothing was verified. -
Fall back if it fails. Delete the selector and dispatch again for the same tag; with the
VSCE_PATsecret set, the token route publishes. A failed publish published nothing, so the same tag can be dispatched again.gh variable delete VSCE_AUTH
Marketplace publishing with Microsoft Entra ID
The Entra route replaces the stored personal access token with a short-lived
token. publish-vscode.yml asks GitHub for an OIDC token, the Azure/login
action exchanges it for an Entra sign-in through a federated credential, and
vsce publish --azure-credential publishes with the resulting token. No client
secret exists. The VSCE_PAT secret stays as a fallback that the operator
selects; the workflow never switches to it on its own.
The vsce release pinned in tools/vsce supports this route. It needs an Entra
tenant, an application registration and publisher membership, which Trusted
Publishing above does not; use it when Trusted Publishing is unavailable to the
publisher.
-
Check the preconditions. Find the account that owns the publisher
ahmed-g-gad(the Members tab athttps://marketplace.visualstudio.com/manage/publishers/ahmed-g-gad) and the Entra tenant where you can register applications. Unconfirmed: whether the publisher owner's tenant must match the application's tenant, and whether the Azure DevOps organizationAhmed-G-Gadmust be connected to Entra (dev.azure.com/Ahmed-G-Gad, Organization settings, Microsoft Entra). No source states either. If step 5 or 6 fails, check these first. -
Register the application. In
https://entra.microsoft.com, open App registrations, then New registration. Name itapothem-vscode-publish, choose a single tenant, and leave the redirect URI empty. From Overview, copy the Application (client) ID and the Directory (tenant) ID. Create no client secret and grant no API permission. The step needs application-owner rights or an Entra role such as Application Administrator. -
Add the federated credential. On that application, open Certificates & secrets, Federated credentials, Add credential, and choose the scenario "GitHub Actions deploying Azure resources". Enter organization
Ahmed-G-Gad, repositoryapothem, entity typeEnvironment, environment namerelease, and credential nameapothem-release. The generated fields must read:issuer: https://token.actions.githubusercontent.com subject: repo:Ahmed-G-Gad/apothem:environment:release audience: api://AzureADTokenExchangeMatching is exact and case-sensitive. If a later sign-in fails with AADSTS700213 or AADSTS7002138, compare the credential with the
subject claimline thatAzure/loginwrites to the job log. AADSTS70021 right after creation means the credential has not propagated yet; wait a few minutes and retry. -
Set the repository variables. While
VSCODE_MARKETPLACE_READYistrue, the Entra route is active as soon as all three are set, so set them when no release tag is waiting for approval. WithoutVSCODE_MARKETPLACE_READY=truenothing signs in and the run skips the publish.gh variable set AZURE_CLIENT_ID --body "<application-client-id>" gh variable set AZURE_TENANT_ID --body "<directory-tenant-id>" gh variable set VSCE_AUTH --body entraLeave
VSCODE_MARKETPLACE_READYandVSCE_PATas they are. WhenVSCE_AUTHisentrabutAZURE_CLIENT_IDorAZURE_TENANT_IDis empty, the workflow logs a warning, whether or notVSCE_PATis set, and does not use the Entra route. It then uses theVSCE_PATsecret if that is set; with no secret it publishes nothing and the run still ends green, so look for the warning. -
Run the dry run. Dispatch the workflow for an existing release tag with
dry_runset. On the Entra route it signs in, prints the Azure DevOps profile, and verifies publisher access without publishing. The token route has no dry run: if the log showsMarketplace credential mode: pat, the run ends green with a warning and has verified nothing. Approve the waitingreleaseenvironment run.gh workflow run publish-vscode.yml --ref main -f tag=vX.Y.Z -f dry_run=trueExpected:
Marketplace credential mode: entra, asubject claimline equal to the subject in step 3, and the profileidanddisplayNameprinted by the first dry-run step. The verify step is expected to fail with a 403 until step 6. Unconfirmed: that a sign-in without an Azure subscription yields a token for the Marketplace through the Azure CLI. This run is the first test of it. If it fails here, register a user-assigned managed identity in a subscription instead, which is the route Microsoft documents. -
Add the application to the publisher. In the Members tab from step 1, sign in as a publisher Owner and add a member using the profile
idfrom step 5, with the role Contributor. Unconfirmed: the dialog's field label, and whether it accepts the id before the application is a member of an Azure DevOps organization. Azure DevOps adds application identities per organization as a general rule. If the dialog rejects the id, add the service principal to the organizationAhmed-G-Gad(Organization settings, Users, using the Enterprise application Object ID) and retry. Also unconfirmed: whether the profile exists before the identity's first Azure DevOps call. -
Repeat the dry run. The verify step now succeeds with "verification succeeded for the publisher"; its messages still say Personal Access Token, because the Entra token travels the same path. The check proves publisher membership (Creator, Owner, Contributor or Reader), not publishing rights. Unconfirmed: that Contributor is enough to publish an update. Microsoft's role description says it is, and no source shows a publish with it, so the first real publish is the proof.
-
Publish a release. Tag as in §2.2 and approve the run. The step
Publish to Visual Studio Marketplace (Entra ID)runs, its environment header lists onlyAZURE_TENANT_ID, the token fallback step is skipped, and vsce printsDONE Published ahmed-g-gad.apothem-vscode. The Marketplace gallery lists the new version after about four minutes. -
Decide on the token. Keep
VSCE_PATuntil the first successful Entra publish, then delete the secret if no fallback is wanted. Once it is gone, a deleted or empty Entra variable leaves the run green with no publish: with a warning whenVSCE_AUTHis stillentra, and with only the skip notice when it is not. Read the run's annotations after each tag. To keep a fallback past the retirement of global personal access tokens, create an organization-scoped token forAhmed-G-Gadwith the custom scope Marketplace Manage and the shortest expiry you accept, and store it asVSCE_PAT. Unconfirmed: the portal labels in this step are from memory, not from a primary source; Microsoft calls organization-scoped tokens a temporary workaround and can stop honoring them; and Microsoft's pages disagree on the retirement date of global tokens. -
Fall back to the token. Delete the selector and dispatch again, then approve the run. A failed Entra publish published nothing, so the same tag can be dispatched again; check the Marketplace page first, because a version that landed cannot be published twice.
gh variable delete VSCE_AUTH gh workflow run publish-vscode.yml --ref main -f tag=vX.Y.Z
When the Entra publish fails, read the step log:
- A message that begins "Can not acquire a Microsoft Entra ID access token"
means the sign-in or the credential chain failed. Check the
Sign in to Microsoft Entra IDstep and the federated credential first. If the sign-in is green, removeAZURE_TENANT_IDfrom the vsce step'senvin the workflow; GitHub's own extension release workflow does not export it. - "InvalidAccessException" or "not allowed" means the token's identity is not a
publisher member. Compare the profile
idfrom the dry run with the Members tab. - A 403 from the dry-run verify step means the application has no role yet.
Three limits apply to this route:
id-token: writeis granted to the whole publish job, so every step in it can request an OIDC token, in token mode too. The token's subject is bound to thereleaseenvironment, which requires a reviewer's approval.- The
releaseenvironment has no deployment branch or tag policy, so any ref a maintainer approves can obtain the environment-subject token. Restricting it tov*.*.*tags needs a check of whatrelease.ymlandpublish-npm.ymldispatch first. - A repository rename or transfer changes the OIDC subject format and silently
breaks the federated credential. Copy the subject from the
Azure/loginlog into the credential again after any rename.
2.5 Refresh the template-digest pin
After the tag exists, set _LATEST_RELEASE and _LATEST_RELEASE_DIGESTS in
tests/unit/test_template_digests.py to the new release and the SHA-256 digest
of each plain-text template as that release shipped it
(git show vX.Y.Z:<template path> | sha256sum). Do it in the next change to
main, and add the previous digest to the manifest entry's earlier_digests
when the template changed. The release-preflight CI job checks out every tag,
so it fails with the tag names until the pin is refreshed.
3. Distribution Surfaces
Each surface has a verification command answering "did the release land here?":
| # | Surface | Artifact | Verification |
|---|---|---|---|
| 1 | GitHub Release (Python distributions) | apothem-X.Y.Z.tar.gz (sdist) + apothem-X.Y.Z-py3-none-any.whl | gh release view vX.Y.Z --json assets --jq '.assets[].name' includes both filenames |
| 2 | GitHub Release (SBOM) | sbom.cdx.json + sbom.cdx.json.cosign.bundle | asset list includes both; the SBOM's metadata.component.purl is pkg:pypi/[email protected] and its components include every package in src/apothem/_vendor/vendor.txt |
| 3 | GitHub Release (signatures) | one <asset>.cosign.bundle per sdist, wheel, SBOM, VS Code extension, platform archive and SHA256SUMS (earlier releases name the archive and SHA256SUMS bundles <asset>.sig) | cosign verify-blob with the exact identity in §5.5 exits 0 |
| 4 | GitHub Release (provenance) | provenance.intoto.jsonl | bash scripts/release/verify-provenance.sh <download-dir> exits 0 (§5.5) |
| 5 | GitHub Release (platform archives) | apothem-vX.Y.Z-darwin.tar.gz, apothem-vX.Y.Z-linux.tar.gz, apothem-vX.Y.Z-windows.zip | asset list includes every archive plus SHA256SUMS. The darwin and linux archives are the same bytes under two names (the runtime is platform-neutral; --platform only picks tar.gz or zip), so their SHA256SUMS lines show one digest twice |
| 6 | GitHub Release (VS Code extension) | apothem.vsix + apothem.vsix.cosign.bundle | asset list includes both; code --install-extension apothem.vsix installs it. Releases before the release workflow packaged the extension carry neither |
| 7 | npmjs.com | @ahmed-g-gad/[email protected] | npm view @ahmed-g-gad/apothem version returns X.Y.Z; npx @ahmed-g-gad/[email protected] --version prints Apothem, version X.Y.Z |
| 8 | Pages (script installers) | install.sh / install.ps1 on the docs site | curl -fsSL https://apothem.ahmedgad.com/install.sh | head -n 1 returns the script header |
| 9 | Visual Studio Marketplace | ahmed-g-gad.apothem-vscode version X.Y.Z | the extension's Marketplace page lists X.Y.Z; code --install-extension ahmed-g-gad.apothem-vscode installs it |
The GitHub Release is the canonical artifact surface; the npm package and the script installers are the install paths layered on top of it. The npm publish runs beside the release workflow on the same tag push.
4. CHANGELOG discipline
The CHANGELOG.md carries one section per release under the
Keep-a-Changelog convention. The release commit's CHANGELOG edit:
- Confirms the version section uses the
## [X.Y.Z] - YYYY-MM-DDheading (a hyphen;bump_version.pywrites it, andextract_release_notes.pymatches the## [X.Y.Z]prefix). - Confirms the standard headings — Added, Changed, Deprecated, Removed, Fixed, Security — are used where applicable. Empty headings are dropped from the rendered file.
- Captures capabilities, not implementation. Domain language only; no plan-internal references; no commit hashes; no plan or phase identifiers.
A CHANGELOG section that survived the maintainer's review reads as the release-notes anchor for the announcement narrative.
5. Install-path smoke
After both stages complete, the maintainer runs the smoke. The smoke is the canonical verification that the user-facing install commands answer.
5.1 Claude Code plugin
/plugin marketplace add ahmed-g-gad/apothem
/plugin install apothem@apothemExpected: the plugin installs and apothem commands are available inside
Claude Code.
5.2 Node.js (npx)
npx @ahmed-g-gad/[email protected] --versionExpected output: Apothem, version X.Y.Z.
5.3 One-shot script installer (POSIX)
The installer writes the apothem launcher to ~/.local/bin, or to the
directory named by APOTHEM_BIN_DIR when that variable is set. The check
below needs that directory on PATH. When it is not, the installer prints the
export PATH line to add. Until that directory is on PATH, the shell
cannot find apothem, and the check fails. If the installer cannot place the
launcher, it prints a warning and no export PATH line. On a host that does
not hold the maintainer signing key yet, the first line imports it; the
installer stops without it.
curl -fsSL https://github.com/ahmed-g-gad.gpg | gpg --import
curl -fsSL https://apothem.ahmedgad.com/install.sh | sh
apothem --versionExpected output: Apothem, version X.Y.Z.
5.4 One-shot script installer (Windows)
The first line imports the maintainer signing key through Git's own bash,
whose gpg keyring is the one git verify-tag reads.
& "$env:ProgramFiles\Git\bin\bash.exe" -c 'curl -fsSL https://github.com/ahmed-g-gad.gpg | gpg --import'
irm https://apothem.ahmedgad.com/install.ps1 | iex
apothem --versionExpected output: Apothem, version X.Y.Z.
5.5 Artifact verification (supply-chain evidence)
Download every asset into an empty directory, then verify. The certificate
identity is the release workflow at this exact tag. Identity checks are
case-sensitive, and the certificates carry the owner as GitHub reports it,
Ahmed-G-Gad, so a lowercase owner fails.
mkdir release-vX.Y.Z && cd release-vX.Y.Z
gh release download vX.Y.Z --repo ahmed-g-gad/apothem
cosign verify-blob \
--bundle apothem-X.Y.Z-py3-none-any.whl.cosign.bundle \
--certificate-identity https://github.com/Ahmed-G-Gad/apothem/.github/workflows/release.yml@refs/tags/vX.Y.Z \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
apothem-X.Y.Z-py3-none-any.whl
slsa-verifier verify-artifact \
--provenance-path provenance.intoto.jsonl \
--source-uri github.com/Ahmed-G-Gad/apothem \
--source-tag vX.Y.Z \
apothem-X.Y.Z-py3-none-any.whl apothem-X.Y.Z.tar.gz
bash ../scripts/release/verify-provenance.sh .Expected: Verified OK, PASSED: SLSA verification passed, and
verify-provenance: chain OK (3 of 3 layers verified). The script checks
SHA256SUMS, every signature bundle, and the provenance in one run; it
derives the tag from the wheel name (--tag vX.Y.Z overrides it).
The smoke produces a one-line PASS / FAIL row per surface; record the rows in the maintainer's release log.
6. Failure recovery
The release workflow has documented failure modes for the release engine and the npm publisher:
| Failure | Diagnostic | Recovery |
|---|---|---|
| Tag push rejected (signing failure) | git push origin vX.Y.Z returns error: failed to push some refs with a GPG-related message | Verify the GPG subkey is loaded (gpg --list-secret-keys --keyid-format=long); re-tag locally with -s after fixing the keyring; re-push |
Release build stops at Check the plugin version changed with the plugin | The build job log names the plugin and the previous tag; no asset was built or published | The plugin package changed since the previous release but its version did not. Delete the tag (git push --delete origin vX.Y.Z, git tag -d vX.Y.Z), run python scripts/release/bump_version.py X.Y.Z, commit, then sign and push the tag again (§2.2). The same check runs on every pull request as a warning annotation, so this normally surfaces before tagging |
release.yml job fails on a single stage | gh run view <run-id> --log names the failing job (build, which also writes the SBOM / sign / provenance / archive / publish) | Re-run the failed job alone via gh run rerun <run-id> --failed; if the failure repeats, fix the job's input at its source before retagging |
| SLSA provenance verification fails | The verify SLSA provenance artifact job exits non-zero | Inspect the provenance artifact for subject-digest mismatch; rebuild from the same tag — provenance binds to the exact artifact digests, so any artifact regeneration requires a full workflow rerun |
| npmjs.com publish rejected | The npm job exits non-zero during npm publish; a registry 404 during PUT is an authorization / trusted-publisher mismatch, not proof that the package code is missing | Verify npm Trusted Publishing for @ahmed-g-gad/apothem names owner ahmed-g-gad, repository apothem, workflow publish-npm.yml, and allowed action npm publish; confirm the job runs with the Node version and npm CLI OIDC floor pinned in the workflow |
| Tag visible but assets missing | gh release view vX.Y.Z returns a stub release | Re-run the release workflow's publish job; it edits the existing release and re-uploads every asset. Delete and re-tag only when nothing from the tag reached users (no assets, no npm version); otherwise fix forward with the next patch release (release-recovery runbook §2) |
If a recovery cycle fails to land a surface within one diagnostic pass, the surface is logged as a known-failure in the release notes with a link to the open issue; downstream users are pointed to the working surfaces while the broken one is fixed in a patch release.
7. Cross-references
- Publisher setup. The publisher-account-setup runbook establishes the npm trusted-publisher binding and the GitHub-side prerequisites this runbook assumes.
- Recovery flow. The release-recovery runbook follows the same additive model: it recovers a failed tag or publisher stage by stage and rolls back by releasing the fix as a new patch version, never by deleting or re-pointing a published tag, release or npm version.
- Pages enablement. The
publish-static-site.ymlworkflow validates and deploys the project website to the custom domain; the Pages-enablement runbook (site/content/docs/runbooks/pages-enablement.mdx) is the upstream procedure that established the custom domain in the first place. - Versioning policy.
site/content/docs/governance/release-engineering-policy.mdxcarries the SemVer + signing + deprecation policy this runbook honors.