Skip to content

Commit 5a6982a

Browse files
committed
add whitelist / blacklist functionality
1 parent f0f191d commit 5a6982a

5 files changed

Lines changed: 88 additions & 38 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ on:
1414
type: boolean
1515
default: false
1616
repositories:
17-
description: "Optional comma-separated subset of discovered repositories"
17+
description: "Optional comma-separated subset of repositories after repository-filter rules are applied"
1818
required: false
1919
type: string
2020

README.md

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ The normal flow is:
3636
| `-- validate-configs.yml
3737
|-- config/
3838
| |-- auto-pruned-labels.jsonc
39-
| |-- blacklisted-repositories.jsonc
4039
| |-- labels.jsonc
41-
| `-- properties.jsonc
40+
| |-- properties.jsonc
41+
| `-- repository-filter.jsonc
4242
`-- scripts/
4343
|-- export-properties.mjs
4444
|-- sync-config-labels.mjs
@@ -115,24 +115,37 @@ The starter file is prefilled with GitHub's default labels:
115115

116116
If any of those labels exist on this repo, `Config-Label_Sync` excludes them from `labels.jsonc`. If they exist on target repos, `Org-Label-Sync` deletes them.
117117

118-
### `config/blacklisted-repositories.jsonc`
118+
### `config/repository-filter.jsonc`
119119

120-
This is the repo blacklist for org sync.
120+
This controls which repositories `Org-Label-Sync` will target.
121121

122-
By default, `Org-Label-Sync` targets every repo in the configured organization. Any repo listed here is skipped.
122+
The file uses:
123123

124-
Entries can be either:
124+
- `useWhitelist`: when `true`, only repositories in `whitelist` are synced; when `false`, all discovered org repositories are synced except those in `blacklist`
125+
- `whitelist`: repos to include when whitelist mode is enabled
126+
- `blacklist`: repos to exclude when whitelist mode is disabled
127+
128+
`useWhitelist` defaults to `false`, so blacklist mode is the default behavior.
129+
130+
Entries in either list can be either:
125131

126132
- `repo-name`
127133
- `owner/repo-name`
128134

129135
Example:
130136

131137
```jsonc
132-
[
133-
"sandbox-repo",
134-
"your-org-name/private-internal-tools"
135-
]
138+
{
139+
"useWhitelist": false,
140+
"whitelist": [
141+
"sandbox-repo",
142+
"your-org-name/important-repo"
143+
],
144+
"blacklist": [
145+
"do-not-touch",
146+
"your-org-name/private-internal-tools"
147+
]
148+
}
136149
```
137150

138151
## Workflows
@@ -176,7 +189,8 @@ Validation includes:
176189
- JSONC parsing
177190
- required property checks
178191
- duplicate label detection
179-
- duplicate blacklist detection
192+
- repository filter shape and `useWhitelist` validation
193+
- duplicate whitelist and blacklist detection
180194
- invalid colors
181195
- invalid repo names
182196
- overlap detection between `labels.jsonc` and `auto-pruned-labels.jsonc`
@@ -193,7 +207,7 @@ Inputs:
193207

194208
- `dry_run`: preview changes without writing them
195209
- `delete_missing`: override `deleteMissingByDefault` for the run
196-
- `repositories`: optional comma-separated subset of discovered repositories
210+
- `repositories`: optional comma-separated subset of repositories after `repository-filter.jsonc` is applied
197211

198212
What it does:
199213

@@ -202,7 +216,7 @@ What it does:
202216
3. Loads shared settings from `config/properties.jsonc`
203217
4. Validates the updated config
204218
5. Discovers repos in the configured organization
205-
6. Skips repos in `config/blacklisted-repositories.jsonc`
219+
6. Applies `config/repository-filter.jsonc`
206220
7. Creates or updates labels from `config/labels.jsonc`
207221
8. Deletes labels listed in `config/auto-pruned-labels.jsonc`
208222
9. Optionally deletes any other unmanaged labels if `delete_missing` or `deleteMissingByDefault` is enabled
@@ -224,14 +238,14 @@ That token needs enough access to:
224238
2. Create the sync token secret in the repo.
225239
3. Update `config/properties.jsonc` for your org and repo.
226240
4. Adjust `config/auto-pruned-labels.jsonc` if you want a different always-delete list.
227-
5. Add any excluded repos to `config/blacklisted-repositories.jsonc`.
241+
5. Configure `config/repository-filter.jsonc` for blacklist mode or whitelist mode.
228242
6. Set the labels on this repository to the label set you want to manage.
229243
7. Run `Config-Label_Sync` once if you want to populate `config/labels.jsonc` immediately.
230244
8. Run `Org-Label-Sync` to propagate the labels across the organization.
231245

232246
## Safe Defaults
233247

234248
- `labels.jsonc` starts empty until you define or sync labels on this repo
235-
- all repos in the org are targeted unless blacklisted
249+
- all repos in the org are targeted unless excluded by `repository-filter.jsonc`
236250
- GitHub default labels are auto-pruned by default
237251
- deleting unmanaged labels is off by default unless you enable it

config/blacklisted-repositories.jsonc

Lines changed: 0 additions & 7 deletions
This file was deleted.

config/repository-filter.jsonc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Set to true to sync only the repositories listed in "whitelist".
2+
// Set to false to sync every discovered org repository except those listed in "blacklist".
3+
{
4+
"useWhitelist": false,
5+
6+
// Used only when "useWhitelist" is true.
7+
"whitelist": [
8+
// "sandbox-repo",
9+
// "your-org-name/important-repo"
10+
],
11+
12+
// Used only when "useWhitelist" is false.
13+
"blacklist": [
14+
// "do-not-touch",
15+
// "your-org-name/private-internal-tools"
16+
]
17+
}

scripts/sync-labels.mjs

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const workspaceRoot = process.cwd();
1212
const propertiesPath = path.join(workspaceRoot, "config", "properties.jsonc");
1313
const labelsPath = path.join(workspaceRoot, "config", "labels.jsonc");
1414
const autoPrunedLabelsPath = path.join(workspaceRoot, "config", "auto-pruned-labels.jsonc");
15-
const blacklistedRepositoriesPath = path.join(workspaceRoot, "config", "blacklisted-repositories.jsonc");
15+
const repositoryFilterPath = path.join(workspaceRoot, "config", "repository-filter.jsonc");
1616

1717
const validateOnly = process.argv.includes("--validate-only");
1818
const dryRun = validateOnly || process.env.DRY_RUN === "true";
@@ -130,25 +130,42 @@ function assertNoLabelOverlap(desiredLabels, deleteLabels) {
130130
);
131131
}
132132

133-
function validateBlacklistedRepositories(blacklist) {
134-
assert(Array.isArray(blacklist), "config/blacklisted-repositories.jsonc must contain an array.");
133+
function validateRepositoryEntries(entries, configKey) {
134+
assert(Array.isArray(entries), `config/repository-filter.jsonc field "${configKey}" must contain an array.`);
135135
const seen = new Set();
136-
return new Set(blacklist.map((entry, index) => {
137-
assert(typeof entry === "string" && entry.trim(), `Blacklist entry at index ${index} must be a non-empty string.`);
136+
return new Set(entries.map((entry, index) => {
137+
assert(typeof entry === "string" && entry.trim(), `"${configKey}" entry at index ${index} must be a non-empty string.`);
138138

139139
const name = entry.trim();
140140
assert(
141141
isRepositoryName(name) || isFullRepositoryName(name),
142-
`Blacklist entry "${name}" must be either "repo-name" or "owner/repo-name".`,
142+
`"${configKey}" entry "${name}" must be either "repo-name" or "owner/repo-name".`,
143143
);
144144

145145
const key = normalizeRepositoryRef(name);
146-
assert(!seen.has(key), `Duplicate blacklist entry detected: "${name}".`);
146+
assert(!seen.has(key), `Duplicate "${configKey}" entry detected: "${name}".`);
147147
seen.add(key);
148148
return key;
149149
}));
150150
}
151151

152+
function validateRepositoryFilter(repositoryFilter) {
153+
assert(
154+
repositoryFilter && typeof repositoryFilter === "object" && !Array.isArray(repositoryFilter),
155+
"config/repository-filter.jsonc must contain an object.",
156+
);
157+
158+
if (repositoryFilter.useWhitelist !== undefined) {
159+
assert(typeof repositoryFilter.useWhitelist === "boolean", 'config/repository-filter.jsonc field "useWhitelist" must be a boolean.');
160+
}
161+
162+
return {
163+
useWhitelist: repositoryFilter.useWhitelist ?? false,
164+
whitelist: validateRepositoryEntries(repositoryFilter.whitelist ?? [], "whitelist"),
165+
blacklist: validateRepositoryEntries(repositoryFilter.blacklist ?? [], "blacklist"),
166+
};
167+
}
168+
152169
async function githubRequest(token, method, apiPath, body) {
153170
const response = await fetch(`https://api.github.com${apiPath}`, {
154171
method,
@@ -210,14 +227,21 @@ async function getOrganizationRepositories(token, orgName) {
210227
}
211228
}
212229

213-
function filterRepositories(repositories, orgName, blacklist) {
230+
function filterRepositories(repositories, orgName, repositoryFilter) {
214231
return repositories
215232
.filter((repository) => {
216233
const shortName = normalizeRepositoryRef(repository.name);
217234
const fullName = normalizeRepositoryRef(repository.full_name);
218235
const orgScopedName = normalizeRepositoryRef(`${orgName}/${repository.name}`);
236+
const matchesFilter = (entries) => (
237+
entries.has(shortName) || entries.has(fullName) || entries.has(orgScopedName)
238+
);
239+
240+
if (repositoryFilter.useWhitelist) {
241+
return matchesFilter(repositoryFilter.whitelist);
242+
}
219243

220-
return !blacklist.has(shortName) && !blacklist.has(fullName) && !blacklist.has(orgScopedName);
244+
return !matchesFilter(repositoryFilter.blacklist);
221245
})
222246
.sort((left, right) => left.full_name.localeCompare(right.full_name));
223247
}
@@ -243,7 +267,7 @@ function applyTargetRepositoryFilter(repositories) {
243267

244268
assert(
245269
missing.length === 0,
246-
`Requested repositories were not found in the discovered org repository set after blacklist filtering: ${missing.join(", ")}.`,
270+
`Requested repositories were not found in the discovered org repository set after repository-filter processing: ${missing.join(", ")}.`,
247271
);
248272

249273
return selected;
@@ -360,10 +384,12 @@ async function main() {
360384
const labels = validateLabels(await readJsonc(labelsPath));
361385
const deleteLabels = validateDeleteLabels(await readJsonc(autoPrunedLabelsPath));
362386
assertNoLabelOverlap(labels, deleteLabels);
363-
const blacklistedRepositories = validateBlacklistedRepositories(await readJsonc(blacklistedRepositoriesPath));
387+
const repositoryFilter = validateRepositoryFilter(await readJsonc(repositoryFilterPath));
388+
const activeFilterCount = repositoryFilter.useWhitelist ? repositoryFilter.whitelist.size : repositoryFilter.blacklist.size;
389+
const activeFilterMode = repositoryFilter.useWhitelist ? "whitelist" : "blacklist";
364390

365391
console.log(
366-
`Loaded ${labels.length} managed labels, ${deleteLabels.length} auto-pruned labels, and ${blacklistedRepositories.size} blacklisted repositories.`,
392+
`Loaded ${labels.length} managed labels, ${deleteLabels.length} auto-pruned labels, and ${activeFilterCount} active repository filter entries from config/repository-filter.jsonc (mode=${activeFilterMode}).`,
367393
);
368394

369395
if (validateOnly) {
@@ -378,15 +404,15 @@ async function main() {
378404

379405
const discoveredRepositories = await getOrganizationRepositories(token, orgName);
380406
const repositories = applyTargetRepositoryFilter(
381-
filterRepositories(discoveredRepositories, orgName, blacklistedRepositories),
407+
filterRepositories(discoveredRepositories, orgName, repositoryFilter),
382408
);
383409

384410
console.log(
385-
`Discovered ${discoveredRepositories.length} repositories in ${orgName}; ${repositories.length} remain after blacklist filtering.`,
411+
`Discovered ${discoveredRepositories.length} repositories in ${orgName}; ${repositories.length} remain after repository-filter processing.`,
386412
);
387413

388414
if (repositories.length === 0) {
389-
console.log("No repositories remain after blacklist and optional subset filtering. Nothing to sync.");
415+
console.log("No repositories remain after repository-filter processing and optional subset filtering. Nothing to sync.");
390416
return;
391417
}
392418

0 commit comments

Comments
 (0)