Skip to content

Commit c74f639

Browse files
authored
Add label syncing workflow (#47)
Add labels.toml to define standard labels that should exist across all repositories in the organization. Add sync-labels workflow that ensures these labels are created in all active repositories when labels.toml is updated. The workflow uses the app token to access all org repositories and creates any missing labels or updates existing labels if their color or description has changed. Uses TOML format with Python's tomllib for clean parsing. Python writes directly to GITHUB_OUTPUT. Uses fromJSON() for secure interpolation of the labels array in the github-script action. Runs on pushes to main or can be triggered manually with test mode. Labels are defined with name, color, and description properties for better control and documentation across all repositories. Assisted-by: Claude Code (Sonnet 4.5)
1 parent 7102c22 commit c74f639

2 files changed

Lines changed: 171 additions & 0 deletions

File tree

.github/workflows/sync-labels.yml

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
name: Sync labels
2+
on:
3+
workflow_dispatch:
4+
inputs:
5+
test_mode:
6+
description: 'Test mode - only sync to ci-sandbox'
7+
type: boolean
8+
default: false
9+
push:
10+
branches:
11+
- main
12+
paths:
13+
- 'labels.toml'
14+
- '.github/workflows/sync-labels.yml'
15+
16+
# Prevent multiple workflow runs from racing
17+
concurrency: ${{ github.workflow }}
18+
19+
permissions:
20+
contents: read
21+
22+
jobs:
23+
init:
24+
name: Discover repositories
25+
runs-on: ubuntu-24.04
26+
outputs:
27+
matrix: ${{ steps.get-repos.outputs.matrix }}
28+
labels: ${{ steps.parse-labels.outputs.labels }}
29+
steps:
30+
- name: Checkout
31+
uses: actions/checkout@v5
32+
33+
- name: Parse labels from TOML
34+
id: parse-labels
35+
run: |
36+
python3 -c '
37+
import tomllib
38+
import json
39+
import sys
40+
import os
41+
42+
try:
43+
with open("labels.toml", "rb") as f:
44+
data = tomllib.load(f)
45+
labels = data["labels"]
46+
47+
with open(os.environ["GITHUB_OUTPUT"], "a") as output:
48+
output.write(f"labels={json.dumps(labels)}\n")
49+
except Exception as e:
50+
print(f"Error parsing labels.toml: {e}", file=sys.stderr)
51+
sys.exit(1)
52+
'
53+
54+
- name: Generate Actions Token
55+
id: token
56+
uses: actions/create-github-app-token@v2
57+
with:
58+
app-id: ${{ secrets.APP_ID }}
59+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
60+
owner: ${{ github.repository_owner }}
61+
62+
- name: Get repository list
63+
id: get-repos
64+
uses: actions/github-script@v8
65+
with:
66+
github-token: ${{ steps.token.outputs.token }}
67+
script: |
68+
const repos = await github.paginate(github.rest.repos.listForOrg, {
69+
org: context.repo.owner,
70+
type: 'all',
71+
per_page: 100
72+
});
73+
74+
// Filter out archived repos
75+
let activeRepos = repos.filter(repo =>
76+
!repo.archived &&
77+
!repo.name.startsWith('.')
78+
);
79+
80+
// Test mode - only sync to ci-sandbox
81+
const testMode = '${{ github.event.inputs.test_mode }}' === 'true';
82+
if (testMode) {
83+
console.log('Test mode enabled - only syncing to ci-sandbox');
84+
activeRepos = activeRepos.filter(repo => repo.name === 'ci-sandbox');
85+
}
86+
87+
const matrix = activeRepos.map(repo => ({
88+
repo: repo.name,
89+
full_name: repo.full_name
90+
}));
91+
92+
console.log('Discovered repositories:', matrix);
93+
core.setOutput('matrix', JSON.stringify(matrix));
94+
95+
sync:
96+
name: Sync to ${{ matrix.repo }}
97+
needs: init
98+
if: needs.init.outputs.matrix != '[]'
99+
runs-on: ubuntu-24.04
100+
strategy:
101+
fail-fast: false
102+
matrix:
103+
include: ${{ fromJSON(needs.init.outputs.matrix) }}
104+
steps:
105+
- name: Generate Actions Token
106+
id: token
107+
uses: actions/create-github-app-token@v2
108+
with:
109+
app-id: ${{ secrets.APP_ID }}
110+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
111+
owner: ${{ github.repository_owner }}
112+
repositories: ${{ matrix.repo }}
113+
114+
- name: Sync labels
115+
uses: actions/github-script@v8
116+
with:
117+
github-token: ${{ steps.token.outputs.token }}
118+
script: |
119+
const labels = ${{ fromJSON(needs.init.outputs.labels) }};
120+
const owner = context.repo.owner;
121+
const repo = '${{ matrix.repo }}';
122+
123+
console.log(`Syncing labels to ${owner}/${repo}`);
124+
125+
for (const label of labels) {
126+
try {
127+
// Try to get the label to see if it exists
128+
const existingLabel = await github.rest.issues.getLabel({
129+
owner,
130+
repo,
131+
name: label.name
132+
});
133+
134+
// Label exists - update it if color or description changed
135+
if (existingLabel.data.color !== label.color ||
136+
existingLabel.data.description !== label.description) {
137+
console.log(`Updating label "${label.name}"`);
138+
await github.rest.issues.updateLabel({
139+
owner,
140+
repo,
141+
name: label.name,
142+
color: label.color,
143+
description: label.description
144+
});
145+
} else {
146+
console.log(`Label "${label.name}" already up to date`);
147+
}
148+
} catch (error) {
149+
if (error.status === 404) {
150+
// Label doesn't exist, create it
151+
console.log(`Creating label "${label.name}"`);
152+
await github.rest.issues.createLabel({
153+
owner,
154+
repo,
155+
name: label.name,
156+
color: label.color,
157+
description: label.description
158+
});
159+
} else {
160+
throw error;
161+
}
162+
}
163+
}
164+
165+
console.log('Label sync complete');

labels.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Standard labels for all bootc-dev repositories
2+
3+
labels = [
4+
{ name = "needs-rebase", color = "fbca04", description = "Used by the rebase helper" },
5+
{ name = "triaged", color = "1d76db", description = "This issue appears to be valid" },
6+
]

0 commit comments

Comments
 (0)