Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
50 changes: 40 additions & 10 deletions scripts/deploy-standard-workflows.sh
Original file line number Diff line number Diff line change
Expand Up @@ -306,14 +306,17 @@ is_already_compliant() {
# the bare `<base>/<tier>` OR the major-scoped `<base>/v<M>-<tier>` form (#657
# F5) — post-migration the templates pin the v-form, so both must enter here.
if ring_is_ring_reusable "$base" && [[ "$ref_after" =~ ^${base}/(v[0-9]+-)?(stable|next|ring[0-9]+)$ ]]; then
# Major-scoped channels (#657 F5, #861): the bare-tier grace is now
# major-AWARE. A bare `<base>/<tier>` stub stays compliant only while the
# agent has NO release (no current major line) — the pre-release fleet has
# nothing to major-scope to yet. Once the agent has a release, a bare stub is
# drift and must migrate to the tier's `v<M>-<tier>` form.
# Major-scoped channels (#657 F5, #861, #870): the bare-tier grace is now
# major-AWARE, keyed on the CHANNEL major (the highest `<base>/v<M>-<tier>`
# channel tag that exists) — NOT the release major. A bare `<base>/<tier>`
# stub stays compliant only while the agent has NO channel tag (nothing to
# major-scope onto yet). Once a channel tag exists, a bare stub is drift and
# must migrate to the tier's `v<M>-<tier>` form. Using the channel major (not
# the release major) is what keeps dev-lead — release v14, channel v1 — pinned
# to the tag that actually resolves (@dev-lead/v1-<tier>, not @dev-lead/v14-…).
local host major
host="$(cut -d/ -f1-2 <<< "$prefix")"
major="$(ring_host_current_major "$host" "$base")"
major="$(ring_host_current_channel_major "$host" "$base")" || return 1
if [[ -z "$major" ]]; then
local ref
while IFS= read -r ref; do
Expand Down Expand Up @@ -361,15 +364,20 @@ reusable_host_of() {

# emit_ref_for <template> <repo> -> the channel ref a (re)deployed stub in <repo>
# should pin (#657 F5). For a ring-managed reusable it is the repo's tier channel,
# major-scoped `v<M>-<tier>` when the agent has a release, else the bare `<tier>`
# form. Empty for a non-ring template (deployed verbatim). Requires GH_TOKEN.
# major-scoped `v<M>-<tier>` when the agent has a CHANNEL tag, else the bare `<tier>`
# form. The major is the CHANNEL major (highest existing `<base>/v<M>-<tier>` tag),
# NOT the release major — so dev-lead (release v14, channel v1) pins the resolving
# `@dev-lead/v1-<tier>`, never the tagless `@dev-lead/v14-<tier>` (#870). Empty for
# a non-ring template (deployed verbatim). Requires GH_TOKEN.
emit_ref_for() {
local template="$1" repo="$2" base host major
base="$(reusable_base_of "$template")"
[[ -z "$base" ]] && return 0
ring_is_ring_reusable "$base" || return 0
host="$(reusable_host_of "$template")"
major="$(ring_host_current_major "$host" "$base")"
if ! major="$(ring_host_current_channel_major "$host" "$base")"; then
return 1
fi
ring_canonical_ref "$base" "$repo" "$major"
return 0
}
Expand Down Expand Up @@ -456,7 +464,24 @@ deploy_repo() {
skip "$repo/$target_path already compliant"
continue
fi
emit="$(emit_ref_for "$template" "$repo")"
if ! emit="$(emit_ref_for "$template" "$repo")"; then
err "$repo/$workflow — channel-major probe failed; skipping"
_OVERALL_FAILED=1
continue
fi
# assert-exists (#870): never pin a stub to a channel ref that has no tag. A
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
# computed `v<M>-<tier>` (or bare tier) that does not resolve on the host would
# break the caller's workflow on the next run, so refuse it here rather than
# open a PR carrying a non-resolving pin.
if [[ -n "$emit" ]]; then
local emit_host
emit_host="$(reusable_host_of "$template")"
if ! ring_tag_exists "$emit_host" "$emit"; then
err "$repo/$workflow — computed channel ref @${emit} does not resolve to a tag on ${emit_host}; refusing to deploy a non-resolving pin"
_OVERALL_FAILED=1
continue
fi
fi
Comment thread
don-petry marked this conversation as resolved.
deploy_template="$repin_source"
if [[ -n "$emit" ]] && [[ "$DRY_RUN" != "true" ]]; then
base="$(reusable_base_of "$template")"
Expand Down Expand Up @@ -533,11 +558,16 @@ main() {

log "Deploying ${#WORKFLOWS[@]} workflow(s) to ${#REPOS[@]} repo(s)"

_OVERALL_FAILED=0
local repo
for repo in "${REPOS[@]}"; do
deploy_repo "$repo"
done

if [[ "$_OVERALL_FAILED" -ne 0 ]]; then
err "Completed with errors — one or more workflows were refused or failed; see above"
exit 1
fi
log "Done."
}

Expand Down
79 changes: 74 additions & 5 deletions scripts/lib/ring-pins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,24 @@ ring_highest_major() {
return 0
}

# _ring_fetch_version_tokens <host-repo> <channel-base> -> strips the
# `refs/tags/<base>/v` prefix from each matching-ref, one token per line.
# Returns 0 (with tokens, or empty stdout if no refs match) on success; returns
# 1 if the gh call itself fails. Callers decide the warning and fallback.
_ring_fetch_version_tokens() {
local out
out="$(gh api "repos/$1/git/matching-refs/tags/$2/v" \
--jq '.[]?.ref' 2>/dev/null)" || return 1
sed -n "s#^refs/tags/$2/v##p" <<< "$out"
}

# ring_host_current_major <host-repo> <channel-base> -> the current major line for
# an agent, derived from its release tags `<base>/vX.Y.Z` on the host repo, or
# empty if the agent has no release (so callers fall back to the bare-tier form).
# gh-backed: reads matching-refs for `<base>/v` and feeds the semver tokens to
# ring_highest_major. Requires GH_TOKEN in the environment.
# gh-backed. Requires GH_TOKEN. Fails open (returns 0) on probe error.
ring_host_current_major() {
local host="$1" base="$2" refs
refs="$(gh api "repos/${host}/git/matching-refs/tags/${base}/v" \
--jq '.[]?.ref' 2>/dev/null \
| sed -n "s#^refs/tags/${base}/v##p")" || {
refs="$(_ring_fetch_version_tokens "$host" "$base")" || {
echo "Warning: failed to fetch matching refs for ${host}/${base}" >&2
return 0
}
Expand All @@ -158,6 +166,67 @@ ring_host_current_major() {
return 0
}

# ring_highest_channel_major <token>... -> the highest major M among CHANNEL tokens
# of the form `<M>-<tier>` (tier = stable|next|ring<N>), or empty if none. A release
# semver token like `14.0.0` is NOT a channel token and is ignored. Pure; always 0.
#
# This is the CALLER-CONTRACT major, deliberately distinct from ring_highest_major's
# release major (#870): dev-lead ships releases `dev-lead/v14.0.0` but its channel
# contract is `dev-lead/v1-<tier>`, so a pin must track the channel major (v1). A
# `<base>/v<release>-<tier>` ref has no tag and fails to resolve — the #870 breakage.
ring_highest_channel_major() {
local tok major best=""
for tok in "$@"; do
[[ "$tok" =~ ^([0-9]+)-(stable|next|ring[0-9]+)$ ]] || continue
major="${BASH_REMATCH[1]}"
if [ -z "$best" ] || [ "$major" -gt "$best" ]; then
best="$major"
fi
done
[ -n "$best" ] && printf '%s' "$best"
return 0
}

# ring_host_current_channel_major <host-repo> <channel-base> -> the highest major M
# for which a CHANNEL tag `<base>/v<M>-<tier>` exists on the host, or empty if the
# agent has no channel tag (so callers fall back to the bare-tier form). This is the
# ref a consumer stub must pin — NOT the release major (#870). gh-backed. Requires
# GH_TOKEN. Fails closed (returns 1) on probe error so callers cannot silently fall
# back to bare-tier pins during an API outage.
ring_host_current_channel_major() {
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
local host="$1" base="$2" refs
refs="$(_ring_fetch_version_tokens "$host" "$base")" || {
echo "Warning: failed to fetch matching refs for ${host}/${base}" >&2
return 1
}
# shellcheck disable=SC2086
ring_highest_channel_major $refs
return 0
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# ring_tag_exists <host-repo> <ref> -> 0 iff refs/tags/<ref> resolves on <host>.
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
# The assert-exists guard: a computed channel ref is validated to exist before a
# stub is pinned to it, so the deploy never opens a PR carrying a non-resolving
# `@<base>/v<M>-<tier>` pin (#870). gh-backed. Requires GH_TOKEN.
# Emits a warning when the gh failure is NOT a 404 (auth, rate-limit, network).
# Caches results in a process-global associative array to avoid duplicate calls.
ring_tag_exists() {
# Self-initializing cache: declare -g creates a global even when called inside a
# function; the 2>/dev/null silences the no-op when already declared correctly.
declare -g -A _RING_TAG_EXISTS_CACHE 2>/dev/null || true
local cache_key="$1/$2"
if [[ "${_RING_TAG_EXISTS_CACHE[$cache_key]+isset}" ]]; then
return "${_RING_TAG_EXISTS_CACHE[$cache_key]}"
fi
local out status
out="$(gh api "repos/$1/git/ref/tags/$2" 2>&1)"; status=$?
if [ "$status" -ne 0 ] && ! grep -qi 'not found\|404' <<< "$out"; then
echo "Warning: tag-existence lookup failed for $1/$2 (not a 404): ${out}" >&2
fi
_RING_TAG_EXISTS_CACHE[$cache_key]="$status"
return "$status"
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
# ring_repin_uses <channel-base> <newref> -> rewrite a workflow stub read on stdin
# so its reusable `uses:` ref (and any matching `agent_ref:`) points at <newref>.
# Only lines referencing THIS agent's reusable are touched; the trailing comment
Expand Down
102 changes: 80 additions & 22 deletions test/scripts/deploy-standard-workflows/emit-vform.bats
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
# a wrong-tier v-form is still drift.
#
# All runs are --dry-run (no mutating gh calls) against a single --repo/--workflow so
# they are deterministic. A fake `gh` returns the reusable's release tags (for the
# major derivation) and the target repo's existing stub content.
# they are deterministic. A fake `gh` returns the reusable's CHANNEL tags (for the
# caller-contract major derivation — #870), the target repo's existing stub content,
# and single-ref existence probes (the assert-exists guard).

setup() {
TT_TMP="$(mktemp -d "$BATS_TEST_TMPDIR/stub.XXXXXX")"
Expand All @@ -19,18 +20,31 @@ setup() {
teardown() { rm -rf "$TT_TMP"; }

# Install a fake gh.
# GH_RELEASE_REFS newline-separated `refs/tags/<agent>/vX.Y.Z` for matching-refs
# (unset/empty → the agent has no release → bare form expected).
# GH_MATCHING_REFS newline-separated `refs/tags/<agent>/…` for matching-refs. The
# major is derived from the CHANNEL tags (`<agent>/v<M>-<tier>`),
# NOT the release tags (`<agent>/vX.Y.Z`) — the #870 fix. Unset/no
# channel tag → the agent has no channel major → bare form.
# GH_CONTENT_B64 base64 of the existing stub (unset → contents 404 = missing stub).
# GH_EXISTING_TAGS newline-separated `<agent>/<ref>` the assert-exists probe treats
# as resolvable. UNSET → every ref resolves (legacy default, so a
# test that does not model tag existence is unaffected).
install_gh_stub() {
local bin="${TT_TMP}/bin"
mkdir -p "$bin"
cat > "$bin/gh" <<'STUB'
#!/usr/bin/env bash
if [ "${1:-}" = "api" ]; then
case "$2" in
*git/ref/tags/*)
# Single-ref existence probe (ring_tag_exists). Unset GH_EXISTING_TAGS means
# "don't model existence" → every ref resolves.
[ -z "${GH_EXISTING_TAGS:-}" ] && exit 0
tag="${2##*/git/ref/tags/}"
printf '%s\n' "$GH_EXISTING_TAGS" | grep -qxF "$tag" && exit 0
printf 'Not Found\n' >&2
exit 1 ;;
*matching-refs/tags/*)
[ -n "${GH_RELEASE_REFS:-}" ] && printf '%s\n' "${GH_RELEASE_REFS}"
[ -n "${GH_MATCHING_REFS:-}" ] && printf '%s\n' "${GH_MATCHING_REFS}"
exit 0 ;;
*contents*)
if [ -n "${GH_CONTENT_B64:-}" ]; then
Expand All @@ -46,6 +60,18 @@ STUB
PATH="${bin}:${PATH}"; export PATH
}

# Channel tags for an agent at major <M> across every ring tier — the caller-contract
# tags a deployed stub can legitimately pin. Emitted as matching-refs `refs/tags/…`.
channel_refs() { # <agent> <M> [extra refs...]
local agent="$1" m="$2"; shift 2
printf 'refs/tags/%s/v%s-stable\n' "$agent" "$m"
printf 'refs/tags/%s/v%s-next\n' "$agent" "$m"
printf 'refs/tags/%s/v%s-ring0\n' "$agent" "$m"
printf 'refs/tags/%s/v%s-ring1\n' "$agent" "$m"
if [ "$#" -gt 0 ]; then printf '%s\n' "$@"; fi
return 0
}

stub_pinning() { # <ref> → base64 of a minimal auto-rebase stub pinning the reusable at <ref>
local body="jobs:
auto-rebase:
Expand All @@ -72,19 +98,20 @@ selfhost_stub() { # <base> → base64 of a meta-repo SELF-HOST stub using a loc
base64 -w 0 <<<"$body" 2>/dev/null || base64 -b 0 <<<"$body"
}

@test "emits @<agent>/v<M>-<tier> when the reusable has a release" {
export GH_RELEASE_REFS="refs/tags/auto-rebase/v2.3.1
refs/tags/auto-rebase/v1.0.0"
@test "emits @<agent>/v<M>-<tier> when the reusable has a channel major" {
GH_MATCHING_REFS="$(channel_refs auto-rebase 2 refs/tags/auto-rebase/v2.3.1)"; export GH_MATCHING_REFS
install_gh_stub # no GH_CONTENT_B64 → missing stub → deploy plans a (re)pin
run env GH_TOKEN=x bash "$SCRIPT" --dry-run --repo markets --workflow auto-rebase.yml
[ "$status" -eq 0 ]
# markets is a stable-tier repo → v2 major line → @auto-rebase/v2-stable
# markets is a stable-tier repo → v2 channel line → @auto-rebase/v2-stable
echo "$output" | grep -qF '@auto-rebase/v2-stable'
echo "$output" | grep -qE 'Would open PR for markets .* auto-rebase.yml'
}

@test "emits the bare @<agent>/<tier> form when the reusable has no release" {
unset GH_RELEASE_REFS # matching-refs empty → no major
@test "emits the bare @<agent>/<tier> form when the reusable has no channel tag" {
# Release tags exist but NO channel tag → no channel major → bare form. Proves the
# major comes from channel tags, not the release semver (#870).
export GH_MATCHING_REFS="refs/tags/auto-rebase/v2.3.1"
install_gh_stub
run env GH_TOKEN=x bash "$SCRIPT" --dry-run --repo markets --workflow auto-rebase.yml
[ "$status" -eq 0 ]
Expand All @@ -93,7 +120,7 @@ refs/tags/auto-rebase/v1.0.0"
}

@test "drift check treats a tier-correct v<M>-tier stub as compliant (no PR)" {
export GH_RELEASE_REFS="refs/tags/auto-rebase/v2.0.0"
GH_MATCHING_REFS="$(channel_refs auto-rebase 2)"; export GH_MATCHING_REFS
GH_CONTENT_B64="$(stub_pinning auto-rebase/v2-ring1)"; export GH_CONTENT_B64
install_gh_stub
run env GH_TOKEN=x bash "$SCRIPT" --dry-run --repo TalkTerm --workflow auto-rebase.yml
Expand All @@ -102,10 +129,10 @@ refs/tags/auto-rebase/v1.0.0"
! echo "$output" | grep -q 'Would open PR'
}

@test "drift check flags a BARE-tier stub as drift when the agent has a release (#861)" {
export GH_RELEASE_REFS="refs/tags/auto-rebase/v2.0.0"
# markets (stable tier) still pins the bare @auto-rebase/stable; with a v2 release
# the bare form is drift → deploy re-pins to the tier v-form.
@test "drift check flags a BARE-tier stub as drift when the agent has a channel major (#861)" {
GH_MATCHING_REFS="$(channel_refs auto-rebase 2)"; export GH_MATCHING_REFS
# markets (stable tier) still pins the bare @auto-rebase/stable; with a v2 channel
# major the bare form is drift → deploy re-pins to the tier v-form.
GH_CONTENT_B64="$(stub_pinning auto-rebase/stable)"; export GH_CONTENT_B64
install_gh_stub
run env GH_TOKEN=x bash "$SCRIPT" --dry-run --repo markets --workflow auto-rebase.yml
Expand All @@ -115,8 +142,8 @@ refs/tags/auto-rebase/v1.0.0"
echo "$output" | grep -qE 'Would open PR for markets .* auto-rebase.yml'
}

@test "drift check keeps a BARE-tier stub compliant when the agent has NO release (#861)" {
unset GH_RELEASE_REFS # no release → no current major → bare tier stays compliant
@test "drift check keeps a BARE-tier stub compliant when the agent has NO channel major (#861)" {
unset GH_MATCHING_REFS # no channel tag → no current major → bare tier stays compliant
GH_CONTENT_B64="$(stub_pinning auto-rebase/stable)"; export GH_CONTENT_B64
install_gh_stub
run env GH_TOKEN=x bash "$SCRIPT" --dry-run --repo markets --workflow auto-rebase.yml
Expand All @@ -126,7 +153,7 @@ refs/tags/auto-rebase/v1.0.0"
}

@test "drift check still flags a WRONG-tier v<M>-tier stub" {
export GH_RELEASE_REFS="refs/tags/auto-rebase/v2.0.0"
GH_MATCHING_REFS="$(channel_refs auto-rebase 2)"; export GH_MATCHING_REFS
# v2-ring0 on TalkTerm (a ring1 repo) is the wrong tier → drift.
GH_CONTENT_B64="$(stub_pinning auto-rebase/v2-ring0)"; export GH_CONTENT_B64
install_gh_stub
Expand All @@ -144,20 +171,20 @@ refs/tags/auto-rebase/v1.0.0"
# skips them and --retire-bare stays blocked.

@test "re-pins a meta-repo's channel-pinned CONSUMER stub to the tier v<M>-form" {
export GH_RELEASE_REFS="refs/tags/dev-lead/v3.2.0"
GH_MATCHING_REFS="$(channel_refs dev-lead 3)"; export GH_MATCHING_REFS
# .github's dev-lead stub is a consumer still on the bare tier channel.
GH_CONTENT_B64="$(devlead_stub_pinning dev-lead/ring0)"; export GH_CONTENT_B64
install_gh_stub
# --force so the bare-tier migration grace does not mark it already-compliant.
run env GH_TOKEN=x bash "$SCRIPT" --dry-run --force --repo .github --workflow dev-lead.yml
[ "$status" -eq 0 ]
# .github is a ring0 repo → v3 major line → @dev-lead/v3-ring0
# .github is a ring0 repo → v3 channel line → @dev-lead/v3-ring0
echo "$output" | grep -qF '@dev-lead/v3-ring0'
echo "$output" | grep -qE 'Would open PR for \.github .* dev-lead.yml'
}

@test "leaves a meta-repo's local ./ SELF-HOST stub exempt (never re-pins it)" {
export GH_RELEASE_REFS="refs/tags/agent-shield/v2.0.0"
GH_MATCHING_REFS="$(channel_refs agent-shield 2)"; export GH_MATCHING_REFS
# .github HOSTS agent-shield's reusable — its stub uses a local ./ ref.
GH_CONTENT_B64="$(selfhost_stub agent-shield)"; export GH_CONTENT_B64
install_gh_stub
Expand All @@ -167,3 +194,34 @@ refs/tags/auto-rebase/v1.0.0"
! echo "$output" | grep -qi 'Would open PR'
! echo "$output" | grep -qF '@agent-shield/v'
}

# ── #870: derive the CHANNEL major, never the higher RELEASE major ──────────────
# dev-lead ships internal releases (dev-lead/v14.0.0) but its caller-contract channel
# tags are dev-lead/v1-<tier>. The driver must pin the channel major (v1), because
# @dev-lead/v14-<tier> has no tag and fails to resolve — the live bmad regression.

@test "#870: pins the channel major v1, not the release major v14, for a dev-lead consumer" {
# matching-refs carries a HIGH release tag AND the low v1 channel tags.
GH_MATCHING_REFS="$(channel_refs dev-lead 1 refs/tags/dev-lead/v14.0.0)"; export GH_MATCHING_REFS
# bmad-bgreat-suite is a ring1 repo — the exact live-broken consumer.
install_gh_stub # missing stub → deploy plans a fresh pin
run env GH_TOKEN=x bash "$SCRIPT" --dry-run --repo bmad-bgreat-suite --workflow dev-lead.yml
[ "$status" -eq 0 ]
echo "$output" | grep -qF '@dev-lead/v1-ring1'
! echo "$output" | grep -qF '@dev-lead/v14'
echo "$output" | grep -qE 'Would open PR for bmad-bgreat-suite .* dev-lead.yml'
}

@test "#870: refuses a computed channel ref that has no tag (assert-exists)" {
# Only v1-stable exists as a channel tag (channel major still resolves to 1), but
# the ring1 tier tag v1-ring1 was never cut. The driver must NOT deploy the
# non-resolving @dev-lead/v1-ring1 pin.
export GH_MATCHING_REFS="refs/tags/dev-lead/v14.0.0
refs/tags/dev-lead/v1-stable"
export GH_EXISTING_TAGS="dev-lead/v1-stable" # v1-ring1 absent
install_gh_stub
run env GH_TOKEN=x bash "$SCRIPT" --dry-run --repo bmad-bgreat-suite --workflow dev-lead.yml
[ "$status" -ne 0 ]
echo "$output" | grep -qi 'does not resolve'
! echo "$output" | grep -qi 'Would open PR'
}
Loading
Loading