Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
4f880bf
feat: implement issue #1297 — Synced stubs in ring repos carry a head…
don-petry Oct 11, 2026
83a1175
fix(reviews): sweep treats stale stable-channel header as drift
don-petry Oct 11, 2026
70ed8af
fix(reviews): migrate stale stable headers in place, stable-tier exem…
don-petry Oct 11, 2026
b6f3d24
test: validate sweep flags and fixes stale stable-channel headers in …
don-petry Oct 11, 2026
4bb767a
fix(reviews): replace whole legacy stub header; exempt idea engines f…
don-petry Oct 11, 2026
3fbd2c9
test: verify sweep processes all ring-managed stubs with stale headers
don-petry Oct 11, 2026
506ded3
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 11, 2026
4b57d18
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 11, 2026
4c421b5
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 11, 2026
0f414c9
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 11, 2026
572180f
fix: stub_has_stale_stable_header must be tier-aware to prevent contr…
don-petry Oct 11, 2026
c638177
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 12, 2026
1f70096
fix(reviews): strip stale stable-channel comment blocks in stub body,…
don-petry Oct 11, 2026
03b5dd3
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 12, 2026
3f0de8a
fix(reviews): exempt present-only callers from repo-template full-bod…
don-petry Oct 12, 2026
f47c49c
fix(bot): revert stub header changes — templates are read-only synced…
don-petry Oct 12, 2026
d6d2d69
fix(reviews): render ring headers tier-neutral from stable-wording te…
don-petry Oct 12, 2026
666b6d9
test(reviews): render ring fixtures via ring_repin_uses so headers ar…
don-petry Oct 12, 2026
a294cb9
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 12, 2026
d7ec4f4
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 13, 2026
1eea28d
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 14, 2026
ec25264
Merge main (org renamed to combsmith) into dev-lead/issue-1297-202610…
don-petry Oct 14, 2026
348aae4
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 14, 2026
01f4602
fix: remove outer quotes from neutral array expansion in ring_repin_uses
don-petry Oct 14, 2026
63444b5
fix(reviews): pass only the ref token to ring_repin_uses in dry-run s…
don-petry Oct 14, 2026
4708ee3
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 14, 2026
9842d6f
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 14, 2026
9051276
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 16, 2026
8e71eb3
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 18, 2026
3ed50a2
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 18, 2026
dd65220
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 18, 2026
2269059
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 20, 2026
767380b
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 20, 2026
9bee24c
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 20, 2026
b2366d9
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 20, 2026
ad33b1b
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 20, 2026
49d3107
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 20, 2026
97df195
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 21, 2026
a7e67e7
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 21, 2026
e6653a6
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 21, 2026
8411623
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 22, 2026
b150f8d
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 25, 2026
1cbfb3a
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 25, 2026
226791d
Merge branch 'main' into dev-lead/issue-1297-20261011-0422
don-petry Oct 25, 2026
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
124 changes: 122 additions & 2 deletions scripts/deploy-standard-workflows.sh
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ DEPLOYABLE_WORKFLOWS=(
pr-auto-review.yml
feature-ideation.yml
pr-review.yml
idea-enhancer.yml
idea-triage.yml
initiative-planner.yml
Comment thread
don-petry marked this conversation as resolved.
)

# Deployable workflows whose stub BODY carries a documented per-repo edit the
Expand Down Expand Up @@ -220,6 +223,11 @@ readonly REPLACE_IF_PRESENT_WORKFLOWS=(
# the sweep must never read, replace or re-pin it.
declare -A ENGINE_HOST_REPOS=(
["pr-review.yml"]="$ENGINE_HOST_REPO"
# The central BMAD idea engines live under these names in .github-private; their
# callers here are stubs, so the sweep must not treat the engines as consumers.
["idea-enhancer.yml"]="$ENGINE_HOST_REPO"
["idea-triage.yml"]="$ENGINE_HOST_REPO"
["initiative-planner.yml"]="$ENGINE_HOST_REPO"
)

# is_replace_if_present_workflow <name.yml> -> 0 if the workflow deploys only over
Expand Down Expand Up @@ -601,6 +609,85 @@ merge_guarded_surfaces() {
return 0
}

# stub_has_stale_stable_header <existing_content> <template> <repo> -> 0 if the stub carries a
# comment naming its agent's stable channel (`<base>/stable`, `<base>/v<M>-stable`,
# `<base> stable channel`) that the template no longer carries (#1297). Such a header
# contradicts a ring pin; the pin alone is compliant, so without this check the sweep
# would never ship the comment-only header fix to existing ring repos. In ring repos,
# even a currently stable-pinned stub must migrate its header because the in-place
# repin will change the pin to ring, leaving the header contradictory.
stub_has_stale_stable_header() {
local existing="$1" template="$2" repo="$3" base re tier
base="$(reusable_base_of "$template")"
[[ -n "$base" ]] || return 1
re="^[[:space:]]*#.*(${base}/(v[0-9]+-)?stable|${base} stable channel)"
grep -qE "$re" <<< "$existing" || return 1
# A stable-pinned stub's header agrees with its pin, BUT only when the repo is
# stable-tier. In ring-managed repos, a stable-pinned stub is itself an anomaly
# that the repin will fix; the header must migrate too, so don't exempt in ring.
if ring_is_ring_reusable "$base" && [[ -n "$repo" ]]; then
tier="$(ring_tier_for_repo "$base" "$repo" 2>/dev/null)" || tier=""
# Ring-managed repo with a tier: the stub should be ring-pinned, so the header
# is stale regardless of the current pin.
if [[ -n "$tier" ]] && [[ "$tier" != "stable" ]]; then
# The template may still carry the stable wording; ring renders (ring_repin_uses)
# and the header cleanup neutralize it, so the template never exempts a ring repo.
return 0 # Flag as stale
fi
fi
! grep -qE "$re" "$template" || return 1
# Non-ring reusable or stable-tier repo: apply the exemption (don't flag if
# the stub is stable-pinned, since the header agrees with the pin).
! grep -qE "^[[:space:]]*uses:.*@${base}/(v[0-9]+-)?stable([[:space:]]|\$)" <<< "$existing"
}

# strip_stale_stable_header <base> [<template>] -> stdin minus the stale stable-channel
# header. With <template>, the stub's whole leading comment block (everything above the
# first top-level YAML key) is replaced by the template's canonical, tier-neutral header,
# so a multiline legacy bullet is never left half-deleted. The body is kept verbatim, so
# per-repo edits (e.g. workflow_run.workflows) survive and the next sweep sees no stale
# header (#1297). Without <template>, only the lines naming the stable channel are dropped.
strip_stale_stable_header() {
local base="$1" template="${2:-}"
if [[ -n "$template" && -r "$template" ]]; then
# Body comment blocks (consecutive comment lines) that name the stable channel are
# dropped whole too, so a legacy `# Pinned to the moving ... stable channel` block under
# `jobs:` does not survive and keep the detector true forever.
awk -v tpl="$template" -v base="$base" -v re="^[[:space:]]*#.*(${base}/(v[0-9]+-)?stable|${base} stable channel)" '
BEGIN {
while ((getline l < tpl) > 0) { if (l ~ /^[^#[:space:]]/) break; hdr = hdr l "\n" }
# The template header may still name the stable channel; render it tier-neutral
# (same wording as ring_repin_uses) so cleanup never reinstalls it.
gsub(base "/(v[0-9]+-)?stable", base "/<tier>", hdr)
gsub(base " stable channel", base "/<tier> channel", hdr)
}
function flush() { if (!stale) printf "%s", blk; blk = ""; stale = 0 }
!body && /^[^#[:space:]]/ { body = 1; printf "%s", hdr }
body && /^[[:space:]]*#/ { blk = blk $0 "\n"; if ($0 ~ re) stale = 1; next }

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.

strip_stale_stable_header deletes body comment blocks instead of rendering them tier-neutral like ring_repin_uses

When strip_stale_stable_header runs with a <template>, the BEGIN block rewrites <base>/(v[0-9]+-)?stable and <base> stable channel to <base>/<tier> in the header (matching ring_repin_uses), but line 666 (body && /^[[:space:]]*#/) marks the entire consecutive comment block as stale = 1 and drops all of its lines (if (!stale) printf "%s", blk).

For example, in standards/workflows/dev-lead.yml (lines 59–63), the 5-line explanation block under jobs.dev-lead starts with # Pinned to the moving dev-lead stable channel tag, not @main...: a fresh/template deploy via ring_repin_uses preserves that comment block with dev-lead/<tier> channel, whereas an in-place header migration via strip_stale_stable_header deletes the entire 5-line block. Applying the same two gsub(...) replacements to body comment lines instead of dropping blk keeps in-place migrations consistent with ring_repin_uses.

body { flush(); print; next }
END { if (body) flush() }
'
return 0
fi
grep -vE "^[[:space:]]*#.*(${base}/(v[0-9]+-)?stable|${base} stable channel)" || true
Comment thread
don-petry marked this conversation as resolved.
}

# Workflows an adopter may or may not have (BMAD idea pipeline). The sweep never
# seeds them; it only migrates an existing caller, re-pinning its OWN body in place.
readonly PRESENT_ONLY_WORKFLOWS=(
idea-enhancer.yml
idea-triage.yml
initiative-planner.yml
)

is_present_only_workflow() {
local w="$1" p
for p in "${PRESENT_ONLY_WORKFLOWS[@]}"; do
[[ "$w" == "$p" ]] && return 0
done
return 1
}

# is_already_compliant <existing_content> <template> <repo> -> 0 if the deployed
# stub needs no re-deploy. A stub is compliant only when it is BOTH pin-compliant
# (is_pin_compliant, below) AND — when its template carries a real `secrets: inherit`
Expand All @@ -615,7 +702,8 @@ merge_guarded_surfaces() {
# rendered at the repo's tier channel (stub_body_matches_template): its callers
# predate the template and carry per-repo edits, which are drift. So must every stub
# in a FULL_BODY_COMPARE_REPOS repo (repo-template, #1285) other than a
# BODY_PRESERVING_WORKFLOWS one, whose per-repo body is never re-synced.
# BODY_PRESERVING_WORKFLOWS or PRESENT_ONLY_WORKFLOWS one, whose per-repo body is
# re-pinned in place and never re-synced from the template.
#
# A pin-correct stub whose guarded `on:` / `permissions:` / `concurrency:` surface
# differs from the template is ALSO drift (#1236): the audit flags it
Expand All @@ -634,8 +722,10 @@ is_already_compliant() {
&& ! stub_has_s7635_marker <<< "$existing_content"; then
return 1
fi
stub_has_stale_stable_header "$existing_content" "$template" "$repo" && return 1
if is_replace_if_present_workflow "$workflow" \
|| { is_full_body_compare_repo "$repo" && ! is_body_preserving_workflow "$workflow"; }; then
|| { is_full_body_compare_repo "$repo" && ! is_body_preserving_workflow "$workflow" \
&& ! is_present_only_workflow "$workflow"; }; then
stub_body_matches_template "$existing_content" "$template" "$repo" || return 1
fi
if ! is_skipped_repo "$repo" && ! is_body_preserving_workflow "$workflow" \
Expand Down Expand Up @@ -887,6 +977,17 @@ deploy_repo() {
repin_source="$(mktemp)"; _TMPFILES+=("$repin_source")
printf '%s\n' "$existing_content" > "$repin_source"
fi
elif is_present_only_workflow "$workflow"; then
# Optional pipeline stub: never seed; otherwise re-pin its OWN body in place.
if [[ -z "$existing_sha" ]]; then
skip "$repo/$workflow (present-only: no existing caller — never seeded)"
continue
fi
mode="repin-in-place"
Comment thread
don-petry marked this conversation as resolved.
if [[ "$DRY_RUN" != "true" ]]; then
repin_source="$(mktemp)"; _TMPFILES+=("$repin_source")
printf '%s\n' "$existing_content" > "$repin_source"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
fi
elif is_body_preserving_workflow "$workflow"; then
# feature-ideation carries a per-repo project_context the sweep must never
# clobber. Seed the generic template ONLY when the stub is absent; when it
Expand Down Expand Up @@ -928,6 +1029,25 @@ deploy_repo() {
fi
fi

# Stale stable-channel header (#1297): migrate by editing the stub's OWN body, never
# by redeploying the template (that would reset per-repo edits such as
# pr-auto-review's workflow_run.workflows). Full-body-compared stubs are exempt —
# their drift is already resolved by a template redeploy.
if [[ "$DRY_RUN" != "true" && -n "$existing_sha" ]] \
&& stub_has_stale_stable_header "$existing_content" "$template" "$repo"; then
base="$(reusable_base_of "$template")"
if [[ "$repin_source" == "$template" ]] && ! is_replace_if_present_workflow "$workflow" \
&& ! { is_full_body_compare_repo "$repo" && ! is_body_preserving_workflow "$workflow"; } \
&& is_pin_compliant "$existing_content" "$template" "$repo"; then
repin_source="$(mktemp)"; _TMPFILES+=("$repin_source")
printf '%s\n' "$existing_content" > "$repin_source"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
fi
if [[ "$repin_source" != "$template" ]]; then
strip_stale_stable_header "$base" "$template" < "$repin_source" > "$repin_source.stripped"
mv "$repin_source.stripped" "$repin_source"
fi
fi

# When repin_source is a copy of existing_content (meta-repo consumer or body-
# preserving repin-in-place) and the template requires the S7635 marker but the
# copy lacks it, inject the marker now. Without this, the deployed stub would
Expand Down
13 changes: 12 additions & 1 deletion scripts/lib/ring-pins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -455,11 +455,22 @@ ring_tag_exists() {
# is re-pinned alongside `uses:` here. This only re-pins the VALUE of an input the
# stub already forwards (never adds/forwards a new one), so it never introduces the
# undeclared-input channel-skew defect (#1052).
# When <newref> is not a stable channel, comment lines naming this agent's stable
# channel (`<base>/v<M>-stable`, `<base> stable channel`) are rendered tier-neutral
# (`<base>/<tier>`), so a header never contradicts the ring pin even while the
# template still carries the stable wording (#1297).
ring_repin_uses() {
local base="$1" newref="$2" file
local base="$1" newref="$2" file neutral=()
file="$(ring_reusable_file "$base")"
file="${file//./\\.}"
if [[ ! "$newref" =~ stable$ ]]; then
neutral=(
-e "/^[[:space:]]*#/s#${base}/(v[0-9]+-)?stable#${base}/<tier>#g"
-e "/^[[:space:]]*#/s#${base} stable channel#${base}/<tier> channel#g"
)
fi
sed -E \
${neutral[@]+"${neutral[@]}"} \
-e "s#^([[:space:]]*uses:[[:space:]]*combsmith/[^@[:space:]]*/${file})@[^[:space:]]+#\1@${newref}#" \
-e "s#^([[:space:]]*agent_ref:[[:space:]]*[\"']?)${base}/[^\"'[:space:]]+#\1${newref}#" \
-e "s#^([[:space:]]*checkout_ref:[[:space:]]*[\"']?)${base}/[^\"'[:space:]]+#\1${newref}#"
Expand Down
3 changes: 2 additions & 1 deletion test/scripts/deploy-standard-workflows/dry-run.bats
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ stub_pinning() { # <ref> → base64 of the auto-rebase template re-pinned to <r
# and the surface-drift check (#1236) stays clean.
local body
body="$(sed -E "s|(auto-rebase-reusable\.yml)@[^[:space:]]+|\1@$1|" \
"${REPO_ROOT}/standards/workflows/auto-rebase.yml")"
"${REPO_ROOT}/standards/workflows/auto-rebase.yml" \
| bash -c 'source "$1"; ring_repin_uses "$2" "$3"' _ "$SCRIPT" auto-rebase "${1%% *}")"
base64 -w 0 <<<"$body" 2>/dev/null || base64 -b 0 <<<"$body"
}

Expand Down
6 changes: 4 additions & 2 deletions test/scripts/deploy-standard-workflows/emit-vform.bats
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ stub_pinning() { # <ref> → base64 of the auto-rebase template re-pinned to <r
# concurrency: surfaces match and the surface-drift check (#1236) stays clean.
local body
body="$(sed -E "s|(auto-rebase-reusable\.yml)@[^[:space:]]+|\1@$1|" \
"${REPO_ROOT}/standards/workflows/auto-rebase.yml")"
"${REPO_ROOT}/standards/workflows/auto-rebase.yml" \
| bash -c 'source "$1"; ring_repin_uses "$2" "$3"' _ "$SCRIPT" auto-rebase "$1")"
base64 -w 0 <<<"$body" 2>/dev/null || base64 -b 0 <<<"$body"
}

Expand Down Expand Up @@ -112,7 +113,8 @@ devlead_stub_pinning_with_marker() { # <ref> → base64 of the dev-lead templat
local body
body="$(sed -E -e "s|(dev-lead-reusable\.yml)@[^[:space:]]+|\1@$1|" \
-e "s|^([[:space:]]*agent_ref:[[:space:]]*).*|\1$1|" \
"${REPO_ROOT}/standards/workflows/dev-lead.yml")"
"${REPO_ROOT}/standards/workflows/dev-lead.yml" \
| bash -c 'source "$1"; ring_repin_uses "$2" "$3"' _ "$SCRIPT" dev-lead "$1")"
base64 -w 0 <<<"$body" 2>/dev/null || base64 -b 0 <<<"$body"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ STUB
# The template rendered at <channel> — what a correctly deployed caller looks like:
# only the two pin lines move; the header prose keeps the template's channel.
rendered_at() { # <channel>
sed -E "/^[[:space:]]*(uses|agent_ref):/s#pr-review/v1-stable#$1#" "$TEMPLATE"
sed -E "/^[[:space:]]*(uses|agent_ref):/s#pr-review/v1-stable#$1#" "$TEMPLATE" \
| bash -c 'source "$1"; ring_repin_uses pr-review "$2"' _ "$SCRIPT" "$1"
}

# The legacy fleet caller shape (TalkTerm-like): bare pr-review/stable pin, an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ STUB

# The template rendered at <channel> — only the reusable pin moves.
rendered_at() { # <channel>
sed -E "/^[[:space:]]*uses:/s#dependabot-rebase/v2-stable#$1#" "$TEMPLATE"
sed -E "/^[[:space:]]*uses:/s#dependabot-rebase/v2-stable#$1#" "$TEMPLATE" \
| bash -c 'source "$1"; ring_repin_uses dependabot-rebase "$2"' _ "$SCRIPT" "$1"
}

# The template at <channel> with a stale header comment — the #1075 shape: the
Expand Down Expand Up @@ -214,3 +215,19 @@ deploy_capture() {
grep -qF 'TUNED-REPO-CONTEXT-SENTINEL' "$OUT/feature-ideation.yml"
grep -qF 'feature-ideation-reusable.yml@feature-ideation/v1-stable' "$OUT/feature-ideation.yml"
}

# ── PRESENT_ONLY_WORKFLOWS are re-pinned in place, never body-compared ───────

@test "repo-template: a present-only idea-triage.yml with a customized cron is already compliant" {
local t="${REPO_ROOT}/standards/workflows/idea-triage.yml"
local existing="${TT_TMP}/idea-triage.yml"
sed "s/'30 13 \* \* 1'/'5 4 * * 2'/" "$t" > "$existing"
! diff -q "$existing" "$t" >/dev/null
GH_MATCHING_REFS="refs/tags/idea-triage/v1-stable
refs/tags/idea-triage/v1-next
refs/tags/idea-triage/v1-ring0
refs/tags/idea-triage/v1-ring1"; export GH_MATCHING_REFS
install_gh_stub
run bash -c 'source "$1"; is_already_compliant "$(< "$2")" "$3" combsmith/repo-template' _ "$SCRIPT" "$existing" "$t"
[ "$status" -eq 0 ]
}
Loading
Loading