Skip to content

bug(ci): the blocking stale-baseline audit reds ssot-coverage on any PR touching 8% of the repo #14912

Closed

Description

@mrveiss

Calibration finding on the guards shipped in #14883 (ac754f5e0). Corrected account, replacing this issue's first version: the guard was not defective and did not cause the base to go red. It fired on real drift and reported it accurately on the pull request that caused it. The base red came from a merge issued against a head whose check was already failing. A blocking guard that fires is not a bug.

What stands on its own is the recovery cost, and the numbers are why it is worth fixing.

The measured problem

baseline entries 1411
distinct files covered 776
tracked files in repo 9510

~8% of the repository is baselined, concentrated exactly where change is frequent:

328  autobot-slm-backend/ansible
297  autobot-infrastructure/shared
117  autobot-backend/services
 99  autobot-backend/api
 74  autobot-frontend/src

Any pull request that edits, moves, or fixes a baselined line strands its entry and reds ssot-coverage. The author is then told what is wrong and not what to do:

STALE  other|autobot-slm-backend/ansible/roles/_shared/tasks/clean_wrong_node_dir.yml|"/opt/autobot/{{ dir_name }}/data

Nothing in that output names a command, and the file to edit —
pipeline-scripts/hardcoded_values_baseline.txt — has nothing to do with the change that tripped it. This taxes the exact behaviour the guard exists to encourage: fixing a hardcoded value. #14890 hit it on the first merge after the guards landed, which is evidence about the rate, not bad luck.

Fix: make the correction one command, keep the audit blocking

  1. ./pipeline-scripts/detect-hardcoded-values.sh --prune-baseline — rewrites the baseline to what the detector currently finds, but only ever removes. It must be structurally incapable of adding a key or raising a count; that property is what stops it becoming the bypass check_baseline_no_growth.sh exists to prevent.
  2. Fix the message, not just the mechanism. --audit-baseline's failure must name the exact command and list the entries it would prune. Most of this cost is discoverability.
  3. Keep the audit blocking. A stranded exemption naming a moved path really does re-permit the value when that path returns, and that is a known recurrence class in this repository. The answer is one-command recovery, not a downgrade to a warning.
  4. Note in docs/developer/HARDCODING_PREVENTION.md so the next person to hit the red finds the answer.

Does the cost land on the change that caused it? Yes — but nothing stops that change

Checked rather than assumed. ssot-coverage did run on #14890's own head and did fail there (744df346a, conclusion failure, 21:48). So the audit already reports on the causing pull request; the deeper fix that question implies is largely in place.

The real gap is enforcement. SSOT Configuration Compliance is not one of the ten required contexts on Dev_new_gui:

smoke-test · code-quality · startup-import-smoke · Unit & Integration Tests
No open blocks-merge issues reference this PR · No commit trailers
verify-generated-types · migration-matrix · verify-precommit-config · api-wiring

So every step in that workflow — the detector, the stale audit, and the new no-growth guard — is advisory at the branch-protection layer. A red ssot-coverage does not block a merge, which is precisely how a failing head could be merged. The whole hardcoded-value gate is a check nobody is required to satisfy.

Making it required is not a one-line change, and the reason is recorded in the workflow itself (#13388): ssot-coverage.yml carries a paths: filter, and a path-filtered required check that never triggers leaves a pull request deadlocked on "Expected — Waiting for status". This repository already has the established answer to that shape — a complementary shim job publishing the same context when the real one does not apply, as python-required-context.yml, frontend-required-context.yml and api-wiring's skip job all do. Promotion to a required context is an owner decision and is deliberately not bundled into this issue's scope; it is recorded here so the enforcement gap is not mistaken for solved once the ergonomics are.

Acceptance criteria

  • --prune-baseline exists and is proven removal-only by test: given a tree containing a NEW finding, a prune run must not add it
  • Mutation-proved that prune cannot be used to grow: fabricate a violation, run prune, confirm check_baseline_no_growth.sh still fires
  • Mutation-proved recovery: strand an entry -> audit RED -> --prune-baseline -> audit GREEN, no-growth still GREEN
  • --audit-baseline's failure message names the command and the entries to be pruned
  • docs/developer/HARDCODING_PREVENTION.md documents the recovery
  • The enforcement gap above is either actioned or carried forward as its own owner-gated issue

Related

Activity

  1. mrveiss commented on Aug 23, 2026

    @mrveiss
    OwnerAuthor

    Body rewritten with a corrected account. Two changes of substance:

    1. The guard did not cause the base red. It fired on real drift and reported accurately on the PR that caused it; the base red came from a merge issued against an already-failing head. The first version of this issue misattributed that, and the record should not stand wrong. The calibration finding — recovery cost — stands on its own.
    2. Checked the 'should the audit run on the causing PR' question rather than assuming it. It already does: ssot-coverage ran on fix(provision): route every component removal through one data-guarded task, so an unknown fact cannot delete a database (#14856) #14890's own head and failed there. The real gap is that SSOT Configuration Compliance is not one of the ten required contexts on Dev_new_gui, so the detector, the stale audit and the new no-growth guard are all advisory at the protection layer. Recorded with the ci: ~25 workflow runs per PR head movement saturate GitHub-hosted concurrency and manufacture threshold-test failures #13388 path-filter constraint and the existing required-context-shim precedent; promotion is an owner decision and deliberately out of this issue's scope.
  2. mrveiss commented on Aug 23, 2026

    @mrveiss
    OwnerAuthor

    Prototyped hv_pruned_baseline against a read-only copy of the merged library while blocked on #14910. Three properties confirmed empirically, not just by construction:

    property result
    brand-new key appears in pruned output 0 — cannot add
    any pruned count raised above its baseline none — cannot raise
    key seen 3x but baselined 2 pruned to 2, not 3

    The implementation iterates only keys already in HV_BASELINE (so it can never introduce one) and emits min(baseline_count, seen_count) (so it can never raise one). hv_partition increments HV_BASELINE_SEEN before the allowance test, so seen counts every finding for a key — which is what makes min correct rather than accidentally lossy.

    Safety gap the prototype exposed, now part of the design. Prune drops any baselined key the scan did not see. That is the intended behaviour against a full tree scan, but it means a broken or partial scan silently guts the baseline: in the prototype a deliberately tiny simulated scan reduced 1411 entries to 2. A scan that fails to load rules, or whose scan directories are missing, produces few or zero findings and would take the whole record with it — and the no-growth guard would not object, because shrinking is allowed by design.

    Two additional acceptance criteria:

    • Prune REFUSES on an empty scan. A tree carrying 1411 baselined findings cannot legitimately drop to zero in one change, and an empty result is indistinguishable from a broken detector — the exact failure shape these guards exist to catch.
    • Prune reports how many entries it will remove before writing, so removing 1 entry and removing 1400 are visibly different in the diff and in review.

    Still holding on #14910 as instructed — it touches the same baseline file. Waiter armed on its merge.

  3. mrveiss commented on Aug 23, 2026

    @mrveiss
    OwnerAuthor

    Implemented in PR #14915 (26c7064a0), branched fresh off origin/Dev_new_gui (192a2d12c) after #14910 merged.

    All acceptance criteria met, each mutation-proved rather than asserted:

    criterion proof
    --prune-baseline removal-only real unbaselined violation in tree -> prune -> key not added, still reported as a violation, no-growth green
    cannot raise a count key baselined 1, found twice -> stays 1 (min(baseline, seen))
    recovery loop strand -> audit RED -> prune -> audit GREEN, baseline byte-identical to pre-mutation
    audit names command + entries failure output carries both, plus why the command is safe
    refuses on empty scan hermetic tree with no scan dirs -> refuses, baseline unwritten
    docs HARDCODING_PREVENTION.md gains the recovery section

    The removal-only property is structural, not a check: hv_pruned_baseline iterates keys already in the baseline (cannot introduce one) and emits min(baseline_count, seen_count) (cannot raise one). That matters because a prune able to add or increment would be the bypass check_baseline_no_growth.sh exists to prevent.

    The empty-scan refusal came out of prototyping, not review: prune drops every key the scan did not see, so a broken detector or a moved scan directory would take all 1410 entries with it — and the no-growth guard would not object, since shrinking is allowed by design.

    The required-context gap recorded in this issue is not addressed by #14915 and remains open here as an owner decision. Separately, #14914 was filed for a distinct finding: 8 of 12 rule classes never block, and 9 unbaselined violations sit on the base right now under "status": "pass".

  4. github-actions commented on Aug 23, 2026

    @github-actions
    Contributor

    PR #14915 (merged to Dev_new_gui) references this issue with a close keyword.

    fix(ci): give the stale-baseline audit a one-command recovery (#14912)

    If this issue is fully resolved, close it manually. If work remains, no action is needed.

  5. mrveiss commented on Aug 23, 2026

    @mrveiss
    OwnerAuthor

    Delivered and merged as c83c2e39e (PR #14915).

    • CI: 39 success / 10 skipped / 0 failures, deduped to the latest run per check name on head c7de43dcd. All three touched steps green on the runner: the detector, the stale-entry audit, and the no-growth guard.
    • Review: independent code-reviewer pass. Removal-only confirmed structural, not merely tested — one write path, no early-write error branch, cannot add a key, cannot raise a count, no escape found for either property. Header round-trips byte-for-byte.

    What this fixes: the recovery cost identified here. A fixed violation can now drop its baseline line with one command, and the audit failure names both the offending entries and the exact command.

    A HIGH finding was raised in review and fixed before merge, not deferred.

    The empty-scan refusal only caught a scan returning literally zero. hv_scan_tree treats a missing root as a silent no-op, so one moved SCAN_DIRS entry yields nothing while the other five yield hundreds — the total stays non-zero, the refusal never fires, and min(kept, 0) drops every key beneath the unreached directory. Reproduced hermetically before fixing:

    hardcoded-values: baseline pruned — 5 key(s) -> 2 key(s), 3 removed
      exit rc=0
    

    --audit-baseline then passed, because every survivor still matched, and the no-growth guard stayed silent because shrinking is allowed by design. Nothing between them checked magnitude. Real blast radius on the committed baseline: 554 entries under autobot-backend, 405 under autobot-slm-backend, 337 under autobot-infrastructure.

    The fix guards every mode, not just prune — a deliberate widening of the suggested fix, and the right one. A partial scan corrupts all three: --json under-reports the counts ssot-coverage.yml gates on, and --audit-baseline reports the unreached directory's entries as STALE. That last one compounds: the audit falsely accuses real entries, and its own remedy is to prune, which would then delete them. Two mechanisms that each look correct in isolation.

    Landed message, verified in base:

    FATAL: scan directory '<dir>' does not exist under <root>.
      Refusing to scan a partial tree: findings under it would be missing,
      its baseline entries would look stale, and a prune would delete them.
    

    A proportional-loss guard was considered and declined, with the reasoning recorded in the code: no percentage threshold has a non-arbitrary value, it would block the one legitimate large prune (a sweep fixing many violations at once), and any loss under the threshold still passes. The existence check removes the cause rather than rationing the symptom.

    Also corrected: the guard's own justification comment cited "a rules file that failed to load", which is unreachable — the script is already fatal on an unsourceable library before scanning. Naming a risk the code already forecloses makes the remaining real one look handled.

    Tests: 8 added, parametrized over all six scan dirs so the guard cannot silently cover five, plus a SCAN_DIRS drift test. Includes the byte-identical no-op prune the PR body had claimed as a proof point with no committed test behind it. Also folded in: LC_ALL=C on the prune sort (unpinned collation could reorder every line in a different CI image) and the temp file added to the cleanup trap.

    This PR does not modify the baseline itself — 1433 lines before and after.

    Remaining in this area: #14914, where STATUS and the workflow exit are keyed on ssot_violations alone, so 8 of 12 emit classes never block and the base currently reports pass over 9 unbaselined violations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions