Problem
scripts/labels-sync.rb can create and update canonical labels, but it has no
delete or migrate code path. Its header states this explicitly: it "never creates,
updates, deletes, or migrates labels unless both --apply and --confirm-apply
are passed" — and in practice only create/update are implemented.
lib/labels.yml already declares the intended cleanup policy, but nothing enforces
or executes it:
sync_policy:
delete_unknown_labels: false
delete_legacy_labels_only_when_unused: true
preserve_labels_on_open_items_before_removal: true
So legacy labels accumulate indefinitely. The canonical set was rolled out
org-wide, which means most repositories now carry both taxonomies at once.
Measured scope
Across 86 active, public, non-fork repositories, counting every issue and pull
request in any state:
| Set |
(repo, label) pairs |
Labelled items |
Policy verdict |
| Legacy, unused |
1,405 |
0 |
Deletable — delete_legacy_labels_only_when_unused |
| Legacy, in use |
60 |
1,002 |
Migrate to canonical first, then delete |
| Unknown |
1,503 |
76 items |
Never delete — delete_unknown_labels: false |
In-use legacy labels, by migration target:
| Legacy label |
Migrates to |
Items |
Repos |
dependencies 📦 |
area:dependencies |
754 |
18 |
ci 🤖 |
area:ci |
99 |
1 |
documentation 📝 |
type:docs |
46 |
5 |
maintenance 📈 |
type:maintenance |
25 |
4 |
enhancement ✨ |
type:maintenance |
21 |
4 |
annex 🌀 |
area:annex |
19 |
7 |
| 13 others |
various |
38 |
— |
Note that the canonical set contains both dependencies (the Dependabot and
Renovate auto-applied label, "Pull requests that update a dependency file") and
area:dependencies. Only the emoji-suffixed dependencies 📦 is legacy. The bare
dependencies label must not be touched.
Proposal
Add two gated code paths to labels-sync.rb, following the existing
--apply / --confirm-apply two-step pattern so previews stay read-only:
--migrate-legacy — for each in-use legacy label, add the mapped canonical
label to every carrying item, verify it landed, and only then remove the legacy
label. Order is load-bearing: deleting first destroys the association
permanently.
--delete-unused-legacy — delete legacy labels whose usage count is zero.
Behavior must be read from sync_policy in labels.yml rather than hardcoded, so
the policy file stays the single source of truth.
Required refusal paths
Mirroring the existing guardrail tests in scripts/test-labels-dry-run.sh:
- Refuse to delete any label not present in
legacy_migrations (unknown labels)
- Refuse to delete an in-use legacy label unless migration has completed for it
- Refuse to act without the matching
--confirm-* flag
- Refuse when
sync_policy disables the corresponding operation
Acceptance criteria
Out of scope
Executing the cleanup against the organization. That is roughly 2,400 irreversible
write operations and needs its own approval, with figures re-measured at run time.
Related
Problem
scripts/labels-sync.rbcan create and update canonical labels, but it has nodelete or migrate code path. Its header states this explicitly: it "never creates,
updates, deletes, or migrates labels unless both
--applyand--confirm-applyare passed" — and in practice only create/update are implemented.
lib/labels.ymlalready declares the intended cleanup policy, but nothing enforcesor executes it:
So legacy labels accumulate indefinitely. The canonical set was rolled out
org-wide, which means most repositories now carry both taxonomies at once.
Measured scope
Across 86 active, public, non-fork repositories, counting every issue and pull
request in any state:
delete_legacy_labels_only_when_unuseddelete_unknown_labels: falseIn-use legacy labels, by migration target:
dependencies 📦area:dependenciesci 🤖area:cidocumentation 📝type:docsmaintenance 📈type:maintenanceenhancement ✨type:maintenanceannex 🌀area:annexNote that the canonical set contains both
dependencies(the Dependabot andRenovate auto-applied label, "Pull requests that update a dependency file") and
area:dependencies. Only the emoji-suffixeddependencies 📦is legacy. The baredependencieslabel must not be touched.Proposal
Add two gated code paths to
labels-sync.rb, following the existing--apply/--confirm-applytwo-step pattern so previews stay read-only:--migrate-legacy— for each in-use legacy label, add the mapped canonicallabel to every carrying item, verify it landed, and only then remove the legacy
label. Order is load-bearing: deleting first destroys the association
permanently.
--delete-unused-legacy— delete legacy labels whose usage count is zero.Behavior must be read from
sync_policyinlabels.ymlrather than hardcoded, sothe policy file stays the single source of truth.
Required refusal paths
Mirroring the existing guardrail tests in
scripts/test-labels-dry-run.sh:legacy_migrations(unknown labels)--confirm-*flagsync_policydisables the corresponding operationAcceptance criteria
sync_policykeys are honoured, not reimplemented in codescripts/test-labels-dry-run.shusing the existing fake-ghharnessOut of scope
Executing the cleanup against the organization. That is roughly 2,400 irreversible
write operations and needs its own approval, with figures re-measured at run time.
Related
labels.ymlfiles have drifted; the consumer audit there should land before large-scale deletion