Repository navigation
refactor(ci): consolidate duplicated, drifted and never-run CI guards (#14371, #13984, #12986, #14034) - #14883
Conversation
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
…#14034) get_staged_files' argv branch was fixed to apply its $pattern, but the two tests covering that branch still asserted the OLD behaviour — that the pattern is ignored — so the one-line fix had nothing holding it in place and the tests were red against the code they cover. Both now pin the fix, and a third asserts the property that was actually broken: the git and argv branches return the same set for the same input. pre-commit-no-module-singletons carried the identical asymmetry in Python. Its .py filter lived inside the git branch only, so CI (which passes an explicit wide-net file list as argv) fed .ts/.vue/.yml paths to ast.parse. It survived only because check_file swallows SyntaxError — a silent mis-scan rather than a loud one. The filter now applies to both branches. Audited every other get_staged_files caller and every argv-vs-git branch in hooks/, tools/lint/ and pipeline-scripts/: no further instance of the shape.
…or the copies GitHub forces (#12986) The backend-Python path list was duplicated across six workflows with no source of truth. Adding a backend directory meant remembering every place, and a miss was SILENT: the gate simply stopped running on the new path, nothing failed, and the coverage quietly shrank. .github/filters/backend-python-paths.yml now defines the set once, following the frontend-paths.yml / python-paths.yml precedent already in that directory, and composes each consumer's filter from it with a YAML alias rather than a retyped copy. Five `changes` jobs load that file by path, so their inline copies are gone outright; their output key names are unchanged, so every `steps.filter.outputs.<name>` reference keeps working verbatim. GitHub offers no include mechanism for `on.*.paths` triggers, so those six copies cannot be removed. pipeline-scripts/check_workflow_path_filters.py polices them instead: every declared inline copy must contain the whole canonical set, every declaration must resolve to a file and a real `paths:` list, a workflow that loads the canonical file must be declared, and api-wiring.yml must keep having NO `pull_request.paths` (#12934 — a path-filtered required check never reports and deadlocks the pull request, and that asymmetry is exactly what a later "tidy the filters" pass would erase). Measured coverage fix found on the way: api-wiring's `wiring` filter omitted autobot_shared/**/*.py while scripts/audit_api_wiring.py imports autobot_shared.api_routing.router_prefixes and autobot_shared.openapi_schema from that tree. Editing those modules changed what the gate computes while the required api-wiring context self-skipped and reported green. It now adopts the canonical set. The guard runs blocking in enforce-precommit.yml rather than code-quality: enforce-precommit carries no `paths:` filter, so its reach is the whole repo, whereas code-quality's `changes` filter does not cover .github/workflows/** and the guard would never run on the exact edit it exists to catch. Verification is 14 mutation tests, not a re-run: dropping a canonical entry from any one of the six inline copies reds that copy, adding an entry to the canonical set alone reds all six, and reintroducing api-wiring's pull_request.paths reds too.
…very changed-files guard (#13984) Five guards each resolved a base ref and status-checked their own git calls: check-pre-commit-hook-pr.sh (5 call sites), code-quality.yml (two inline copies), enforce-precommit.yml (a fifth copy the issue had not counted), lint-conventions.sh and verify-done.sh. All of them encode the same hard-won rules, so the next correction had to be applied five times or the copies drifted. scripts/lib/git-scope.sh now carries the UNION of what those five did, not a pick of one of them. Nothing was dropped: the merge-commit rule, the event payload fallback, the origin/<base> branch-name fallback (only check-pre-commit-hook-pr.sh had it), the last-resort parent, the explicitly-supplied-base rule, three-dot range semantics and range splitting (only lint-conventions.sh had them), the anti-no-op zero-files guard (only code-quality's conventions step had it), the deleted-path filter, the explicit --base validation (only verify-done.sh had it), and "an unresolvable ref is FATAL, never 'nothing changed'" throughout. The explicit-base rule is a SEPARATE entry point from the payload fallback, and deliberately so. Collapsing them was the first version of this change, and check-pre-commit-hook-pr_test.py caught it: an explicitly supplied base that does not resolve was being silently replaced by "<head>^" — a different, smaller scope — and the scan of it reported success. That is precisely the fail-open shape the library exists to prevent, so the two are distinct functions with the difference documented on both. verify-done.sh's branch_state() held six decisions in 113 lines. It is now six named predicates (_bs_commits_ahead, _bs_merges_are_judgeable, _bs_cherry_tally, _bs_collect_diff_paths, _bs_touched_paths, _bs_content_in_base) plus a 22-line decision that combines them; every function in the file is under 30 lines and every guard comment moved with its guard. Not one rule changed. scripts/lib/git-scope_test.sh builds REAL throwaway repositories rather than stubbing git — the rules are all about what git actually does with merge commits and unresolvable refs, so a stub would only re-assert this file's own assumptions. 33 assertions, registered in repo_tests/shell_lib_test.py (whose own guard fails on an unregistered suite). Removing the merge-commit rule reds it; so does weakening the explicit-base rule. lint_conventions_test.py's fixture now ships scripts/lib/git-scope.sh into its throwaway repo, because a checkout has scripts/lib/ in it, plus one new assertion that a missing library is fatal rather than a clean report.
…ts (#14371) Three independent detectors implemented "find hardcoded values". Measured, not assumed: each had rules the other two did not, and only one of the three ever ran over the repository tree — the CI one, which reported ZERO violations. scripts/lib/hardcoded-value-rules.sh is now the single rule set, carrying the UNION of all three. Two thin entry points read it and differ only in where the lines come from and what they do with the verdict: pipeline-scripts/detect-hardcoded-values.sh tree scan; always exits 0, because ssot-coverage.yml has decided pass/fail from the reported counts since #2874 .../hooks/pre-commit-hardcoded-values staged files (or an argv list from CI); exits 1, because it is the one that can stop a commit autobot-infrastructure/shared/scripts/detect-hardcoded-values.sh — the dormant third fork — is retired. Nothing invoked it; the only references were in HARDCODING_PREVENTION.md under a path prefix that never existed. Its rules are not gone, they are in the library: the per-value SSOT suggestion tables, the generic model-tag regex, the hardcoded-URL rule and six context skips nothing else had. The docs now point at the canonical entry points. Detector 1's PORT_PATTERN was declared and never referenced by any scan function — 8443, 5432 and 9090 existed only as a string. A rule that existed only as a string is still a rule the merge has to carry, so it is wired in. scripts/lib/hardcoded_value_rules_test.py is the union proof. It names each former detector and a rule ONLY that one had, and fails by name if the merge lost it. It also asserts every registered rule has a fixture that trips it AND that the fixture survives hv_prefilter_pattern — which caught a real defect: the role and category call-argument rules fired under hv_scan_line and were UNREACHABLE on a tree scan, because the prefilter anchored on an `=`/`:` that a `.get("category", "general")` call does not have. Registered, tested, and no sink. Running the merged rules over the tree found 1977 findings where the CI detector reported 0. Every one was already true; the rules that would have found it lived in a fork nothing invoked or in a hook that only sees staged files. pipeline-scripts/hardcoded_values_baseline.txt records them so the merged set can be enforced today without reddening ssot-coverage on every pull request and blocking every local commit. It only ever shrinks, the count is part of the key so a second occurrence is a new finding, every run prints how many it suppressed, and a new --audit-baseline CI step fails on entries that no longer match anything. Performance is a correctness property here: the first version used `printf | grep` per rule per line and had not finished a tree scan after ten minutes, and a scan that times out reports nothing. The per-line path now uses bash's own `[[ =~ ]]` with no subprocess — 1m36s for six trees. Two hook tests that pinned documented false negatives are flipped, as they each asked to be: YAML/shell files are in scope (the tree scanner has covered them since #14316) and an unlisted model tag is caught by the generic regex. The hook's `types:` widens to match its own rules. A third stale test in lib/_common_test.py, red on the base branch since #14151, is fixed with the other two from #14034 — the Python suite is not a required check here, so a red test in it blocks nothing, which is how three of them accumulated.
check_script_exec_bits.py read the bare path in the new "Script Location" block as a documented direct invocation of a mode-644 file, and failed code-quality for it. The checker was right: the block did not say how the library is used. scripts/lib/hardcoded-value-rules.sh is SOURCED by both entry points and never executed, so mode 644 is correct and the fix belongs in the prose, not the index. The block now shows each artifact with the form it is actually invoked by, and says why the library's mode differs. Verified by mutation, not by a re-run: dropping `source` from that line puts the checker back to exit 1 on the same line.
…ree (#14371) `pre-commit run --all-files` hands the staged-files entry point every tracked file, and `verify-precommit-config` is a REQUIRED check. Measured without a prefilter on that path: 200 files in 78s — about 50 minutes over the 7639 tracked in-scope files. That is not a slow check, it is a check that never reports, which is the same fail-open outcome the rules exist to catch. The tree-scan path already applied hv_prefilter_pattern. Applying it per line on the staged path too is the smaller half of why: the larger half is that the two entry points must agree about what is in scope. A tree scan that filtered lines and a staged scan that did not would judge the same file differently depending on which one looked at it — the exact shape of the get_staged_files defect in #14034 that reported a Vue file as a direct Redis connection. Verified it costs no coverage rather than assuming: the same 200-file sample produces byte-identical output before and after (43 findings both ways), and hardcoded_value_rules_test.py already asserts per rule that the prefilter is a true superset of that rule's trigger. Line numbers stay exact because the counter increments before the filter. 78s -> 16s on the sample.
…s logic went (#14371) Rebased onto d70a1f9. One file overlapped: #14877 (35f48ba) converted autobot-infrastructure/shared/scripts/detect-hardcoded-values.sh's `source lib/ssot-config.sh 2>/dev/null || true` into the fail-loud abort, and #14371 retires that file. Resolved as a union of capabilities rather than a survivor-pick. The abort idiom travels; the ssot-config.sh dependency deliberately does not, and the reason is recorded in scripts/lib/hardcoded-value-rules.sh rather than left to be re-derived. ssot-config.sh `set -a`-loads the master .env and exports all of it, AUTOBOT_REDIS_PASSWORD included. The consolidated detector is a scanner whose entire output is matched source lines, printed to a public CI log and posted to pull requests: pulling a live secret into that process is a disclosure risk it gains nothing from. Its SSOT tables are detection patterns — the literals it hunts and the config key that replaces each — not runtime config it reads to work, which is why detector 2's tables were literal too, and PROJECT_ROOT now resolves from each entry point's own location, needing no .env and working on a deployed install with no checkout. What did travel is the capability: every `source` in the library and both entry points ends in an explicit failure block that exits non-zero, and an absent or empty baseline is fatal for the same reason — an unread exemption set and an empty one are indistinguishable to every caller. The hook's repo-root variable is renamed REPO_ROOT -> PROJECT_ROOT to match the spelling ssot-config.sh exports and the only one repo_tests/shell_lib_sources_resolve_test.py can expand. Stated precisely in the comment: this buys nothing today, because that guard walks `*.sh` and every hook in the directory is extensionless, so it cannot see the file at all. The spelling means the site is verified rather than silently skipped once that walk is widened. Retiring the fork takes that guard's distinct-script count from 56 to 55, its exact floor. The floor is left where it is rather than lowered for headroom, so it still catches a regressed matcher, with the retirement recorded next to it so the next legitimate retirement is a deliberate edit and not a mystery.
c5350ad to
59aac5c
Compare
…14371) Review found a HIGH bypass of the gate this change exists to build, and it was right. pipeline-scripts/hardcoded_values_baseline.txt suppresses findings, so anything that can append to it can silence the detector. `--audit-baseline` only ever checked one direction — that no entry had been STRANDED. Nothing checked that none had been ADDED: hardcode a fleet IP, and in the SAME change append 1|ssot|autobot-backend/new_file.py|<ip> to the baseline hv_partition suppresses it as already-known, ssot-coverage reports ssot_violations=0, and the pull request merges green over a finding the detector made correctly. The count-in-key design already made a bump on an existing key cost something; a brand-new key was the more direct route and was undefended. The comment at hardcoded-value-rules.sh states the invariant exactly — "the baseline would grow silently while appearing to hold" — and asserted it nowhere. A rule that lives in a comment is the shape of defect this whole change is about, so it is now an assertion. pipeline-scripts/check_baseline_no_growth.sh diffs the baseline against the base ref and fails on any new key or increased count. Base resolution comes from scripts/lib/git-scope.sh, built in this same PR: a hardcoded origin/main would be wrong in a repository whose pull requests target Dev_new_gui. Shrinking stays silent — removals and decreases are how a fixed violation leaves, and a guard that blocked those would make the file unmaintainable. Fail-closed throughout, and hv_baseline_growth reuses hv_parse_baseline_into rather than carrying a second parser — the guard needs two baselines in one process, and a private parser would be the exact fork this change exists to stop. That refactor also adds parse validation the loader never had: a non-numeric count and a line with no separator are now FATAL. Mutation-proved in both directions, in a scratch repo, nothing committed: fabricated new key -> RED; count bump -> RED; entry removed -> GREEN; count decreased -> GREEN; corrupt count -> RED; baseline missing -> RED; unresolvable base -> RED. Mutation also caught the guard's own untidiness: with the totals computed before the verdict, a missing baseline exited out of `awk` under `set -e` and never reached the designed FATAL. Fail-closed either way, but a failure path nobody can reason about is not one to keep. ssot-coverage.yml gains the blocking step and fetch-depth: 2, without which the merge-commit rule resolves nothing and the guard reds every run instead of comparing. Also from review, both non-blocking and both taken because this PR is where the secret-exposure thread lives: * _hv_rule_dsn and _hv_rule_url emitted the matched literal verbatim, so `postgresql://user:pass@host/db` reached a tracked file and a public CI log. Demonstrated, not hypothetical — two such values were already in the generated baseline. One shared _hv_redact_userinfo now serves both rules (they had the same defect for the same reason); scheme and host survive so the finding stays actionable, and a path like `https://h/a@b` is untouched. Baseline regenerated accordingly. * enforce-precommit.yml's git-scope calls use the explicit `|| exit 1` form rather than leaning on the runner's default shell flags, matching every other call site in this change.
|
HIGH finding addressed in
Mutation-proved both directions, scratch repo, nothing committed:
Mutation also caught the guard's own untidiness: with totals computed before the verdict, a missing baseline exited out of
Both non-blocking notes taken, since this PR is where the secret-exposure thread lives:
Not merging — over to you. #14181 stays dropped. |
Thinking Path
Five issues, one cluster: guards that are duplicated, drifted, or never run. Each was measured before being changed, and in three cases the measurement changed the plan.
#14034 — the shared hook lib's argv/git filter disagreement was already fixed in
_common.sh, but the two tests covering that branch still asserted the OLD behaviour. They were red against the code they cover, on the base branch. A third test in the same file was red for the same reason since #14151. The Python suite is not a required check here, so a red test in it blocks nothing — which is how three accumulated in one file. Audited every otherget_staged_filescaller and every argv-vs-git branch inhooks/,tools/lint/andpipeline-scripts/: one more instance, inpre-commit-no-module-singletons.#12986 —
.github/filters/already held two canonical filter files loaded bydorny/paths-filter, so the pattern was established and the job was to extend it. GitHub has no include mechanism foron.*.pathstriggers, so those copies cannot be removed — they get a guard instead, which turns a silent shrink into a red check.#13984 — the issue counted four copies; there are five (
enforce-precommit.ymlhas one too). The union is not a pick of one: each copy had rules the others lacked. The first version of the resolver collapsed the "explicit base" and "event payload base" cases into one, andcheck-pre-commit-hook-pr_test.pycaught it — an explicitly supplied base that does not resolve was being silently replaced by a smaller scope. They are separate entry points now, with the difference documented on both.#14371 — enumerated all three detectors' rules as sets first. Detector 1 was the only one scanning
.sh/.yml/.yamland the only one with the account rules; detector 2 (dormant, never invoked) was the only one with per-value SSOT suggestions, the generic model regex and the URL rule; detector 3 was the only one with magic numbers, roles, categories, DSNs, timeouts and the.get()call-argument shape. Detector 1'sPORT_PATTERNwas declared and never referenced by any scan function. Running the merged set over the tree found 1977 findings where the CI detector reported 0.#14181 is NOT in this PR. See below.
What Changed
#14371 — union table, all three detectors
Measuring only decided which file was the cleanest base to build on. It never decided which capabilities live.
172.16.168.[0-9]+)_hv_rule_ip— keeps the WIDE rangeHV_SSOT_VM_IPStable:portcontext_hv_rule_portPORT_PATTERNonlyHV_SSOT_PORTS— wired inHV_SSOT_PORTStableHV_SSOT_MODELS— union of bothHV_MODEL_GENERIC_hv_rule_url/home/<user>_hv_rule_accountUser=,chown, sudoers)_hv_rule_account_hv_rule_path_hv_rule_dsn_hv_rule_role_hv_rule_category_hv_rule_timeout_hv_rule_magic_numberoffset=0as a WARNING, not a violation_hv_rule_magic_number— severity preserved.get("field", default)call-argument shape_hv_get_call_argument.sh/.yml/.yamlHV_SCAN_EXTENSIONSautobot-infrastructure/in scan scopeSCAN_DIRSnoqasuppression_HV_NOQA_RE_hv_rule_ip_hv_rule_port_hv_rule_port_hv_rule_model_hv_rule_url_hv_rule_url*continuation skip_HV_COMMENT_RE_HV_UNION_TYPE_RE--json/--reportoutput modesZero rules dropped.
scripts/lib/hardcoded_value_rules_test.pynames each former detector and a rule only it had, and fails by name if the merge lost it.The dormant fork is retired: nothing invoked it, and its only references were in
HARDCODING_PREVENTION.mdunder a path prefix (autobot-autobot-infrastructure/) that never existed. Docs repointed.pipeline-scripts/hardcoded_values_baseline.txtrecords the 1977 findings the merged set finds today, so it can be enforced without reddeningssot-coverageon every PR and blocking every local commit. It only ever shrinks; the count is part of the key, so a second occurrence of a known value is a new finding; every run prints how many it suppressed; and a new--audit-baselineCI step fails on entries that no longer match anything.#13984 — one base-ref resolver
scripts/lib/git-scope.shcarries the union of all five copies: the merge-commit rule, the event-payload fallback, theorigin/<base>branch-name fallback (onlycheck-pre-commit-hook-pr.shhad it), the last-resort parent, the explicit-base rule, three-dot range semantics and range splitting (onlylint-conventions.sh), the anti-no-op zero-files guard (onlycode-quality's conventions step), the deleted-path filter, the explicit--basevalidation (onlyverify-done.sh), and "an unresolvable ref is FATAL" throughout. All five call sites source it; no inline copy remains.branch_state()goes from 113 lines to six named predicates plus a 22-line decision. Every function inverify-done.shis now under 30 lines. Not one rule changed.#12986 — one backend-Python path filter
.github/filters/backend-python-paths.ymldefines the set once and composes each consumer's filter from it with a YAML alias. Fivechangesjobs load it by path; their inline copies are gone and their output key names are unchanged. The sixon.*.pathstriggers GitHub forces to stay literal are policed bypipeline-scripts/check_workflow_path_filters.py, which also assertsapi-wiring.ymlstill has NOpull_request.paths(#12934) so that asymmetry cannot be tidied back into a deadlock.Also recorded, not fixed here:
verify-generated-types.ymlwatches neither backend tree'sautobot_shareddependency, so it can self-skip on a change that alters both generatedapi.tsfiles. That is a behaviour change to a gate rather than a deduplication, so it is filed as #14885 and registered in the guard'sNOT_CONSUMERStable with the reasoning, so it reads as considered rather than missed.Coverage fix found on the way:
api-wiring'swiringfilter omittedautobot_shared/**/*.pywhilescripts/audit_api_wiring.pyimportsautobot_shared.api_routing.router_prefixesandautobot_shared.openapi_schemafrom that tree. The requiredapi-wiringcontext self-skipped and reported green over an audit that never ran.#14034 — the two invocation paths agree, and it is pinned
_common_test.py's argv tests now assert the fix instead of the bug, plus a new test that the git and argv branches return the same set for the same input — the property that was actually broken.pre-commit-no-module-singletonshad the identical asymmetry in Python: its.pyfilter lived only on the git branch, so CI fed.ts/.vue/.ymlpaths toast.parse. It survived only becausecheck_fileswallowsSyntaxError— a silent mis-scan rather than a loud one.Rebase onto
d70a1f99e(#14877 conflict)Base moved three commits and one file overlapped: #14877 (
35f48ba92) convertedautobot-infrastructure/shared/scripts/detect-hardcoded-values.sh'ssource lib/ssot-config.sh 2>/dev/null || trueinto the fail-loud abort, and this PR retires that file. Rebased (not merged) so the branch stays linear andverify-done.sh's own merge-content check can still judge it.Resolved as a union of capabilities, not a survivor-pick:
sourceinscripts/lib/hardcoded-value-rules.shand both entry points ends in an explicit failure block that exits non-zero, and an absent or empty baseline is fatal for the same reason.ssot-config.shdependency deliberately does not, and the reason is recorded in the library rather than left to be re-derived:ssot-config.shset -a-loads the master.envand exports all of it,AUTOBOT_REDIS_PASSWORDincluded. The consolidated detector is a scanner whose entire output is matched source lines, printed to a public CI log and posted onto pull requests — pulling a live secret into that process is a disclosure risk it gains nothing from. Its SSOT tables are detection patterns, not runtime config it reads to work (which is why detector 2's tables were literal too), andPROJECT_ROOTnow resolves from each entry point's own location, needing no.envand working on a deployed install with no checkout.ssot-config.shso a renumbered fleet is still caught is a real improvement with its own secret-handling question — noted in the library, not bundled here.Read the three guards #14877 added. My work adds no competing shell-source guard, so there was nothing to consolidate — but the read found a gap, filed as #14891:
shell_lib_sources_resolve_test.pywalks*.shonly, and all 16 hooks inautobot-infrastructure/shared/scripts/hooks/are extensionless, so none is covered — including two live offenders (post-commit-doc-syncsources with no failure block;pre-commit-warn-untrackeddiscards stderr on the decisive attempt). Not fixed here because widening the walk needs a design decision first:pre-commit-warn-untracked's documented contract isexit 0, which the guard'sexit [1-9]rule cannot express. Two things were in scope and are done: this PR's hook now uses thePROJECT_ROOTspelling that guard can expand, so its site resolves rather than being silently skipped once the walk widens; and retiring the fork takes the guard's distinct-script count 56 → 55, its exact floor, recorded next to the floor so the next legitimate retirement is a deliberate edit rather than a mystery.Review round: the baseline could be silently grown (HIGH) — fixed
The reviewer found a bypass of the gate this PR exists to build, and it was right. The baseline suppresses findings, so anything that can append to it can silence the detector.
--audit-baselineonly checked that no entry had been stranded; nothing checked that none had been added:The count-in-key design already made a bump on an existing key cost something; a brand-new key was the more direct route and was undefended. My own comment stated the invariant exactly — "the baseline would grow silently while appearing to hold" — and asserted it nowhere. A rule living in a comment is the shape of defect this whole PR is about.
pipeline-scripts/check_baseline_no_growth.shnow diffs the baseline against the base ref and fails on any new key or increased count. Base resolution comes fromscripts/lib/git-scope.sh(built in this same PR) — a hardcodedorigin/mainwould be wrong in a repo whose PRs targetDev_new_gui. Fail-closed on an unresolvable base, an unreadable baseline on either side, or a parse failure.hv_baseline_growthreuseshv_parse_baseline_intorather than carrying a second parser — the guard needs two baselines in one process, and a private parser would be the exact fork this PR exists to stop; that refactor also adds parse validation the loader never had.Mutation-proved in both directions (scratch repo, nothing committed):
Mutation also caught the guard's own untidiness: with totals computed before the verdict, a missing baseline exited out of
awkunderset -eand never reached the designed FATAL. Fail-closed either way, but a failure path nobody can reason about is not one to keep — the verdict is now settled first.ssot-coverage.ymlgains the blocking step andfetch-depth: 2, without which the merge-commit rule resolves nothing and the guard would red every run instead of comparing.Both non-blocking review notes were taken, because this PR is where the secret-exposure thread lives:
_hv_rule_dsnand_hv_rule_urlemitted the matched literal verbatim, sopostgresql://user:pass@host/dbreached a tracked file and a public CI log. Demonstrated, not hypothetical — two such values were already in the generated baseline. One shared_hv_redact_userinfonow serves both (same defect, same reason); scheme and host survive so the finding stays actionable, and a path likehttps://h/a@bis untouched. Baseline regenerated.enforce-precommit.yml's git-scope calls use the explicit|| exit 1form rather than the runner's default shell flags, matching every other call site here.Verification
Every guard was verified by MUTATION, not by re-running it. No mutation was ever committed; each was applied to a scratch copy outside the repo, or reverted before staging.
_common.shargv filterno-module-singletons_python_only(args)→argsSyntaxErrorcannot mask it)autobot_sharedfromapi-wiring.ymlapi-wiringpull_request.pathsgit-scope.shgit-scope.shSuites, run locally on Python 3.10.12 / pytest 9.0.2 (this interpreter — the CI runner is 3.14):
scripts/lib/git-scope_test.sh— 33 assertions, 0 failed (real throwaway repos, not stubbed git)scripts/lib/hardcoded_value_rules_test.py— 40 passedpipeline-scripts/check_workflow_path_filters_test.py— 14 passed (every one a mutation test)pipeline-scripts/detect-hardcoded-values_test.py— 9 passedautobot-infrastructure/shared/scripts/hooks/pre-commit-hardcoded-values_test.py— 60 passedlint_conventions_test.py,verify_done_test.py,check-pre-commit-hook-pr_test.py,shell_lib_test.py— 97 passedpipeline-scripts/,hooks/,scripts/,repo_tests/shell_lib_test.py) — 499 passed, 8 failedThose 8 failures are pre-existing and not from this branch, proven rather than asserted:
pre-commit-no-tag-pinned-action{,_test}.pyandpre-commit-worktree-branch-guard{,_test}.pyare byte-identical toorigin/Dev_new_gui(compared by blob hash), and this branch touches none of them. Theno-tag-pinned-actionset fails withFileNotFoundError: 'command'— the harness invokes a shell builtin throughsubprocess.run. Filed as #14884 rather than dropped; it matters beyond the red, because one of the eight is the fail-closed test proving agit worktree listfailure is not reported as a clean tree, and that property is unverified while it cannot run.Performance is a correctness property here, and two rounds of it were found by measuring rather than by review:
The first version expressed every predicate as
printf … | grep, forking two processes per rule per line. It had not finished a tree scan after ten minutes. The per-line path now uses bash's own[[ =~ ]]with no subprocess — 1m36s over six trees.The staged-file path had no prefilter, and
pre-commit run --all-fileshands it every tracked file on a REQUIRED check: 200 files in 78s, about 50 minutes over the 7639 tracked in-scope files. A check that never reports is the same fail-open outcome the rules exist to catch. Both paths now apply the same filter — 78s → 16s on the sample — and that also removes an asymmetry between the two entry points, which is the larger reason: a tree scan that filtered lines while a staged scan did not would judge the same file differently depending on which one looked at it, exactly the bug(hooks): get_staged_files ignored its file-type filter in CI, and a parse crash was reported as a Redis violation #14034 shape. Proven to cost no coverage: the same sample gives byte-identical output before and after (43 findings both ways).Measured against the hook this replaces, on the same 200 files: the base-branch hook did not finish in 10 minutes (killed at the timeout) where the new one takes 16s. Its per-line path forked two processes per rule; this is a large improvement on a required check, not a regression.
End-to-end:
detect-hardcoded-values.sh --json→status: pass, 0 new, 1977 baselined, 1m36s over six treesdetect-hardcoded-values.sh --audit-baseline→ every baseline entry still matches somethingcheck_workflow_path_filters.py→ 6 inline path lists match the canonical setcheck_hook_exec_bits.py→ no new hook entry missing its exec bitlint-conventions.sh --range origin/Dev_new_gui...HEAD→ clean, 4 commits conformgit ls-files -sand set withgit update-index --chmod=+x—core.fileMode=falsemeanschmodnever reaches the index. Entry points100755, sourced libraries100644.actionlintis not installed locally and did not run here. #12986's acceptance criterion for it is verified by CI, not by me.#14181 dropped from this PR — reason
Its remaining work cannot be delivered here, so it is not in the
Closeslist. Measured against the current base, most of it is already done:enforce-precommit.yml's|| exit 0is gone (a dedicated step now fails on hooks that could not RUN), andcheck_hook_exec_bits.py's_KNOWN_DORMANThas shrunk from sixteen entries to two. The two that remain aretools/lint/check_no_blocking_io_in_async.py(201 blocking-I/O calls, owned by epic #7442) andtools/lint/check_no_local_schemas.py(~1370 local Pydantic schemas). The acceptance criterion is that the baseline is EMPTY at the end, which requires clearing both backlogs — each a substantial piece of work in its own right, and one of them already has its own epic. Bundling either into a CI-guard consolidation PR would be partial delivery, and partial delivery never closes.Model Used
Claude Opus 5 (1M context), model id
claude-opus-5[1m].Closes #14371, #13984, #12986, #14034