Skip to content

Commit 5a4488a

Browse files
committed
make github label pruning a checkbox and format fixes
1 parent 4c9a6d0 commit 5a4488a

9 files changed

Lines changed: 105 additions & 81 deletions

‎.github/workflows/org-label-sync.yml‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@ on:
99
type: boolean
1010
default: false
1111
delete_missing:
12-
description: "Delete labels that are not present in config"
12+
description: "Delete extra non-default labels that are not present in config"
1313
required: false
1414
type: boolean
1515
default: false
16+
delete_github_default_labels:
17+
description: "Delete Github default labels"
18+
required: false
19+
type: boolean
20+
default: true
1621
repositories:
1722
description: "Config Override: Run workflow on this set of repositories exclusively. (Type name(s), separated by comma)"
1823
required: false
@@ -73,6 +78,7 @@ jobs:
7378
ORG_NAME: ${{ steps.properties.outputs.organization }}
7479
DRY_RUN: ${{ inputs.dry_run }}
7580
DELETE_MISSING: ${{ inputs.delete_missing }}
81+
DELETE_GITHUB_DEFAULT_LABELS: ${{ inputs.delete_github_default_labels }}
7682
TARGET_REPOSITORIES: ${{ inputs.repositories }}
7783
run: node scripts/sync-labels.mjs
7884

‎.github/workflows/remove-labels.yml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
workflow_dispatch:
55
inputs:
66
run_on_issues:
7-
description: "Remove from Issues"
7+
description: "Remove From Issues"
88
required: true
99
type: boolean
1010
default: true
@@ -14,10 +14,10 @@ on:
1414
type: boolean
1515
default: false
1616
run_on_pull_requests:
17-
description: "Remove from Pull Requests"
17+
description: "Remove From Pull Requests"
1818
required: true
1919
type: boolean
20-
default: false
20+
default: true
2121
target_only_closed_pull_requests:
2222
description: "Target Only Closed Pull Requests"
2323
required: true

‎README.md‎

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ The reverse flow is:
4848
| |-- reverse-config-label-sync.yml
4949
| `-- validate-configs.yml
5050
|-- config/
51-
| |-- auto-pruned-labels.jsonc
51+
| |-- github-default-labels.jsonc
5252
| |-- labels.jsonc
5353
| |-- properties.jsonc
5454
| `-- repository-filter.jsonc
@@ -134,17 +134,19 @@ Example:
134134
]
135135
```
136136

137-
### `config/auto-pruned-labels.jsonc`
137+
### `config/github-default-labels.jsonc`
138138

139-
This is the list of exact labels that should be removed from synced repositories when they are not managed by `config/labels.jsonc`.
139+
This is the list of exact GitHub default labels that can be removed from synced repositories when they are not managed by `config/labels.jsonc`.
140+
141+
You will likely never need to modify this file unless GitHub changes the default labels it creates for new repositories.
140142

141143
Each entry must include:
142144

143145
- `name`
144146
- `color`
145147
- `description`
146148

147-
The starter file is prefilled with GitHub's default labels as exact specs:
149+
The file is prefilled with GitHub's default labels as exact specs:
148150

149151
```jsonc
150152
[
@@ -161,9 +163,9 @@ The starter file is prefilled with GitHub's default labels as exact specs:
161163
]
162164
```
163165

164-
`Org-Label-Sync` only deletes a configured auto-pruned label when the target repository label has the same name casing, color, and description. A custom label such as `Enhancement`, or an `enhancement` label with a different description or color, is not deleted by this file.
166+
`Org-Label-Sync` only deletes an entry from this file when the `delete_github_default_labels` workflow checkbox is checked and the target repository label has the same name casing, color, and description. A custom label such as `Enhancement`, or an `enhancement` label with a different description or color, is not deleted as a GitHub default label.
165167

166-
Managed labels are the source of truth. If a label exists in `config/labels.jsonc`, it is created or updated on target repositories and is not deleted by `config/auto-pruned-labels.jsonc`, even when it has the same name as an auto-pruned default.
168+
Managed labels are the source of truth. If a label exists in `config/labels.jsonc`, it is created or updated on target repositories and is not deleted by `config/github-default-labels.jsonc`, even when it has the same name as a GitHub default label.
167169

168170
### `config/repository-filter.jsonc`
169171

@@ -216,7 +218,7 @@ What it does:
216218
3. Resolves either the configured PAT or a GitHub App installation token
217219
4. Reads the current labels on the source repository
218220
5. Rewrites `config/labels.jsonc` so it exactly matches the source repository labels
219-
6. Validates `config/auto-pruned-labels.jsonc` so exact default-delete specs remain well-formed
221+
6. Validates `config/github-default-labels.jsonc` so exact GitHub default label specs remain well-formed
220222
7. Commits and pushes the change if the config was updated
221223

222224
This workflow is the bridge between "the labels on this repo right now" and "the managed config we sync elsewhere."
@@ -244,7 +246,7 @@ Validation includes:
244246
- duplicate whitelist and blacklist detection
245247
- invalid colors
246248
- invalid repo names
247-
- exact auto-pruned label shape validation
249+
- exact GitHub default label shape validation
248250
- validation for the shared config used by `Remove-Labels`
249251

250252
### `Reverse-Config-Label-Sync`
@@ -264,7 +266,7 @@ What it does:
264266
5. Resolves either the configured PAT or a GitHub App installation token
265267
6. Validates the config again as a local guard
266268
7. Creates or updates source repository labels from `config/labels.jsonc`
267-
8. Deletes exact auto-pruned labels and any other source repository labels that are not in `config/labels.jsonc`
269+
8. Deletes exact GitHub default labels and any other source repository labels that are not in `config/labels.jsonc`
268270

269271
This workflow is the bridge from "the managed config was changed by a person" back to "the source repository label settings should now match that config."
270272

@@ -280,6 +282,7 @@ Inputs:
280282

281283
- `dry_run`: preview changes without applying repository label changes; when previewed changes exist, writes a fake changelog under `fake-changelogs/`
282284
- `delete_missing`: delete extra labels that are not in `config/labels.jsonc`; unchecked keeps extra labels
285+
- `delete_github_default_labels`: delete exact GitHub default labels from `config/github-default-labels.jsonc`; checked by default, unchecked keeps them
283286
- `repositories`: comma-separated config override that runs exclusively on those repositories and ignores `repository-filter.jsonc`
284287

285288
What it does:
@@ -292,8 +295,8 @@ What it does:
292295
6. Discovers repos in the configured organization
293296
7. Applies `config/repository-filter.jsonc`, unless `repositories` was provided as a workflow dispatch config override
294297
8. Creates or updates labels from `config/labels.jsonc`
295-
9. Deletes labels that exactly match entries in `config/auto-pruned-labels.jsonc` unless that label name is managed by `config/labels.jsonc`
296-
10. Optionally deletes any other unmanaged labels only when the `delete_missing` workflow checkbox is checked
298+
9. Deletes labels that exactly match entries in `config/github-default-labels.jsonc` only when the `delete_github_default_labels` workflow checkbox is checked, unless that label name is managed by `config/labels.jsonc`
299+
10. Optionally deletes any other unmanaged labels only when the `delete_missing` workflow checkbox is checked; exact GitHub default labels are reserved for `delete_github_default_labels`
297300
11. If at least one target repo changed or would change, writes a changelog and commits it with `[skip ci]`; real runs write to `changelogs/YYYY-MM-DD/`, while preview runs write fake changelogs to `fake-changelogs/YYYY-MM-DD/` and do not apply repository changes
298301

299302
### `Remove-Labels`
@@ -333,7 +336,7 @@ Each changelog file is created only when a workflow actually changes, or in prev
333336

334337
- created labels
335338
- updated labels, including changed fields
336-
- configured auto-pruned labels that were deleted
339+
- GitHub default labels that were deleted
337340
- unmanaged labels that were deleted when delete-missing is enabled
338341

339342
`Remove-Labels` changelogs include:
@@ -376,7 +379,7 @@ The GitHub App installation must be granted access to this configuration reposit
376379
1. Fork or clone this repository into the organization you want to manage.
377380
2. Create either the PAT secret or the GitHub App secrets in the repo.
378381
3. Update `config/properties.jsonc` for your org, repo, and auth mode.
379-
4. Adjust `config/auto-pruned-labels.jsonc` if you want different exact default-delete specs.
382+
4. Leave `config/github-default-labels.jsonc` alone unless GitHub changes its default labels.
380383
5. Configure `config/repository-filter.jsonc` for blacklist mode or whitelist mode.
381384
6. Set the labels on this repository to the label set you want to manage.
382385
7. Run `Config-Label_Sync` once if you want to populate `config/labels.jsonc` immediately.
@@ -386,5 +389,5 @@ The GitHub App installation must be granted access to this configuration reposit
386389

387390
- `labels.jsonc` starts empty until you define or sync labels on this repo
388391
- all repos in the org are targeted unless excluded by `repository-filter.jsonc`
389-
- GitHub default labels are auto-pruned by default only when they exactly match the stored name, color, and description and are not managed in `labels.jsonc`
392+
- GitHub default labels are pruned by default when they exactly match `config/github-default-labels.jsonc`; uncheck `delete_github_default_labels` to keep them
390393
- deleting unmanaged labels is off by default unless you check `delete_missing` when running `Org-Label-Sync`
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// GitHub's default label set rarely changes.
2+
// You will likely never need to modify this file unless GitHub changes the default labels it creates for new repositories.
3+
//
14
// Example:
25
// [
36
// {

‎scripts/lib/changelog-utils.mjs‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,13 @@ export function renderLabelSyncSection(result) {
122122
lines.push("");
123123
}
124124

125-
const deletedConfigured = renderList(
126-
result.deletedConfiguredLabels,
127-
(label) => `Deleted configured auto-pruned label \`${label.name}\``,
125+
const deletedGithubDefaults = renderList(
126+
result.deletedGithubDefaultLabels,
127+
(label) => `Deleted GitHub default label \`${label.name}\``,
128128
);
129-
if (deletedConfigured) {
130-
lines.push("Deleted configured labels:");
131-
lines.push(deletedConfigured);
129+
if (deletedGithubDefaults) {
130+
lines.push("Deleted GitHub default labels:");
131+
lines.push(deletedGithubDefaults);
132132
lines.push("");
133133
}
134134

‎scripts/lib/config-validation.mjs‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -197,20 +197,20 @@ export function validateLabels(labels) {
197197
});
198198
}
199199

200-
export function validateDeleteLabels(deleteLabels) {
201-
assert(Array.isArray(deleteLabels), "config/auto-pruned-labels.jsonc must contain an array.");
200+
export function validateGithubDefaultLabels(githubDefaultLabels) {
201+
assert(Array.isArray(githubDefaultLabels), "config/github-default-labels.jsonc must contain an array.");
202202

203203
const seen = new Set();
204204

205-
return deleteLabels.map((entry, index) => {
206-
assert(entry && typeof entry === "object" && !Array.isArray(entry), `Delete label at index ${index} must be an object.`);
207-
assert(typeof entry.name === "string" && entry.name.trim(), `Delete label at index ${index} is missing a valid name.`);
208-
assert(typeof entry.color === "string" && /^[0-9a-fA-F]{6}$/.test(normalizeColor(entry.color)), `Delete label "${entry.name}" must have a 6-character hex color.`);
209-
assert(typeof entry.description === "string", `Delete label "${entry.name}" must include a string description.`);
205+
return githubDefaultLabels.map((entry, index) => {
206+
assert(entry && typeof entry === "object" && !Array.isArray(entry), `GitHub default label at index ${index} must be an object.`);
207+
assert(typeof entry.name === "string" && entry.name.trim(), `GitHub default label at index ${index} is missing a valid name.`);
208+
assert(typeof entry.color === "string" && /^[0-9a-fA-F]{6}$/.test(normalizeColor(entry.color)), `GitHub default label "${entry.name}" must have a 6-character hex color.`);
209+
assert(typeof entry.description === "string", `GitHub default label "${entry.name}" must include a string description.`);
210210

211211
const normalized = normalizeLabelSpec(entry);
212212
const key = labelSpecKey(normalized);
213-
assert(!seen.has(key), `Duplicate exact delete label detected: "${normalized.name}".`);
213+
assert(!seen.has(key), `Duplicate GitHub default label detected: "${normalized.name}".`);
214214
seen.add(key);
215215
return normalized;
216216
});

‎scripts/reverse-config-label-sync.mjs‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import {
88
readJsonc,
99
} from "./lib/config-utils.mjs";
1010
import {
11-
validateDeleteLabels,
11+
validateGithubDefaultLabels,
1212
validateLabels,
1313
validateProperties,
1414
} from "./lib/config-validation.mjs";
1515

1616
const workspaceRoot = process.cwd();
1717
const propertiesPath = path.join(workspaceRoot, "config", "properties.jsonc");
1818
const labelsPath = path.join(workspaceRoot, "config", "labels.jsonc");
19-
const autoPrunedLabelsPath = path.join(workspaceRoot, "config", "auto-pruned-labels.jsonc");
19+
const githubDefaultLabelsPath = path.join(workspaceRoot, "config", "github-default-labels.jsonc");
2020

2121
const validateOnly = process.argv.includes("--validate-only");
2222
const dryRun = validateOnly || process.env.DRY_RUN === "true";
@@ -77,19 +77,19 @@ function summarizeLabelDiff(existing, desired) {
7777
return "update";
7878
}
7979

80-
function isExactAutoPrunedLabel(label, deleteLabels) {
81-
return deleteLabels.some((deleteLabel) => labelsExactlyMatch(label, deleteLabel));
80+
function isExactGithubDefaultLabel(label, githubDefaultLabels) {
81+
return githubDefaultLabels.some((githubDefaultLabel) => labelsExactlyMatch(label, githubDefaultLabel));
8282
}
8383

84-
async function syncSourceRepository(token, repository, desiredLabels, deleteLabels) {
84+
async function syncSourceRepository(token, repository, desiredLabels, githubDefaultLabels) {
8585
console.log(`Syncing ${repository} labels from config`);
8686
const existingLabels = await getAllLabels(token, repository);
8787
const existingByName = new Map(existingLabels.map((label) => [normalizeName(label.name), label]));
8888
const desiredKeys = new Set(desiredLabels.map((label) => normalizeName(label.name)));
8989

9090
let created = 0;
9191
let updated = 0;
92-
let deletedConfigured = 0;
92+
let deletedGithubDefaults = 0;
9393
let deletedMissing = 0;
9494
let unchanged = 0;
9595

@@ -130,12 +130,12 @@ async function syncSourceRepository(token, repository, desiredLabels, deleteLabe
130130
for (const existing of existingLabels) {
131131
const existingKey = normalizeName(existing.name);
132132

133-
if (desiredKeys.has(existingKey) || !isExactAutoPrunedLabel(existing, deleteLabels)) {
133+
if (desiredKeys.has(existingKey) || !isExactGithubDefaultLabel(existing, githubDefaultLabels)) {
134134
continue;
135135
}
136136

137-
deletedConfigured += 1;
138-
console.log(` - ${existing.name} (configured delete)`);
137+
deletedGithubDefaults += 1;
138+
console.log(` - ${existing.name} (GitHub default label)`);
139139

140140
if (!dryRun) {
141141
await githubRequest(
@@ -149,7 +149,7 @@ async function syncSourceRepository(token, repository, desiredLabels, deleteLabe
149149
for (const existing of existingLabels) {
150150
const existingKey = normalizeName(existing.name);
151151

152-
if (desiredKeys.has(existingKey) || isExactAutoPrunedLabel(existing, deleteLabels)) {
152+
if (desiredKeys.has(existingKey) || isExactGithubDefaultLabel(existing, githubDefaultLabels)) {
153153
continue;
154154
}
155155

@@ -166,7 +166,7 @@ async function syncSourceRepository(token, repository, desiredLabels, deleteLabe
166166
}
167167

168168
console.log(
169-
`Summary for ${repository}: created=${created}, updated=${updated}, deletedConfigured=${deletedConfigured}, deletedMissing=${deletedMissing}, unchanged=${unchanged}`,
169+
`Summary for ${repository}: created=${created}, updated=${updated}, deletedGithubDefaults=${deletedGithubDefaults}, deletedMissing=${deletedMissing}, unchanged=${unchanged}`,
170170
);
171171
}
172172

@@ -176,12 +176,12 @@ async function main() {
176176
defaultSourceRepository: process.env.GITHUB_REPOSITORY ?? "",
177177
});
178178
const labels = validateLabels(await readJsonc(labelsPath));
179-
const deleteLabels = validateDeleteLabels(await readJsonc(autoPrunedLabelsPath));
179+
const githubDefaultLabels = validateGithubDefaultLabels(await readJsonc(githubDefaultLabelsPath));
180180
const repository = process.env.SOURCE_REPOSITORY ?? properties.sourceRepository;
181181
assert(repository, "SOURCE_REPOSITORY or properties.sourceRepository is required.");
182182

183183
console.log(
184-
`Loaded ${labels.length} managed labels and ${deleteLabels.length} exact auto-pruned label specs for ${repository}.`,
184+
`Loaded ${labels.length} managed labels and ${githubDefaultLabels.length} exact GitHub default label specs for ${repository}.`,
185185
);
186186

187187
if (validateOnly) {
@@ -192,7 +192,7 @@ async function main() {
192192
const token = process.env.LABEL_SYNC_TOKEN;
193193
assert(token, "LABEL_SYNC_TOKEN is required unless --validate-only is used.");
194194

195-
await syncSourceRepository(token, repository, labels, deleteLabels);
195+
await syncSourceRepository(token, repository, labels, githubDefaultLabels);
196196

197197
if (dryRun) {
198198
console.log("Dry-run mode did not apply label changes.");

‎scripts/sync-config-labels.mjs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import {
66
readJsonc,
77
writeJsoncPreservingHeader,
88
} from "./lib/config-utils.mjs";
9-
import { validateDeleteLabels, validateProperties } from "./lib/config-validation.mjs";
9+
import { validateGithubDefaultLabels, validateProperties } from "./lib/config-validation.mjs";
1010

1111
const workspaceRoot = process.cwd();
1212
const propertiesPath = path.join(workspaceRoot, "config", "properties.jsonc");
1313
const labelsPath = path.join(workspaceRoot, "config", "labels.jsonc");
14-
const autoPrunedLabelsPath = path.join(workspaceRoot, "config", "auto-pruned-labels.jsonc");
14+
const githubDefaultLabelsPath = path.join(workspaceRoot, "config", "github-default-labels.jsonc");
1515

1616
async function githubRequest(token, method, apiPath) {
1717
const response = await fetch(`https://api.github.com${apiPath}`, {
@@ -69,14 +69,14 @@ async function main() {
6969
const repository = process.env.SOURCE_REPOSITORY ?? properties.sourceRepository;
7070
assert(repository, "SOURCE_REPOSITORY or GITHUB_REPOSITORY is required.");
7171

72-
const deleteLabels = validateDeleteLabels(await readJsonc(autoPrunedLabelsPath));
72+
const githubDefaultLabels = validateGithubDefaultLabels(await readJsonc(githubDefaultLabelsPath));
7373
const repositoryLabels = await getAllLabels(token, repository);
7474
const managedLabels = toManagedLabels(repositoryLabels);
7575

7676
await writeJsoncPreservingHeader(labelsPath, managedLabels);
7777

7878
console.log(
79-
`Synced ${managedLabels.length} managed labels from ${repository} into config/labels.jsonc. Source labels take precedence over ${deleteLabels.length} exact auto-pruned label specs.`,
79+
`Synced ${managedLabels.length} managed labels from ${repository} into config/labels.jsonc. Source labels take precedence over ${githubDefaultLabels.length} exact GitHub default label specs.`,
8080
);
8181
}
8282

0 commit comments

Comments
 (0)