IronClaw Release Runbook¶
Operational guide for cutting, verifying, and yanking an IronClaw release.
Owner: Relay (Release Engineer). Scope: .github/workflows/release.yml,
.github/workflows/image.yml, scripts/install.sh, scripts/install.ps1,
scripts/update-homebrew-formula.sh, Formula/ironclaw.rb.
This runbook documents the pipeline as it ships on main. Where a section names an
in-flight ticket (e.g. the post-release smoke gate), that capability is landing
separately and the runbook is updated when it merges.
Trust model (non-negotiable). A release a user cannot verify is not a secured release. Every published set is checksummed, the checksum file is signed keylessly with cosign, and every archive carries a build-provenance attestation tied to the source commit and workflow. Never weaken or skip signing, checksums, or attestation to make a build go green — yank a bad release instead (see Yanking a release).
1. Pipeline at a glance¶
A release is cut automatically on every push to main (and can be run manually). Three
GitHub Actions workflows are involved:
| Workflow | File | Trigger | Produces |
|---|---|---|---|
| CI | ci.yml |
every push + PR | build / vet / test with CGO_ENABLED=1 (gating check) |
| Release | release.yml |
push to main, or workflow_dispatch |
tag, GitHub Release, archives, SHA256SUMS, SBOMs, cosign signature, attestations |
| Image | image.yml |
workflow_run after Release completes (every blocking Release job green — see 3.5), or workflow_dispatch |
GHCR control-plane image (ghcr.io/<owner>/ironclaw-controlplane) + image attestation |
The Release workflow runs as a chain of jobs, each gated on the previous:
version ──> build (5-target matrix) ──> release ──> [smoke]
│ │ │ │
derive tag CGO build per OS/arch checksums, install via install.sh
(skip if archive + upload GH Release, on each target, assert
already SBOM, cosign "Checksum OK" + version
tagged) sign, attest (fail-closed gate)
buildusesfail-fast: falsebutreleaseneeds: [version, build], so a release is published only if every matrix target built — no partial release sets.- All post-publish steps (SBOM, cosign signature, attestation) run after the binaries and checksums are uploaded, so a Sigstore/tooling hiccup cannot block the artifacts from shipping. See Partial-failure semantics — this is the one case that needs an operator decision.
2. How the version tag is derived¶
The version is v<BASE_VERSION>.<total commit count>, e.g. v0.1.123.
BASE_VERSIONis hard-coded in theversionjob ofrelease.yml(currently0.1). Bump it there to roll the major/minor (e.g. set0.2to start thev0.2.xline).- The patch number is
git rev-list --count HEAD— the total number of commits reachable from the released commit. It is monotonic, needs no manual bumps, and ties the tag to one exact commit. - The resolved tag is stamped into the binaries at build time via
-ldflags "-X github.com/IronSecCo/ironclaw/internal/version.Version=<tag>", soironctl versionreports the exact release tag. (Unstamped/source builds reportdev.)
Idempotency / re-run safety. The version job checks whether the tag already exists
(git rev-parse --verify refs/tags/<tag>). If it does, exists=true and the build,
release, and smoke jobs are all skipped (if: needs.version.outputs.exists == 'false').
This means re-running Release on a commit that is already released is a safe no-op — it
will not overwrite or duplicate an existing release.
Consequence for yanking. Because the patch number is the commit count, the only way to
get the same tag again is to release the same commit. If you delete a tag and push a new
commit, the count increments and you get a new tag — you cannot accidentally collide with
a yanked tag's number. If you delete a tag and re-run Release on the same commit, it
rebuilds cleanly (exists is false again).
3. How to cut a release¶
3.1 The normal path (automatic)¶
Merge to main. The Release workflow fires on push: branches: [main], derives the next
v0.1.<count> tag, builds the matrix, and publishes the Release + tag at the merged commit.
No manual action is required. Releases are serialized (concurrency: group: release,
cancel-in-progress: false) so two pushes never race or orphan a tag.
Watch the run:
A green Release run means: all five archives built, SHA256SUMS written and signed, SBOMs
and the cosign signature/cert attached, and provenance attested. The Image workflow then
chains off the success and publishes the GHCR image for that commit.
3.2 Manual dispatch (re-cut or pin a specific tag)¶
Use workflow_dispatch when you need to re-run a release or stamp a specific tag:
# Auto-derive the tag (same as a push to main):
gh workflow run release.yml
# Override the tag explicitly (e.g. to re-cut after a yank, or hotfix a specific number):
gh workflow run release.yml -f version=v0.1.99
If the supplied/derived tag already exists, the run is a safe no-op (see §2).
3.3 The build matrix (must stay in sync with the README)¶
| OS / arch | Runner | C toolchain |
|---|---|---|
darwin/amd64 |
macos-14 |
clang, cross via CGO_CFLAGS/LDFLAGS=-arch x86_64 on the universal SDK |
darwin/arm64 |
macos-14 |
native clang |
linux/amd64 |
ubuntu-latest |
native gcc |
linux/arm64 |
ubuntu-latest |
cross aarch64-linux-gnu-gcc (apt: gcc-aarch64-linux-gnu) |
windows/amd64 |
windows-latest |
native mingw-w64 gcc |
Every target builds with CGO_ENABLED=1, Go 1.23 — CGO is mandatory because the
encrypted-SQLite (SQLCipher) binding compiles a vendored C amalgamation. A pure-Go assumption
will break the build. If you change the matrix, confirm each target still compiles with cgo,
and update the README's Platform support / Installation tables to match — a platform that
silently drops out of the matrix is a release defect.
3.4 What a successful release contains¶
Attached to the GitHub Release for tag <tag> (version <ver> = tag without the leading v):
ironclaw_<ver>_<os>_<arch>.tar.gz(and.zipfor Windows) — one per matrix target. Each archive holdsironctl,ironclaw-controlplane,ironclaw-sandbox,LICENSE,README.md.SHA256SUMS— checksums of every archive (the trust anchor).SHA256SUMS.sig+SHA256SUMS.pem— the keyless cosign signature and its certificate.ironclaw_<ver>.spdx.json+ironclaw_<ver>.cdx.json— SBOMs (syft, SPDX + CycloneDX).- Build-provenance attestations for each archive and for each raw binary
(
ironctl,ironclaw-controlplane,ironclaw-sandboxon every platform), sogh attestation verifyworks whether you point it at the downloaded.tar.gz/.zipor at a binary extracted from it.
3.5 Post-release verification gate (smoke — in flight, IRO-15)¶
A smoke job installs the freshly-cut release through the real, checksum-verifying
scripts/install.sh (the normal user path, not --dev) on linux/amd64, linux/arm64,
darwin/arm64, and darwin/amd64, asserts the installer printed Checksum OK, and asserts
ironctl version reports the exact tag. It is fail-closed: a failure turns the whole
Release run red, which also blocks the Image workflow (smoke is a blocking job for the
image chain — see below). A red smoke run on an already-published release is the signal to
yank (the assets are out by that point). This gate lands with IRO-15.
What a failing Release job does to the Image chain¶
The Image workflow used to chain on the Release run's aggregate conclusion == 'success',
which made every Release job a hard gate on the whole publish chain. A flaky formula job — a
cosmetic Homebrew bump that runs long after the release is cut, smoke-tested and signed — was
therefore able to silently suppress the control-plane image, ironclaw-mcp, their SBOM and
provenance attestations, and the MCP Registry publish, with no red anywhere obvious (IRO-621).
image.yml's prepare job now inspects the upstream Release run per job and is
fail-closed by default — every job blocks the image unless it is explicitly advisory:
| Release job | Blocks the image? |
|---|---|
version, build, release |
Yes — these produce the released artifacts |
containment-report, smoke, smoke_windows |
Yes — these are the yank signals for a bad release |
formula |
No — post-release packaging metadata the image does not consume |
A failing formula job still turns the Release run red and still writes its manual runbook to
the job summary; it simply no longer suppresses a security-critical publish. If a blocking
job fails, the Image run now fails loud with the offending job names rather than skipping
silently. To make another job advisory, add it to ADVISORY in that step — and justify it,
because the default is "blocking".
A blocking job passes the gate only when the jobs API reports it status=completed and
conclusion in success / skipped / neutral. A job that is still queued/in_progress
reports conclusion: null, and a null is read as a block, not a pass (IRO-626): this gate
only runs for a Release run GitHub already called completed, so a not-completed job means the
jobs API is lagging the event, and publishing on it would be publishing off a release whose
smoke we never actually watched pass.
3.6 Bump the Homebrew formula (after a release you want brew install to track)¶
brew install ironsecco/ironclaw/ironclaw is served by Formula/ironclaw.rb in this repo, tapped
with brew tap IronSecCo/ironclaw https://github.com/IronSecCo/ironclaw. The formula pins each
platform archive to the SHA-256 recorded in that release's signed SHA256SUMS — the same
trust anchor install.sh uses — so a brew user gets the same checksum-verified bytes.
Name collision (important). homebrew-core ships an unrelated formula also named
ironclaw, and core wins the bare name. Always install/verify the fully-qualifiedironsecco/ironclaw/ironclaw— a barebrew install ironclawfetches the core package, not ours. The explicit tap URL above is also required (the tap is this repo, not ahomebrew-ironclawrepo).
Because a new release is cut on every push to main, the formula is intentionally pinned, not
auto-tracking: it points at one specific tag and is bumped deliberately. There is no CI job
that pushes to main to do this — that would need a branch-protection bypass, which we do not
grant. Bump it with the generator instead, which reads the published SHA256SUMS and never
invents a checksum:
# Pin the formula to a specific release (or omit the tag for the latest):
scripts/update-homebrew-formula.sh v0.1.123
# Review, then commit + open a PR (it goes through the normal required checks):
git add Formula/ironclaw.rb
git commit -m "chore(brew): bump formula to v0.1.123"
gh pr create --fill
Verify locally before merging (requires Homebrew):
brew style Formula/ironclaw.rb # lint
# install through a throwaway tap and run the test block:
TAP="$(brew --repository)/Library/Taps/ironsecco/homebrew-ironclaw"
mkdir -p "$TAP/Formula" && cp Formula/ironclaw.rb "$TAP/Formula/"
brew install ironsecco/ironclaw/ironclaw && brew test ironclaw # asserts `ironctl version`
brew uninstall ironclaw; rm -rf "$TAP"
You don't have to bump on every push — refresh the formula on releases you want brew users to
land on. Pinning to an old/yanked tag is fail-safe: the URLs 404 and brew install errors rather
than installing something unverifiable.
3.7 Container image tags: what the Image workflow will and won't publish¶
An image tag is an assertion about the source inside the image, so image.yml refuses to
publish a tag it cannot back with the commit it just built. Two rules, both fail-closed:
The :<version> tag must name the commit being built. On the normal workflow_run
path the version is read from the tag the Release workflow put on that exact commit, so it
agrees by construction. On a workflow_dispatch that supplies the tag input, prepare
resolves the tag through the commits API and requires it to equal the build commit:
# Correct: the workflow ref and the tag input name the same commit.
gh workflow run image.yml --ref v0.1.404 -f tag=v0.1.404
# Equivalent and harder to get wrong — blank input auto-detects that commit's tag.
gh workflow run image.yml --ref v0.1.404
Dispatching from main with -f tag=v0.1.403 now fails the run with both SHAs printed,
rather than minting an image whose tag names a commit it does not contain. This matters
beyond cosmetics: publish-mcp-registry stamps the image ref into an MCP Registry version,
and registry versions are immutable — a wrong ref there can only be deprecated, never
corrected.
:latest moves forwards only. It is applied only when the built commit is the current
default-branch tip (compare status identical). A dispatch that rebuilds an older release,
or builds from a side branch, publishes its immutable :<version> tag alone. Without this,
re-running the Image workflow on an old tag would silently demote every
docker pull …:latest consumer to older code. A commit that is neither the branch tip nor
tagged has no meaningful tag to publish, and the run fails rather than guessing.
Repairing a mis-tagged image. Note the consequence of the two rules together: an already
mis-stamped :<version> cannot be repaired by dispatch, because workflow_dispatch runs the
workflow file from the ref it builds — the only workflow file at an old tag is the one that
predates these guards. Rebuilding from that tag would restore the version tag but demote
:latest on the way. Prefer to withdraw a mis-tagged image (record it below, let the next
release supersede :latest) over publishing a second known-wrong tag to fix the first.
Withdrawn image tags:
| Tag | Built from | Should have been | Disposition |
|---|---|---|---|
ironclaw-controlplane:v0.1.403, ironclaw-mcp:v0.1.403 |
9ac92b64 (the v0.1.404 commit) |
9ae3b9be |
Withdrawn, do not consume (IRO-625). Published by dispatch run 30408078319 before the tag gate existed. The build-provenance attestation is honest and records 9ac92b64, so gh attestation verify on these tags will truthfully report the v0.1.404 commit — the tag, not the provenance, is the lie. Superseded by v0.1.404 and later. |
4. How to verify a release (user-facing)¶
This is the procedure a user — or you, post-release — runs to prove a release is trustworthy.
It is also documented in the README's Verifying a release section. The trust chain is:
cosign signature → SHA256SUMS → your archive, plus an independent provenance attestation.
Download the archive for your platform, plus SHA256SUMS, SHA256SUMS.sig, and
SHA256SUMS.pem from the release.
Step 1 — verify the signature over SHA256SUMS (keyless cosign; no key to manage).
The signing identity is the release workflow's OIDC identity, not a long-lived key:
cosign verify-blob SHA256SUMS \
--signature SHA256SUMS.sig --certificate SHA256SUMS.pem \
--certificate-identity-regexp '^https://github.com/IronSecCo/ironclaw/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
This proves SHA256SUMS was produced by the IronClaw release workflow and not tampered with.
Step 2 — verify your archive against the (now-trusted) checksum file:
Step 3 — verify build provenance (ties the artifact to the source commit + workflow):
# the downloaded archive:
gh attestation verify ironclaw_<ver>_<os>_<arch>.tar.gz --repo IronSecCo/ironclaw
# a binary extracted from it (e.g. after `tar xzf`):
gh attestation verify ./ironctl --repo IronSecCo/ironclaw
# and the container image:
gh attestation verify oci://ghcr.io/ironsecco/ironclaw-controlplane:<tag> --repo IronSecCo/ironclaw
A green verify is necessary, not sufficient — check which build it attests
Attestations are keyed by digest, are append-only, and are backed by an
immutable transparency log. So one digest can carry more than one statement, and
gh attestation verify exits 0 if any of them verifies. On success it prints
nothing at all (gh 2.95.0) — the exit code alone will not tell you how many
statements it accepted, or which build they credit.
So ask explicitly. Count them, and verify subject correspondence — that the run
claiming the build is the run whose build output this actually is. Our pipeline
publishes an immutable :<version> tag naming the index each run built, so the
version tag is the anchor:
IMAGE=ghcr.io/ironsecco/ironclaw-controlplane
# 1. How many statements, and which run(s) claim this image?
gh attestation verify "oci://${IMAGE}:<tag>" --repo IronSecCo/ironclaw --format json \
| jq -r 'length as $n | "statements: \($n)", (.[].verificationResult.statement
| " \(.predicate.runDetails.metadata.invocationId) subject=sha256:\(.subject[0].digest.sha256)")'
# 2. The immutable version tag for that run's release must resolve to that same subject.
docker buildx imagetools inspect "${IMAGE}:<version>" --format '{{ .Manifest.Digest }}'
(The invocation id lives at runDetails.metadata.invocationId. There is no
runDetails.invocation.id — that path silently yields null, which reads as
"no claim" rather than as a typo.)
A statement whose run published a different :<version> digest than the subject it
claims is not evidence about this image, however green it verifies. See
runbooks/mcp-registry.md
for a worked example and the disposition of a known instance, and
One of our container images carries two green provenance statements. Only one of them is true.
for the full write-up: how the wrong digest got signed, why it cannot be retracted, the two
checks an outsider can run to tell the two statements apart, and what to go check on your
own pipeline.
Step 4 — confirm the sandbox was proven contained for this version (IRO-267).
Every release ships a signed containment report — ironclaw_<ver>.containment.json
(machine-verifiable) and ironclaw_<ver>.containment.txt (human-readable) — generated by
running the red-team escape harness
against the released commit. It lists every isolation invariant, the assertion that proved
it, and pass/fail, bound to the commit and the runtime tested. Verify it the same way you
verified the binaries, using its own checksum file:
# Download ironclaw_<ver>.containment.json, .txt, CONTAINMENT-SHA256SUMS(.sig/.pem).
cosign verify-blob CONTAINMENT-SHA256SUMS \
--signature CONTAINMENT-SHA256SUMS.sig --certificate CONTAINMENT-SHA256SUMS.pem \
--certificate-identity-regexp '^https://github.com/IronSecCo/ironclaw/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
sha256sum -c CONTAINMENT-SHA256SUMS # macOS: shasum -a 256 -c CONTAINMENT-SHA256SUMS
# Then read the verdict: overall must be "contained", commit must match this release.
jq '{version, commit, overall: .summary.overall, invariants: [.invariants[] | {id, verdict}]}' \
ironclaw_<ver>.containment.json
# The report files also carry build-provenance attestations:
gh attestation verify ironclaw_<ver>.containment.json --repo IronSecCo/ironclaw
A report whose summary.overall is "contained" means every core containment invariant
held for that commit. (The release pipeline would have failed the run — and the release
would have been yanked — if a core assertion had failed, so a published release always
carries a contained report.)
The installers verify automatically. scripts/install.sh and scripts/install.ps1
download SHA256SUMS and refuse to install on a checksum mismatch (install.sh prints
Checksum OK on success and dies on mismatch; install.ps1 throws on mismatch). The
installers do not perform the cosign/attestation/containment-report steps — run those
manually (Steps 1, 3 & 4) when you need full supply-chain assurance beyond the checksum.
4a. Reproducing the published Linux binaries from source (IRO-127)¶
The strongest guarantee a reproducible-builds reviewer wants is to rebuild from the source
commit and get bit-for-bit the same artifacts the release published — proving the binaries
contain nothing that isn't in the public source. IronClaw's linux/amd64 and linux/arm64
artifacts are reproducible this way because the release builds them inside a digest-pinned
container (golang:1.23.12-bullseye), so the Go compiler/linker and the gcc/glibc that
compile the vendored SQLCipher C amalgamation are byte-identical for every rebuilder — not
whatever patch a given ubuntu-latest runner happened to ship. (The bullseye base also floors
the binary's glibc requirement at 2.31 so it runs on every mainstream server LTS — IRO-192.)
Same commit → same toolchain → same bytes.
Scope. Only the Linux targets carry this guarantee. macOS and Windows archives are built natively on the host (different SDK /
zipimplementation) and are not bit-reproducible across machines — verify those via the cosign signature and provenance attestation (Section 4) instead.
The builder image is pinned by digest in one place each in release.yml (the build job's
container:) and reproducibility.yml (the verify job's container:). To roll the toolchain,
bump both to the new golang:<ver>-bullseye@sha256:<digest> and update the command below.
To reproduce linux/amd64 (use linux/arm64 by swapping GOARCH + adding the cross-gcc):
# 1. Check out the EXACT released commit (the tag points at it).
git clone https://github.com/IronSecCo/ironclaw && cd ironclaw
git checkout v0.1.<n> # the release tag you are verifying
TAG=v0.1.<n>
# 2. Build the binaries inside the same digest-pinned container the release used.
docker run --rm -v "$PWD":/src -w /src \
golang:1.23.12-bullseye@sha256:161b8513c09cbfa4c174fd32e46eddc5eddf487a43958b9cf8b07d628e9e0f85 \
bash -c '
set -euo pipefail
git config --global --add safe.directory /src # root-in-container reads the mounted .git
export CGO_ENABLED=1 GOMAXPROCS=1 GOOS=linux GOARCH=amd64
go mod download all
ldflags="-s -w -buildid= -X github.com/IronSecCo/ironclaw/internal/version.Version='"$TAG"' -extldflags=-Wl,--build-id=none"
mkdir -p dist/pkg
for c in controlplane ironctl sandbox; do
out=ironctl; [ "$c" = controlplane ] && out=ironclaw-controlplane; [ "$c" = sandbox ] && out=ironclaw-sandbox
go build -trimpath -ldflags "$ldflags" -o "dist/pkg/$out" "./cmd/$c"
done
cp LICENSE README.md dist/pkg/
# Deterministic archive (fixed mtime from the commit, sorted, numeric-0 owner, gzip -n):
SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)"
tar --sort=name --mtime="@${SOURCE_DATE_EPOCH}" --owner=0 --group=0 --numeric-owner \
-cf - -C dist/pkg . | gzip -n > "ironclaw_${TAG#v}_linux_amd64.tar.gz"
sha256sum "ironclaw_${TAG#v}_linux_amd64.tar.gz" dist/pkg/iron*'
Compare to the release. The printed archive hash must equal the linux_amd64 row of the
published SHA256SUMS, and each printed binary hash must equal what gh attestation verify
./<binary> --repo IronSecCo/ironclaw attests. A match proves the published linux/amd64 set is
reproducible from source; a mismatch means either you built a different commit/tag or a
non-pinned input crept into the release — investigate before trusting it.
This is enforced in CI. reproducibility.yml builds all three commands twice inside the
pinned container on two different host runner images (ubuntu-22.04 + ubuntu-24.04) and
fails the build unless the binaries and the deterministic archive are byte-identical across
hosts — i.e. it tests cross-machine reproducibility on every PR and weekly, not just
same-runner determinism.
5. Partial-failure semantics (operator decision)¶
The release job uploads the binaries + SHA256SUMS first, then attaches SBOMs, the cosign
signature, and attestations. This ordering guarantees a tooling outage can't withhold the
binaries — but it means a failure after publish can leave a release that is published but
not fully signed/attested. That is not a verifiable release.
If a Release run goes red after the Publish release step:
- Check which post-publish step failed (
gh run view <run-id> --log-failed). - Re-run the failed job (
gh run rerun <run-id> --failed). The signing/SBOM/attest steps--clobbertheir uploads, so re-running is safe and idempotent and will complete the set. - If re-running cannot complete the signature/attestation (e.g. Sigstore is down for an extended window), yank the release rather than leave an unverifiable set published. Do not advertise or chain an image off a partially-signed release.
Never hand-sign or hand-upload a SHA256SUMS.sig from a local key — signing is keyless/OIDC
by design; there is no long-lived signing secret. If you encounter one, stop and escalate.
6. Yanking a release¶
Yank when a published release is bad: a smoke failure, a broken/partially-signed artifact, a critical defect, or a wrong tag. Prefer a yanked release over a misleading green one.
TAG=v0.1.123 # the bad tag
# 1. Delete the GitHub Release AND its tag (so install.sh can no longer resolve it).
gh release delete "$TAG" --yes --cleanup-tag
# (equivalently: gh release delete "$TAG" --yes && git push origin ":refs/tags/$TAG")
# 2. If the yanked release was marked --latest, repoint "latest" to the last good release
# so `install.sh` (default: latest) stops serving the bad one.
gh release edit <previous-good-tag> --latest
# 3. Remove or repoint the GHCR image tags built from the bad commit.
# Delete the version tag, and repoint :latest to the last good image if needed.
# (GHCR package versions are managed under the repo owner's Packages settings / API.)
gh api -X DELETE "/orgs/IronSecCo/packages/container/ironclaw-controlplane/versions/<version-id>"
Notes:
- Deleting the tag frees the
v0.1.<count>number only for the same commit (see §2). To ship a fix, push the fix tomain; the commit count increments and a fresh tag is cut. To re-cut the same commit after fixing tooling (not code), delete the tag and re-run Release —existsisfalseagain and it rebuilds cleanly. install.sh/install.ps1default to the GitHub latest release, so Step 2 is what actually stops new installs of a yanked build. Users who pinnedIRONCLAW_VERSION=<bad tag>will get a clean "no asset / release not found" error once the release is deleted — which is the intended fail-closed behavior.- Announce the yank (and the replacement tag) wherever releases are tracked, and record it on the triggering issue.
7. Pausing & resuming the pipeline¶
If prebuilt releases need to be paused (the README may carry a "paused" banner directing users to build from source), pause/resume cleanly without deleting the workflow:
gh workflow disable release.yml # stop auto-cutting releases on push to main
gh workflow disable image.yml # (optional) also stop image publishes
# ...resume:
gh workflow enable release.yml
gh workflow enable image.yml
When the pipeline is paused, the README directs users to build from source. When you resume, update the README's Installation / Verifying a release notes to drop the "paused" banners so the one-liner install path is advertised again.
8. Required status checks & branch protection¶
main is intended to be protected by enforced required checks. The spec lives at
.github/rulesets/main.json (build + CodeQL required, linear history, signed commits,
no force-push/deletion). Applying that file as an active GitHub ruleset is tracked
separately (IRO-14); ratcheting protection up (more enforced checks)
is the default direction. Never relax or disable a required check to unblock a merge — fix the
check on its own ticket instead.
9. Quick command reference¶
# Watch the current release
gh run list --workflow=release.yml --limit 5
gh run watch <run-id>
# Manually cut / re-cut
gh workflow run release.yml # auto-derive tag
gh workflow run release.yml -f version=v0.1.99
# Re-run a failed (post-publish) release job — idempotent
gh run rerun <run-id> --failed
# Republish a container image (dispatch FROM the tag; see 3.7 — a tag input that
# does not name the built commit fails the run, and :latest only moves forwards)
gh workflow run image.yml --ref v0.1.99
# Inspect a release
gh release view <tag>
gh release view <tag> --json assets -q '.assets[].name'
# Yank
gh release delete <tag> --yes --cleanup-tag
gh release edit <previous-good-tag> --latest
# Bump the Homebrew formula to a release (omit the tag for the latest)
scripts/update-homebrew-formula.sh v0.1.123
# Verify (user path)
cosign verify-blob SHA256SUMS --signature SHA256SUMS.sig --certificate SHA256SUMS.pem \
--certificate-identity-regexp '^https://github.com/IronSecCo/ironclaw/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
sha256sum -c SHA256SUMS
gh attestation verify ironclaw_<ver>_<os>_<arch>.tar.gz --repo IronSecCo/ironclaw
gh attestation verify ./ironctl --repo IronSecCo/ironclaw # extracted binary
Related tickets: pipeline handoff IRO-12; this runbook
IRO-16; arm64 image IRO-13; ruleset enforcement
IRO-14; release smoke test IRO-15; Homebrew formula IRO-175;
image tag/:latest gating IRO-625.