Skip to content

feat(#613): relocate canary-rollout engine + bootstrap into .github (workflow disabled)#624

Merged
don-petry merged 5 commits into
mainfrom
epic-613-relocate-canary-engine
Jul 9, 2026
Merged

feat(#613): relocate canary-rollout engine + bootstrap into .github (workflow disabled)#624
don-petry merged 5 commits into
mainfrom
epic-613-relocate-canary-engine

Conversation

@don-petry

@don-petry don-petry commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

What

PR-B of epic #613 — relocate the release-promotion / repo-bootstrap tooling into petry-projects/.github so all org tooling + standards share one canonical home (the #596 principle, extended). .github already owns the standards (standards/rulesets/, lib/ring-pins.sh, the canonical apply-repo-settings.sh); this brings the orchestrators alongside them.

Ships operationally inert

The workflow ships disabledschedule: is commented out, leaving workflow_dispatch only — so this PR lands with zero operational effect. The live cutover is a follow-up (PR-C): re-enable schedule here and delete the copy from .github-private in the same window, so the two repos never both run schedule and race to move the same protected channel tags.

Files

Relocated byte-identical from .github-private:

  • scripts/canary-rollout.sh, scripts/lib/canary-rollout.sh (+ tests/canary_rollout.bats)
  • standards/canary-rings.json — ring registry, now co-located with lib/ring-pins.sh (read via a repo-relative path, so this removes a cross-repo hop)
  • docs/bootstrap/new-repo-validation.md, docs/release/versioning.md, docs/initiatives/canary-rollout-adr.md

Changed (3):

File Change Why
.github/workflows/canary-rollout.yml schedule: commented out inert until the PR-C cutover
scripts/bootstrap-new-repo.sh bridge dry-run onto DRY_RUN (keep DEV_LEAD_DRY_RUN) canonical .github settings script reads DRY_RUN; without this a real DRY_RUN=true bootstrap would issue LIVE settings PATCHes — a latent bug the move exposes
tests/test_bootstrap_new_repo.bats dummy GH_TOKEN; non-{} gh read-stub; assertion wording the real-sub-script tests now hit the canonical settings script, which requires a token + fetches state even under dry-run, and treats {} as a failed fetch

Auth prerequisite (already applied — org admin)

petry-projects/.github added to the selected-repos list for the RELEASE_MANAGER_APP_KEY secret + RELEASE_MANAGER_APP_CLIENT_ID variable, so the workflow can mint the release-manager App token once enabled. The App (id 4193127) is installed org-wide → no per-repo install gap.

Validation

  • tests/canary_rollout.bats88/88
  • tests/test_bootstrap_new_repo.bats13/13
  • shellcheck clean; canary-rings.json valid JSON; workflow YAML parses (on: = [workflow_dispatch] only)
  • Full dry-run bootstrap walks all 5 steps green against the canonical sub-scripts

Follow-up (not this PR)

  • PR-C: cutover — re-enable schedule here + delete engine/workflow/duplicate apply-rulesets.sh/apply-repo-settings.sh from .github-private; de-dupe lib/push-protection.sh.

Refs #613 #596

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Introduced canary rollout automation with ring-staged, health-gated promotion/rollback, read-only evaluation, drift auditing, and blocker issue syncing.
    • Added one-command repository bootstrapping with ring-aware registration and CODEOWNERS verification.
    • Added formal ring and versioning/channel definitions for agent workflows.
  • Documentation
    • Added onboarding walkthroughs, rollout ADR, and end-to-end versioning/release-channel specs.
  • Tests
    • Added Bats coverage for rollout decisioning/orchestration and bootstrap flows, including DRY_RUN behavior.

…workflow disabled)

PR-B of epic #613: move the release-promotion / repo-bootstrap tooling into
petry-projects/.github so all org tooling + standards share one canonical home
(the #596 principle, extended). .github already owns the standards
(standards/rulesets/, lib/ring-pins.sh, the canonical apply-repo-settings.sh);
this brings the orchestrators alongside them.

Relocated from .github-private (byte-identical):
  - scripts/canary-rollout.sh, scripts/lib/canary-rollout.sh (+ tests/canary_rollout.bats)
  - standards/canary-rings.json (ring registry — now co-located with lib/ring-pins.sh)
  - docs/{bootstrap/new-repo-validation,release/versioning,initiatives/canary-rollout-adr}.md
  - .github/workflows/canary-rollout.yml — SHIPS DISABLED (schedule commented out,
    workflow_dispatch only) so this PR lands with zero operational effect. The live
    cutover (re-enable schedule here + delete the copy from .github-private in the
    same window, to avoid a double-run race on protected channel tags) is PR-C.

Reconcile fixes required by the move (bootstrap now sits next to .github's
canonical 346L apply-repo-settings.sh, not .github-private's 129L subset):
  - bootstrap-new-repo.sh: bridge dry-run onto DRY_RUN (the var the canonical
    settings script reads), keeping DEV_LEAD_DRY_RUN for backward-compat. Without
    this a real `DRY_RUN=true` bootstrap would issue LIVE settings PATCHes — a
    latent bug the relocation exposes.
  - test_bootstrap_new_repo.bats: the real-sub-script tests now exercise the
    canonical settings script, which requires GH_TOKEN and fetches current state
    even under dry-run. Provide a dummy token (gh is stubbed) and return a
    non-"{}" repo object from the gh read-stub (canonical treats "{}" as a failed
    fetch). Assertion wording updated to the canonical script's dry-run messages.

Auth prerequisite (already applied, org-admin): petry-projects/.github added to the
selected-repos list for the RELEASE_MANAGER_APP_KEY secret + RELEASE_MANAGER_APP_CLIENT_ID
variable, so the workflow can mint the release-manager App token once enabled. The
App (id 4193127) is installed org-wide, so no per-repo install gap.

Validation: canary_rollout.bats 88/88, test_bootstrap_new_repo.bats 13/13,
shellcheck clean, canary-rings.json valid, workflow YAML parses (dispatch-only).

Refs #613 #596

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@don-petry
don-petry requested a review from a team as a code owner July 8, 2026 11:41
Copilot AI review requested due to automatic review settings July 8, 2026 11:41
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@don-petry don-petry added the dev-lead:hands-off Exclude this PR/issue from the dev-lead agent label Jul 8, 2026
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 24d7e236-7af9-4c76-9de2-d2fe4359cb3e

📥 Commits

Reviewing files that changed from the base of the PR and between b720f88 and 685f4b5.

📒 Files selected for processing (9)
  • .github/workflows/canary-rollout.yml
  • .github/workflows/feature-ideation-tests.yml
  • docs/bootstrap/new-repo-validation.md
  • docs/initiatives/canary-rollout-adr.md
  • docs/release/versioning.md
  • scripts/bootstrap-new-repo.sh
  • scripts/canary-rollout.sh
  • scripts/lib/canary-rollout.sh
  • tests/canary_rollout.bats

📝 Walkthrough

Walkthrough

This PR adds a ring-staged canary rollout system, including rollout decision logic, orchestration, workflow wiring, ring registry data, bootstrap automation, supporting documentation, and Bats tests for rollout and onboarding flows.

Changes

Canary rollout system

Layer / File(s) Summary
Registry schema and decision core
standards/canary-rings.json, scripts/lib/canary-rollout.sh
Defines the ring registry schema and the pure helpers for sample sizing, semver, dwell/time checks, graduation decisions, failure triage, channel ordering, set subtraction, and summary formatting.
Canary rollout orchestration
scripts/canary-rollout.sh
Implements tag resolution, evaluate/promote/rollback, blocker issue syncing, autocut, drift auditing, and command dispatch with validation.
GitHub Actions rollout workflow
.github/workflows/canary-rollout.yml, .github/workflows/feature-ideation-tests.yml
Wires the rollout workflow inputs, permissions, token minting, command execution, deployment recording, sync/drift steps, and a YAML step-name quoting fix.
Bootstrap and onboarding docs
scripts/bootstrap-new-repo.sh, docs/*
Adds the new-repo bootstrap flow, cross-repo ring registration helpers, repo compliance steps, and docs for rollout, versioning, and onboarding validation.
Bats coverage for rollout and bootstrap
tests/canary_rollout.bats, tests/test_bootstrap_new_repo.bats
Adds tests for the canary helpers and orchestrator commands, plus bootstrap DRY_RUN, sequencing, ring registration, and end-to-end onboarding coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: needs-human-review

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: relocating the canary-rollout and bootstrap tooling into .github while noting the workflow is disabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch epic-613-relocate-canary-engine

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.

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

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.

Code Review

This pull request introduces a comprehensive, ring-staged, health-gated canary rollout system for agent releases, along with repository bootstrapping automation, detailed documentation, and extensive test suites. The code review highlights several critical correctness and portability issues: a silent command failure bug in _run_json that could lead to premature promotions, macOS compatibility issues with base64 flags, a redundant CLI check, a style guide violation regarding bare return statements, and opportunities to improve BATS test assertions.

Comment thread scripts/canary-rollout.sh
Comment thread scripts/bootstrap-new-repo.sh Outdated
Comment thread scripts/bootstrap-new-repo.sh Outdated
Comment thread scripts/lib/canary-rollout.sh Outdated
Comment thread scripts/bootstrap-new-repo.sh Outdated
Comment thread tests/canary_rollout.bats

Copilot AI left a comment

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.

Pull request overview

Relocates the canary-rollout engine and new-repo bootstrap orchestration tooling into petry-projects/.github as the canonical org-standards home, with the canary-rollout workflow shipping dispatch-only (scheduled trigger commented out) to keep the change operationally inert until the follow-up cutover.

Changes:

  • Adds the canary-rollout orchestrator + pure decision core and the ring registry (standards/canary-rings.json) to this repo.
  • Adds/updates bootstrap orchestration (scripts/bootstrap-new-repo.sh) and its Bats coverage, including DRY_RUN bridging.
  • Ports supporting documentation and the (currently schedule-disabled) Canary Rollout workflow.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
.github/workflows/canary-rollout.yml Adds the canary-rollout workflow in dispatch-only mode (schedule commented out) and wires GitHub App token minting.
docs/bootstrap/new-repo-validation.md Documents the bootstrap flow and recorded DRY_RUN walkthrough.
docs/initiatives/canary-rollout-adr.md Adds ADR describing the canary rollout model and operations.
docs/release/versioning.md Adds agent versioning/channel-tag documentation (including cross-repo notes).
scripts/bootstrap-new-repo.sh Adds DRY_RUN-aware repo bootstrap orchestrator and cross-repo PR helpers.
scripts/canary-rollout.sh Adds the canary-rollout engine (autocut/evaluate/promote/rollback/sync-issues/drift).
scripts/lib/canary-rollout.sh Adds the pure decision core used by the orchestrator (unit-testable functions).
standards/canary-rings.json Adds the ring registry (agents, ring members, gate knobs).
tests/test_bootstrap_new_repo.bats Adds Bats coverage for bootstrap orchestration (dry-run, sequencing, ring behavior, e2e dry-run).

Comment thread scripts/canary-rollout.sh
Comment thread scripts/bootstrap-new-repo.sh Outdated
Comment thread .github/workflows/canary-rollout.yml Outdated
Comment thread docs/release/versioning.md Outdated
Comment thread docs/initiatives/canary-rollout-adr.md Outdated
Comment thread docs/initiatives/canary-rollout-adr.md Outdated
Comment thread docs/initiatives/canary-rollout-adr.md Outdated
@don-petry

Copy link
Copy Markdown
Contributor Author

Dev-Lead — review-changes (applied)

Changes committed and pushed.

@don-petry
don-petry enabled auto-merge (squash) July 8, 2026 11:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/canary-rollout.yml:
- Line 110: The `::error::` message in the canary rollout workflow is too long
and fails linting; shorten the string emitted by the shell step in the canary
rollout job so it stays under the line-length limit. Keep the message focused on
the refusal to fall back to `github.token`, and rely on the nearby comment for
the app token requirement details.
- Around line 114-121: The checkout step currently persists the App token in git
credentials even though later tag moves use gh api with GH_TOKEN, so update the
actions/checkout step to disable credential persistence. Keep the existing
Checkout (with tags) setup and App token usage, but add persist-credentials:
false so the workflow does not leave an unnecessary credential behind for later
steps.

In `@docs/bootstrap/new-repo-validation.md`:
- Line 45: The markdown file has fenced code blocks opened without a language
specifier, triggering MD040 in CI. Update both bare fences in the affected
examples to use a language tag (for example, text) while keeping the existing
content unchanged. Use the fenced block openings in the bootstrap instructions
section as the unique markers to locate and fix both occurrences.

In `@docs/initiatives/canary-rollout-adr.md`:
- Line 3: The markdownlint MD013 failure is coming from several overly long
prose lines in the ADR content, so reflow the affected paragraphs at natural
clause boundaries while preserving meaning and formatting. Update the text
around the “Status” line and the nearby narrative about the implemented model,
superseding the rings sketch, and the rollout caveat so each line stays under
the 200-character limit. Keep the same wording and structure, just wrap the
content in the document sections that mention initiative `#495`, issues `#499`–#502
and `#868`, and the supersession note.

In `@scripts/bootstrap-new-repo.sh`:
- Around line 211-214: The duplicate check in the bootstrap flow is too broad
because it searches the entire ring-pins content instead of the target ring
entry, which can wrongly mark a repo as already synced. Update the
repo-existence check in the bootstrap script’s ring registration logic so it is
scoped to the target ring’s array entry (the `ring=(` line) before checking for
the repo, and keep the existing success path and logging behavior in sync with
that narrower match.

In `@scripts/lib/canary-rollout.sh`:
- Around line 56-66: The `robust_sample_target` header comment is stale and
still documents the 4th argument as daily counts while the function signature
now uses `cap_multiple` (`robust_sample_target` with `cap_multiple="${4:-3}";
shift 4`). Update the docstring to name the 4th positional parameter as
`cap_multiple`, describe it as the spike-cap multiplier instead of a hardcoded
3×, and clarify that daily counts begin after the first four args so callers
don’t misread the interface.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ab680e63-8818-4d79-a437-b5f4d95c0292

📥 Commits

Reviewing files that changed from the base of the PR and between f0e01b5 and b720f88.

📒 Files selected for processing (10)
  • .github/workflows/canary-rollout.yml
  • docs/bootstrap/new-repo-validation.md
  • docs/initiatives/canary-rollout-adr.md
  • docs/release/versioning.md
  • scripts/bootstrap-new-repo.sh
  • scripts/canary-rollout.sh
  • scripts/lib/canary-rollout.sh
  • standards/canary-rings.json
  • tests/canary_rollout.bats
  • tests/test_bootstrap_new_repo.bats

Comment thread .github/workflows/canary-rollout.yml Outdated
Comment thread .github/workflows/canary-rollout.yml
Comment thread docs/bootstrap/new-repo-validation.md Outdated
Comment thread docs/initiatives/canary-rollout-adr.md Outdated
Comment thread scripts/bootstrap-new-repo.sh Outdated
Comment thread scripts/lib/canary-rollout.sh Outdated
Correctness / behavior:
  - canary-rollout.sh _run_json: fail CLOSED on a genuine gh failure (return 1 +
    ::error:: instead of silent []) — a swallowed error read as "zero failures" could
    green-light a bad promotion. Empty-but-successful still returns [] (Gemini critical).
  - canary-rollout.sh _reusable_differs: host-aware — for a cross-repo agent (host !=
    THIS_REPO, e.g. dev-lead once the engine runs from .github) the reusable blob isn't in
    the local checkout, so resolve it on the host via gh api (_gh_blob_sha) and fail CLOSED
    (unresolvable compare → differs=1, disabling the benign-failure allowlist). +3 unit
    tests (Copilot). this-repo agents keep the local git rev-parse path (existing tests green).
  - bootstrap-new-repo.sh ring-pins dup check: scope to the target ring's array line so a
    repo registered in a DIFFERENT ring no longer false-positives "in sync" (CodeRabbit).

Portability:
  - bootstrap-new-repo.sh base64: encode via `base64 | tr -d '\n'` (GNU wraps, BSD has no
    -w0); decode via `base64 -d || base64 -D` at all three sites (Gemini/Copilot).

Hardening / hygiene:
  - canary-rollout.yml: persist-credentials: false on checkout (tag moves use gh api, not
    git push, so the git credential is unused — CodeRabbit/zizmor).
  - lib/canary-rollout.sh _semver_gt: explicit `return $?` (Gemini); robust_sample_target
    docstring documents the cap_multiple arg (CodeRabbit).
  - bootstrap-new-repo.sh: drop the redundant local gh-CLI check (main() checks) (Gemini).
  - canary_rollout.bats: `run grep -c` idiom instead of `$(... || true)` (Gemini).

Docs (relocated ADR/versioning canonicalized in .github — refreshed to live behavior):
  - canary-rollout.yml header + ADR §4: tag moves are `gh api` ref updates on the host, not
    `git tag -f` + push; mover credential is the release-manager GitHub App token, not a PAT.
  - ADR §3 gate: replace the old 7-day-window / failure-rate model with the #548 graduated
    dwell+sample floors and cumulative zero-failures-since-cut; gate-state table matches impl.
  - ADR cadence: timer runs autocut → promote-all (arm-gated by CANARY_AUTO_PROMOTE) /
    evaluate-all → sync-issues, not "evaluate-only, never promotes on timer".
  - versioning.md: the six #482 reusables are now registered (cross-repo gh api moves, #870/#1054);
    only feature-ideation remains absent (Copilot).

Validation: canary_rollout.bats 91/91 (+3), test_bootstrap_new_repo.bats 13/13,
shellcheck no new findings (22 pre-existing info-only), markdownlint 0, yamllint clean.

Refs #613 #596

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@don-petry

Copy link
Copy Markdown
Contributor Author

Advisory findings addressed (commit 6491f3f)

Thanks Gemini / Copilot / CodeRabbit. All findings triaged and fixed — summary:

Correctness / behavior

  • _run_json fail-closed (Gemini critical): a genuine gh failure now returns non-zero (+::error::) instead of a silent [] that could read as "zero failures" and green-light a bad promotion. An empty-but-successful result still yields [].
  • _reusable_differs cross-repo (Copilot): made host-aware — for a cross-repo agent (host ≠ THIS_REPO, e.g. dev-lead once the engine runs from .github) the reusable blob isn't local, so it's resolved on the host via gh api (_gh_blob_sha) and fails closed (unresolvable → differs=1, disabling the benign-failure allowlist). This-repo agents keep the local git rev-parse path. +3 unit tests.
  • ring-pins dup check (CodeRabbit): now scoped to the target ring's array line, so a repo registered in a different ring no longer false-positives "in sync".

Portability

  • base64 (Gemini/Copilot): encode via base64 | tr -d '\n'; decode via base64 -d || base64 -D at all three sites (GNU/BSD).

Hardening / hygiene

  • persist-credentials: false on checkout (tag moves use gh api, not git push) — CodeRabbit/zizmor.
  • _semver_gt explicit return $?; robust_sample_target docstring documents cap_multiple; dropped the redundant local gh check; run grep -c idiom in bats — Gemini/CodeRabbit.

Docs (the relocated ADR/versioning are being canonicalized here, so refreshed to live behavior)

The 3 CodeRabbit "Major" lint findings (MD013/MD040/line-length) were already resolved by an earlier lint commit — CI Lint is green.

Validation: canary_rollout.bats 91/91 (+3), test_bootstrap_new_repo.bats 13/13, shellcheck no new findings, markdownlint 0, yamllint clean.

Note: the workflow still ships disabled (schedule commented out) — this PR remains operationally inert; the live cutover is PR-C.

@sonarqubecloud

sonarqubecloud Bot commented Jul 9, 2026

Copy link
Copy Markdown

@don-petry don-petry removed the dev-lead:hands-off Exclude this PR/issue from the dev-lead agent label Jul 9, 2026

@don-petry don-petry left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All advisory findings addressed (6491f3f) and their review threads resolved; CI fully green. Re-triggering auto-review readiness check.

@don-petry don-petry left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All 26 checks green, threads resolved — auto-review readiness re-check.

@don-petry
don-petry merged commit 9b27602 into main Jul 9, 2026
35 checks passed
@don-petry
don-petry deleted the epic-613-relocate-canary-engine branch July 9, 2026 02:38
don-petry added a commit that referenced this pull request Jul 9, 2026
PR-C2 of the epic #613 cutover. Re-enables the 4-hourly `schedule:` on the
relocated canary-rollout workflow (shipped disabled in #624), making .github the
live host for autocut → promote-all → sync-issues.

MUST land AFTER petry-projects/.github-private#1128 (which deletes the engine +
workflow there, stopping that repo's schedule). Both CANARY_AUTO_CUT and
CANARY_AUTO_PROMOTE are 'true', so the two schedules must never overlap — this is
sequenced strictly after the .github-private removal merges.

Live-validated already: workflow_dispatch evaluate run 28990172900 in .github
minted the release-manager App token, resolved cross-repo dev-lead channel tags on
.github-private, and computed the #548 gate — confirming the engine runs correctly
from its new home.

Refs #613

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
don-petry added a commit that referenced this pull request Jul 9, 2026
…ive) (#632)

docs(#631): remove duplicate versioning.md; point canary ADR to .github-private canonical

Epic #613 (PR #624) copied docs/release/versioning.md into .github, creating a second
copy that has since diverged from the .github-private original. .github's own standards
already name .github-private as authoritative (ci-standards.md §"authoritative process
lives in that repo's docs/release/"; agent-canary-rings-adr.md links there), and the doc
documents cut-release.sh which lives in .github-private.

- Remove docs/release/versioning.md (the redundant #624 copy).
- Repoint canary-rollout-adr.md's two links to the .github-private canonical URL.

No content lost — canonical copy stays in .github-private.

Closes #631. Refs #613

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
don-petry added a commit that referenced this pull request Jul 12, 2026
…I gap from #613/#624) (#685)

* ci: add canary-rollout-tests.yml — run the engine's bats suite (closes CI gap from #613/#624 relocation)

The canary-rollout engine + tests/canary_rollout.bats were relocated into .github
by #613/#624 but no CI runner came with them, so the 149+ test suite has been
unenforced ever since — every #668 increment (#672/#676/#678/#684) merged without
its tests actually running in CI. This workflow runs shellcheck + the full bats
suite on any PR touching the engine, its pure core, the suite, or this workflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4z5uYVjwdyQjh2wFZxkut

* chore: dev-lead update (review-changes) [skip ci-relay]

* chore: dev-lead update (review-changes) [skip ci-relay]

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
don-petry added a commit that referenced this pull request Jul 17, 2026
* feat(personas): add the persona @-mention router

Serves the addressing contract that .github#752 defined. ONE router for every
persona — personas do NOT each ship a mention workflow (§4.1); a stub per agent
is exactly the drift the manifest exists to prevent, and it is how the fleet got
two divergent registries the last time.

There is no persona index, by design

Because address.handle's slug MUST equal `id`, and `id` MUST equal the persona's
directory name (both enforced by validate-personas.py), '@petry-projects/qa-lead'
resolves to personas/qa-lead/persona.yml by convention. The manifest is the
index-of-record (§1.1) — nothing derives, caches or duplicates it. This also
avoids inverting the established derivation direction: canary-rings.json
(.github) -> release/registry.yml (.github-private), whereas a persona index
would have had to flow .github-private -> .github.

A 404 is a meaningful answer ("not a persona"), which is also how a real,
non-persona team like @petry-projects/org-leads falls through harmlessly.

What decides what

The manifest, not this router: whether the mention surface is enabled, in what
mode, behind which trust floor, under which opt_out_label. The router restates
none of it. A persona may TIGHTEN the job-level trust default but never loosen
it — an undeclared floor denies, so a persona that forgot to declare trust is
not more permissive than one that did.

Recursion (the reason the core is a tested library, not inline shell)

Comments posted via a PAT re-trigger workflows, unlike GITHUB_TOKEN.
.github-private#860 burned 1,481 identical acks in 4.5h from a SINGLE self-loop.
With N mutually addressable personas the cycles are combinatorial, not
self-loops: qa-lead answering a thread that mentions dev-lead is enough. So the
guard is two independent axes — bot actor AND agent marker — enforced both in
the job-level `if` (so secrets never reach a bad run) and in pm_should_route
(so it is testable). The marker match is a PREFIX: #860's first fix matched one
exact ack string and still self-looped through a different agent comment.

Contents
- scripts/lib/persona-mention.sh — the pure, network-free decision core.
  Fetching is the workflow's job, so the suite pins routing behaviour with no
  API access at all.
- tests/persona_mention.bats — 31 tests, recursion guards most heavily.
- .github/workflows/persona-mention-tests.yml — the runner. A bats suite with no
  CI runner is unenforced (the #613/#624 gap canary-rollout-tests.yml exists to
  close); added WITH the suite rather than after it.
- .github/workflows/persona-mention-reusable.yml — the router. Sources the lib
  at `github.job_workflow_sha` so the logic always matches the pinned workflow
  version (the #465/#528 tooling_ref lesson); every event field arrives via env,
  never inline `${{ }}` in a run: body.
- standards/workflows/persona-mention.yml — the single caller stub, pinned to
  persona-mention/v1-stable (major-scoped per #657, matching live stubs).
- standards/canary-rings.json — registers the reusable so a release can be cut.

Deliberately NOT here
- ring-pins.sh RING_REUSABLES is untouched: adding persona-mention would make
  compliance-audit demand the stub in repos that do not have it and make the
  deploy sweep report drift — manufacturing failures for a reusable with no cut
  release. That belongs to rollout.
- No release cut, no repo enrolled, and no persona runtime exists yet, so
  NOTHING routes on merge. The router dispatches `persona-mention` to
  .github-private, where nothing listens yet — a safe no-op.

Verification (CI's exact invocations, run locally)
- bats tests/persona_mention.bats: 31/31.
- shellcheck --severity=warning -x scripts/**/*.sh: clean.
- actionlint -ignore SC2129 -ignore '"job_workflow_sha" is not defined'
  .github/workflows/*.yml: clean. (That ignore is pre-existing and correct —
  job_workflow_sha is a real context property actionlint 1.7.7's schema omits;
  the merged auto-rebase-reusable.yml trips the same false positive.)
- markdownlint-cli2: clean.
- canary-rings.json: spliced as TEXT, not re-serialized — the file's encoding is
  genuinely mixed (em-dashes escaped, arrows half-literal) and its agent order is
  insertion order, not alphabetical, so a json round-trip rewrote ~140 untouched
  lines. Result is 63 insertions / 0 deletions, and the entry is structurally
  identical to pr-review-mention's but for host/reusable/run_workflow.

One bug the tests caught: pm_manifest_query piped python into jq, so the
pipeline reported JQ's status and a parse failure returned empty output with
exit 0 — indistinguishable from "the manifest says no". Now captured and
checked.

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

* fix(reviews): address review comments [skip ci-relay]

* fix(personas): close the fail-open paths and enforce the write gate

Addresses the review findings on this PR (#755) and the bot review.

1. Fail-open trio — errors were read as negative answers

The systemic flaw #755 names as its headline finding. Three instances here:

- Manifest fetch: `curl -f` collapses 404 and 5xx into one exit code, so a
  raw.githubusercontent hiccup silently disabled EVERY persona fleet-wide, with
  no error anywhere and indistinguishable from nobody being addressed. Now
  switched on the status code: 200 routes, 404 is a real answer ("not a persona"
  — a plain team like @petry-projects/org-leads, or a typo), anything else is a
  hard failure.
- Labels: `|| echo ""` turned an API error into "no labels", which would bypass
  an opt-out AND arm an ungated write. Now fails closed. Discussions genuinely
  have no labels surface, so an empty set is the TRUE answer there — that
  difference is load-bearing (see 2).
- pm_manifest_query already fixed in 518ff2b (pipeline reported jq's status).

2. gate_label is now ENFORCED (security)

The schema makes gate_label required for write surfaces, but only enforces that
it is DECLARED — declaring a lock is not locking the door. The router read
`mode`, passed it in the payload, and dispatched. '@petry-projects/dev-lead do X'
would have been an ungated write the moment the first write persona onboarded.
Now: write-mode requires the gate label to be APPLIED; a write persona with no
gate_label is a hard error; and write-mode on a surface where labels are
unavailable (discussions) is refused, because the gate cannot be verified.

gate_label is read by its own function, not a 4th space-separated field on
pm_mention_decision: an empty optional field silently shifts the next one into
its place ("true write  qa-lead" parses the gate AS the opt_out_label). A
security gate is the last place to accept that. Test 42 pins it.

3. contents: read + PyYAML (Copilot)

The job checks out the tooling repo but never granted contents: read. PyYAML is
NOT documented as preinstalled on the runner images, and this reusable runs on
the CALLER's runner, so it cannot be assumed — our own tests install it
explicitly, which proved the gap. Added a cheap no-op-when-present step. Also
persist-credentials: false on the tooling checkout.

4. Mention precision (#755 finding 9)

The router fired where GitHub itself renders NO mention: fenced code, inline
code. Pasting a usage example or documenting the handle summoned the persona.
Blockquotes are a DELIBERATE divergence — GitHub does notify on a quoted
mention, but its one-click "Quote reply" copies a whole prior comment prefixed
'>', and re-running an agent over quoted text is not what the quoter meant.
Neither recursion axis catches it (a human quoting a human is not a bot and
carries no marker). To address a persona, mention it outside the quote.

Verification (CI's exact invocations)
- bats tests/persona_mention.bats: 42/42 (31 + 11 new covering fences, tilde
  fences, inline code, blockquotes, the two must-still-fire cases, and the
  gate_label read-shift trap).
- shellcheck --severity=warning -x scripts/**/*.sh: clean.
- actionlint (CI's ignores): clean.

NOT fixed here, deliberately
- aliases (#755 finding 1): the merged schema promises address.aliases[] but
  index-free routing cannot honour it. Dropping it needs an ORDERED cross-repo
  pair — .github-private must remove `aliases: []` from qa-lead's manifest
  first, or dropping it from the schema (additionalProperties: false) breaks
  validate-personas on main fleet-wide. Tracked in #755; it is a contract change
  and deserves its own PR.
- review_requested routing (#755 finding 10) and the 👀 ack (finding 11) are
  scoped out to keep this PR about correctness and security.

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

* fix(personas): drop the dead review_requested trigger from the stub

The stub subscribed to pull_request/review_requested, but the router has no
path for it — the job's `if` never admits the event, so every reviewer
assignment fleet-wide would start a run that immediately skips.

§4 does say "Reviewer-assignment counts here too", and it is a real gap
(#755 finding 10). But the trigger lands WITH the routing,
not before it: a trigger firing into a router that ignores it is dead weight
that only burns runs and makes the stub lie about what it supports.

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

* fix(personas): hash-lock the PyYAML install

The dependency-hardening gate caught the PyYAML fix from 1953ccd:

  not ok 1 every 'pip install' in a workflow is hash- and binary-locked

It was right to. `pip install --quiet pyyaml` is an unpinned, unverified
supply-chain install — and this reusable runs on EVERY repo in the fleet that
adopts the stub, so it is the worst possible place to resolve a package at
runtime from whatever PyPI serves that day.

Replaced with the org's established pattern (ci.yml, dependency-audit-reusable):

  pip install --require-hashes --only-binary :all: -r <name>-requirements.txt

Adds scripts/persona-mention-requirements.txt — PyYAML 6.0.2 pinned with all 53
wheel hashes, generated by pip-compile --generate-hashes, with the regeneration
command in its header like the sibling requirements files. Satisfies SonarCloud
githubactions S8541/S8543/S8544/S8545 as the other files document.

The `python3 -c 'import yaml' || ...` fast path is kept, so the install is a
no-op on runners that already have it.

Verification
- bats test/workflows/dependency-hardening.bats: 3/3 (was 1 failing) — including
  "each hash-locked requirements file referenced by a workflow exists".
- bats tests/persona_mention.bats: 42/42.
- shellcheck + actionlint (CI's invocations): clean.

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

* fix(reviews): address review comments [skip ci-relay]

* fix(personas): do NOT authenticate the manifest fetch — it adds a silent-death path

Reverts the Authorization header added in 9333997, which was made in response to
CodeRabbit's 🔴 Critical "the repo is private, so raw.githubusercontent will 404
and every persona gets skipped".

That premise is false, and acting on it introduces the exact bug this PR exists
to fix.

petry-projects/.github-private is PUBLIC despite its name (private: false —
every one of the org's 11 repos is public). Measured against the real manifest
URL:

    unauthenticated -> 200      valid token -> 200
    expired token   -> 404      empty token -> 404

raw.githubusercontent answers a bad or MISSING token with 404, not 401. Our
router — correctly — treats 404 as a real answer: "not a persona". So with an
Authorization header, the moment GH_PAT_WORKFLOWS expires, or a repo adopts the
stub before the secret exists, or a fork PR runs without secrets, EVERY persona
404s and is silently skipped as "not a persona". Fleet-wide, silent, and
indistinguishable from nobody being addressed.

Unauthenticated cannot fail that way for a public repo. The header buys nothing
(200 either way) and adds a silent-death path — the precise fail-open class this
PR closes. Reverted, with the measurements recorded inline so the next reader
(human or agent) does not re-add it on the same false premise.

The stub's comment re-indent from 9333997 is kept; it is cosmetic and fine.

Verification
- bats tests/persona_mention.bats: 42/42.
- actionlint (CI's ignores): clean.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants