Repository navigation
fix(compliance): remove config that reads as live but does nothing - #77
Conversation
Three knobs in this repo look like working configuration and are read by
no code. Each is the same failure: something plausible, sitting where
configuration lives, that silently does nothing when used.
1. LICENSES_JSON in required-compliance.yml pointed at
.github-tools/data/licenses_all.json, but that job's checkout is
`sparse-checkout: scripts`, so the path never existed on the runner.
Inert only by luck — policy_selector fetches both catalogues over the
API and passes them to requires_cla in memory, and license_detector
reads LICENSES_JSON from disk only when handed no catalog_data. Any
future caller that omitted catalog_data would have found the missing
file in production, where ruleset 12239008 pins this workflow to
refs/heads/main and it cannot be tested pre-merge. Removed.
2. `spdx_aliases` and a per-repo `force_spdx` were documented in
cla/allowlist.yml as ready-to-uncomment examples. No code has ever
read either. A commented example is a promise: uncomment it, see no
error, conclude it worked. Removed, and the header now says plainly
that only the listed keys do anything.
3. The top-level `repos:` block is a FALLBACK, not a supplement — read
only when license_overrides.repos is absent or empty. That block is
populated, so anything added to a top-level `repos:` is dropped
without a word. It now logs a warning naming what it ignored.
Also fixed while in those lines: `.get("license_overrides", {})` returns
None for a key that is present but null, so `license_overrides:` with
nothing under it raised AttributeError, which the surrounding except
swallowed into "enforce CLA for every repo". One empty key cost the
entire policy. Both lookups now use `or {}`.
And widened the tests.yml paths filter to `.github/workflows/**`. The new
TestGateWorkflowPaths guards required-compliance.yml, which the filter did
not cover — so the guard would not have run when the file it protects was
edited. That is the third instance of this exact bug (`cla/**` in #74,
`data/**` in #76), so TestTestsWorkflowPathsFilter now asserts the filter
covers every file the suite reads, deriving the list from the same
constants the tests use rather than a hand-kept copy.
Verification:
* 95 tests pass (was 92).
* Every new guard mutation-tested: reinstating LICENSES_JSON, restoring
either commented knob, dropping the shadowing warning, turning the
fallback into a merge, restoring the fragile .get(), reverting the
paths filter, and desyncing the two filters are each caught by the
named test and by no other. Tree restored clean after each.
* The allowlist edit is provably inert: replaying origin/main's file and
this one through the real fetch_shared_config offline gives identical
allowlist_ok, allowlist_repos and allowlist_data, and identical
decisions for Broadcom Source Available, Broadcom Proprietary,
Apache-2.0 and GPL-2.0. Done offline because fetch_mothership_file has
no ?ref= — a pre-merge sweeper run would test the old file and prove
nothing while writing real statuses to real PRs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Re-probing malformed allowlist shapes against origin/main found that the
shadowing warning added in the previous commit was itself a regression.
`sorted(legacy_repos)` assumed a dict. A top-level `repos:` holding a list
of dicts raises TypeError, and an int raises too — both swallowed by the
enclosing except into "Enforcing CLA for every repo". origin/main handled
both fine, so this commit's diagnostic would have made things worse than
the bug it describes. Building a log line must never be the thing that
discards the policy. Now renders defensively.
Fixed alongside it, same failure family three lines down and pre-existing:
`r_config.get("require_cla")` on a null or boolean entry raised
AttributeError, taking the whole file with it. A single missing indent
under one repo cost every gated repo in the org. Malformed entries are now
skipped with a warning naming the repo. Direction stays safe: absence from
allowlist_repos means CLA, so a skipped entry is held at the stricter
document, and the readable entries survive.
Measured against origin/main, per shape of a top-level `repos:`:
list of dicts main ok -> branch DISCARDED (regression) -> now ok
integer main ok -> branch DISCARDED (regression) -> now ok
nested null main DISCARDED (pre-existing) -> now ok
nested bool main DISCARDED (pre-existing) -> now ok
Verification:
* 101 tests pass (was 95).
* All 11 mutations across both harnesses caught by their named tests.
* test_warning_renders_a_non_dict_legacy_block_without_raising now uses
TWO dicts. With one, `sorted()` never performs a comparison, so the
test passed with the broken code in place — it could not fail for the
scenario its own name describes. Mutation testing caught that; review
did not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up commit:
|
| shape | origin/main | e423b61 |
now |
|---|---|---|---|
| list of dicts | ok | discarded | ok |
| integer | ok | discarded | ok |
nested entry null |
discarded | discarded | ok |
nested entry true |
discarded | discarded | ok |
The bottom two are pre-existing and in the same failure family three lines down: r_config.get("require_cla") on a null or boolean entry raises AttributeError, taking the whole file with it. A single missing indent under one repo cost every gated repo in the org. Fixed alongside, since I was already in those lines.
Malformed entries are now skipped with a warning naming the repo. The direction stays safe — allowlist_repos is the DCO-only set, so a skipped entry is held at CLA, the stricter document, and the readable entries survive. Blast radius goes from org-wide to one repo.
One test was lying
test_warning_renders_a_non_dict_legacy_block_without_raising originally used a one-element list. sorted() on a single element never performs a comparison, so it passed with the broken code in place — it could not fail for the scenario its own name describes. It now uses two dicts.
Mutation testing caught that. Review did not, and neither did the first pass of this PR.
Verification
- 101 tests (was 95).
- All 11 mutations across both harnesses caught by their named tests, tree restored clean after each — including re-introducing the
sorted()regression and letting a skipped entry through into the DCO-only set.
Worth noting one of my mutations was initially invalid: I tried to make the malformed entry fall through, but the continue guard above made that line unreachable, so it was a no-op that looked like a missed test. Re-cut as appending to allowlist_repos from the skip branch, which is the real "let it through" case, and it's caught.
test_commented_key_guard_can_actually_see_a_commented_key declared its own copy of the regex, so it validated a duplicate rather than the pattern test_commented_out_keys_are_also_read_by_live_code actually uses. Editing the real one would not have tripped the guard — the exact failure the guard exists to prevent. Declared once as COMMENTED_KEY_RE and used by both; broadening it now fails both tests rather than neither. Found by a vacuity sweep: reverting each changed file in turn and checking which new tests die. Ten survived every revert, which is correct for characterisation tests but meant they were unproven, so each was mutation-tested individually afterwards. All 19 new tests are now provably killable by at least one of 16 mutations. Also confirmed by a 30-case differential against origin/main and a 2,788-case fuzz: no input reaches the DCO-only set without an explicit `require_cla: false` or a `repositories:` entry, and no input escapes an exception. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clears three items from the compliance-engine backlog. They are one bug wearing three hats: something that looks like working configuration, sits where configuration lives, and silently does nothing when used.
No behaviour change to enforcement. Proven below, not asserted.
1.
LICENSES_JSONpointed at a path that is never checked outrequired-compliance.ymlset it to.github-tools/data/licenses_all.json, but that job's checkout issparse-checkout: scripts. The directory never existed on the runner.It was inert only by luck:
policy_selectorfetches both catalogues over the API and hands them torequires_clain memory, andlicense_detectorconsultsLICENSES_JSONfrom disk only when given nocatalog_data. A future caller that omittedcatalog_datawould have hit a missing file in production — and ruleset12239008pins this workflow torefs/heads/main, so there is no pre-merge signal there at all.Removed the variable rather than adding
datato the sparse-checkout: deleting makes the intent true, and matches how the code actually works.2.
spdx_aliasesandforce_spdxwere documented but unreadBoth sat in
cla/allowlist.ymlas ready-to-uncomment examples. Neither has ever been read by any code —_override_requires_clareads onlyrequire_claandallow_dco.A commented-out example is a promise. Someone uncomments it, gets no error, and concludes it took effect. That is worse than no documentation, because this file is the most authoritative-looking source a reader has.
3. The top-level
repos:block is a fallback, not a supplementfetch_shared_configreadslicense_overrides.reposand falls back to a top-levelrepos:only when that is absent or empty. It is populated, so anything added to a top-levelrepos:is dropped in silence. It now warns and names what it ignored.Fixed while in those lines
allowlist_data.get("license_overrides", {})returnsNonewhen the key is present but null — a.get()default only applies to an absent key. So a file containing a barelicense_overrides:raisedAttributeError, which the surroundingexceptswallowed into "Enforcing CLA for every repo". One empty key cost the entire policy. Both lookups now useor {}.Fails closed, so this was never a security hole — but one stray key taking the whole org to CLA is a harsh failure mode for a typo.
The paths filter, a third time
TestGateWorkflowPathsguardsrequired-compliance.yml— which was not in thetests.ymlpaths filter, so the guard would not have run when the file it protects was edited.That is the third instance:
cla/**was missing in #74,data/**in #76. Each time the guard existed and simply never ran. Rather than fix the instance again,TestTestsWorkflowPathsFilternow asserts the filter covers every file the suite reads, deriving that list from the same constants the tests use instead of a hand-kept copy that would drift the same way.Verification
95 tests pass (was 92).
Every new guard mutation-tested. Each mutation is caught by the named test and by no other, with the tree restored clean after each:
LICENSES_JSONoutside the sparse-checkouttest_every_tools_path_in_env_is_actually_checked_outforce_spdxtest_commented_out_keys_are_also_read_by_live_codespdx_aliasestest_commented_out_keys_are_also_read_by_live_codetest_shadowing_is_reported_rather_than_silenttest_nested_repos_shadows_top_level_entirely.get()defaulttest_null_license_overrides_does_not_discard_the_whole_allowlisttest_filter_covers_every_file_the_suite_readstest_both_triggers_declare_the_same_filterTwo tests guard the guards (
test_commented_key_guard_can_actually_see_a_commented_key,test_coverage_check_rejects_a_path_outside_the_filter), because both new checks use narrow regex/glob matching that could otherwise pass vacuously on an empty set.The allowlist edit is provably inert. Replaying
origin/main's file and this one through the realfetch_shared_configoffline gives identicalallowlist_ok,allowlist_reposandallowlist_data, and identical decisions for Broadcom Source Available, Broadcom Proprietary, Apache-2.0 and GPL-2.0.Done offline deliberately:
fetch_mothership_filehas no?ref=, so a pre-merge sweeper run reads the default branch — it would test the old file, prove nothing, and write real statuses and comments to real PRs on the way.Scope notes
normansdenallowlist bypass was already removed by chore(compliance): remove dead workflow-override section from the CLA allowlist #74; nothing left to do.build_unowned_repo_owners_csv.pyis not here — that script lives outside this repo by design, since it queries an internal PII endpoint. Being handled separately.🤖 Generated with Claude Code