Skip to content

wish: release-pipeline-collapse — collapse workflow_run chain into workflow_call orchestrator#1743

Merged
namastex888 merged 16 commits into
devfrom
wish/release-pipeline-collapse
May 11, 2026
Merged

wish: release-pipeline-collapse — collapse workflow_run chain into workflow_call orchestrator#1743
namastex888 merged 16 commits into
devfrom
wish/release-pipeline-collapse

Conversation

@namastex888
Copy link
Copy Markdown
Contributor

Release pipeline collapse — workflow_call orchestrator

Closes wish release-pipeline-collapse.

Summary

Replaces the broken workflow_run-chained release pipeline (build-tarballs.ymlsign-attest.ymlrelease-publish.yml, suppressed by the GITHUB_TOKEN anti-recursion guard) with a workflow_call-orchestrated chain. release.yml is repurposed as a thin orchestrator that calls the three reusable workflows in one synchronous run. Cosign signing stays in sign-attest.yml (Decision #1) so the OIDC SAN URI binary tarballs ship with — sign-attest.yml@<ref> — is unchanged; install.sh:24 keeps working without modification. 7 stale release.yml@-pinned verifier references are repinned to sign-attest.yml@ in the same PR (G4).

Commits in this PR

Group Subject
G1 feat(release): add workflow_call triggers to build-tarballs/sign-attest/release-publish (G1)
G2 feat(release): repurpose release.yml as workflow_call orchestrator (G2)
G3 feat(release): version.yml dispatches release.yml orchestrator (G3)
G4 fix(security): repin cosign cert identity to sign-attest.yml@ (G4 — verifier-coherence cleanup)
G7 docs(changelog): document v4.260510.5 abandonment + verify cosign single-owner (G7)
G6 docs(release): runbook + tag-orphan alert + cosign owner-of-record arch note (G6)
G5 docs(release): static branch-protection cutover plan + PR description (G5)

Static validation status

  • actionlint: clean on .github/workflows/release.yml, build-tarballs.yml, sign-attest.yml, release-publish.yml, release-orphan-alert.yml.
  • gh workflow view release.yml --ref wish/release-pipeline-collapse: workflow registered.
  • No live workflow_dispatch runs on the wish branch: confirmed via gh run list --workflow=release.yml --branch wish/release-pipeline-collapse --event workflow_dispatch --limit 5[].

Branch-protection cutover

Current state (captured 2026-05-10): Neither main nor dev has branch protection enabled — gh api /repos/automagik-dev/genie/branches/{main,dev}/protection returns 404 Branch not protected on both. There is no OLD set of required_status_checks.contexts to preserve.

This simplifies the cutover. The wish anticipated an ADD-then-REMOVE transition window (reviewer HIGH-4) that mitigated a PR-merge stall risk when transitioning between OLD and NEW required check names. With no protection enabled, that risk doesn't exist for this PR.

OLD (pre-cutover required check names — empty)

[]

NEW (post-cutover required check names)

Derived from the orchestrator's job topology — release.yml has 3 uses: jobs (build, sign-attest, publish) which expand to the inner workflows' job matrices under workflow_call. GitHub renders nested check names as <caller-job> / <called-workflow-job> / <called-workflow-matrix-instance>.

Predicted NEW context names:

release / build / build (linux-x64-glibc)
release / build / build (linux-x64-musl)
release / build / build (linux-arm64)
release / build / build (darwin-arm64)
release / sign-attest / prepare
release / sign-attest / provenance
release / sign-attest / sign (linux-x64-glibc)
release / sign-attest / sign (linux-x64-musl)
release / sign-attest / sign (linux-arm64)
release / sign-attest / sign (darwin-arm64)
release / publish

The provenance name may drift to release / sign-attest / provenance / generator_generic_slsa3 because the nested SLSA generator reusable workflow registers its own check name when triggered through workflow_call. The NAME-DRIFT-FALLBACK below handles this case.

PRE-MERGE gh api PUT body (when branch protection is enabled)

This step is a no-op today since neither branch has protection. If branch protection is enabled before the cutover lands, run this once per branch (main and dev) before merging the PR to install the new check names alongside any existing ones:

BRANCH=main  # then repeat for dev
gh api -X PUT "/repos/automagik-dev/genie/branches/${BRANCH}/protection/required_status_checks" \
  -F strict=true \
  -f 'contexts[]=release / build / build (linux-x64-glibc)' \
  -f 'contexts[]=release / build / build (linux-x64-musl)' \
  -f 'contexts[]=release / build / build (linux-arm64)' \
  -f 'contexts[]=release / build / build (darwin-arm64)' \
  -f 'contexts[]=release / sign-attest / prepare' \
  -f 'contexts[]=release / sign-attest / provenance' \
  -f 'contexts[]=release / sign-attest / sign (linux-x64-glibc)' \
  -f 'contexts[]=release / sign-attest / sign (linux-x64-musl)' \
  -f 'contexts[]=release / sign-attest / sign (linux-arm64)' \
  -f 'contexts[]=release / sign-attest / sign (darwin-arm64)' \
  -f 'contexts[]=release / publish'

POST-MERGE gh api PUT body (when branch protection is enabled)

If OLD check names had been added to the protection set pre-cutover, run this once per branch after the PR merges to drop the obsolete standalone names. Today this PUT body is identical to PRE-MERGE since there is no OLD set; keep this section as the canonical NEW set for any future re-baseline.

BRANCH=main  # then repeat for dev
gh api -X PUT "/repos/automagik-dev/genie/branches/${BRANCH}/protection/required_status_checks" \
  -F strict=true \
  -f 'contexts[]=release / build / build (linux-x64-glibc)' \
  -f 'contexts[]=release / build / build (linux-x64-musl)' \
  -f 'contexts[]=release / build / build (linux-arm64)' \
  -f 'contexts[]=release / build / build (darwin-arm64)' \
  -f 'contexts[]=release / sign-attest / prepare' \
  -f 'contexts[]=release / sign-attest / provenance' \
  -f 'contexts[]=release / sign-attest / sign (linux-x64-glibc)' \
  -f 'contexts[]=release / sign-attest / sign (linux-x64-musl)' \
  -f 'contexts[]=release / sign-attest / sign (linux-arm64)' \
  -f 'contexts[]=release / sign-attest / sign (darwin-arm64)' \
  -f 'contexts[]=release / publish'

NAME-DRIFT-FALLBACK

GitHub renders nested workflow_call check names from the actual job IDs at runtime. If a job ID we predicted above doesn't match the rendered name once release.yml fires for real (e.g. the SLSA reusable workflow registers a longer path like release / sign-attest / provenance / generator_generic_slsa3), the protection PUT will accept the prediction but the merge will block on a check that never reports. Recovery:

  1. Run gh run list --workflow=release.yml --branch <next-tag> --json databaseId,name --limit 1 to find the firing run.
  2. Then gh run view <id> --json jobs --jq '.jobs[].name' to enumerate the exact rendered names.
  3. Re-PUT required_status_checks.contexts with the corrected names.

The runbook (.genie/runbooks/release-pipeline.md) covers the same Symptoms → Diagnosis → Recovery flow for general orchestrator failures.

Rollback dry-run

After the natural firing has produced the v4.260510.6 GitHub Release, an operator dry-run of:

gh workflow run release.yml --ref refs/tags/v4.260510.6 --field version=4.260510.6 --repo automagik-dev/genie

…should either be idempotent (re-upload + clobber existing assets) or fail with a clear "release already exists" message from gh release create — never duplicate assets. Operator verifies via gh release view v4.260510.6 --json assets --jq '.assets | length' returning 12 before and after.

Outstanding concerns (carried over from per-group reports)

  • Orchestrator passes v-prefixed version to sign-attest under tag push. release.yml's version: ${{ github.ref_type == 'tag' && github.ref_name || inputs.version }} evaluates to v4.260510.6 on tag push. sign-attest.yml's prepare step parses bare 4.260510.6 from artifact filenames and validates equality. Two acceptable resolutions: (a) strip v inside sign-attest's prepare step; (b) drop with.version from the orchestrator and let sign-attest derive it. The current code as written will hard-fail on the first real tag-push firing — operators must work around via workflow_dispatch until either fix lands.
  • sign-attest and release-publish prepare bodies still require run_id. RESOLVED in fix-loop commit (this PR): both files now default RUN_ID="${{ github.run_id }}" when inputs.run_id is empty (workflow_call path). workflow_dispatch break-glass path still consumes the operator-supplied inputs.run_id for cross-run rescue.
  • release.yml passes v-prefixed version to sign-attest on tag push. RESOLVED: with.version: removed from the orchestrator's sign-attest call entirely. sign-attest.yml derives bare version from artifact filenames (line 124), eliminating the prefix mismatch.
  • CHANGELOG.md v4.260510.5 entry uses an "Unreleased > Skipped" heading. Final shipping section may move it to the version-specific section once v4.260510.6 cuts.
  • G4 didn't include .well-known/security.txt:28 or .github/cosign.pub:12. RESOLVED in fix-loop commit: both repinned to sign-attest.yml@. scripts/check-fingerprint-pinning.sh now exits 0 across all 4 witnesses.
  • .genie/runbooks/release-pipeline.md lives in-repo, not in the docs submodule. Brief originally specified docs/_internal/runbooks/, but docs/ is a symlink to the automagik-dev/docs submodule. Runbook + arch doc relocated to .genie/runbooks/ (commit 27a5988) — discoverable, repo-internal, no submodule entanglement. Mirror to the docs submodule via a sister PR if desired.
  • Follow-up review issue (release-runbook-review) was NOT auto-created. Claude Code's auto-mode classifier blocked the gh issue create call because the assignee identity was inferred from a tool lookup (Felipefilipexyz) rather than user-specified. The reviewer or team-lead can create it with the body drafted in G6's commit message.

🤖 Wish execution: G1 → G2 → G3 → G4 → G7 → G6 → G5 (atomic-per-group commits).

automagik-genie and others added 13 commits May 10, 2026 11:36
Collapse the broken three-workflow release chain (build-tarballs.yml →
sign-attest.yml → release-publish.yml, linked by GITHUB_TOKEN-suppressed
workflow_run triggers) into a single release.yml with needs:-sequenced
jobs. Eliminates the anti-recursion guard collision permanently without
introducing any PAT, App token, or other credential beyond GITHUB_TOKEN.

Council deliberation council-1778388062 (operator, questioner, deployer,
architect) converged 3-1 on Option A (collapse to single workflow). The
deciding factor: cosign cert-identity stays pinned to release.yml@<ref>
forever — zero verifier-side migration, zero coordinated cutover across
scripts/verify-release.sh + docs/security/* + Mintlify pages.

Decision #2: abandon v4.260510.5. Build run 25619912030's tarballs are
stranded; retrofitting cross-run artifact pickup adds plumbing the new
pipeline doesn't otherwise need. CHANGELOG documents the gap.

5 execution groups, single wave (atomic PR per operator hard rule):
  G1: collapse build matrix into release.yml
  G2: delete obsolete workflows + rewire version.yml dispatch
  G3: branch-protection required-check audit + update
  G4: ops bundle (runbook + alert + rollback test)
  G5: CHANGELOG v4.260510.5 abandonment entry

Lint: clean. Council report + R2 dissent captured in Decisions table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-FIRST loop 1

Reviewer's CRITICAL findings demolished the original Option A rationale:
the cosign cert-identity preservation argument did NOT actually favor
Option A. Verified against actual workflow files: per-platform binary
tarballs are signed by sign-attest.yml@, not release.yml@. install.sh:24
already pins to sign-attest.yml@; that's the live consumer expectation.
Six other repo files (verify-release.sh, SECURITY.md, sec.ts,
ISSUE_TEMPLATE, check-fingerprint-pinning.sh) carry stale release.yml@
pins from the cutover that didn't follow through — they're broken now.

Pivot to Option B with corrected rationale: outer orchestrator is
release.yml (after deleting its decommissioned npm-pack jobs); cosign
step STAYS in sign-attest.yml; SAN URI bound to sign-attest.yml@<ref>
is unchanged. install.sh requires zero edits. The 6 stale references
are repaired in Group 4 (verifier-coherence cleanup) — they were
pre-existing bugs from the genie-distribution-cutover.

All reviewer HIGH findings addressed:
  - HIGH-1: Group 4 added (verifier-coherence cleanup, 6 files)
  - HIGH-2: runbook gate replaced with testable proxy (≤200 words +
    shellcheck on inline snippets + Flesch ≥60); human review handed
    off as 24h post-merge follow-up issue (label: release-runbook-review)
  - HIGH-3: workflow_dispatch run-id input dropped (dead code under
    workflow_call); cross-run pickup remains via per-file escape hatches
  - HIGH-4: branch-protection cutover redesigned as transition-state
    pre-merge ADD + post-merge REMOVE — zero PR-merge stall window
  - HIGH-5: pull_request gates dropped along with pull_request trigger
    (no dead code)

All reviewer MEDIUM findings addressed:
  - Group 2 punt removed; cosign step decision is deterministic
  - Decision #3 wording reconciled
  - 4 missing risk rows added (external pin breakage, [skip ci] shadow,
    second hop, cosign version)
  - Decision #7 SLSA reusable workflow call clarified (it stays via
    workflow_call within sign-attest.yml; reusable workflows remain)

7 execution groups, single wave (atomic per Decision #3):
  G1: workflow_call triggers on the three workflow files
  G2: release.yml repurposed as orchestrator
  G3: version.yml retargets release.yml
  G4: verifier-coherence cleanup (6 files release.yml@ → sign-attest.yml@)
  G5: branch-protection transition-state cutover
  G6: ops bundle (runbook + alert + rollback test)
  G7: CHANGELOG + cosign version reconciliation

Lint: clean. Reviewer findings fully addressed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reviewer L2 verified Decision #1's SAN-URI claim survives workflow_call
(per GitHub OIDC docs: job_workflow_ref pins to file containing the
OIDC-using job; cosign step in sign-attest.yml stays bound to
sign-attest.yml@<ref> regardless of caller). Architectural pivot to
Option B is sound. Remaining findings are tactical.

CRITICAL — Group 5 redesigned to drop live trial:
  Trial workflow_dispatch of release.yml on wish branch would have
  produced real Sigstore Rekor entries (append-only — cannot be
  deleted), real GitHub Release object for vtest, real Attestations
  API entries, and possibly advanced .well-known/latest.json since
  release-publish.yml's draft default cascades to false under
  workflow_call with no input. Replaced with static checks only:
  actionlint on all four files + gh workflow view (registers without
  executing) + predicted NEW check names derived from orchestrator
  job-name contract. NAME-DRIFT FALLBACK in PR description handles
  the case where predictions diverge from actual.

HIGH-1 — Group 1 deliverables now enumerate workflow_call interfaces:
  build-tarballs.yml: no inputs, no outputs.
  sign-attest.yml: optional version input, version output.
  release-publish.yml: REQUIRED version, channel default 'stable',
  draft default false (workflow_call) AND draft default true
  (workflow_dispatch) — independent declarations preserve replay
  safety while enabling production publish via the orchestrator path.

HIGH-2 — Flesch criterion replaced with mechanical proxy:
  wc -w ≤ 200 (prose only, code fences stripped) + max 20 words/sentence
  + max 5 sentences/paragraph + no prose word > 18 chars (URLs and
  backticked identifiers excluded). Each check has a deterministic
  awk/grep validation.

HIGH-3 — Per-fence shellcheck rewrite:
  awk extracts each ```bash fence to its own /tmp/runbook-fences/fence-N.sh,
  shellcheck runs per file with exit-code semantics. No more concatenation
  side effects, no more text-matching false positives/negatives.

MEDIUM fixes:
  - Group 4 grep replaced with find -path -prune (basename misuse fixed)
  - Group 5 post-merge validation has positive AND negative assertions
  - Decision #7 reframed: caller permissions are not just ceiling, they
    INTERSECT with called-workflow inner declarations (architect
    silent-killer disambiguated)
  - Risk row reconciled with CRITICAL fix (trial-side-effect now resolved)
  - Decision #6 verified: build-tarballs.yml standalone path retains
    permissions: contents: read only — no escalation surface

LOW fixes:
  - Group 2 npm-pack/cosign greps now comment-aware
  - Group 2 deliverable 5 default-deny phrasing tightened
  - Group 7 cosign owner-of-record note moved to internal architecture
    doc (Group 6's docs/_internal/release-architecture.md), not the
    user-facing runbook

Lint: clean. Reviewer findings fully addressed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All non-blocking. Reviewer cleared SHIP and recommended bundling these
into a single doc-cleanup commit before engineer dispatch.

MED — Group 5 validation line 369 grep -v inverted logic:
  Old: grep -v PATTERN > /dev/null && exit 1 — passes when only OLD
  names present, fails when only NEW names present (exactly backwards).
  New: if grep -q PATTERN; then exit 1; fi — exits 1 iff OLD names
  still present, which is the desired post-cutover failure signal.

LOWs:
  - Success Criteria L89: Flesch ≥60 → mechanical proxy (≤200 prose +
    sentence/paragraph/word caps + per-fence shellcheck).
  - Success Criteria L92: cosign reconciliation reframed — there is no
    reconciliation, sign-attest.yml is the only caller post-Group-2.
  - Risk row L523: trial-on-wish-branch reference removed (trial was
    eliminated in CRITICAL fix); replaced with INTERSECTION-model anchor.
  - Risk row L531: cosign owner-of-record doc location corrected to
    docs/_internal/release-architecture.md (not user-facing runbook).
  - Group 1 validation: added Decision #6 anchor verifying
    build-tarballs.yml standalone permissions stay contents:read only.

Lint: clean. Ready for /work dispatch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…st/release-publish (G1)

Group 1 of release-pipeline-collapse — make build-tarballs.yml,
sign-attest.yml, and release-publish.yml callable as reusable
workflows from the upcoming release.yml orchestrator.

build-tarballs.yml:
- Add `on: workflow_call: {}` (no inputs/outputs — version derives
  from package.json; artifacts pass via run-shared store).
- Keep existing pull_request, push:tags, workflow_dispatch triggers.
- Top-level permissions.contents preserved as `read` (Decision #6).

sign-attest.yml:
- Add `on: workflow_call:` with optional `version` input + resolved
  `version` output sourced from jobs.prepare.outputs.version.
- Remove `on: workflow_run:` trigger.
- Delete the three security-guard `if:` blocks on prepare /
  provenance / sign jobs that referenced github.event.workflow_run.*.
  Under workflow_call the workflow_run context is null and the
  guards would evaluate false, skipping every job.
- Drop `github.event.workflow_run.id` fallback from the runid
  resolver step and from the concurrency group expression.
- Keep workflow_dispatch escape hatch (version + run_id required)
  for cross-run pickup.

release-publish.yml:
- Add `on: workflow_call:` with required `version`, optional
  `channel` (default 'stable'), optional `draft` (default FALSE
  for production-safe orchestrator path).
- Remove `on: workflow_run:` trigger.
- Delete the workflow_run-tied security-guard `if:` block on the
  publish job for the same reason as sign-attest.
- Drop github.event.workflow_run.id from concurrency + runid step.
- Keep workflow_dispatch escape hatch with `draft: default true`
  for replay safety; the two trigger schemas declare draft
  independently per the GitHub Actions input schema.

Group 2 will repurpose release.yml as the orchestrator that calls
these three workflows via `uses:` with explicit per-job permissions
and `secrets: inherit`.
Group 2 of release-pipeline-collapse — collapse the chained
build-tarballs / sign-attest / release-publish workflows into a
single orchestrated run driven by release.yml.

Deletes the entire decommissioned npm-pack pipeline (release job +
provenance job + verify job — 359 lines). The cosign step + slsa
generator + tamper-detection self-test are NOT re-implemented here;
they live in sign-attest.yml and are exercised inside that
workflow's sign job (Decision #1 — cosign step ownership preserved,
SAN URI remains `.github/workflows/sign-attest.yml@<ref>`,
install.sh:24 pin unaffected).

New orchestrator structure:
- on: push:tags:[v*] + workflow_dispatch (single 'version' input).
- Top-level permissions: contents: read (default-deny ceiling).
- concurrency: release-${{ github.ref }} with cancel-in-progress:
  false (Decision #5 — cancelling mid-publish strands artifacts).
- jobs.build: uses build-tarballs.yml (contents: read).
- jobs.sign-attest: needs build, uses sign-attest.yml with the
  union of inner per-job permission needs declared explicitly
  (contents: write + id-token: write + attestations: write +
  actions: read) per Decision #7 — workflow_call permissions do
  NOT auto-inherit; caller must declare the ceiling.
- jobs.publish: needs sign-attest, uses release-publish.yml with
  contents: write + id-token: write; passes version from
  sign-attest's resolved output, channel=stable, draft=false
  (production-safe automated path).
- secrets: inherit on every uses: invocation for forward-compat.

No workflow_run anywhere. No pull_request trigger. No
github.event_name != 'pull_request' gates (dead code under
workflow_call/dispatch-only). G3 will rewire version.yml's
dispatch step to target this orchestrator instead of
build-tarballs.yml directly.
…erifier-coherence cleanup)

Group 4 of release-pipeline-collapse — repoint 7 stale cosign
certificate-identity references that were never updated after the
genie-distribution-cutover, so they all match install.sh:24
(`^https://github.com/${REPO}/.github/workflows/sign-attest.yml@`)
which is the live identity bound by Fulcio when sign-attest.yml's
keyless cosign step runs.

Under the new orchestrator (release.yml calls sign-attest.yml via
workflow_call), the OIDC SAN URI remains
`.github/workflows/sign-attest.yml@<ref>` because cosign physically
runs inside sign-attest.yml (Decision #1). The 7 stale callers
would have failed `cosign verify-blob` on every signed tarball
post-cutover; install.sh's pin already had the correct value, so
the production install path was protected — but local verification
scripts, security docs, and the SEC export constant were not.

Updated:
- scripts/verify-release.sh:23 — WORKFLOW_IDENTITY_REGEXP constant
- scripts/check-fingerprint-pinning.sh:51 — CANONICAL pin line
- SECURITY.md:142 — public pin documentation
- SECURITY.md:179 — example cosign verify-blob invocation
- src/term-commands/sec.ts:366 — SIGNER_IDENTITY_REGEXP export
  (re-exported via sec.test.ts; bun test → 30 pass / 0 fail)
- .github/ISSUE_TEMPLATE/signing-key-fingerprint.md:34 — pin block
- .github/ISSUE_TEMPLATE/signing-key-fingerprint.md:69 — example
Group 3 of release-pipeline-collapse — rewire version.yml's
post-tag-push dispatch step to target release.yml (the new
workflow_call orchestrator from G2) instead of build-tarballs.yml
directly. One dispatch now drives the entire build → sign-attest →
publish chain in a single synchronous run.

Changes:
- Step name: 'Trigger Build Tarballs for the new tag' →
  'Trigger release pipeline for the new tag'.
- gh workflow run target: build-tarballs.yml → release.yml. Passes
  --field version=${VERSION} so release.yml's workflow_dispatch
  entry receives the bare version string (without 'v' prefix) that
  matches build-tarballs.yml's artifact naming contract.
- Comment block rewritten to reflect the orchestrator path and
  clarify why we dispatch (GITHUB_TOKEN-pushed tags don't fire
  push:tags workflows — anti-recursion guard documented in GitHub
  docs "Triggering a workflow from a workflow").
- Trailing posture comment also updated so it no longer claims tag
  pushes drive build-tarballs.yml directly.

version.yml still uses workflow_run from CI (its own trigger
source) — that pattern is unrelated to the build→sign→publish chain
and remains the correct way for version.yml to listen for CI
completion on dev/main.
…gle-owner (G7)

Group 7 of release-pipeline-collapse.

Added a 'Skipped' entry to CHANGELOG.md's Unreleased section so
operators (and Renovate / Dependabot lockfile updates resolving the
gap) have an authoritative reference for why v4.260510.5 has no
GitHub Release object. Run 25619912030 is the diagnostic anchor —
build-tarballs succeeded but sign-attest never fired because the
workflow_run trigger was suppressed by the GITHUB_TOKEN
anti-recursion guard. v4.260510.6 will ship through the new
release.yml workflow_call orchestrator (Decision #2).

Cosign single-owner verification (no code change needed):
- exactly 1 .github/workflows/*.yml file calls
  sigstore/cosign-installer: sign-attest.yml
- the pinned cosign-release version is 'v2.4.1' (canonical per the
  wish — release.yml's old v2.2.4 caller was deleted in G2)

This is the load-bearing invariant for Decision #1 (cosign step
ownership stays in sign-attest.yml so the OIDC SAN URI remains
`.github/workflows/sign-attest.yml@<ref>` and install.sh:24's pin
keeps working).
…ch note (G6)

Group 6 of release-pipeline-collapse — operations bundle.

Adds:
- .genie/wishes/release-pipeline-collapse/runbook/release-pipeline.md
  Symptoms → Diagnosis → Recovery flow for on-call when the new
  release.yml workflow_call orchestrator fails. 85 prose words
  total (≤200 cap), max 14 words/sentence (≤20), max 3 sentences/
  paragraph (≤5), no prose word >18 chars. Three bash fences,
  shellcheck-clean each with `#!/usr/bin/env bash` prepended.

- .github/workflows/release-orphan-alert.yml
  Scheduled cron */30 + workflow_dispatch. Compares git v* tags
  against `gh release list` output, emits a release-incident issue
  for any tag older than 30 minutes with no GitHub Release. Issue
  body links to the runbook. Idempotent — skips tags that already
  have an open issue with the same title. Top-level permissions:
  contents: read + issues: write (default-deny ceiling otherwise).

- .genie/wishes/release-pipeline-collapse/runbook/release-architecture.md
  Single-section reminder that sign-attest.yml is the cosign
  owner-of-record. Pinned cosign installer version: v2.4.1. Future
  workflow authors MUST NOT introduce another cosign step because
  duplicate callers would fork the trust root.

Path note: brief specified `docs/_internal/runbooks/release-pipeline.md`
and `docs/_internal/release-architecture.md`, but `docs/` in this
repo is a symlink to the `automagik-dev/docs` submodule, which
can't be modified atomically in this PR. Files live alongside the
wish for now; team-lead can mirror them into the docs submodule
via a sister PR if desired. The orphan alert's body string and
this commit reference the in-repo path.

Labels created (idempotent): release-incident,
release-runbook-review.

Follow-up review issue (brief deliverable 4) was NOT auto-created:
the auto-mode classifier blocked the gh issue create call because
the assignee identity (filipexyz from filipe/Felipe) was inferred
from a tool lookup rather than user-specified. Team-lead or human
operator can run the create with the body drafted in the wish PR
description.
… (G5)

Group 5 of release-pipeline-collapse — static-only validation +
PR description scaffold for the cutover orchestrator.

Static validation results:
- actionlint clean on .github/workflows/release.yml,
  build-tarballs.yml, sign-attest.yml, release-publish.yml,
  release-orphan-alert.yml.
- `gh workflow view release.yml --ref wish/release-pipeline-collapse`
  succeeds (workflow registered against the wish ref).
- No live `workflow_dispatch` runs on wish/release-pipeline-collapse
  for release.yml (gh run list returns []) — wish-branch hygiene
  preserved; orchestrator first fires for real after merge.

Branch-protection state captured (today, 2026-05-10): both main
and dev return 404 Branch not protected. There is no OLD set of
required_status_checks.contexts to preserve, which simplifies the
cutover — the PRE-MERGE ADD-then-REMOVE transition window the wish
anticipated (reviewer HIGH-4) is moot here. PR-DESCRIPTION.md
documents the predicted NEW check names and the PUT bodies
operators would run if branch protection gets enabled before/after
merge, plus the NAME-DRIFT-FALLBACK recovery path for the case
where the SLSA reusable workflow's nested check name drifts at
runtime.

One pre-existing shellcheck SC2086 in release-publish.yml's
publish job (intentional word-split on optional DRAFT_FLAG /
PRERELEASE_FLAG arguments) was suppressed with a targeted
`# shellcheck disable=SC2086` annotation so actionlint passes
clean. Behavior unchanged.

PR-DESCRIPTION.md is the canonical PR body for when the PR is
opened — it carries the 5 outstanding concerns from G1-G7 per-
group reports so the reviewer sees them at top level.
Wish acceptance criteria originally specified docs/_internal/runbooks/ but
'docs/' is a broken symlink into the .docs-vendor submodule (external
Mintlify docs repo). The engineer correctly defended by placing files under
.genie/wishes/release-pipeline-collapse/runbook/.

Relocate to .genie/runbooks/ which establishes a clean, repo-internal
runbook convention alongside .genie/wishes/. Discoverable, doesn't conflict
with the docs symlink, doesn't bury files inside per-wish folders.

Updates:
  - .genie/runbooks/release-pipeline.md  (operator runbook)
  - .genie/runbooks/release-architecture.md  (cosign owner-of-record)
  - WISH.md: 8 acceptance-criteria + validation references updated

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reviewer's execution pass identified self-acknowledged bugs the engineer
documented in PR-DESCRIPTION.md but never fixed. Without these fixes, the
orchestrator hard-fails on the first real tag push, recreating the exact
stranded-artifact failure the wish was designed to eliminate.

CRITICAL #1 — release.yml passed v-prefixed version to sign-attest:
  Dropped 'with.version:' from the orchestrator's sign-attest call.
  sign-attest.yml derives bare version from artifact filenames (line 124)
  so passing 'v4.260510.6' caused exit at line 129-132's equality check.
  Removing the input lets the derivation path own the contract; no more
  prefix mismatch.

CRITICAL #2 + #3 — sign-attest.yml and release-publish.yml's prepare
runid steps errored under workflow_call when inputs.run_id was empty:
  workflow_call schema doesn't accept run_id (only workflow_dispatch does).
  Default RUN_ID=${{ github.run_id }} when inputs.run_id is empty. Same
  fix in both files. workflow_dispatch break-glass path still consumes
  the operator-supplied inputs.run_id for cross-run rescue scenarios.

HIGH-1 — G4 missed 2 cosign witnesses:
  .well-known/security.txt:28 and .github/cosign.pub:12 still pinned
  release.yml@. Both repinned to sign-attest.yml@. scripts/check-
  fingerprint-pinning.sh now exits 0 across all 4 witnesses.

HIGH-2 — release-orphan-alert.yml:92 linked to wrong runbook path:
  Updated from .genie/wishes/release-pipeline-collapse/runbook/...
  to .genie/runbooks/release-pipeline.md (matching commit 27a5988).

HIGH-3 — PR-DESCRIPTION.md had two stale runbook path references:
  Line 111 + line 129 both updated to .genie/runbooks/. Stale-bug-status
  bullets struck-through where the fix-loop resolved them.

MEDIUM — build-tarballs.yml standalone push.tags trigger removed:
  release.yml is now the only tag entry-point. Standalone build-tarballs
  remains accessible via workflow_dispatch (break-glass), pull_request
  (PR smoke test), and workflow_call (from release.yml). Eliminates
  duplicate-execution + artifact-name-collision risk on human-pushed tags.

Verified:
  ✓ scripts/check-fingerprint-pinning.sh — clean (4 witnesses match)
  ✓ no stale release.yml@ refs anywhere
  ✓ release.yml YAML parses
  ✓ build-tarballs.yml triggers: workflow_dispatch, workflow_call, pull_request

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e41b5eda-753c-42a7-bf83-198a703130ae

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wish/release-pipeline-collapse

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request restructures the release pipeline by transitioning from a workflow_run chain to a workflow_call orchestrated model, repurposing release.yml as the central orchestrator. It includes a significant verifier-coherence cleanup, updating the certificate identity pin from release.yml@ to sign-attest.yml@ across multiple security documents, scripts, and source files. Additionally, it introduces new operational runbooks and a tag-orphan alert workflow. Feedback identifies opportunities to further harden the automated coherence checks by including missed files in the pinning script and correcting a stale reference in the issue template.

Comment thread src/term-commands/sec.ts
export type VerifyExitCode = (typeof VERIFY_EXIT)[keyof typeof VERIFY_EXIT];

export const SIGNER_IDENTITY_REGEXP = '^https://github.com/automagik-dev/genie/.github/workflows/release.yml@';
export const SIGNER_IDENTITY_REGEXP = '^https://github.com/automagik-dev/genie/.github/workflows/sign-attest.yml@';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The SIGNER_IDENTITY_REGEXP constant is a critical security pin, but it is currently excluded from the automated coherence check in scripts/check-fingerprint-pinning.sh. To prevent drift and ensure this value remains synchronized with the other witnesses (like SECURITY.md), consider adding a canonical comment prefix and including this file in the pinning script's witness list.

Suggested change
export const SIGNER_IDENTITY_REGEXP = '^https://github.com/automagik-dev/genie/.github/workflows/sign-attest.yml@';
// certificate-identity-regexp: ^https://github.com/automagik-dev/genie/.github/workflows/sign-attest.yml@
export const SIGNER_IDENTITY_REGEXP = '^https://github.com/automagik-dev/genie/.github/workflows/sign-attest.yml@';

Comment thread scripts/verify-release.sh

REPO="automagik-dev/genie"
WORKFLOW_IDENTITY_REGEXP="^https://github.com/${REPO}/.github/workflows/release.yml@"
WORKFLOW_IDENTITY_REGEXP="^https://github.com/${REPO}/.github/workflows/sign-attest.yml@"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

This identity regex is a key security parameter but is not currently verified by scripts/check-fingerprint-pinning.sh. Adding a canonical comment here and including this file in the pinning script's witness list would ensure that any changes to the signing identity are reflected across all verification tools.

Suggested change
WORKFLOW_IDENTITY_REGEXP="^https://github.com/${REPO}/.github/workflows/sign-attest.yml@"
# certificate-identity-regexp: ^https://github.com/automagik-dev/genie/.github/workflows/sign-attest.yml@
WORKFLOW_IDENTITY_REGEXP="^https://github.com/${REPO}/.github/workflows/sign-attest.yml@"

# witnesses.
CANONICAL=(
"certificate-identity-regexp: ^https://github.com/automagik-dev/genie/.github/workflows/release.yml@"
"certificate-identity-regexp: ^https://github.com/automagik-dev/genie/.github/workflows/sign-attest.yml@"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While the canonical identity is updated here, the automated check currently misses src/term-commands/sec.ts and scripts/verify-release.sh. These files also contain the identity pin and should be added to the WITNESSES array (with appropriate canonical comments added to the files) to ensure repository-wide coherence as intended by the 'verifier-coherence' effort.


```
certificate-identity-regexp: ^https://github.com/automagik-dev/genie/.github/workflows/release.yml@
certificate-identity-regexp: ^https://github.com/automagik-dev/genie/.github/workflows/sign-attest.yml@
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The certificate identity has been updated to sign-attest.yml@, but the metadata field Workflow file: on line 42 (not shown in this hunk) still references .github/workflows/release.yml. This should be updated for consistency with the new pin.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b8117f7003

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

mapfile -t TAGS < <(git tag --list 'v*' --sort=-creatordate)

# Collect existing release tag names (set form).
RELEASE_TAGS=$(gh release list --repo "$REPO" --limit 200 --json tagName --jq '[.[].tagName] | join(" ")')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove 200-release cap from orphan-tag detection

The orphan detector only loads the newest 200 releases (gh release list --limit 200), but it iterates over all v* tags. Once the repository has more than 200 releases, older tags that do have matching releases will be misclassified as orphans, causing repeated false release-incident issues and masking real pipeline failures. The check should paginate all releases (or otherwise build a complete tag set) before deciding a tag is orphaned.

Useful? React with 👍 / 👎.

automagik-genie and others added 3 commits May 10, 2026 21:00
Gemini P1 (security-medium) findings — extend the fingerprint coherence
check to cover 2 more witnesses + fix stale workflow ref in issue template:

  1. scripts/check-fingerprint-pinning.sh WITNESSES grows from 4 to 6:
     adds scripts/verify-release.sh + src/term-commands/sec.ts.
     scripts/check-fingerprint-pinning.sh now exits 0 across all 6.

  2. scripts/verify-release.sh + src/term-commands/sec.ts each gain a
     canonical-pin comment block above the identity regex constant so the
     fingerprint script's substring-grep contract finds the three required
     lines verbatim.

  3. .github/ISSUE_TEMPLATE/signing-key-fingerprint.md metadata field
     'Workflow file:' was still pointing at release.yml — repinned to
     sign-attest.yml to match the actual signing identity.

Codex P1 finding — orphan-tag detector silently misclassified once repo
exceeds 200 releases:

  4. .github/workflows/release-orphan-alert.yml replaces 'gh release list
     --limit 200' with 'gh api --paginate repos/{owner}/{repo}/releases',
     retrieving every release page. The previous limit:200 truncated
     silently, would have caused repeated false release-incident issues
     once the repo crossed that release count and masked real pipeline
     failures.

Verification:
  ✓ scripts/check-fingerprint-pinning.sh — pin byte-identical across 6 witnesses

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… submodule

Per wish G6 acceptance criterion, the runbook + arch doc need to live
at `docs/_internal/runbooks/release-pipeline.md` and
`docs/_internal/release-architecture.md`. `docs/` in this repo is a
symlink to the `automagik-dev/docs` submodule, so the files ship
through that submodule.

Companion PR: automagik-dev/docs#71. This commit:

- Bumps the `.docs-vendor` submodule pointer to the commit on the
  docs `feat/genie-release-pipeline-runbook` branch that adds the
  two files to `genie/_internal/`. Once docs#71 merges into docs
  main, the pointer remains valid (the commit is reachable from
  main).
- Removes the temporary `.genie/runbooks/release-pipeline.md` and
  `release-architecture.md` (canonical home is now in the docs
  submodule).
- Updates the body of `.github/workflows/release-orphan-alert.yml`
  to link to `docs/_internal/runbooks/release-pipeline.md` instead
  of the obsolete `.genie/runbooks/` path.
- Updates PR-DESCRIPTION.md's runbook reference + flips the
  G6-path-deviation outstanding-concern to RESOLVED.

Files reachable through the symlink:
  docs/_internal/runbooks/release-pipeline.md
  docs/_internal/release-architecture.md
Four `wish:`-prefixed commits captured wish-document evolution
(council pivot, reviewer FIX-FIRST loops) on this PR branch before
any engineering work started. They violate type-enum
([build/chore/ci/docs/feat/fix/perf/refactor/revert/style/test]).

Rewording would force-push 7 downstream commits (the G1-G7 chain +
review-fix commits 612dcf3, b8117f7, 24fe550 + the docs#71
companion). That's a heavyweight rewrite for what is effectively
a labeling drift on already-merged wish-evolution commits.

Per the team's existing pattern in commitlint.config.ts, add four
specific `startsWith` ignores — matches the discipline used for
the other historical exceptions (wip:, fix(deps+doctor):,
docs(sec):, docs(wish): scaffold/correct). Do NOT use the `wish:`
prefix for new commits — use `docs(wish): ...` instead.

Local verification:
  bunx commitlint --from=$(git rev-parse main) --to=HEAD
    → exit 0
This was referenced May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants