Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions .github/workflows/dev-lead.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,22 @@

jobs:
dev-lead:
# Pinned to the moving dev-lead/v139-stable channel tag, not @main, so a broken
# Pinned to the moving dev-lead/v139-ring0 channel tag, not @main, so a broken
# change to dev-lead can no longer gate its own fix (the self-host circular
# dependency). This repo sits in ring `next`, but this stub DELIBERATELY pins a
# STABLE tier: Safe Release SC2 (#503 / epic #495) requires that a broken
# in-development dev-lead cannot block the PR fixing its own breakage. The
# `next` canary duty in this repo belongs to pr-review-trigger.yml, which pins
# pr-review/v1-next on purpose. Asserted by tests/test_sc2_self_review_channel.bats
# — do not "fix the ring drift" by repinning this to next.
# PROMOTED tier (ring0 — promoted from next only after its dwell): Safe Release
# SC2 (#503 / epic #495) requires that a broken in-development dev-lead cannot
# block the PR fixing its own breakage, and a bad ring0 is rolled back by moving
# the tag, with no PR. ring0 rather than the stable tier gets this repo dev-lead
# fixes days sooner. The `next` canary duty in this repo belongs to
# pr-review-trigger.yml, which pins pr-review/v1-next on purpose. Asserted by
# tests/test_sc2_self_review_channel.bats — never repin this to next.
# Promotion is done by moving the channel tags centrally; this
# caller is never edited on release. agent_ref threads the same channel into
# dev-lead's own scripts/prompts checkout. See https://github.com/petry-projects/.github/blob/main/standards/ci-standards.md#dev-lead-agent.
uses: petry-projects/.github-private/.github/workflows/dev-lead-reusable.yml@dev-lead/v139-stable # NOSONAR(githubactions:S7637) first-party channel ref
uses: petry-projects/.github-private/.github/workflows/dev-lead-reusable.yml@dev-lead/v139-ring0 # NOSONAR(githubactions:S7637) first-party channel ref
Comment thread
don-petry marked this conversation as resolved.
Resolved
with:
agent_ref: dev-lead/v139-stable
agent_ref: dev-lead/v139-ring0
secrets: inherit # NOSONAR(githubactions:S7635) first-party trusted reusable
permissions:
contents: write
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@
tests/test_pr_review_force_review_wiring.bats \
tests/test_review_one_pr_nonrequired_gate.bats \
tests/test_review_one_pr_force_re_review.bats \
tests/test_review_one_pr_escalation_hold.bats \
tests/test_review_one_pr_self_dismissed_reeval.bats \
tests/test_review_one_pr_metadata_rearm.bats \
tests/test_review_one_pr_claim_observe.bats \
tests/test_pr_metadata_digest.bats \
Expand Down Expand Up @@ -310,7 +312,7 @@
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# NOTE: persist-credentials is intentionally left at the default (true) —

Check warning on line 315 in .github/workflows/lint.yml

View workflow job for this annotation

GitHub Actions / Lint

315:9 [comments-indentation] comment not indented like content
# vci_resolve_reusable needs `git fetch origin` to authenticate against this
# private repo when resolving same-repo channel tags. Setting
# persist-credentials: false would silently push every such caller into the
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ breaks post-merge (the #1034 defect). This guard closes that gap.
The `caller-stub-freeze` job in `lint.yml` (#1255, epic #1052 Part B) is the byte-identity **backstop** to
`validate-caller-inputs` for the specific **ring-0 / self-host** caller stubs — the ones whose reusable lives
in **this** repo and is pinned to a canary channel tag (`docs/initiatives/agentic-release-strategy.md` §5):
`dev-lead.yml` (`@dev-lead/v1-stable`), `pr-review-trigger.yml` (`@pr-review/v1-next`), and
`dev-lead.yml` (`@dev-lead/v1-ring0`), `pr-review-trigger.yml` (`@pr-review/v1-next`), and
Comment thread
don-petry marked this conversation as resolved.
Resolved
`ci-failure-analyst.lock.yml` (`@ci-failure-analyst/v1-stable`). A trigger/`with:` forwarding change to a
channel-pinned self-host stub is exercised by nothing in PR CI and only breaks post-merge (the #1034 defect
class), so each stub's `on:` trigger + `uses:`/`with:` forwarding block is frozen byte-for-byte against a
Expand Down
15 changes: 9 additions & 6 deletions docs/release/sc2-game-day.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ eliminate (`dev-lead`/`pr-review` build, review, and merge changes *to themselve
Two mechanisms together guarantee it. For downstream consumer repos pinned to
`stable`, either alone is sufficient. For **this repo** (the ring-0 canary) the
two duties are split by design: `dev-lead.yml` (the dev/merge duty) pins a
**stable-tier** channel (`@dev-lead/v1-stable`) so mechanism (1) protects it here
**promoted** channel (`@dev-lead/v1-ring0`) so mechanism (1) protects it here
exactly as it does a downstream consumer, while `pr-review-trigger.yml`
deliberately pins `next` to dogfood the candidate, so mechanism (2) is that
path's operative SC2 safety net:
Expand All @@ -20,8 +20,11 @@ path's operative SC2 safety net:
([versioning.md](./versioning.md)). A break in `next` does not, by
construction, take out a `stable`-pinned gate — the fix PR is still evaluated by
the last known-good `stable`. **This repo keeps its dev/merge duty
(`dev-lead.yml`) pinned to `@dev-lead/v1-stable` even though it sits in ring
`next`** — the deliberate SC2 exception (#1624). `pinned-version-report` flags
(`dev-lead.yml`) pinned to a promoted channel — `@dev-lead/v1-ring0` since
Comment thread
don-petry marked this conversation as resolved.
Resolved
2026-10 (#2091), `stable` before that — even though it sits in ring `next`** —
the deliberate SC2 exception (#1624). ring0 is promoted from `next` only after
its dwell, and a bad ring0 is rolled back by moving the tag, with no PR to
block; it gets this repo dev-lead fixes days before `stable`. `pinned-version-report` flags
that pin as a ⚠️ ring mismatch (it expects `next` for a ring-`next` repo); that
flag is correct from the ring-rollout view and the stable pin is correct from
the SC2 view — the two are reconciled, not contradictory (see
Expand Down Expand Up @@ -55,9 +58,9 @@ path's operative SC2 safety net:

`tests/test_sc2_self_review_channel.bats` codifies mechanism (1) for this repo:
it parses the `uses:` channel pin of `dev-lead.yml` (never a hardcoded version)
and asserts it resolves to a **stable-tier** channel — failing with an
SC2-naming message if a future "fix the ring drift" PR repins the dev/merge duty
to `next`/`ring*`/`@main`/a bare SHA and silently restores the circular
and asserts it resolves to a **promoted** channel (`ring<N>` or `stable`) —
failing with an SC2-naming message if a future "fix the ring drift" PR repins the
dev/merge duty to `next`/`@main`/a bare SHA and silently restores the circular
dependency. It is the automatable half of #503 (this repo's structural SC2
property), complementing the game-day drill below.

Expand Down
11 changes: 6 additions & 5 deletions docs/release/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,20 @@ machine-readable source of truth is `standards/canary-rings.json`, now hosted in
`petry-projects/.github` and consumed by the promotion automation (#501, relocated there under #613).

> **Deliberate SC2 exception — `.github-private`'s own `dev-lead.yml` (#1624).** This repo sits in ring
> `next`, yet its self-review/dev duty stub `dev-lead.yml` pins `@dev-lead/v1-stable`. This is **not**
> `next`, yet its self-review/dev duty stub `dev-lead.yml` pins a promoted channel, `@dev-lead/v1-ring0`
Comment thread
don-petry marked this conversation as resolved.
Resolved
> (it pinned `stable` until #2091). This is **not**
> drift to fix: it is the structural half of Safe Release **SC2** (epic #495 / story #503) — a broken
> in-development (`next`) version cannot block the PR that fixes its own breakage, because the dev/merge
> gate runs the last known-good `stable`. The `pinned-version-report` audit (`scripts/pinned-version-report.sh`
> gate runs promoted code, and a bad ring0 is rolled back by moving the tag, with no PR to block. The `pinned-version-report` audit (`scripts/pinned-version-report.sh`
> in `petry-projects/.github`) flags this pin as a ⚠️ **ring mismatch** (a ring-`next` repo is expected to
> pin `next`). Both surfaces are correct and reconciled here: the report is right from the ring-rollout
> perspective, and the stable pin is right from the SC2 perspective. **Do not "fix the ring drift" by
> perspective, and the promoted pin is right from the SC2 perspective. **Do not "fix the ring drift" by
> repinning `dev-lead.yml` to `next`** — that silently re-arms the self-hosting circular dependency. The
> property is enforced continuously by `tests/test_sc2_self_review_channel.bats`, which parses the stub's
> channel pin and fails with an SC2-naming message on any non-`stable` tier. (This repo's
> channel pin and fails with an SC2-naming message on any unpromoted pin (`next`, `@main`, a SHA). (This repo's
> `pr-review-trigger.yml` deliberately pins `@pr-review/v1-next` as the ring-0 pr-review canary — a
> different duty whose SC2 net is the break-glass, see [`sc2-game-day.md`](./sc2-game-day.md) — so it is
> **not** part of this stable-pin assertion.)
> **not** part of this promoted-pin assertion.)

A staged rollout advances the channels in order — `next` → `ring0` → `ring1` → `stable` — validating
at each step (see [`runbook.md` §2c](./runbook.md#2c-staged-canary--ring-rollout)). All four channels
Expand Down
2 changes: 1 addition & 1 deletion scripts/dev-lead-hold-notice.sh
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ collapse_stale_notices() {
if [ -n "$keep_label" ] && [ "$cur_label" = "$keep_label" ]; then
continue
fi
new_body=$(hold_notice_supersede_body "$old_body")
new_body=$(hold_notice_supersede_body "$old_body" "$keep_label")
if [ "$DRY_RUN" = "true" ]; then
echo "[dry-run] would collapse stale hold notice $cid (label=$cur_label) on $REPO#$SUBJECT_NUMBER"
continue
Expand Down
6 changes: 3 additions & 3 deletions scripts/dev-lead-retry.sh
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ scan_pr_for_rate_limits() {
# (#926) — re-dispatching either would re-ignite the #860 "amplifier". We reuse
# the already-fetched labels to avoid a redundant PR fetch.
local labels_json
labels_json=$(jq -c '[.labels[]?.name]' <<< "$pr_obj" 2>/dev/null || echo '[]')
labels_json=$(jq -c '[.labels[]?.name]' <<< "$pr_obj" 2>/dev/null || echo 'null')
if pr_resume_suppressed "$pr_number" "$repo" "$labels_json"; then
echo "0"
return 0
Expand Down Expand Up @@ -697,7 +697,7 @@ scan_pr_for_undispositioned_bot_comments() {
echo "0"; return 0
fi
local labels_json
labels_json=$(jq -c '[.labels[]?.name]' <<< "$pr_obj" 2>/dev/null || echo '[]')
labels_json=$(jq -c '[.labels[]?.name]' <<< "$pr_obj" 2>/dev/null || echo 'null')
if pr_resume_suppressed "$pr_number" "$repo" "$labels_json"; then
echo "0"; return 0
fi
Expand Down Expand Up @@ -923,7 +923,7 @@ scan_pr_for_unreplied_bot_threads() {
echo "0"; return 0
fi
local labels_json
labels_json=$(jq -c '[.labels[]?.name]' <<< "$pr_obj" 2>/dev/null || echo '[]')
labels_json=$(jq -c '[.labels[]?.name]' <<< "$pr_obj" 2>/dev/null || echo 'null')
if pr_resume_suppressed "$pr_number" "$repo" "$labels_json"; then
echo "0"; return 0
fi
Expand Down
16 changes: 13 additions & 3 deletions scripts/lib/hold-notice.sh
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,20 @@ body_has_active_hold_notice() {
esac
}

# hold_notice_supersede_body <comment-body>
# hold_notice_supersede_body <comment-body> [replacing-label]
# Rewrite a live notice IN PLACE to collapse it (AC4): drop the active marker,
# stamp the superseded marker, and wrap the remaining prose in a collapsed
# <details> block — reusing the supersede convention in post-pr-review.sh
# (mark_prior_agent_items_obsolete). Idempotent: an already-collapsed body is
# returned unchanged, so re-running never nests wrappers.
#
# With no [replacing-label] the hold was lifted (dev-lead picked the item up).
# With one, a notice for a DIFFERENT hold label is replacing this one — the old
# label may well still be on the item, so the summary must not claim it was
# lifted (#2089: a `dev-lead:hands-off` notice read "lifted" on a PR that still
# carried the label, because `needs-human-review` sorts first in the hold set).
hold_notice_supersede_body() {
local body="${1:-}"
local body="${1:-}" replacing="${2:-}"
case "$body" in
*"<!-- dev-lead-hold-notice superseded"*) printf '%s' "$body"; return 0 ;;
esac
Expand All @@ -124,7 +130,11 @@ hold_notice_supersede_body() {
# longer reads as still-held and no longer suppresses a future notice.
stripped="${body//"$active"/}"
printf '%s\n' "$(hold_notice_superseded_marker "$label")"
printf '<details><summary><em>Resolved — the `%s` hold was lifted; dev-lead has picked this item up. Click to expand the prior hold notice.</em></summary>\n\n' "$label"
if [ -n "$replacing" ] && [ "$replacing" != "$label" ]; then
printf '<details><summary><em>Superseded — dev-lead is now withholding action under the `%s` label. Click to expand the prior `%s` hold notice.</em></summary>\n\n' "$replacing" "$label"
else
printf '<details><summary><em>Resolved — the `%s` hold was lifted; dev-lead has picked this item up. Click to expand the prior hold notice.</em></summary>\n\n' "$label"
fi
printf '%s\n\n' "$stripped"
printf '</details>'
}
42 changes: 39 additions & 3 deletions scripts/lib/pr-automation-budget.sh
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,25 @@ pr_has_escalation_label() {
<<<"$labels_json" >/dev/null 2>&1
}

# The fleet's human-hold label set (needs-human-review, dev-lead:needs-human,
# dev-lead:hands-off) lives in hold-gate.sh (#1595). Sourced here so the resume
# gate below honours every hold, not just NEEDS_HUMAN_REVIEW_LABEL (#2089).
# shellcheck source=scripts/lib/hold-gate.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hold-gate.sh"

# pr_hold_gate_label <labels_json>
# Print the first hold-gate label (hold_gate_labels) present in the JSON array
# of label NAMES and exit 0; print nothing and exit 1 when none is present.
# Malformed/empty/missing input degrades to "not held" (exit 1), like
# pr_has_escalation_label.
pr_hold_gate_label() {
local labels_json="${1:-[]}" names
names=$(jq -r 'if type == "array" then .[] | strings else empty end' \
<<<"$labels_json" 2>/dev/null) || return 1
Comment thread
don-petry marked this conversation as resolved.
Resolved
[ -n "$names" ] || return 1
hold_gate_first_match "$names"
}

# pr_hold_kind <items_json> <head_sha>
# Classify WHY a PR carrying NEEDS_HUMAN_REVIEW_LABEL is held, so a re-trigger
# timer can log the exact hold it honors (#1550 AC#5) and exempt the ONE hold
Expand Down Expand Up @@ -156,8 +175,8 @@ _pr_items_have_marker() {

# pr_resume_suppressed <pr> <repo> [labels_json] [events_json]
# Exit 0 (SUPPRESS — do NOT resume / re-dispatch) when the PR is human-gated
# (carries NEEDS_HUMAN_REVIEW_LABEL) OR its per-PR automation budget is
# exhausted; exit 1 (proceed) otherwise.
# (carries NEEDS_HUMAN_REVIEW_LABEL or any other hold-gate label, #2089) OR its
# per-PR automation budget is exhausted; exit 1 (proceed) otherwise.
#
# This is the SINGLE stop-condition consulted before any automated resume of a
# blocked/rate-limited dev-lead state — by the event-first resume bridge
Expand All @@ -170,18 +189,35 @@ _pr_items_have_marker() {
#
# labels_json / events_json may be passed in by a caller that already fetched
# them (avoids a redundant API round-trip); when omitted they are fetched here.
# A labels_json that is not a JSON array (a failed fetch or parse) suppresses.
pr_resume_suppressed() {
local pr="$1" repo="$2" labels_json="${3:-}" events_json="${4:-}"
[ -n "$pr" ] && [ -n "$repo" ] || return 1

if [ -z "$labels_json" ]; then
labels_json=$(gh api "repos/${repo}/pulls/${pr}" \
--jq '[.labels[]?.name]' 2>/dev/null || echo '[]')
--jq '[.labels[]?.name]' 2>/dev/null) || labels_json=''
fi
# An unreadable label set may be hiding a hold, so it suppresses (fail closed,
# #2089 review); `[]` is reserved for a label set that was read and is empty.
if ! jq -e 'type == "array"' <<<"$labels_json" >/dev/null 2>&1; then
echo " [suppress] PR #${pr} in ${repo} — labels unavailable (API error or unreadable); suppressing fail-closed" >&2
return 0
fi
if pr_has_escalation_label "$labels_json"; then
echo " [suppress] PR #${pr} in ${repo} carries ${NEEDS_HUMAN_REVIEW_LABEL} — human-gated; not resuming (#946)" >&2
return 0
fi
# Every other human hold (dev-lead:hands-off, dev-lead:needs-human) stops the
# resume/retry paths too. Before #2089 only the escalation label did, so a held
# PR still got retry markers, dispatch guards and a dispatched run that then
# skipped at intent time — clutter plus spent retry attempts.
local hold hold_rc=0
hold=$(pr_hold_gate_label "$labels_json") || hold_rc=$?
if [ "$hold_rc" -eq 0 ]; then
echo " [suppress] PR #${pr} in ${repo} carries ${hold} — human hold; not resuming (#2089)" >&2
return 0
fi

if [ -z "$events_json" ]; then
if ! events_json=$(gather_pr_automation_events "$pr" "$repo"); then
Expand Down
Loading
Loading