|
| 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