name: Sync Labels on: workflow_dispatch: push: paths: - ".github/labels.yml" permissions: contents: read issues: write jobs: sync: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install dependencies run: python -m pip install --disable-pip-version-check pyyaml - name: Sync labels env: GITHUB_TOKEN: ${{ github.token }} GITHUB_REPOSITORY: ${{ github.repository }} run: | python - <<'PY' import json import os import urllib.error import urllib.parse import urllib.request import yaml token = os.environ["GITHUB_TOKEN"] owner_repo = os.environ["GITHUB_REPOSITORY"] base_url = f"https://api.github.com/repos/{owner_repo}" with open(".github/labels.yml", "r", encoding="utf-8") as fh: config = yaml.safe_load(fh) or {} desired_labels = config.get("labels", []) desired_by_name = {label["name"]: label for label in desired_labels} headers = { "Accept": "application/vnd.github+json", "Authorization": f"Bearer {token}", "X-GitHub-Api-Version": "2022-11-28", "User-Agent": "mirofish-label-sync", } def request(method, url, payload=None): data = None if payload is not None: data = json.dumps(payload).encode("utf-8") req = urllib.request.Request(url, data=data, headers=headers, method=method) with urllib.request.urlopen(req) as response: raw = response.read() return json.loads(raw.decode("utf-8")) if raw else None existing = [] page = 1 while True: url = f"{base_url}/labels?per_page=100&page={page}" batch = request("GET", url) if not batch: break existing.extend(batch) if len(batch) < 100: break page += 1 existing_by_name = {label["name"]: label for label in existing} for name, desired in desired_by_name.items(): payload = { "new_name": desired["name"], "color": desired["color"], "description": desired["description"], } if name in existing_by_name: url = f"{base_url}/labels/{urllib.parse.quote(name, safe='')}" request("PATCH", url, payload) print(f"updated {name}") else: url = f"{base_url}/labels" request("POST", url, { "name": desired["name"], "color": desired["color"], "description": desired["description"], }) print(f"created {name}") for name in sorted(existing_by_name): if name not in desired_by_name: url = f"{base_url}/labels/{urllib.parse.quote(name, safe='')}" try: request("DELETE", url) print(f"deleted {name}") except urllib.error.HTTPError as exc: raise RuntimeError(f"Failed to delete label '{name}': {exc.read().decode('utf-8')}") from exc PY