Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions .github/README_LABELING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

---

Expand Down
88 changes: 88 additions & 0 deletions .github/labels.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/detect-label-drift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down Expand Up @@ -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