From 0166a6d02af5d3ebb4a979d42e987defaee88b6a Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 24 May 2026 03:28:47 -0500 Subject: [PATCH] feat(ci): add upstream bug sync workflow Adds a scheduled workflow that runs daily at 08:00 UTC to automatically detect new bug fixes merged into upstream Gitea's release/v1.26 branch and create corresponding issues in the MokoGitea tracker. The workflow: - Queries GitHub Search API for recently merged fix/security PRs - Cross-references against existing MokoGitea issues to avoid duplicates - Creates labeled issues (type: bug, upstream, priority, security) - Supports manual dispatch with configurable lookback period Requires secrets: GH_TOKEN (GitHub), GITEA_TOKEN (MokoGitea) Co-Authored-By: Claude Opus 4.6 (1M context) --- .mokogitea/workflows/upstream-bug-sync.yml | 167 +++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 .mokogitea/workflows/upstream-bug-sync.yml diff --git a/.mokogitea/workflows/upstream-bug-sync.yml b/.mokogitea/workflows/upstream-bug-sync.yml new file mode 100644 index 0000000000..8fbf590806 --- /dev/null +++ b/.mokogitea/workflows/upstream-bug-sync.yml @@ -0,0 +1,167 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# BRIEF: Sync upstream Gitea bug fixes into MokoGitea issue tracker + +name: Upstream Bug Sync + +on: + schedule: + - cron: '0 8 * * *' # daily at 08:00 UTC + workflow_dispatch: + inputs: + days_back: + description: 'How many days back to scan (default: 7)' + required: false + default: '7' + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Sync upstream bugs + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + MOKOGITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + MOKOGITEA_URL: https://git.mokoconsulting.tech + MOKOGITEA_REPO: MokoConsulting/MokoGitea + UPSTREAM_BRANCH: release/v1.26 + DAYS_BACK: ${{ github.event.inputs.days_back || '7' }} + run: | + python3 << 'PYEOF' + import json, os, re, sys, urllib.parse, urllib.request + from datetime import datetime, timedelta, timezone + + GH_TOKEN = os.environ["GH_TOKEN"] + MOKO_TOKEN = os.environ["MOKOGITEA_TOKEN"] + MOKO_URL = os.environ["MOKOGITEA_URL"] + MOKO_REPO = os.environ["MOKOGITEA_REPO"] + BRANCH = os.environ["UPSTREAM_BRANCH"] + DAYS = int(os.environ.get("DAYS_BACK", "7")) + + # Label IDs in MokoGitea + LABELS = { + "type_bug": 5757, "upstream": 5758, "security": 5032, + "critical": 5018, "high": 5019, "medium": 5020, "low": 5021, + } + + def gh_get(url): + req = urllib.request.Request(url, headers={ + "Authorization": f"token {GH_TOKEN}", + "Accept": "application/vnd.github.v3+json", + }) + with urllib.request.urlopen(req) as r: + return json.loads(r.read()) + + def moko_get(path): + req = urllib.request.Request(f"{MOKO_URL}/api/v1/{path}", headers={ + "Authorization": f"token {MOKO_TOKEN}", + }) + with urllib.request.urlopen(req) as r: + return json.loads(r.read()) + + def moko_post(path, data): + payload = json.dumps(data).encode() + req = urllib.request.Request(f"{MOKO_URL}/api/v1/{path}", + data=payload, method="POST", headers={ + "Authorization": f"token {MOKO_TOKEN}", + "Content-Type": "application/json", + }) + with urllib.request.urlopen(req) as r: + return json.loads(r.read()) + + # ── Step 1: Find recently merged upstream PRs ── + since = (datetime.now(timezone.utc) - timedelta(days=DAYS)).strftime("%Y-%m-%dT%H:%M:%SZ") + query = f"repo:go-gitea/gitea is:pr is:merged base:{BRANCH} merged:>={since}" + encoded = urllib.parse.quote(query) + print(f"Scanning: {query}") + + result = gh_get(f"https://api.github.com/search/issues?q={encoded}&per_page=100&sort=updated&order=desc") + total = result["total_count"] + print(f"Found {total} merged PRs in the last {DAYS} days") + + if total == 0: + print("Nothing to sync.") + sys.exit(0) + + # ── Step 2: Filter for bug/security fixes ── + bugs = [] + for pr in result["items"]: + title = pr["title"] + label_names = [l["name"].lower() for l in pr.get("labels", [])] + is_fix = title.lower().startswith("fix") + is_security = any("security" in l for l in label_names) or "[security]" in title.lower() + is_bug = any("bug" in l for l in label_names) + + if not (is_fix or is_security or is_bug): + continue + + refs = re.findall(r"#(\d+)", title) + severity = "critical" if is_security and "[security]" in title.lower() else \ + "high" if is_security else "medium" + + bugs.append({ + "number": pr["number"], "title": title, "url": pr["html_url"], + "severity": severity, "is_security": is_security, "refs": refs, + "merged": pr.get("pull_request", {}).get("merged_at", "")[:10], + }) + + print(f"Filtered to {len(bugs)} bug/security fixes") + if not bugs: + sys.exit(0) + + # ── Step 3: Collect already-tracked PR numbers ── + tracked = set() + for state in ["open", "closed"]: + try: + issues = moko_get(f"repos/{MOKO_REPO}/issues?state={state}&type=issues&limit=50&labels=upstream") + for iss in issues: + text = (iss.get("body") or "") + " " + (iss.get("title") or "") + tracked.update(re.findall(r"(?:#|/pull/)(\d{4,})", text)) + except Exception: + pass + + print(f"Already tracked: {len(tracked)} upstream PRs") + + # ── Step 4: Create issues for new bugs ── + created = skipped = errors = 0 + for bug in bugs: + if any(r in tracked for r in bug["refs"]): + print(f" SKIP #{bug['number']}: {bug['title'][:55]} (tracked)") + skipped += 1 + continue + + labels = [LABELS["type_bug"], LABELS["upstream"], LABELS[bug["severity"]]] + if bug["is_security"]: + labels.append(LABELS["security"]) + + body = ( + f"## Summary\n\n" + f"Upstream bug fix merged into `{BRANCH}`.\n\n" + f"## Upstream Reference\n\n" + f"- PR: {bug['url']}\n" + f"- Merged: {bug['merged']}\n" + f"- Branch: {BRANCH}\n\n" + f"## Severity: {bug['severity'].title()}" + f"{' (security)' if bug['is_security'] else ''}\n\n" + f"## Action\n\n" + f"Cherry-pick from upstream `{BRANCH}` branch.\n\n" + f"---\n" + f"*Auto-created by upstream-bug-sync workflow*\n" + f"*Authored-by: Claude Opus 4.6 (1M context) *" + ) + + try: + iss = moko_post(f"repos/{MOKO_REPO}/issues", { + "title": bug["title"], "body": body, "labels": labels, + }) + print(f" CREATED #{iss['number']}: {bug['title'][:55]}") + created += 1 + except Exception as e: + print(f" ERROR #{bug['number']}: {e}") + errors += 1 + + print(f"\n=== Done: {created} created, {skipped} skipped, {errors} errors ===") + PYEOF -- 2.52.0