From 961a0cf09bde9d4a2af1e522982e774245aafb94 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Sat, 3 Oct 2026 13:21:25 +0800 Subject: [PATCH] Sync GitHub metadata by parsing push mirrors dynamically --- .gitea/workflows/sync-github-metadata.yml | 209 ++++++++++++---------- 1 file changed, 119 insertions(+), 90 deletions(-) diff --git a/.gitea/workflows/sync-github-metadata.yml b/.gitea/workflows/sync-github-metadata.yml index 7651664..f5e36b4 100644 --- a/.gitea/workflows/sync-github-metadata.yml +++ b/.gitea/workflows/sync-github-metadata.yml @@ -1,45 +1,45 @@ -name: Sync GitHub mirror metadata - -on: - push: - workflow_dispatch: - schedule: - - cron: "17 3 * * *" - -jobs: - sync-metadata: - runs-on: ubuntu-latest - steps: - - name: Sync description and topics - env: - GITEA_API_URL: ${{ gitea.api_url }} - GITEA_TOKEN: ${{ gitea.token }} - SOURCE_REPOSITORY: ${{ gitea.repository }} - GITHUB_CREDENTIALS: ${{ secrets.CREDENTIALS_FOR_GITHUB }} - GITHUB_REPO: Tirsvad-Udemy-100_days_of_code - run: | - python3 - <<'PY' - import json - import os - import urllib.error - import urllib.request - - def request_json(url, method="GET", headers=None, body=None): - request = urllib.request.Request( - url, - data=json.dumps(body).encode("utf-8") if body is not None else None, - headers=headers or {}, - method=method, - ) - try: - with urllib.request.urlopen(request, timeout=30) as response: - content = response.read() - return json.loads(content) if content else None - except urllib.error.HTTPError as error: - raise RuntimeError( - f"API request failed with HTTP {error.code} ({error.reason})" - ) from None - +name: Sync GitHub mirror metadata + +on: + push: + workflow_dispatch: + schedule: + - cron: "17 3 * * *" + +jobs: + sync-metadata: + runs-on: ubuntu-latest + steps: + - name: Sync description and topics + env: + GITEA_API_URL: ${{ gitea.api_url }} + GITEA_TOKEN: ${{ gitea.token }} + SOURCE_REPOSITORY: ${{ gitea.repository }} + GITHUB_CREDENTIALS: ${{ secrets.CREDENTIALS_FOR_GITHUB }} + run: | + python3 - <<'PY' + import json + import os + import urllib.error + import urllib.parse + import urllib.request + + def request_json(url, method="GET", headers=None, body=None): + request = urllib.request.Request( + url, + data=json.dumps(body).encode("utf-8") if body is not None else None, + headers=headers or {}, + method=method, + ) + try: + with urllib.request.urlopen(request, timeout=30) as response: + content = response.read() + return json.loads(content) if content else None + except urllib.error.HTTPError as error: + raise RuntimeError( + f"API request failed with HTTP {error.code} ({error.reason})" + ) from None + raw_credentials = os.environ.get("GITHUB_CREDENTIALS", "").strip() if not raw_credentials: raise RuntimeError("CREDENTIALS_FOR_GITHUB is missing or empty.") @@ -57,53 +57,82 @@ jobs: ) github_token = credentials.get("GITHUB_PAT") - github_user = credentials.get("GITHUB_USER") - if not github_token or not github_user: + if not isinstance(github_token, str) or not github_token.strip(): raise RuntimeError( - "CREDENTIALS_FOR_GITHUB must contain GITHUB_PAT and GITHUB_USER." + "CREDENTIALS_FOR_GITHUB must contain a non-empty GITHUB_PAT." ) - source_owner, separator, source_repo = os.environ[ - "SOURCE_REPOSITORY" - ].partition("/") - if not separator or not source_owner or not source_repo: - raise RuntimeError("Could not determine the Gitea source repository.") - - gitea_api_url = os.environ["GITEA_API_URL"].rstrip("/") - source_url = ( - f"{gitea_api_url}/repos/{source_owner}/{source_repo}" - ) - source = request_json( - source_url, - headers={ - "Authorization": f"token {os.environ['GITEA_TOKEN']}", - "Accept": "application/json", - }, - ) - - github_repo = os.environ["GITHUB_REPO"] - github_api_url = ( - f"https://api.github.com/repos/{github_user}/{github_repo}" - ) - github_headers = { - "Authorization": f"Bearer {github_token}", - "Accept": "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - "Content-Type": "application/json", - } - - request_json( - github_api_url, - method="PATCH", - headers=github_headers, - body={"description": source.get("description") or ""}, - ) - - request_json( - f"{github_api_url}/topics", - method="PUT", - headers=github_headers, - body={"names": source.get("topics") or []}, - ) - - print(f"Synced description and topics to {github_user}/{github_repo}.") - PY + + source_owner, separator, source_repo = os.environ[ + "SOURCE_REPOSITORY" + ].partition("/") + if not separator or not source_owner or not source_repo: + raise RuntimeError("Could not determine the Gitea source repository.") + + gitea_api_url = os.environ["GITEA_API_URL"].rstrip("/") + source_url = f"{gitea_api_url}/repos/{source_owner}/{source_repo}" + gitea_headers = { + "Authorization": f"token {os.environ['GITEA_TOKEN']}", + "Accept": "application/json", + } + source = request_json(source_url, headers=gitea_headers) + mirrors = request_json( + f"{source_url}/push_mirrors", + headers=gitea_headers, + ) + if not isinstance(mirrors, list): + raise RuntimeError("Gitea returned an invalid push mirror list.") + + github_targets = [] + for mirror in mirrors: + remote_address = mirror.get("remote_address", "") + if remote_address.startswith("git@github.com:"): + mirror_path = remote_address.split(":", 1)[1] + else: + parsed_remote = urllib.parse.urlsplit(remote_address) + if parsed_remote.hostname != "github.com": + continue + mirror_path = parsed_remote.path.lstrip("/") + + mirror_path = mirror_path.removesuffix(".git").strip("/") + path_parts = mirror_path.split("/") + if len(path_parts) != 2 or not all(path_parts): + raise RuntimeError( + "Could not determine the GitHub owner and repository " + "from a configured push mirror." + ) + github_targets.append(tuple(path_parts)) + + if len(github_targets) != 1: + raise RuntimeError( + "Expected exactly one GitHub push mirror for this repository; " + f"found {len(github_targets)}." + ) + + github_owner, github_repo = github_targets[0] + github_api_url = ( + "https://api.github.com/repos/" + f"{urllib.parse.quote(github_owner, safe='')}/" + f"{urllib.parse.quote(github_repo, safe='')}" + ) + github_headers = { + "Authorization": f"Bearer {github_token.strip()}", + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "Content-Type": "application/json", + } + + request_json( + github_api_url, + method="PATCH", + headers=github_headers, + body={"description": source.get("description") or ""}, + ) + request_json( + f"{github_api_url}/topics", + method="PUT", + headers=github_headers, + body={"names": source.get("topics") or []}, + ) + + print(f"Synced description and topics to {github_owner}/{github_repo}.") + PY \ No newline at end of file