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
4 changes: 3 additions & 1 deletion scripts/fleet_monitor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,9 @@ fi
# Produces fleet_stub_drift.json — the DRIFTED enrolled repos across every
# processed stub, each row tagged with its `stub`/`stub_file` so the workflow's
# "Track stub drift" step can group/route per stub kind. HAS_STUB_DRIFT wakes
# dev-lead. A combined empty array is emitted when no stub drifted.
# dev-lead. A combined empty array is emitted when no stub drifted. Role rows
# classified from a legacy per-role file (repo has no agent-ingress.yml) are left
# out by stub_drift_alert_json and appear only in the report, marked legacy (#2289).
# ---------------------------------------------------------------------------
STUB_DRIFT_JSON="fleet_stub_drift.json"
if [ ${#stub_drift_files[@]} -gt 0 ]; then
Expand Down
49 changes: 35 additions & 14 deletions scripts/fleet_stub_drift.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
# GitHub's contents API returns in `.sha` (git blob object IDs), so equal SHAs
# mean byte-identical files. Network fetching lives in fleet_monitor.sh.
#
# Drift TSV format (4 fields, tab-separated):
# 1:repo 2:status 3:repo_sha 4:canonical_sha
# Drift TSV format (4 fields, tab-separated, plus an optional 5th):
# 1:repo 2:status 3:repo_sha 4:canonical_sha [5:source]
# source is `legacy` on a collapsed-role row classified from the legacy per-role
# file because the repo has no agent-ingress.yml (#2289); absent otherwise.
# status ∈ { ALIGNED, DRIFTED, MISSING }
# ALIGNED — stub present and byte-identical to the canonical template
# DRIFTED — stub present but its blob SHA differs (the alertable set)
Expand Down Expand Up @@ -151,19 +153,21 @@ classify_role_drift() {
# through the identical report/alert path as whole-file (#822/#886) rows. The
# emitted repo_sha/canonical_sha are the pair the classification was actually made
# on: the block SHAs when an agent-ingress.yml is present, else the legacy
# whole-file SHAs — so the rendered table stays meaningful either way.
# whole-file SHAs — so the rendered table stays meaningful either way. A
# legacy-fallback row carries a 5th field `legacy` (#2289): it is not
# agent-ingress.yml drift, so stub_drift_alert_json keeps it out of the alert /
# remediation set while generate_stub_drift_report still lists it.
stub_drift_row_role() {
local repo="${1:-}" ingress_present="${2:-}" canon_block="${3:-}"
local ingress_block="${4:-}" legacy_canon="${5:-}" legacy_repo="${6:-}"
local status repo_sha canonical
local status
status="$(classify_role_drift "$ingress_present" "$canon_block" \
"$ingress_block" "$legacy_canon" "$legacy_repo")"
if [ "$ingress_present" = "yes" ]; then
repo_sha="$ingress_block"; canonical="$canon_block"
printf '%s\t%s\t%s\t%s\n' "$repo" "$status" "$ingress_block" "$canon_block"
else
repo_sha="$legacy_repo"; canonical="$legacy_canon"
printf '%s\t%s\t%s\t%s\tlegacy\n' "$repo" "$status" "$legacy_repo" "$legacy_canon"
fi
printf '%s\t%s\t%s\t%s\n' "$repo" "$status" "$repo_sha" "$canonical"
}

# count_stub_drift <tsv_file> <status>
Expand All @@ -182,6 +186,11 @@ count_stub_drift() {
# <role> is given (a collapsed agent-ingress.yml job, #1726), each row is also
# tagged with `role` so remediation patches the RIGHT job block — the stub_file
# alone is ambiguous once several roles share one agent-ingress.yml.
# Rows tagged `legacy` (classified from a legacy per-role file because the repo
# has no agent-ingress.yml, #2289) are EXCLUDED: stamping them with the
# agent-ingress.yml stub_file would tell dev-lead / remediation to write an
# ingress into a repo that has none — the ADR-0007 double-dispatch trap. The
# compliance audit's stub-surface checks govern legacy per-role stubs.
stub_drift_alert_json() {
local f="${1:-}" stub_label="${2:-}" stub_file="${3:-}" role="${4:-}"
if [ -z "$f" ] || [ ! -s "$f" ]; then
Expand All @@ -192,7 +201,7 @@ stub_drift_alert_json() {
[ inputs
| select(length > 0)
| split("\t")
| select(length >= 4 and .[1] == "DRIFTED")
| select(length >= 4 and .[1] == "DRIFTED" and .[4] != "legacy")
| { repo: .[0], status: .[1], repo_sha: .[2], canonical_sha: .[3] }
| if $stub != "" then . + { stub: $stub } else . end
| if $stub_file != "" then . + { stub_file: $stub_file } else . end
Expand Down Expand Up @@ -224,10 +233,22 @@ generate_stub_drift_report() {
return 0
fi

printf 'These enrolled repos have drifted from the canonical stub — re-sync them:\n\n'
printf '| Repo | Status | Stub SHA | Canonical SHA |\n'
printf '|---|---|---|---|\n'
awk -F'\t' '$2 == "DRIFTED" {
printf "| `%s` | 🔴 %s | `%s` | `%s` |\n", $1, $2, substr($3,1,7), substr($4,1,7)
}' "$f"
if awk -F'\t' '$2 == "DRIFTED" && $5 != "legacy" { found = 1; exit } END { exit !found }' "$f"; then
printf 'These enrolled repos have drifted from the canonical stub — re-sync them:\n\n'
printf '| Repo | Status | Stub SHA | Canonical SHA |\n'
printf '|---|---|---|---|\n'
awk -F'\t' '$2 == "DRIFTED" && $5 != "legacy" {
printf "| `%s` | 🔴 %s | `%s` | `%s` |\n", $1, $2, substr($3,1,7), substr($4,1,7)
}' "$f"
fi

if awk -F'\t' '$2 == "DRIFTED" && $5 == "legacy" { found = 1; exit } END { exit !found }' "$f"; then
printf '\n### Legacy per-role stubs (repos without agent-ingress.yml)\n\n'
printf 'These repos use per-role stubs and are not enrolled in agent-ingress.yml. They are not alerted or remediated in the above; the compliance audit stub-surface checks cover them.\n\n'
printf '| Repo | Status | Stub SHA | Canonical SHA |\n'
printf '|---|---|---|---|\n'
awk -F'\t' '$2 == "DRIFTED" && $5 == "legacy" {
printf "| `%s` | 🔴 %s | `%s` | `%s` |\n", $1, $2, substr($3,1,7), substr($4,1,7)
}' "$f"
fi
}
49 changes: 47 additions & 2 deletions tests/fleet_stub_drift.bats
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,54 @@ INGRESS_NO_DEVLEAD="${BATS_TEST_DIRNAME}/fixtures/agent-ingress/collapsed-no-dev
[ "$output" = "$(printf 'petry-projects/charlie\tMISSING\t\t%s' "$canon_dl")" ]
}

@test "stub_drift_row_role: no ingress (legacy repo) emits the legacy whole-file SHAs (AC #4)" {
@test "stub_drift_row_role: no ingress (legacy repo) emits the legacy whole-file SHAs, tagged legacy (AC #4, #2289)" {
local canon="cafecafecafecafecafecafecafecafecafecafe"
run stub_drift_row_role "petry-projects/delta" "no" "block-sha-ignored" "" "$canon" "$canon"
[ "$status" -eq 0 ]
[ "$output" = "$(printf 'petry-projects/delta\tALIGNED\t%s\t%s' "$canon" "$canon")" ]
[ "$output" = "$(printf 'petry-projects/delta\tALIGNED\t%s\t%s\tlegacy' "$canon" "$canon")" ]
}

# ---------------------------------------------------------------------------
# Legacy-fallback rows (#2289): a role row classified from the legacy per-role
# file (no agent-ingress.yml) is reported but never alerted or remediated as
# agent-ingress.yml drift.
# ---------------------------------------------------------------------------

@test "stub_drift_alert_json: a legacy-tagged DRIFTED row is excluded; an ingress row is kept (#2289)" {
tsv="$(mktemp)"
stub_drift_row_role "petry-projects/legacy" "no" "" "" "$CANON" "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" > "$tsv"
stub_drift_row_role "petry-projects/markets" "yes" "$CANON" "ffffffffffffffffffffffffffffffffffffffff" "" "" >> "$tsv"
run stub_drift_alert_json "$tsv" "Dev-lead" ".github/workflows/agent-ingress.yml" "dev-lead"
rm -f "$tsv"
[ "$status" -eq 0 ]
[ "$(printf '%s' "$output" | jq -c '[.[] | {repo, stub_file, role}]')" = \
'[{"repo":"petry-projects/markets","stub_file":".github/workflows/agent-ingress.yml","role":"dev-lead"}]' ]
}

@test "generate_stub_drift_report: a legacy DRIFTED row stays in the table, marked legacy and not alerted (#2289)" {
tsv="$(mktemp)"
stub_drift_row_role "petry-projects/legacy" "no" "" "" "$CANON" "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" > "$tsv"
stub_drift_row_role "petry-projects/markets" "yes" "$CANON" "ffffffffffffffffffffffffffffffffffffffff" "" "" >> "$tsv"
run generate_stub_drift_report "$tsv" "$CANON" "Dev-lead"
rm -f "$tsv"
[ "$status" -eq 0 ]
[[ "$output" == *"🔴 DRIFTED: 2"* ]]
local legacy_section before_legacy
legacy_section="$(printf '%s\n' "$output" | sed -n '/^### Legacy per-role stubs/,$p')"
before_legacy="$(printf '%s\n' "$output" | sed '/^### Legacy per-role stubs/,$d')"
[[ "$legacy_section" == *'`petry-projects/legacy`'* ]]
[[ "$legacy_section" != *'`petry-projects/markets`'* ]]
[[ "$before_legacy" == *'`petry-projects/markets`'* ]]
[[ "$before_legacy" != *'`petry-projects/legacy`'* ]]
[[ "$output" == *"not alerted"* ]]
}

@test "generate_stub_drift_report: legacy-only drift omits the re-sync remediation table (#2289)" {
tsv="$(mktemp)"
stub_drift_row_role "petry-projects/legacy" "no" "" "" "$CANON" "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" > "$tsv"
run generate_stub_drift_report "$tsv" "$CANON" "Dev-lead"
rm -f "$tsv"
[ "$status" -eq 0 ]
[[ "$output" != *"re-sync them"* ]]
[[ "$output" == *"### Legacy per-role stubs"* ]]
}
65 changes: 65 additions & 0 deletions tests/fleet_stub_registry.bats
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,68 @@ registry_rows() {
[ "$(role_counts Ci-failure-analyst)" = "$(counts_line 0 0 1)" ]
grep -qF "repos/petry-projects/legacy/contents/.github/workflows/dev-lead.yml" "$CALLS"
}

# ---------------------------------------------------------------------------
# #2289 — a legacy repo (no agent-ingress.yml) is classified from its per-role
# file, but its drift is NOT agent-ingress.yml drift: it is reported (marked
# legacy) and kept out of fleet_stub_drift.json, the alert + remediation input.
# ---------------------------------------------------------------------------

# drift_dev_lead_block — print the markets ingress with ONLY the dev-lead job
# block changed (one line inside it), so its block SHA differs from canon.
drift_dev_lead_block() {
awk '
$0 ~ /^ dev-lead:/ { injob = 1 }
injob && /^ uses:/ && !done { sub(/@dev-lead\/v139-stable/, "@dev-lead/v1-stable"); done = 1 }
{ print }
' "$MARKETS_INGRESS"
}

@test "#2289(a): a legacy repo whose per-role file drifts is reported, marked legacy, but not in fleet_stub_drift.json" {
publish_canonicals
printf 'name: drifted\n' | put_file "petry-projects/legacy" ".github/workflows/dev-lead.yml"

run_monitor petry-projects/legacy
[ "$status" -eq 0 ]
[ "$(role_counts Dev-lead)" = "$(counts_line 0 1 0)" ]
sed -n '/^### Legacy per-role stubs/,$p' "${WORK}/fleet_monitor_report.md" | grep -qF '`petry-projects/legacy`'
[ "$(jq -c . "${WORK}/fleet_stub_drift.json")" = "[]" ]
}

@test "#2289(b): a collapsed repo whose block drifts is in fleet_stub_drift.json against agent-ingress.yml" {
publish_canonicals
drift_dev_lead_block | put_file "petry-projects/markets" "$INGRESS_STUB"
# Sanity: only the dev-lead block differs from canon.
[ "$(job_block_sha dev-lead < "${FAKE_ROOT}/petry-projects/markets/${INGRESS_STUB}")" != \
"$(job_block_sha dev-lead < "$MARKETS_INGRESS")" ]

run_monitor petry-projects/markets
[ "$status" -eq 0 ]
[ "$(role_counts Dev-lead)" = "$(counts_line 0 1 0)" ]
[ "$(jq -c '[.[] | {repo, stub_file, role}]' "${WORK}/fleet_stub_drift.json")" = \
"$(jq -nc --arg sf "$INGRESS_STUB" '[{repo: "petry-projects/markets", stub_file: $sf, role: "dev-lead"}]')" ]
}

@test "#2289(c): the remediation input never lists agent-ingress.yml for a repo that has no ingress" {
publish_canonicals
drift_dev_lead_block | put_file "petry-projects/markets" "$INGRESS_STUB"
# The legacy repo drifts on every role that has a per-role file.
local role
for role in "${ROLES[@]}"; do
printf 'name: drifted %s\n' "$role" | put_file "petry-projects/legacy" ".github/workflows/${role}.yml"
done

run_monitor petry-projects/legacy petry-projects/markets
[ "$status" -eq 0 ]
# The legacy repo is reported DRIFTED (dev-lead row) yet absent from the JSON.
[ "$(role_counts Dev-lead)" = "$(counts_line 0 2 0)" ]
[ "$(jq '[.[] | select(.repo == "petry-projects/legacy")] | length' "${WORK}/fleet_stub_drift.json")" -eq 0 ]

# The remediation plan built from that JSON targets only markets' ingress.
source "$REMEDIATE"
plan="$(build_remediation_plan "${WORK}/fleet_stub_drift.json")"
[ "$(printf '%s' "$plan" | jq -r '[.[] | select(.stub_file == "'"$INGRESS_STUB"'") | .repo] | unique | join(",")')" = "petry-projects/markets" ]
run bash "$REMEDIATE" "${WORK}/fleet_stub_drift.json"
[ "$status" -eq 0 ]
[[ "$output" != *"petry-projects/legacy"* ]]
}
Loading