diff --git a/.github/labeler.yml b/.github/labeler.yml index aa1a7aa..6ec4baf 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -7,6 +7,18 @@ # these globs implement. Scopes locate, they do not alert — a path that maps # to nothing is fine (the mapping is advisory), so these rows chase the big # surfaces, not every file. +# +# A row that matches everything is worse than a missing one: it costs the +# same silence and adds a wrong answer. `changelog.d/**` sat under +# scope:release-flow until #267 measured it — the last 20 PRs (#197–#263) +# all carried scope:release-flow and 3 of them touched a release surface, +# because BUILDER.md makes every behavior change write a fragment, so the +# glob was "any PR that changes behavior" by doctrine. CHANGELOG.md stays: +# the same doctrine forbids editing it for an entry, so only the release PR +# does. The other rows #267 added — the issueflow reconciler, the three +# unmapped guard actions, RELEASES.md, README (which this tree spells +# README.md, so the old glob could match nothing) — are the same read of the +# same file, gaps rather than wrong answers. scope:release-flow: - changed-files: - any-glob-to-any-file: @@ -18,7 +30,6 @@ scope:release-flow: - bin/** - VERSION - CHANGELOG.md - - changelog.d/** - drills/** - test/decide.test.sh - test/facts.test.sh @@ -30,14 +41,20 @@ scope:guards: - changed-files: - any-glob-to-any-file: - actions/changelog-armed/** + - actions/changelog-assembled/** - actions/changelog-monotonic/** + - actions/docs-sync/** - actions/drill-recorded/** - actions/refs-not-closing/** + - actions/runner-isolated/** - .github/workflows/refs-guard.yml - test/changelog-armed.test.sh + - test/changelog-assembled.test.sh - test/changelog-monotonic.test.sh + - test/docs-sync.test.sh - test/drill-recorded.test.sh - test/refs-not-closing.test.sh + - test/runner-isolated.test.sh scope:labels: - changed-files: - any-glob-to-any-file: @@ -45,19 +62,26 @@ scope:labels: - .github/workflows/self-labels.yml - .github/labeler.yml - .github/labels.conf + - actions/issueflow-reconcile/** - actions/labels-reconcile/** - actions/labels-scope/** + # shared by both reconcilers; lib/** keeps scope:release-flow too, + # and a mixed file honestly wears both labels (#267 D4) + - lib/read.sh + - lib/ruling.sh - LABELS.md + - test/issueflow-reconcile.test.sh - test/labels.test.sh - test/labels-reconcile.test.sh - test/labels-scope.test.sh scope:docs: - changed-files: - any-glob-to-any-file: - - README + - README.md - docs/** - AGENTS.md - BUILDER.md + - RELEASES.md - REVIEWER.md - TRIAGE.md - CONTRIBUTING.md diff --git a/test/labels-scope.test.sh b/test/labels-scope.test.sh index d34687f..cbedf99 100644 --- a/test/labels-scope.test.sh +++ b/test/labels-scope.test.sh @@ -107,6 +107,68 @@ EOF check "derive: the real mapping labels this test file" 0 \ "scope:labels" derive_labels "$real_rows" 'test/labels-scope.test.sh' + # --- the real mapping locates: one file set in, the whole label set out --- + # #267 measured the old map at 100% recall / 15% precision — 20 of the last + # 20 PRs wore scope:release-flow and 3 touched a release surface — so these + # cases assert the DERIVED SET WHOLE, brackets and all. A substring check + # cannot tell scope:labels from scope:labels plus a wrong second label, and + # a wrong second label is the whole defect. + derives() { # → "[label,label]" for the real map + printf '[%s]\n' "$(derive_labels "$real_rows" "$1" | paste -sd, -)" + } + files() { printf '%s\n' "$@"; } + + # D1: a fragment is written by every behavior change (BUILDER.md), so it + # carries no locating information. Asserted as an empty set on its own, not + # as an absence inside a longer list: this is the case that fails first if + # the glob is ever restored. + check "derive: a fragment-only path derives nothing at all" 0 \ + "[]" derives 'changelog.d/999.md' + + # D2: the issue-flow sweep is a reconciler of the label taxonomy + check "derive: the issueflow reconciler is scope:labels" 0 \ + "[scope:labels]" derives 'actions/issueflow-reconcile/issueflow-reconcile.sh' + check "derive: the issueflow reconciler's test is scope:labels" 0 \ + "[scope:labels]" derives 'test/issueflow-reconcile.test.sh' + + # the reported bug, replayed: #261's exact file set wore scope:release-flow, + # inherited from its fragment, pointing at the one surface it does not touch + check "derive: #261's file set is scope:labels alone" 0 "[scope:labels]" \ + derives "$(files actions/issueflow-reconcile/issueflow-reconcile.sh \ + changelog.d/252.md test/issueflow-reconcile.test.sh)" + + # D1's cost, checked rather than assumed: dropping the fragment glob must + # not cost the release surface its label + check "derive: a release PR is still scope:release-flow" 0 \ + "[scope:release-flow]" \ + derives "$(files VERSION CHANGELOG.md drills/0.6.0.md changelog.d/236.md)" + + # D3: the docs block matched a literal README this tree does not have + check "derive: README.md is scope:docs" 0 "[scope:docs]" derives 'README.md' + check "derive: RELEASES.md is scope:docs" 0 "[scope:docs]" derives 'RELEASES.md' + check "derive: TRIAGE.md is scope:docs" 0 "[scope:docs]" derives 'TRIAGE.md' + + # D3: three guard actions and their tests were in no block at all + for guard in changelog-assembled docs-sync runner-isolated; do + check "derive: actions/$guard is scope:guards" 0 "[scope:guards]" \ + derives "$(files "actions/$guard/$guard.sh" "test/$guard.test.sh")" + done + + # D4: lib/ is genuinely mixed, so the shared files wear both labels rather + # than lib/** being re-carved into a row per file + check "derive: lib/ruling.sh is release-flow AND labels" 0 \ + "[scope:release-flow,scope:labels]" derives 'lib/ruling.sh' + check "derive: lib/read.sh is release-flow AND labels" 0 \ + "[scope:release-flow,scope:labels]" derives 'lib/read.sh' + check "derive: lib/version.sh is release-flow only" 0 \ + "[scope:release-flow]" derives 'lib/version.sh' + + # D6: the map stays advisory. An unmapped path derives an empty set and + # exits 0 — a guard that redded here would fail every PR touching FLEET.md + # or ci.yml, neither of which this map claims. + check "derive: an unmapped path is silence, not an error" 0 "[]" \ + derives "$(files FLEET.md .github/workflows/ci.yml)" + # refusals: unsupported shapes fail loudly, naming the label cat >"$TMP/allglobs.yml" <<'EOF' scope:x: