Skip to content

Commit 39ba4b5

Browse files
committed
refactor(ci): migrate label sync logic to Python
- Replace brittle Bash workflow logic with robust Python script - Fix syntax errors in usage reporting - Improve label update and deletion reliability - Handle special characters and emojis in label names correctly - Add Python cache files to .gitignore Generated-by: GitHub Copilot <copilot@github.com> Signed-off-by: Ashley Childress <6563688+anchildress1@users.noreply.github.com>
1 parent a1ce198 commit 39ba4b5

3 files changed

Lines changed: 113 additions & 68 deletions

File tree

.github/scripts/sync_labels.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import json
2+
import subprocess
3+
import sys
4+
import os
5+
6+
def run_command(args):
7+
result = subprocess.run(args, capture_output=True, text=True)
8+
if result.returncode != 0:
9+
print(f"Error running {' '.join(args)}: {result.stderr}", file=sys.stderr)
10+
return None
11+
return result.stdout
12+
13+
def get_repos(org):
14+
output = run_command(["gh", "repo", "list", org, "--limit", "1000", "--json", "nameWithOwner,isArchived"])
15+
if not output:
16+
return []
17+
repos = json.loads(output)
18+
return [r["nameWithOwner"] for r in repos if not r["isArchived"]]
19+
20+
def get_repo_labels(repo):
21+
output = run_command(["gh", "label", "list", "--repo", repo, "--json", "name,description,color"])
22+
if not output:
23+
return []
24+
return json.loads(output)
25+
26+
def sync_labels(repo, target_labels):
27+
existing_labels = {l["name"]: l for l in get_repo_labels(repo)}
28+
29+
for target in target_labels:
30+
name = target["name"]
31+
color = target["color"]
32+
description = target["description"]
33+
34+
if name in existing_labels:
35+
existing = existing_labels[name]
36+
if existing["color"].lower() != color.lower() or existing["description"] != description:
37+
print(f"Updating label '{name}' in {repo}")
38+
run_command(["gh", "label", "edit", name, "--repo", repo, "--color", color, "--description", description])
39+
else:
40+
print(f"Creating label '{name}' in {repo}")
41+
run_command(["gh", "label", "create", name, "--repo", repo, "--color", color, "--description", description])
42+
43+
def cleanup_labels(repo, target_label_names):
44+
existing_labels = get_repo_labels(repo)
45+
extra_labels = [l["name"] for l in existing_labels if l["name"] not in target_label_names]
46+
47+
usage_report = []
48+
49+
for label in extra_labels:
50+
# Check issues and PRs (gh issue list covers both if not specified, but let's be explicit)
51+
issues = json.loads(run_command(["gh", "issue", "list", "--repo", repo, "--label", label, "--json", "number,url,title"]) or "[]")
52+
prs = json.loads(run_command(["gh", "pr", "list", "--repo", repo, "--label", label, "--json", "number,url,title"]) or "[]")
53+
54+
# Discussions might fail if not enabled
55+
discussions_output = run_command(["gh", "discussion", "list", "--repo", repo, "--label", label, "--json", "number,url,title"])
56+
discussions = json.loads(discussions_output) if discussions_output else []
57+
58+
if not issues and not prs and not discussions:
59+
print(f"Deleting unused label '{label}' from {repo}")
60+
run_command(["gh", "label", "delete", label, "--repo", repo, "--yes"])
61+
else:
62+
for item in issues:
63+
usage_report.append({"label": label, "repo": repo, "type": "Issue", "number": item["number"], "url": item["url"], "title": item["title"]})
64+
for item in prs:
65+
usage_report.append({"label": label, "repo": repo, "type": "PR", "number": item["number"], "url": item["url"], "title": item["title"]})
66+
for item in discussions:
67+
usage_report.append({"label": label, "repo": repo, "type": "Discussion", "number": item["number"], "url": item["url"], "title": item["title"]})
68+
69+
return usage_report
70+
71+
def main():
72+
org = "ChecKMarKDevTools"
73+
labels_file = ".github/org-labels.json"
74+
75+
with open(labels_file, "r") as f:
76+
target_labels = json.load(f)
77+
78+
target_label_names = [l["name"] for l in target_labels]
79+
repos = get_repos(org)
80+
81+
all_usage = []
82+
83+
for repo in repos:
84+
print(f"Processing {repo}...")
85+
sync_labels(repo, target_labels)
86+
usage = cleanup_labels(repo, target_label_names)
87+
all_usage.extend(usage)
88+
89+
if all_usage:
90+
# Group by repo for the summary
91+
all_usage.sort(key=lambda x: (x["repo"], x["label"]))
92+
current_repo = ""
93+
for item in all_usage:
94+
if item["repo"] != current_repo:
95+
if current_repo:
96+
print("::endgroup::")
97+
print(f"::group::Extra labels in {item['repo']}")
98+
current_repo = item["repo"]
99+
print(f"- Label '{item['label']}' is in use:")
100+
print(f" - {item['type']} [#{item['number']}]({item['url']}): {item['title']}")
101+
if current_repo:
102+
print("::endgroup::")
103+
else:
104+
print("No extra labels in use.")
105+
106+
if __name__ == "__main__":
107+
main()

.github/workflows/sync-labels.yml

Lines changed: 2 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -26,74 +26,8 @@ jobs:
2626
- name: Checkout
2727
uses: actions/checkout@v6
2828

29-
- name: Sync labels to all org repos
29+
- name: Sync and Cleanup Labels
3030
env:
3131
GH_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }}
3232
run: |
33-
repos=$(gh repo list ChecKMarKDevTools --limit 1000 --json nameWithOwner,isArchived --jq '.[] | select(.isArchived == false) | .nameWithOwner')
34-
35-
for repo in $repos; do
36-
echo "Syncing labels to $repo"
37-
jq -c '.[]' .github/org-labels.json | while read -r label; do
38-
name=$(echo "$label" | jq -r .name)
39-
color=$(echo "$label" | jq -r .color)
40-
desc=$(echo "$label" | jq -r .description)
41-
if gh label list --repo "$repo" --json name | jq -e ".[] | select(.name == \"$name\")" > /dev/null; then
42-
gh label edit "$name" --color "$color" --description "$desc" --repo "$repo"
43-
else
44-
gh label create "$name" --color "$color" --description "$desc" --repo "$repo"
45-
fi
46-
done
47-
done
48-
- name: Clean up extra labels
49-
env:
50-
GH_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }}
51-
run: |
52-
labels_in_json=$(jq -r '.[].name' .github/org-labels.json)
53-
temp_file=$(mktemp)
54-
found_any=false
55-
repos=$(gh repo list ChecKMarKDevTools --limit 1000 --json nameWithOwner,isArchived --jq '.[] | select(.isArchived == false) | .nameWithOwner')
56-
for repo in $repos; do
57-
gh label list --repo "$repo" --json name | jq -r '.[].name' | while read -r existing_label; do
58-
if ! echo "$labels_in_json" | grep -q "^$existing_label$"; then
59-
issues=$(gh issue list --label "$existing_label" --repo "$repo" --json url,number,title)
60-
prs=$(gh pr list --label "$existing_label" --repo "$repo" --json url,number,title)
61-
discussions=$(gh discussion list --label "$existing_label" --repo "$repo" --json url,number,title)
62-
if [ "$(echo "$issues" | jq '. | length')" -eq 0 ] && [ "$(echo "$prs" | jq '. | length')" -eq 0 ] && [ "$(echo "$discussions" | jq '. | length')" -eq 0 ]; then
63-
echo "Deleting unused label '$existing_label' from $repo"
64-
gh label delete "$existing_label" --repo "$repo" --yes
65-
else
66-
found_any=true
67-
# Collect data for summary
68-
echo "$issues" | jq -r '.[] | "'"$existing_label|$repo|Issue|\(.number)|\(.url)|\(.title)"' >> "$temp_file"
69-
echo "$prs" | jq -r '.[] | "'"$existing_label|$repo|PR|\(.number)|\(.url)|\(.title)"' >> "$temp_file"
70-
echo "$discussions" | jq -r '.[] | "'"$existing_label|$repo|Discussion|\(.number)|\(.url)|\(.title)"' >> "$temp_file"
71-
fi
72-
fi
73-
done
74-
done
75-
if [ "$found_any" = true ]; then
76-
current_repo=""
77-
current_label=""
78-
sort "$temp_file" | while IFS='|' read -r label repo type number url title; do
79-
if [ "$current_repo" != "$repo" ]; then
80-
if [ -n "$current_repo" ]; then
81-
echo "::endgroup::"
82-
fi
83-
echo "::group::Extra labels in $repo"
84-
current_repo="$repo"
85-
current_label=""
86-
fi
87-
if [ "$current_label" != "$label" ]; then
88-
echo "- Label '$label' is in use:"
89-
current_label="$label"
90-
fi
91-
echo " - $type [#$number]($url): $title"
92-
done
93-
if [ -n "$current_repo" ]; then
94-
echo "::endgroup::"
95-
fi
96-
else
97-
echo "No extra labels in use."
98-
fi
99-
rm "$temp_file"
33+
python3 .github/scripts/sync_labels.py

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
*.tmp
2+
__pycache__/
3+
*.pyc
4+
*.pyo
5+
*.pyd

0 commit comments

Comments
 (0)