diff --git a/.github/README_LABELING.md b/.github/README_LABELING.md index 4c6022f2..f9160fc8 100644 --- a/.github/README_LABELING.md +++ b/.github/README_LABELING.md @@ -74,6 +74,10 @@ Apply special labels: ``` /help # Mark as help-wanted (good for contributors) +/level archived # Set project level (removes other level/* labels) +/level graduation +/level incubation +/level sandbox ``` ### Remove Commands @@ -103,17 +107,36 @@ Labels are automatically applied based on the files changed in a PR: | `Kubestronaut/**` | `area/kubestronaut` | Kubestronaut program | | `tests/**` | `area/infrastructure` | Testing infrastructure | -### Missing Label Indicators -If certain required labels are missing, helper labels are automatically applied: +### `needs-*` Helper Labels -- **`needs-kind`** — Applied when a PR/issue lacks a `kind/*` label -- **`needs-priority`** — Applied when a PR/issue lacks a `priority/*` label -- **`needs-area`** — Applied when a PR/issue lacks an `area/*` label -- **`needs-status`** — Applied when a PR/issue lacks a `status/*` label -- **`needs-triage`** — Applied when a PR/issue lacks a `triage/*` label -- **`needs-group`** — Applied when lacking `toc`, `tag/*`, or `sub/*` label +These labels are added automatically when a required category is absent, and removed automatically when the corresponding label is present — whether applied via a `/` command, the GitHub UI, or a file-path rule. -These helper labels encourage proper labeling without requiring it. +| Helper label | Removed when | +|---|---| +| `needs-triage` | Any `triage/*` label is present | +| `needs-kind` | Any `kind/*` label is present | +| `needs-group` | Any `toc`, `tag/*`, or `sub/*` label is present | +| `needs-priority` | Any `priority/*` label is present | +| `needs-area` | Any `area/*` label is present | +| `needs-status` | Any `status/*` label is present | +| `dd/needs-triage` | Any `dd/triage/*` label is present | + +### Mutually Exclusive Label Groups + +The following label groups enforce mutual exclusivity automatically. When a label in the group is applied (by any means), conflicting labels in the same group are removed. + +| Group | Labels | +|---|---| +| `level/*` | `level/archived`, `level/graduation`, `level/incubation`, `level/sandbox` — applying one removes the others via `/level` | +| `triage/*` | Enforced via `/triage` command | +| `kind/*` — no, kind can be multi | n/a | +| `priority/*` | Enforced via `/priority` command | +| `status/*` | Enforced via `/status` command | +| `area/*` | Enforced via `/area` command | +| `vote/open` + `vote/closed` | Enforced via `/vote` command | +| `init/*` | Enforced via `/init` command | + +In short: use `/` commands when you want explicit control, and rely on automatic labeling for baseline triage, path-based routing, and mutual exclusivity enforcement. --- diff --git a/.github/labels.yaml b/.github/labels.yaml index 4cbb13ba..5d745456 100644 --- a/.github/labels.yaml +++ b/.github/labels.yaml @@ -467,6 +467,16 @@ ruleset: spec: label: needs-triage +- name: remove-needs-triage + kind: label + spec: + match: triage/* + matchCondition: AND + actions: + - kind: remove-label + spec: + match: needs-triage + - name: needs-kind kind: label spec: @@ -477,6 +487,16 @@ ruleset: spec: label: needs-kind +- name: remove-needs-kind + kind: label + spec: + match: kind/* + matchCondition: AND + actions: + - kind: remove-label + spec: + match: needs-kind + - name: needs-group kind: label spec: @@ -487,6 +507,16 @@ ruleset: spec: label: needs-group +- name: remove-needs-group + kind: label + spec: + match: "{toc,tag/*,sub/*}" + matchCondition: AND + actions: + - kind: remove-label + spec: + match: needs-group + - name: needs-priority kind: label spec: @@ -497,6 +527,16 @@ ruleset: spec: label: needs-priority +- name: remove-needs-priority + kind: label + spec: + match: priority/* + matchCondition: AND + actions: + - kind: remove-label + spec: + match: needs-priority + - name: needs-area kind: label spec: @@ -507,6 +547,16 @@ ruleset: spec: label: needs-area +- name: remove-needs-area + kind: label + spec: + match: area/* + matchCondition: AND + actions: + - kind: remove-label + spec: + match: needs-area + - name: needs-status kind: label spec: @@ -517,6 +567,16 @@ ruleset: spec: label: needs-status +- name: remove-needs-status + kind: label + spec: + match: status/* + matchCondition: AND + actions: + - kind: remove-label + spec: + match: needs-status + - name: apply-kind kind: match spec: @@ -695,6 +755,34 @@ ruleset: spec: label: area/{{ argv.0 }} +- name: apply-level + kind: match + spec: + command: /level + rules: + - matchList: + - level/archived + - level/graduation + - level/incubation + - level/sandbox + actions: + - kind: remove-label + spec: + match: level/* + - kind: apply-label + spec: + label: level/{{ argv.0 }} + +- name: remove-dd-needs-triage + kind: label + spec: + match: dd/triage/* + matchCondition: AND + actions: + - kind: remove-label + spec: + match: dd/needs-triage + ############################################################################## # DD commands diff --git a/.github/workflows/detect-label-drift.yml b/.github/workflows/detect-label-drift.yml index 34f9bd8b..a18cc194 100644 --- a/.github/workflows/detect-label-drift.yml +++ b/.github/workflows/detect-label-drift.yml @@ -12,6 +12,7 @@ jobs: permissions: contents: read issues: write + pull-requests: write steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -211,3 +212,64 @@ jobs: with: name: label-drift-report path: /tmp/label_drift.json + + - name: Fix stale helper labels on open issues and PRs + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + python3 - <<'PY' + import json + import os + import subprocess + + REPO = os.environ["GITHUB_REPOSITORY"] + + def list_items(kind): + return json.loads(subprocess.check_output( + ["gh", kind, "list", "--repo", REPO, "--state", "open", + "--limit", "500", "--json", "number,labels"], + text=True, + )) + + def remove_label(kind, number, label): + subprocess.run( + ["gh", kind, "edit", str(number), "--repo", REPO, "--remove-label", label], + check=True, + ) + print(f" [{kind} #{number}] removed '{label}'") + + def matches(label_names, pattern): + if pattern == "{toc,tag/*,sub/*}": + return any( + l == "toc" or l.startswith("tag/") or l.startswith("sub/") + for l in label_names + ) + if pattern.endswith("/*"): + prefix = pattern[:-1] + return any(l.startswith(prefix) for l in label_names) + return pattern in label_names + + # (trigger pattern, stale label to remove) + RULES = [ + ("triage/*", "needs-triage"), + ("kind/*", "needs-kind"), + ("{toc,tag/*,sub/*}", "needs-group"), + ("priority/*", "needs-priority"), + ("area/*", "needs-area"), + ("status/*", "needs-status"), + ("dd/triage/*", "dd/needs-triage"), + ] + + total_fixed = 0 + for kind in ("issue", "pr"): + for item in list_items(kind): + number = item["number"] + label_names = {l["name"] for l in item["labels"]} + for trigger, stale in RULES: + if stale in label_names and matches(label_names, trigger): + remove_label(kind, number, stale) + label_names.discard(stale) + total_fixed += 1 + + print(f"\nTotal label corrections made: {total_fixed}") + PY