diff --git a/scripts/canary-rollout.sh b/scripts/canary-rollout.sh index 68b40c4b2..5ed2516ba 100755 --- a/scripts/canary-rollout.sh +++ b/scripts/canary-rollout.sh @@ -1017,6 +1017,14 @@ _registered_reusables_for_host() { _jq -r --arg h "$1" '[.agents[]? | select(.host==$h) | .reusable // empty] | unique | .[]' } +# _unmanaged_reusables_for_host — reusable paths on INTENTIONALLY out of the +# ring-gate model, recorded in the `unmanaged` block (#651): single-hop channel reusables, +# frozen-major `@vN` infra, or dispatch-only workflows. drift excludes these so its +# "unregistered" signal only flags reusables that genuinely still need onboarding. +_unmanaged_reusables_for_host() { + _jq -r --arg h "$1" '[.unmanaged[]? | select(.host==$h) | .reusable // empty] | unique | .[]' +} + # _agents_for_reusable — the registry agent name(s) whose (host,reusable) # match, joined by ", " (for the missing-file finding message). _agents_for_reusable() { @@ -1061,7 +1069,7 @@ cmd_drift() { while IFS= read -r host; do [ -z "$host" ] && continue echo "──────── host: $host ────────" - local present registered unregistered missing p agents + local present registered unmanaged_r unregistered missing p agents if ! present="$(_gh_list_reusables "$host")"; then # Could not read the host's workflows dir — skip it rather than false-flag every # registered reusable as missing-file. Read-only + best-effort: a warning, not a failure. @@ -1069,15 +1077,19 @@ cmd_drift() { continue fi registered="$(_registered_reusables_for_host "$host")" - local reg_arr=() pres_arr=() reg_str="" pres_str="" + unmanaged_r="$(_unmanaged_reusables_for_host "$host")" + local reg_arr=() pres_arr=() um_arr=() reg_str="" pres_str="" um_str="" if [ -n "$registered" ]; then mapfile -t reg_arr <<< "$registered"; fi if [ -n "$present" ]; then mapfile -t pres_arr <<< "$present"; fi + if [ -n "$unmanaged_r" ]; then mapfile -t um_arr <<< "$unmanaged_r"; fi [ "${#reg_arr[@]}" -gt 0 ] && reg_str=" ${reg_arr[*]}" [ "${#pres_arr[@]}" -gt 0 ] && pres_str=" ${pres_arr[*]}" + [ "${#um_arr[@]}" -gt 0 ] && um_str=" ${um_arr[*]}" echo " registered reusables (${#reg_arr[@]}):$reg_str" echo " present *-reusable.yml (${#pres_arr[@]}):$pres_str" - # unregistered = present on the host but not in the registry. - unregistered="$(set_difference "$present" "$registered")" + [ "${#um_arr[@]}" -gt 0 ] && echo " unmanaged (intentional, out of ring gate) (${#um_arr[@]}):$um_str" + # unregistered = present on the host, minus registered agents AND intentionally-unmanaged (#651). + unregistered="$(set_difference "$(set_difference "$present" "$registered")" "$unmanaged_r")" while IFS= read -r p; do [ -z "$p" ] && continue echo "::warning::DRIFT[unregistered] $host: $p present on host but absent from canary-rings.json (no cut/soak/gate/dashboard until registered)" diff --git a/standards/canary-rings.json b/standards/canary-rings.json index 1d3ad8b54..b29783f72 100644 --- a/standards/canary-rings.json +++ b/standards/canary-rings.json @@ -593,5 +593,22 @@ } } } + }, + "unmanaged": { + "add-to-project": { + "host": "petry-projects/.github", + "reusable": ".github/workflows/add-to-project-reusable.yml", + "reason": "Single-hop @add-to-project/stable channel (5 consumers); low-risk project-automation plumbing, intentionally out of the full ring gate (#651)." + }, + "claude-code": { + "host": "petry-projects/.github", + "reusable": ".github/workflows/claude-code-reusable.yml", + "reason": "Frozen-major @v2 base wrapper \u2014 not a canary-promoted agent." + }, + "pr-auto-review": { + "host": "petry-projects/.github", + "reusable": ".github/workflows/pr-auto-review-reusable.yml", + "reason": "Frozen-major @v2 review-dispatcher infra \u2014 not a canary-promoted agent." + } } } diff --git a/tests/canary_rollout.bats b/tests/canary_rollout.bats index 798f92595..b09a2e668 100644 --- a/tests/canary_rollout.bats +++ b/tests/canary_rollout.bats @@ -1094,6 +1094,29 @@ _drift_rings_one_agent() { [[ "$output" != *"ci.yml present on host"* ]] } +@test "orchestrator: drift excludes an intentionally-unmanaged reusable (#651)" { + # Registry: dev-lead agent + an `unmanaged` entry for foo-reusable.yml on .github-private. + DRIFT_RINGS="$BATS_TEST_TMPDIR/drift-rings-um.json" + jq '{version, description, org_infra_repos, member_tokens, + agents: {"dev-lead": .agents["dev-lead"]}, + unmanaged: {"foo": {"host":"petry-projects/.github-private","reusable":".github/workflows/foo-reusable.yml","reason":"single-hop infra (#651)"}}}' \ + "$RINGS" > "$DRIFT_RINGS" + # host has dev-lead (registered) + foo (unmanaged) + bar (genuinely unregistered) + _drift_stub '[ + {"type":"file","name":"dev-lead-reusable.yml","path":".github/workflows/dev-lead-reusable.yml"}, + {"type":"file","name":"foo-reusable.yml","path":".github/workflows/foo-reusable.yml"}, + {"type":"file","name":"bar-reusable.yml","path":".github/workflows/bar-reusable.yml"} + ]' '[]' + run env CANARY_RINGS="$DRIFT_RINGS" bash "$ORCH" drift + [ "$status" -eq 0 ] + # foo is reported as unmanaged, NOT flagged as unregistered drift + [[ "$output" == *"unmanaged (intentional"* ]] + [[ "$output" != *"DRIFT[unregistered] petry-projects/.github-private: .github/workflows/foo-reusable.yml"* ]] + # bar (neither registered nor unmanaged) IS still flagged, and it's the ONLY one + [[ "$output" == *"DRIFT[unregistered] petry-projects/.github-private: .github/workflows/bar-reusable.yml"* ]] + [[ "$output" == *"1 unregistered"* ]] +} + @test "orchestrator: drift flags a registry entry whose reusable file no longer exists on the host (missing-file)" { # Registry has 'ghost' pointing at a reusable that is NOT present on the host. DRIFT_RINGS="$BATS_TEST_TMPDIR/drift-ghost.json"