Skip to content

Commit 0937f03

Browse files
committed
fix workflow layout
1 parent cec838f commit 0937f03

4 files changed

Lines changed: 188 additions & 11 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Config-Label_Sync
2+
3+
on:
4+
workflow_dispatch:
5+
workflow_call:
6+
7+
permissions:
8+
contents: write
9+
10+
jobs:
11+
sync-config:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Check out repository
16+
uses: actions/checkout@v4
17+
with:
18+
ref: ${{ github.event.repository.default_branch }}
19+
token: ${{ secrets.LABEL_SYNC_TOKEN }}
20+
21+
- name: Set up Node.js
22+
uses: actions/setup-node@v4
23+
with:
24+
node-version: "20"
25+
26+
- name: Sync current repo labels into config
27+
env:
28+
CONFIG_LABEL_SYNC_TOKEN: ${{ secrets.LABEL_SYNC_TOKEN }}
29+
SOURCE_REPOSITORY: ${{ github.repository }}
30+
run: node scripts/sync-config-labels.mjs
31+
32+
- name: Commit config updates
33+
run: |
34+
git config user.name "github-actions[bot]"
35+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
36+
if git diff --quiet -- config/labels.json; then
37+
echo "No config changes detected."
38+
exit 0
39+
fi
40+
git add config/labels.json
41+
git commit -m "Sync labels config from source repository"
42+
git push origin "HEAD:${{ github.event.repository.default_branch }}"
Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Sync Labels
1+
name: Org-Label-Sync
22

33
on:
44
workflow_dispatch:
@@ -18,26 +18,37 @@ on:
1818
required: false
1919
type: string
2020

21+
permissions:
22+
contents: write
23+
2124
jobs:
22-
sync:
25+
refresh-config:
26+
uses: ./.github/workflows/config-label-sync.yml
27+
secrets: inherit
28+
29+
sync-org:
30+
needs: refresh-config
2331
runs-on: ubuntu-latest
24-
permissions:
25-
contents: read
2632

2733
steps:
28-
- name: Check out repository
34+
- name: Check out latest default branch
2935
uses: actions/checkout@v4
36+
with:
37+
ref: ${{ github.event.repository.default_branch }}
3038

3139
- name: Set up Node.js
3240
uses: actions/setup-node@v4
3341
with:
3442
node-version: "20"
3543

36-
- name: Sync labels
44+
- name: Validate updated config
45+
run: node scripts/sync-labels.mjs --validate-only
46+
47+
- name: Sync labels across the organization
3748
env:
3849
LABEL_SYNC_TOKEN: ${{ secrets.LABEL_SYNC_TOKEN }}
3950
ORG_NAME: ${{ github.repository_owner }}
40-
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }}
41-
DELETE_MISSING: ${{ github.event_name == 'workflow_dispatch' && inputs.delete_missing || 'false' }}
42-
TARGET_REPOSITORIES: ${{ github.event_name == 'workflow_dispatch' && inputs.repositories || '' }}
51+
DRY_RUN: ${{ inputs.dry_run }}
52+
DELETE_MISSING: ${{ inputs.delete_missing }}
53+
TARGET_REPOSITORIES: ${{ inputs.repositories }}
4354
run: node scripts/sync-labels.mjs
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
name: Validate Config
1+
name: Validate-Configs
22

33
on:
4-
workflow_dispatch:
4+
push:
5+
paths:
6+
- "config/**"
7+
pull_request:
8+
paths:
9+
- "config/**"
510

611
jobs:
712
validate:

scripts/sync-config-labels.mjs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import fs from "node:fs/promises";
2+
import path from "node:path";
3+
4+
const workspaceRoot = process.cwd();
5+
const labelsPath = path.join(workspaceRoot, "config", "labels.json");
6+
const deleteLabelsPath = path.join(workspaceRoot, "config", "delete-labels.json");
7+
8+
function assert(condition, message) {
9+
if (!condition) {
10+
throw new Error(message);
11+
}
12+
}
13+
14+
function normalizeColor(color) {
15+
return color.replace(/^#/, "").toLowerCase();
16+
}
17+
18+
function normalizeDescription(description) {
19+
return description ?? "";
20+
}
21+
22+
function normalizeName(name) {
23+
return name.trim().toLowerCase();
24+
}
25+
26+
async function readJson(filePath) {
27+
const contents = await fs.readFile(filePath, "utf8");
28+
return JSON.parse(contents);
29+
}
30+
31+
async function writeJson(filePath, value) {
32+
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
33+
}
34+
35+
function validateDeleteLabels(deleteLabels) {
36+
assert(Array.isArray(deleteLabels), "config/delete-labels.json must contain an array.");
37+
38+
const seen = new Set();
39+
40+
return new Set(
41+
deleteLabels.map((entry, index) => {
42+
assert(typeof entry === "string" && entry.trim(), `Delete label at index ${index} must be a non-empty string.`);
43+
44+
const name = normalizeName(entry);
45+
assert(!seen.has(name), `Duplicate delete label detected: "${entry}".`);
46+
seen.add(name);
47+
return name;
48+
}),
49+
);
50+
}
51+
52+
async function githubRequest(token, method, apiPath) {
53+
const response = await fetch(`https://api.github.com${apiPath}`, {
54+
method,
55+
headers: {
56+
Accept: "application/vnd.github+json",
57+
Authorization: `Bearer ${token}`,
58+
"User-Agent": "label-sync-config",
59+
"X-GitHub-Api-Version": "2022-11-28",
60+
},
61+
});
62+
63+
if (!response.ok) {
64+
const message = await response.text();
65+
throw new Error(`${method} ${apiPath} failed with ${response.status}: ${message}`);
66+
}
67+
68+
return response.json();
69+
}
70+
71+
async function getAllLabels(token, repo) {
72+
const labels = [];
73+
let page = 1;
74+
75+
while (true) {
76+
const batch = await githubRequest(token, "GET", `/repos/${repo}/labels?per_page=100&page=${page}`);
77+
labels.push(...batch);
78+
79+
if (batch.length < 100) {
80+
return labels;
81+
}
82+
83+
page += 1;
84+
}
85+
}
86+
87+
function toManagedLabels(labels, deleteLabels) {
88+
return labels
89+
.filter((label) => !deleteLabels.has(normalizeName(label.name)))
90+
.map((label) => ({
91+
name: label.name.trim(),
92+
color: normalizeColor(label.color),
93+
description: normalizeDescription(label.description),
94+
}))
95+
.sort((left, right) => left.name.localeCompare(right.name));
96+
}
97+
98+
async function main() {
99+
const token = process.env.CONFIG_LABEL_SYNC_TOKEN ?? process.env.GITHUB_TOKEN;
100+
assert(token, "CONFIG_LABEL_SYNC_TOKEN or GITHUB_TOKEN is required.");
101+
102+
const repository = process.env.SOURCE_REPOSITORY ?? process.env.GITHUB_REPOSITORY;
103+
assert(repository, "SOURCE_REPOSITORY or GITHUB_REPOSITORY is required.");
104+
105+
const deleteLabels = validateDeleteLabels(await readJson(deleteLabelsPath));
106+
const repositoryLabels = await getAllLabels(token, repository);
107+
const managedLabels = toManagedLabels(repositoryLabels, deleteLabels);
108+
109+
await writeJson(labelsPath, managedLabels);
110+
111+
console.log(
112+
`Synced ${managedLabels.length} managed labels from ${repository} into config/labels.json after excluding ${deleteLabels.size} auto-delete labels.`,
113+
);
114+
}
115+
116+
main().catch((error) => {
117+
console.error(error.message);
118+
process.exitCode = 1;
119+
});

0 commit comments

Comments
 (0)