Skip to content

Commit 84f1211

Browse files
committed
feat(labels): add script for unifying labels
Related to #799 Signed-off-by: Matej Focko <[email protected]>
1 parent a4d0fdd commit 84f1211

File tree

1 file changed

+288
-0
lines changed

1 file changed

+288
-0
lines changed

unify-labels/sync_labels.py

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
#!/usr/bin/env python3
2+
3+
import itertools
4+
import os
5+
from collections.abc import Iterator
6+
from dataclasses import dataclass, field
7+
8+
import click
9+
from github import Auth, Github, Repository
10+
from github import Label as GithubLabel
11+
12+
13+
@dataclass
14+
class Label:
15+
name: str
16+
color: str | None = None
17+
description: str | None = None
18+
sublabels: list["Label"] = field(default_factory=list)
19+
20+
def __iter__(self) -> Iterator["Label"]:
21+
if not self.sublabels:
22+
yield self
23+
return
24+
25+
yield from (
26+
Label(
27+
name=f"{self.name}/{sublabel.name}",
28+
color=sublabel.color or self.color,
29+
description=sublabel.description or self.description,
30+
)
31+
for category in self.sublabels
32+
for sublabel in category
33+
)
34+
35+
36+
LABELS: list[Label] = [
37+
# Area
38+
Label(
39+
name="area",
40+
color="e4f3f4",
41+
sublabels=[
42+
# Packit-based area
43+
Label(name="general", description="Not tied to a specific area"),
44+
Label(name="cli", description="Impact on packit's command-line interface"),
45+
Label(name="config", description="Related to the configuration"),
46+
Label(name="database", description="Related to the database"),
47+
Label(name="testing", description="Related to internal tests"),
48+
Label(name="user-experience", description="Related to the UX"),
49+
Label(name="other", description="Not specific to any other of the areas"),
50+
Label(name="source-git", description="Upstream + downstream in one repo"),
51+
Label(name="api", description="API of Packit"),
52+
Label(name="dashboard", description="Related to the Packit Dashboard"),
53+
Label(name="deployment", description="Related to the Packit's deployment"),
54+
# Git forges
55+
Label(name="github", description="GitHub-forge related", color="FBCA04"),
56+
Label(name="gitlab", description="GitLab-forge related", color="FBCA04"),
57+
Label(name="forgejo", description="Forgejo-forge related", color="FBCA04"),
58+
# Integrated services
59+
Label(
60+
name="copr",
61+
description="Related to the Copr integration",
62+
color="3c6eb4",
63+
),
64+
Label(
65+
name="image-builder",
66+
description="Related to the integration with Image Builder",
67+
color="3c6eb4",
68+
),
69+
Label(
70+
name="testing-farm",
71+
description="Related to the integration with Testing Farm",
72+
color="3c6eb4",
73+
),
74+
Label(
75+
name="openscanhub",
76+
description="Related to the OpenScanHub integration",
77+
color="3c6eb4",
78+
),
79+
# Distro ecosystems
80+
Label(
81+
name="fedora", description="Related to Fedora ecosystem", color="51a2da"
82+
),
83+
Label(
84+
name="fedora-ci",
85+
description="Related to the Fedora CI service",
86+
color="51a2da",
87+
),
88+
Label(
89+
name="rhel-ecosystem",
90+
description="Related to RHEL, CentOS Stream, etc.",
91+
color="EE0000",
92+
),
93+
],
94+
),
95+
# Gain
96+
Label(
97+
name="gain",
98+
sublabels=[
99+
Label(
100+
name="low",
101+
color="f2ca92",
102+
description="Doesn't bring much value to users",
103+
),
104+
Label(
105+
name="high",
106+
color="e59728",
107+
description="Brings a lot of value to users",
108+
),
109+
],
110+
),
111+
# Impact
112+
Label(
113+
name="impact",
114+
sublabels=[
115+
Label(
116+
name="low", color="cfbddd", description="Affects only few of the users"
117+
),
118+
Label(name="high", color="a07cbc", description="Affects a lot of users"),
119+
],
120+
),
121+
# Kind
122+
Label(
123+
name="kind",
124+
color="1D76DB",
125+
sublabels=[
126+
Label(
127+
name="bug",
128+
color="D93F0B",
129+
description="An unexpected problem or behavior",
130+
),
131+
Label(name="documentation", description="Improvements to docs"),
132+
Label(name="feature", description="A request, idea, or new functionality"),
133+
Label(
134+
name="internal", description="Task that doesn't affect users directly"
135+
),
136+
Label(name="other", description="A specific piece of work"),
137+
Label(
138+
name="technical-debt", description="Consequences of previous decisions"
139+
),
140+
Label(name="role", description="Regular chore for the role rotation"),
141+
Label(name="security", color="e00f16", description="Security concern"),
142+
Label(
143+
name="recurring",
144+
description="Recurring task that needs to be done periodically",
145+
),
146+
],
147+
),
148+
# Complexity
149+
Label(
150+
name="complexity",
151+
color="bbed97",
152+
sublabels=[
153+
Label(
154+
name="single-task",
155+
description="Regular task; should be done within days",
156+
),
157+
Label(
158+
name="epic",
159+
description="Lots of work ahead; planning/design is required",
160+
),
161+
],
162+
),
163+
# PR states
164+
Label(
165+
name="do-not-merge",
166+
color="D93F0B",
167+
description="Do not merge! Work in progress",
168+
),
169+
Label(name="mergeit", color="0E8A16", description="Merge via Zuul"),
170+
Label(name="needs-review", color="F81880", description="Requires review"),
171+
Label(name="ready-for-review", color="18e033", description="Ready for review"),
172+
# Events
173+
Label(
174+
name="events",
175+
color="6318F9",
176+
sublabels=[
177+
Label(name="GSOC", description="Contribution from the GSOC mentoring"),
178+
Label(name="Hacktoberfest", description="Participation in Hacktoberfest"),
179+
Label(
180+
name="Outreachy",
181+
description="Contribution from the Outreachy mentoring",
182+
),
183+
],
184+
),
185+
# Miscellaneous labels
186+
Label(
187+
name="blocked", color="D93F0B", description="Blocked on external dependencies"
188+
),
189+
Label(name="demo", color="5319E7", description="Should be accompanied by a demo"),
190+
Label(name="discuss", color="316dc1", description="To be discussed within team"),
191+
Label(name="good-first-issue", color="7057ff", description="Good for newcomers"),
192+
Label(
193+
name="release",
194+
color="ededed",
195+
description="Denotes a PR/issue involved in a release",
196+
),
197+
Label(
198+
name="resource-reduction",
199+
color="81AA58",
200+
description="Can reduce required resources",
201+
),
202+
Label(
203+
name="workaround-exists",
204+
color="000000",
205+
description="There is a workaround that can be used in the meantime",
206+
),
207+
]
208+
EXPANDED_LABELS = {label.name: label for label in itertools.chain.from_iterable(LABELS)}
209+
210+
RENAME = [
211+
("GSOC", "events/GSOC"),
212+
("Hacktoberfest", "events/Hacktoberfest"),
213+
("Outreachy", "events/Outreachy"),
214+
("security", "kind/security"),
215+
("source-git", "area/source-git"),
216+
("API", "area/api"),
217+
("dashboard", "area/dashboard"),
218+
("deployment", "area/deployment"),
219+
("recurring", "kind/recurring"),
220+
]
221+
222+
223+
def get_labels(repo: Repository) -> dict[str, GithubLabel]:
224+
return {label.name: label for label in repo.get_labels()}
225+
226+
227+
def handle_repo(repo: Repository):
228+
if repo.archived:
229+
click.secho(f" [INFO] Skipping archived {repo.full_name}", fg="yellow")
230+
return
231+
232+
click.secho(f" [INFO] Syncing {repo.full_name}")
233+
234+
current_labels = get_labels(repo)
235+
236+
for label in EXPANDED_LABELS.values():
237+
if existing_label := current_labels.get(label.name):
238+
if (
239+
existing_label.color == label.color
240+
and existing_label.description == label.description
241+
):
242+
continue
243+
click.secho(f" [INFO] Updating label {label.name}", fg="yellow")
244+
existing_label.edit(
245+
name=label.name, color=label.color, description=label.description
246+
)
247+
else:
248+
click.secho(f" [INFO] Creating label {label.name}", fg="green")
249+
repo.create_label(
250+
name=label.name,
251+
description=label.description,
252+
color=label.color,
253+
)
254+
255+
current_labels = get_labels(repo)
256+
for old_name, new_name in RENAME:
257+
old_label = current_labels.get(old_name)
258+
if old_label is None:
259+
# if there's no label with the old name, skip
260+
continue
261+
262+
click.secho(f" [INFO] Renaming {old_name} to {new_name}", fg="yellow")
263+
264+
# check if there is a need to get rid of the new name
265+
if new_name in current_labels:
266+
click.secho(f" [INFO] Deleting the new label {new_name}", fg="red")
267+
current_labels[new_name].delete()
268+
269+
# update the old one
270+
label = EXPANDED_LABELS[new_name]
271+
old_label.edit(name=new_name, color=label.color, description=label.description)
272+
273+
274+
def main():
275+
click.secho("[INFO] Authenticating against GitHub", fg="blue")
276+
auth = Auth.Token(os.getenv("GITHUB_TOKEN"))
277+
g = Github(auth=auth)
278+
279+
click.secho("[INFO] Fetching Packit org", fg="blue")
280+
org = g.get_organization("packit")
281+
282+
click.secho("[INFO] Syncing repos", fg="blue")
283+
for repo in org.get_repos():
284+
handle_repo(repo)
285+
286+
287+
if __name__ == "__main__":
288+
main()

0 commit comments

Comments
 (0)