Build and release a new version of Erfana via the multi-platform CI release workflow. Enforces main-branch discipline, assembles two-tier release notes, pushes a signed tag, polls the release pipeline in GitHub Actions, cryptographically verifies every artifact, and gates the final publish on explicit operator approval. Use when Erfana is ready to ship.
3 stars
0 votes
0 copies
2 views
Added September 3, 2026
ai-agentsgoshellbashnodeazuregitapisecurity
Works with
terminal
cli
api
Security analysis
B84/100
criticalSends environment variables or credentials to an external URL
mediumInstalls packages at runtime which could introduce malicious dependencies
Installs into .claude/skills of the current project.
Are you the author of Releasing Erfana?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/qodeca-releasing-erfana)
---
name: releasing-erfana
description: Build and release a new version of Erfana via the multi-platform CI release workflow. Enforces main-branch discipline, assembles two-tier release notes, pushes a signed tag, polls the release pipeline in GitHub Actions, cryptographically verifies every artifact, and gates the final publish on explicit operator approval. Use when Erfana is ready to ship.
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, Task, AskUserQuestion, TodoWrite
capabilities:
- release-orchestration
- ci-monitoring
- artifact-verification
model: opus
user-invocable: true
---
# Releasing Erfana
Orchestrates the Erfana release flow that ends with **one GitHub release containing signed, notarized artifacts for Windows + macOS** (the Linux distribution target was dropped). The CI matrix (`.github/workflows/release.yml`) does all build, sign, notarize, verify, and draft-upload work. This skill handles pre-tag sanity, tag push, CI polling, local cryptographic verification, and the human approval checkpoint.
Detailed ops reference: [docs/build/release.md](../../../docs/build/release.md). Design anchor: #174 (pre-migration issue; no longer resolves on the public repo).
> **Note**: This skill is project-scope-only. The relative path `../../../docs/build/release.md` is intentional — moving the skill to user-scope (`~/.claude/skills/`) would break the doc reference and orphan the project-local `release-failure-analyzer` agent. See "Architectural exception" below.
## When this skill applies
Activate when the user says:
- "release Erfana"
- "prepare release"
- "cut a release"
- "ship v0.9.5" (or similar version)
- "new release"
Activate only when the working copy can reasonably be released.
**Anti-triggers — do NOT activate for:**
- Build/test troubleshooting (no signed tag involved)
- Dev-only tagging (e.g., `v0.0.0-dev`, internal milestones)
- Hotfix work to a version that hasn't been released
- Investigating CI without intent to ship
- Tag deletion or rollback of a published release (separate ops, not in skill scope)
> **Shell requirement**: Phase 4–5 bash uses array constructs and `<( ... )` process substitution. Run via `bash` (not `zsh`) — `#!/usr/bin/env bash` shebang. See `phases/phase-4-verify.md` §4.2 note.
## Prerequisites
| Dependency | Purpose | Check |
|-----------|---------|-------|
| `git` | Signed tag, push | `git --version` |
| `node` (≥24) | Read package.json, run git-cliff if installed via npx | `node --version` |
| `gh` ≥ 2.55.0 | Release polling, asset download, draft publish | `gh --version` (verified with 2.91.0. Not every field name in this skill's history exists on `gh release view` — `isLatest` never did; fall back to `gh api` for those, see `phases/phase-4-verify.md` § 4.6) |
| `git cliff` | Technical section for release notes | `git cliff --version` (skill will fall back to `npx git-cliff` if needed) |
| `minisign` | Verify `SHA256SUMS.minisig` | `minisign -v` |
| `sha256sum` | Recompute asset hashes locally | `command -v sha256sum` |
| `jq` | Parse `gh` JSON in §0.4 / §0.4.5 | `jq --version` |
All external credentials (Apple Developer, Azure Artifact Signing, minisign release keypair) live in **GitHub secrets** and never flow through the local machine. See [docs/build/release.md § Secrets](../../../docs/build/release.md#secrets) — the rotation calendar is the subsection directly below it.
## Constants
These values appear in multiple places. Update here first, then update `EXPECTED_ASSETS=4` in §0.4 — which uses the number as a **floor** (`-ge`) to classify a draft as `draft-ready`, not as an equality assertion — and the expected-set line `phase-4-verify.md` §4.6 shows the operator. Neither place hard-fails on an unexpected extra asset.
| Constant | Value | Note |
|---|---|---|
| Expected binary count | 2 | macOS-arm64 (`erfana-{version}-arm64.dmg`), Windows x64 NSIS (`erfana-{version}-setup.exe`) |
| Expected total asset count | 4 | 2 binaries + `SHA256SUMS` + `SHA256SUMS.minisig` |
| Phase 3 polling cadence | 240 s × 22 polls | 88 min ceiling for `release.yml` **running** time; polls observing `waiting` (approval gate) do not consume the budget |
| Per-leg stuck-leg threshold | 2700 s (45 min) | Single leg `in_progress` beyond this triggers warning. Does **not** fire for a leg in `waiting` — that is the approval gate, handled separately |
## Agents
| Agent | Purpose | Source | Used in |
|-------|---------|--------|---------|
| `release-quality-runner` | Enforce Phase 0 pre-flight checklist (branch, tree, version, CHANGELOG, tools, green `checks.yml`, secrets, workflow lint, electron-builder schema, Windows status snapshot) | shared (project override) | Phase 0 |
| `release-notes-drafter` | Emit two-tier release-notes markdown via `git cliff` + operator summary | shared (project override) | Phase 1 |
| `release-failure-analyzer` | On Phase 3 CI failure: identify failed leg, match log against the troubleshooting cookbook, write structured incident memo to `docs/release-incidents/` | project-local | Phase 3 (failure path) |
**`release-build-executor` is retired** (removed in #174). CI owns the build. The skill watches, verifies, and publishes.
## Quick reference
| Item | Value |
|------|-------|
| Release branch | `main` (skill refuses elsewhere) |
| Tag pattern | `v[0-9]+.[0-9]+.[0-9]+` (strict; pre-release suffixes rejected) |
| Version source | `package.json` → `"version"` |
| Release notes path | `docs/release-notes/v{version}.md` (two-tier with `<details>`) |
| CI workflow | `.github/workflows/release.yml` |
| Expected release assets | See `## Constants` above (2 binaries + 2 = 4 total) |
| Provenance attestations | **Not used** — off pending a deliberate pipeline change, not a plan restriction: the old blocker (Enterprise-only for private repos) expired when the repo went public on 2026-06-16. Authenticity covered by minisign + per-platform OS signing. |
| Minisign release pubkey | `docs/release-pubkey.txt` (canonical; `docs/security.md` § Release signing is the one mirror `checks.yml` Guard 5 enforces — `README.md` only links to the canonical file) |
## Critical enforcement rules (NON-NEGOTIABLE)
1. **Main only.** Phase 0 aborts if `git branch --show-current` is not `main`.
2. **Strict semver.** Phase 0 rejects anything other than `v[0-9]+.[0-9]+.[0-9]+`.
3. **Signed tags only.** Protected-tag rule on the remote enforces this; skill surfaces actionable errors if tag push is rejected.
4. **Verify before publish.** Phase 4 must complete minisign + per-file sha256 verification before the operator approval prompt is shown.
5. **No auto-publish.** Marking the draft as `--latest` requires explicit operator approval after verification is green.
6. **No bypass.** A verification failure in Phase 4 aborts — do not prompt for approval, do not suggest manual overrides. The release is burned; bump the patch.
7. **MUST delegate every executor step.** The quality checklist (Phase 0) and release notes drafting (Phase 1) go through agents. Documented exceptions (e.g., Phase 5.1 minisign re-verify) must be explicitly justified in-place with a `Rule #7 exception` comment block.
8. **Idempotency with honesty.** If a tag is already pushed, Phase 0 offers resume-to-Phase-3 (non-destructive) or delete-and-retry (destructive, explicit operator confirmation).
### Architectural exception (Rule #2 — project-local agent)
`release-failure-analyzer` is intentionally project-local at `.claude/agents/release-failure-analyzer.md` rather than `~/.claude/agents/`. Rationale: the agent's `<workflow>` step 4 has a hard structural contract with `guides/troubleshooting.md` (Erfana-specific cookbook format, six typed labels per row, platform classification, signature samples). Promoting it to user-scope without splitting the cookbook would require parameterizing a format invariant — a worse abstraction than today's honest project-local agent. Promoting it *with* the cookbook would scatter project-specific operational knowledge across user-scope, breaking the principle that a project skill is the single source of truth for project-specific operational knowledge. This exception is bounded to project skills with bundled domain cookbooks and should be revisited only if a second project develops a structurally-identical cookbook.
---
## Todo list (MANDATORY)
At release start, the skill MUST call `TodoWrite` once with all six phase entries pre-registered, each in `pending` status. The six entries are:
- **Phase 0:** Pre-flight (main-branch + semver + CHANGELOG)
- **Phase 1:** Release notes (two-tier)
- **Phase 2:** Signed tag + push
- **Phase 3:** Watch release workflow
- **Phase 4:** Verify + publish checkpoint
- **Phase 5:** Post-publish verification + summary
Each entry is one object in the `TodoWrite` array with `{content: "<phase summary>", status: "pending", activeForm: "<present-continuous form>"}`. As each phase begins, call `TodoWrite` again with that entry's status set to `in_progress`; on checkpoint pass, set it to `completed`. The list above provides the `content` field text — not literal source code.
---
## Phase 0: Pre-flight
### 0.1 Branch gate
```bash
BR=$(git branch --show-current)
if [ "$BR" != "main" ]; then
echo "FAIL: Release must run from main, got: $BR"
exit 1
fi
```
- [ ] Current branch is exactly `main` (no exceptions)
### 0.2 Working tree is clean
```bash
git fetch origin
git status --porcelain
```
- [ ] No uncommitted or untracked files (skill aborts otherwise)
- [ ] Local `main` is not behind `origin/main` (skill surfaces a clear message if so)
### 0.3 Version + CHANGELOG gate
Ask the operator (AskUserQuestion) for the **bump type** (patch / minor / major). The skill computes the proposed version from `package.json`. Then:
```bash
VERSION=$(node -p "require('./package.json').version")
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "FAIL: version '$VERSION' is not strict semver"
exit 1
fi
LAST_TAG=$(git tag --list 'v*' --sort=-v:refname | head -1)
# CHANGELOG must already contain a "## {version}" section for this release.
grep -q "^## $VERSION" docs/CHANGELOG.md || {
echo "FAIL: docs/CHANGELOG.md is missing a '## $VERSION' section"
exit 1
}
LOCK_VERSION=$(node -p "JSON.parse(require('fs').readFileSync('package-lock.json','utf8')).version")
if [ "$LOCK_VERSION" != "$VERSION" ]; then
echo "FAIL: package-lock.json says '$LOCK_VERSION', package.json says '$VERSION'"
echo "Remediation: npm install --package-lock-only --ignore-scripts, then commit the lock."
exit 1
fi
```
- [ ] `package.json` version is strict semver
- [ ] `package-lock.json` version matches `package.json` (both copies are rewritten by `npm install`)
- [ ] `docs/CHANGELOG.md` contains `## {version}` heading
- [ ] Proposed version > last tag
### 0.4 Idempotency check: does the tag already exist?
```bash
TAG_EXISTS_LOCAL=false
TAG_EXISTS_REMOTE=false
RELEASE_STATE="none" # one of: none | draft-empty | draft-ready | published
# Expected total asset count: see SKILL.md `## Constants` table
EXPECTED_ASSETS=4
if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then
TAG_EXISTS_LOCAL=true
fi
if git ls-remote --exit-code --tags origin "refs/tags/v${VERSION}" >/dev/null; then
TAG_EXISTS_REMOTE=true
fi
# If the remote tag is already pushed, classify the GitHub release state so
# we can pick the correct resume point (per spec issue #174 §Idempotency).
if [ "$TAG_EXISTS_REMOTE" = "true" ]; then
RELEASE_JSON=$(gh release view "v${VERSION}" --json isDraft,assets 2>/dev/null || echo "")
if [ -z "$RELEASE_JSON" ]; then
RELEASE_STATE="none" # tag pushed but release doesn't exist yet — run probably in flight
else
IS_DRAFT=$(printf '%s' "$RELEASE_JSON" | jq -r '.isDraft')
ASSET_COUNT=$(printf '%s' "$RELEASE_JSON" | jq -r '.assets | length')
if [ "$IS_DRAFT" = "false" ]; then
RELEASE_STATE="published"
elif [ "$ASSET_COUNT" -ge "$EXPECTED_ASSETS" ]; then
RELEASE_STATE="draft-ready" # 2 binaries + SHA256SUMS + .minisig = 4
else
RELEASE_STATE="draft-empty" # finalize hasn't yet sealed the draft
fi
fi
fi
```
If a tag already exists, branch on `RELEASE_STATE` and present options via `AskUserQuestion`:
**Case A — `RELEASE_STATE=none`** (tag on origin, no draft yet → run probably in flight):
| Option | Meaning | Risk |
|--------|---------|------|
| Resume at Phase 3 | Wait for CI run to finish, then verify. | Low |
| Bump to the next patch and restart | The tag ruleset forbids deleting `v*.*.*` (no bypass actors), so the version cannot be reused. Bump the patch and re-enter from Phase 1; `git tag -d v${VERSION}` clears only the local tag. | **DESTRUCTIVE** — voids any in-flight signed artifact. Require explicit confirmation. |
| Abort | Exit the skill. | None |
**Case B — `RELEASE_STATE=draft-empty`** (draft exists but `finalize` not yet complete):
Same as Case A — finalize is still in flight. Resume at Phase 3.
**Case C — `RELEASE_STATE=draft-ready`** (draft has all 4 expected assets, finalize completed):
| Option | Meaning | Risk |
|--------|---------|------|
| Resume at Phase 4 (verify + approve) | Skip Phase 3 polling; jump straight to cryptographic verification of the existing draft. | Low — Phase 4 is structurally re-entrant and idempotent (read-only verification, then operator approval). |
| Delete draft, bump patch, restart | `gh release delete "v${VERSION}" --yes --cleanup-tag=false`, then bump to the next patch and re-enter from Phase 1. The tag itself cannot be deleted (ruleset `deletion` rule, no bypass actors). | **DESTRUCTIVE** — voids the signed artifacts. Require explicit confirmation. |
| Abort | Exit the skill. | None |
**Case D — `RELEASE_STATE=published`** (release already published as latest):
```bash
URL=$(gh release view "v${VERSION}" --json url --jq .url)
echo "Release v${VERSION} is already published and marked latest: $URL"
echo "Nothing to do. Exiting."
exit 0
```
No options — exit cleanly. Re-running the skill on a published tag means the operator already approved publication; the only follow-up is a hotfix at the next patch version.
### 0.4.5 Branch protection allows direct push
The skill assumes a solo-developer direct-push workflow. If branch protection on `main` requires PRs, `git push origin main` (Phase 1.5) will be rejected and the skill cannot proceed. Detect this at Phase 0 rather than at tag-time.
**Direct push is not the same as unconditional push.** `main` also carries required status checks, and GitHub enforces them on *push*, not only on merge: a commit whose checks have not yet **completed successfully for that SHA** is rejected with `GH006 … 7 of 7 required status checks have not succeeded`. That is a timing condition, not a misconfiguration, and §1.5 handles it with the develop-first sequence. This gate only asserts the PR rule.
```bash
PROT=$(gh api repos/qodeca/erfana/branches/main/protection 2>/dev/null || echo '{}')
PR_REQ=$(printf '%s' "$PROT" | jq -r '.required_pull_request_reviews // {} | keys | length')
if [ "${PR_REQ:-0}" -gt 0 ]; then
echo "FAIL: main requires PR; skill assumes direct push (single-developer workflow)" >&2
echo "Remediation (one-time admin action):" >&2
echo " gh api -X DELETE \\" >&2
echo " repos/qodeca/erfana/branches/main/protection/required_pull_request_reviews" >&2
echo "Other protection rules (signed tags, required status checks) stay intact." >&2
exit 1
fi
```
- [ ] `required_pull_request_reviews` is unset on the `main` branch protection rule
### 0.4.6 Windows status snapshot is current
`docs/windows/implementation-plan.md` must be re-anchored on v{version} before tagging — script, semantics and traps in [`./guides/pre-flight-checks.md`](./guides/pre-flight-checks.md); also enforced as the agent's `windows_snapshot` gate.
### 0.5 Delegate the rest of the checklist to `release-quality-runner`
```
Task(subagent_type: "release-quality-runner",
prompt: "Run the Phase 0 release-readiness checklist for Erfana at {project_path}
on branch main. Do NOT re-run lint / typecheck / test / audit — they are
required status checks on main; assert the green checks.yml run instead.
Return: {
status: 'success'|'error',
gates: { <gate-key>: { result: 'PASS'|'FAIL'|'WARN'|'SKIP', ... } },
overall: 'pass'|'fail',
failures: string[],
warnings: string[]
}")
```
Any `fail` stops the skill. Warnings are surfaced to the operator, who can continue. **Retired:** this prompt used to ask the agent to check for *running dev servers*, and no agent gate replaced it. It was dropped deliberately with the #174 CI migration — nothing is built locally any more (every binary comes from `release.yml` on GitHub-hosted runners), so a local `electron-vite dev` cannot contaminate a release artifact, and the clean-tree gate already catches anything it wrote to disk.
### Checkpoint 0.A
**Ask operator:** "All pre-flight gates passed for v{version}. Proceed to draft the release notes?"
Options: **Proceed** / **Abort**.
---
## Phase 1: Two-tier release notes
### 1.1 Generate the technical section
```bash
# Requires cliff.toml at repo root.
git cliff --tag "v${VERSION}" --unreleased > .release-notes-technical.tmp.md
```
If `git cliff` is not installed, fall back:
```bash
npx -y git-cliff --tag "v${VERSION}" --unreleased > .release-notes-technical.tmp.md
```
### 1.2 Collect the user-facing summary from the operator
Subagents cannot call `AskUserQuestion` — it must be called from the skill. Use it now to collect 3–5 bullet points that describe the release in user-facing terms.
Present via `AskUserQuestion`: ask for 3–5 brief bullet points summarising what changed from the end user's perspective. Prompt text example:
> "Summarise v{VERSION} in 3–5 bullets for end users. Examples: 'Releases are now cryptographically signed on macOS and Windows' / 'Fixed a bug where terminal scroll lost the last line'. Skip developer-internal changes — the technical section below covers those from commit history."
Persist the operator's reply into a variable (e.g., `$USER_SUMMARY`) as a single markdown string containing the bullets. Do NOT invent bullets if the operator's reply is too short — re-prompt instead.
### 1.3 Delegate composition to the agent
```
Task(subagent_type: "release-notes-drafter",
prompt: "Compose two-tier release notes for Erfana v${VERSION}.
Inputs:
- version: ${VERSION}
- technical_section_path: .release-notes-technical.tmp.md
- user_summary: <pasted $USER_SUMMARY verbatim>
- output_path: docs/release-notes/v${VERSION}.md
Write the file with the exact template:
# Erfana v${VERSION}
_Released: <UTC YYYY-MM-DD>_
${user_summary}
<details><summary>Technical changes</summary>
<technical section contents>
</details>
Do NOT invent content beyond inputs. Do NOT call AskUserQuestion.
Return the composed content + output path.")
```
### 1.4 Operator review
Present the generated `docs/release-notes/v${VERSION}.md` and ask whether to accept, edit, or re-draft.
- [ ] Two sections present: user-facing summary and collapsible technical section
- [ ] Operator explicitly approved
### 1.5 Single-commit bundle
Pre-flight check before §1.5 commit bundle: see [`./guides/git-signing.md`](./guides/git-signing.md) (added per #174 reviewer finding — verifies `user.signingkey` and `gpg.format`; soft-warns on missing `gpg.ssh.allowedSignersFile`).
```bash
# The version lives in TWO files. `package-lock.json` carries its own copy of
# `version` (twice: root and the "" package entry), and editing package.json alone
# leaves it behind — v0.19.0 shipped with the lock still reading 0.18.0. The field
# is metadata and changes no dependency resolution, so nothing fails; it just
# rewrites itself under the next person who runs `npm install`, as a stray diff
# they did not ask for. `--package-lock-only` touches no node_modules.
npm install --package-lock-only --ignore-scripts
# One commit bundles: package.json bump (already done pre-skill or done here),
# the lockfile's copy of it, CHANGELOG append (pre-skill), release notes file,
# and the root CLAUDE.md "Current version" banner, which carries a third copy of
# the version and is easy to leave behind (nothing fails if it drifts, so only a
# reader notices). `git add` tolerates an unchanged path, so listing it is safe
# even when the bump shipped earlier.
git add package.json package-lock.json docs/CHANGELOG.md \
"docs/release-notes/v${VERSION}.md" CLAUDE.md
# Pick the commit message based on what is actually staged. If the bump
# already shipped earlier (e.g., develop→main merge), this commit only
# adds the release-notes file — labelling it "bump version" would be a
# false description of the diff.
if git diff --cached --name-only | grep -qx 'package.json'; then
COMMIT_MSG="chore(release): bump version to ${VERSION}"
else
COMMIT_MSG="docs(release): add release notes for v${VERSION}"
fi
git commit -S -m "$COMMIT_MSG"
# Develop-first, then main. A push of this brand-new SHA straight to `main` is
# rejected — `GH006 … 7 of 7 required status checks have not succeeded` — because
# required checks are enforced on push and none has run for the SHA yet. Landing
# the same commit on `develop` first produces that SHA's checks.yml run; once it
# is green, the identical SHA is accepted on `main` immediately. Observed twice
# on the v0.20.0 release; see docs/build/release.md § Branch protection.
git push origin main:develop
TIP_SHA=$(git rev-parse HEAD)
# Poll until BOTH runs for this SHA read completed/success (≈4 min; 20 s interval).
# Wait on the runs, not on the seven check contexts: the advisory Windows job
# keeps the run in_progress for ~3 min after the contexts go green, and `prepare`
# asserts the *run*. On failure: abort, fix, re-enter Phase 0.
gh run list --branch develop --limit 10 \
--json name,status,conclusion,headSha \
--jq "[.[] | select(.headSha==\"$TIP_SHA\")] | .[] | \"\(.name): \(.status) \(.conclusion)\""
git push origin main
```
If `main` has new commits on `origin` (raced), re-fetch and confirm with operator before retrying.
### Checkpoint 1.A
- [ ] Commit is on `origin/main` (`chore(release): bump version to {version}` if `package.json` was in the staged diff, else `docs(release): add release notes for v{version}`)
- [ ] The same SHA is on `origin/develop`, so the two integration branches do not diverge over release-prep commits
- [ ] Both `checks.yml` and `secret-scan.yml` runs for this SHA reached `completed`/`success` **before** the push to `main` (the push cannot succeed otherwise) and therefore before tagging, which is what the release workflow's `prepare` job asserts
---
## Phase 2: Signed tag + push
### 2.1 Create the signed tag
```bash
git tag -s "v${VERSION}" -m "Release v${VERSION}"
```
If tag creation fails because no signing key is configured: **stop**. Direct the operator to configure SSH/GPG signing (`git config user.signingkey`, `commit.gpgsign true`, `tag.gpgsign true`).
### 2.2 Push the tag
```bash
# One tag at a time — not `git push --tags`. Only the first tag on a
# bulk push reliably fires the release workflow (actions/runner#3644).
git push origin "v${VERSION}"
```
If the push is rejected by the protected-tag rule, surface the exact rejection message and stop.
### Checkpoint 2.A
- [ ] Tag `v{version}` exists on `origin` and is signature-verified
---
## Phase 3: Watch release.yml
Full instructions live in [`phases/phase-3-watch.md`](phases/phase-3-watch.md) — Phase 3 uses a polling pattern (4-minute cadence, 22-poll ceiling on running time) rather than a foreground `gh run watch`. **The run pauses indefinitely at the `production-signing` environment approval before either build leg starts**; that wait is unbounded and must not count toward the ceiling. Reasons: `timeout` is GNU-only (missing on macOS by default), and the orchestrator's per-tool budget cannot span the full pipeline anyway.
Summary table:
| Step | What | Why it's required |
|---|---|---|
| 3.1 | Resolve `RUN_ID` from the tag's dereferenced commit SHA, with retry loop | `release.yml` may take up to 60 s to appear after the push |
| 3.2 | Poll `gh run view --json status,conclusion` every 240 s, hard ceiling 22 polls of non-`waiting` state | Foreground watch exceeds the orchestrator's 600 s Bash budget; 240 s polls keep the prompt cache warm |
| [3.2a](phases/phase-3-watch.md) | On `status=waiting`, surface the pending `production-signing` approval to the operator, then keep polling (re-announcing hourly) without consuming budget | The gate is unbounded (2 h 22 min on v0.17.0); counting it down reports a healthy release as failed |
| 3.3 | On non-success terminal state, dispatch `release-failure-analyzer` with run id + memo path | Structured incident memo to `docs/release-incidents/`; cookbook-driven fix |
⛔ **Failure aborts.** A failed `release.yml` burns the tag — next attempt requires a patch bump.
### Checkpoint 3.A
- [ ] `RUN_ID` resolved per `phases/phase-3-watch.md` §3.1
- [ ] Polling reached `completed/success`, OR
- [ ] Failure handed to `release-failure-analyzer` with run id + memo path
- [ ] Tag-burn rule observed on retries (new patch version)
---
## Phase 4: Verify + publish checkpoint (CRITICAL)
Full instructions live in [`phases/phase-4-verify.md`](phases/phase-4-verify.md) — Phase 4 is the longest phase, contains the three independent cryptographic gates that are the entire point of this skill, and is the most likely to be read standalone (during release audits, post-incident forensics, or by an operator who only needs to verify a draft). Other phases are short enough to live inline in this file.
Summary table:
| Step | What | Why it's required |
|---|---|---|
| 4.1 | `gh release view --json isDraft` returns `true` | Sanity: `release.yml` produced a draft, not a published release |
| 4.2 | `gh release download` all assets to a temp dir | Local material for verification |
| 4.3 | `minisign -V` over `SHA256SUMS` + `.minisig` | Proves the sums were signed by the release minisign key |
| 4.4 | `sha256sum` every asset, `diff` against `SHA256SUMS` | Proves each asset matches what was signed |
| 4.5 | `gh run download --name sha256sums-digest` + `diff -q` against the asset | Catches post-`finalize` tampering of the draft asset |
| 4.6 | `AskUserQuestion` — Publish + mark latest / Leave as draft / Abort and delete | Explicit operator approval; no auto-publish |
⛔ **Any failure in 4.3–4.5 aborts before the operator is asked.** Do not prompt for approval, do not suggest manual override. The release is burned; bump the patch.
### Checkpoint 4.A
- [ ] Steps 4.1–4.5 all green per `phases/phase-4-verify.md`
- [ ] Operator explicitly chose Publish or Leave-as-draft via `AskUserQuestion`
- [ ] Release visibility matches operator's choice
---
## Phase 5: Post-publish verification + summary
### 5.1 Re-verify the now-public release
> **Rule #7 exception (inline executor work).** The 3 commands below are intentionally inline rather than delegated to an agent. They re-run the Phase 4.3 minisign check against the now-public URL — pure read+verify, zero mutations, identical crypto operation to the agent-delegated check just performed. Wrapping in a new agent for 3 commands adds maintenance burden without diagnostic improvement.
```bash
# Re-download and re-verify minisign on the published release URL.
PUBLISHED=$(gh release view "v${VERSION}" --json url --jq .url)
gh release download "v${VERSION}" --repo qodeca/erfana --pattern 'SHA256SUMS*' --clobber --dir "$WORK/published"
minisign -V -P "$(cat "$WORK/release-primary.pub")" \
-m "$WORK/published/SHA256SUMS" -x "$WORK/published/SHA256SUMS.minisig"
```
### 5.2 Final summary
Present:
- Published URL
- Artifact list with sizes
- Verification log (all green)
- Rotation reminder if any of Apple API key / Azure cert / minisign key are within 60 days of expiry (skill reads the calendar stored alongside the secrets table in `docs/build/release.md`)
- Next steps: announce internally, update release tracker, etc.
---
## Anti-patterns
See [`./guides/anti-patterns.md`](./guides/anti-patterns.md) for the full Don't/Do table.
---
## Reference files
- [`guides/anti-patterns.md`](guides/anti-patterns.md) — Don't/Do table for release-day patterns
- [`guides/examples.md`](guides/examples.md) — Worked examples (success, lockfile-drift, hash-mismatch)
- [`guides/git-signing.md`](guides/git-signing.md) — Pre-flight git signing check (Phase 1.5)
- [`guides/pre-flight-checks.md`](guides/pre-flight-checks.md) — Phase 0.4.6 Windows status-snapshot gate
- [`guides/troubleshooting.md`](guides/troubleshooting.md) — failure recovery and rollback procedures
- [`templates/release-notes.md`](templates/release-notes.md) — two-tier release-notes template
- [`docs/build/release.md`](../../../docs/build/release.md) — full operator reference (matrix, secrets, incident response)
---
## Examples
See [`./guides/examples.md`](./guides/examples.md) for worked examples (successful release, lockfile-drift abort, hash mismatch).