From d0b857eb106f3b8fb77cc986cabc5f284f8f7cea Mon Sep 17 00:00:00 2001 From: claude-bot-andresmgsl Date: Fri, 24 Jul 2026 11:57:10 +0000 Subject: [PATCH] docs: the additive-scope contract in CONSUMERS/LABELS; changelog fragment Part of #130. Co-Authored-By: Claude Fable 5 --- LABELS.md | 6 ++++-- actions/labels-scope/labels-scope.sh | 4 ++-- changelog.d/130.md | 1 + docs/CONSUMERS.md | 15 +++++++++++++++ test/labels-scope.test.sh | 3 +++ 5 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 changelog.d/130.md diff --git a/LABELS.md b/LABELS.md index fc5830b..2db8ba5 100644 --- a/LABELS.md +++ b/LABELS.md @@ -170,8 +170,10 @@ re-arms mentions, but only the writer can declare that a move is owed (#83). ## Scope — which surface? (PRs and issues, any number) All scopes share one calm color, `#C5DEF5` — scopes locate, states alert. The -set is per-repo: PRs get theirs from changed paths via actions/labeler, issues -get theirs from triage. This file never enumerates a set — it is mirrored +set is per-repo: PRs get theirs from changed paths via the labels workflow's +scope job — an additive write only, so a label applied by hand or by an agent +while the machine runs always survives it (#130) — and issues get theirs from +triage. This file never enumerates a set — it is mirrored byte-identically into every governed repo, and any list it carried would be true in one repo and false in the rest (#104). The set for the repo you are standing in lives in the two places that are true wherever you read them: its diff --git a/actions/labels-scope/labels-scope.sh b/actions/labels-scope/labels-scope.sh index 17f4eb6..b8a4544 100644 --- a/actions/labels-scope/labels-scope.sh +++ b/actions/labels-scope/labels-scope.sh @@ -56,9 +56,9 @@ glob_to_regex() { # $1 = glob (the subset above) → anchored ERE, one line i=$((i + 2)) continue fi - out="$out[^/]*" + out="${out}[^/]*" ;; - \?) out="$out[^/]" ;; + \?) out="${out}[^/]" ;; [a-zA-Z0-9_/-]) out="$out$c" ;; *) out="$out\\$c" ;; # every other byte is literal — ., +, {, (, … esac diff --git a/changelog.d/130.md b/changelog.d/130.md new file mode 100644 index 0000000..a2d35aa --- /dev/null +++ b/changelog.d/130.md @@ -0,0 +1 @@ +- `actions/labels-scope` replaces `actions/labeler@v5` in the labels workflow's scope job: labeler wrote the whole label set (`PUT`) even under `sync-labels: false`, silently removing any label applied while it ran — #128 lost its `release` that way — so the scope job now derives from the same `.github/labeler.yml` mapping (the `changed-files`/`any-glob-to-any-file` shape, block or flow; anything else refuses loudly) and its only write is an additive `POST`. The reconcile sweep also warns — never sets — when a non-draft PR is release-shaped (bare version differing from its base) but carries no `release` label (#130). diff --git a/docs/CONSUMERS.md b/docs/CONSUMERS.md index 5ad6ee8..79b438d 100644 --- a/docs/CONSUMERS.md +++ b/docs/CONSUMERS.md @@ -276,6 +276,21 @@ never writes it). The consumer keeps its path mapping in `.github/labeler.yml` and its review panel plus scope taxonomy in `.github/labels.conf`. +**Additive means additive** (unreleased — #130): the scope job's only label +write is `POST /issues/{n}/labels`, which adds the derived scopes and removes +nothing, so a label applied while the job runs survives it. Earlier tags used +`actions/labeler@v5`, which — even under `sync-labels: false` — replaces the +whole label set and silently drops a label written mid-job (ceremony#128 lost +its `release` that way). With the same pin bump, `.github/labeler.yml` keeps +its format but the accepted shape becomes exactly the one this guide has +always shown: label → `changed-files` → `any-glob-to-any-file`, block or flow +style, globs over `**`, `*` and `?` (`**` crosses `/`, the others do not; the +whole path must match). Any other labeler key — `all-globs-to-all-files`, +branch matchers, negations — fails the run loudly instead of being +half-honoured. The reconcile sweep also warns (never sets) when a non-draft +PR carries a bare `X.Y.Z` version differing from its base but no `release` +label — the merge door would refuse that merge, and the sweep says so first. + The complete caller is: ```yaml diff --git a/test/labels-scope.test.sh b/test/labels-scope.test.sh index 8ea2997..03db569 100644 --- a/test/labels-scope.test.sh +++ b/test/labels-scope.test.sh @@ -31,12 +31,15 @@ scope:docs${TAB}README" check "derive: ** matches nested paths" 0 "scope:release-flow" \ derive_labels "$cfg" 'lib/deep/facts.sh' +# shellcheck disable=SC2016 # expansion belongs to the nested bash check "derive: ** does not match the bare directory" 1 "" \ bash -c 'source "$1"; [ -n "$(derive_labels "$2" lib)" ]' _ \ "$ROOT/actions/labels-scope/labels-scope.sh" "$cfg" +# shellcheck disable=SC2016 # expansion belongs to the nested bash check "derive: literal glob does not match a nested twin" 1 "" \ bash -c 'source "$1"; [ -n "$(derive_labels "$2" docs2/README)" ]' _ \ "$ROOT/actions/labels-scope/labels-scope.sh" "$cfg" +# shellcheck disable=SC2016 # expansion belongs to the nested bash check "derive: one label per line, config order, deduped" 0 "" \ bash -c 'source "$1"; got="$(derive_labels "$2" "$(printf "%s\n" README VERSION lib/x docs/a.md)")" [ "$got" = "$(printf "%s\n" scope:release-flow scope:docs)" ] || { printf "%s\n" "$got"; exit 1; }' _ \