From 39df888412c02e1a7873c4ab4d2c007e7bd03a02 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Tue, 22 Sep 2026 15:37:34 -0700 Subject: [PATCH] CI: open an issue when upstream publishes a release not yet imported Reads metadata only: upstream's releases list from GitHub's API and the head of the upstream branch from Gitea's. Nothing of upstream's is fetched, so its history can't land here. Daily at 06:17 UTC. --- .gitea/workflows/upstream-watch.yml | 88 +++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 .gitea/workflows/upstream-watch.yml diff --git a/.gitea/workflows/upstream-watch.yml b/.gitea/workflows/upstream-watch.yml new file mode 100644 index 0000000..1dc8e47 --- /dev/null +++ b/.gitea/workflows/upstream-watch.yml @@ -0,0 +1,88 @@ +# Watch upstream for releases the fork hasn't imported yet, and open an issue +# for each one so it waits in the tracker until someone strips it in. +# +# Reads metadata only -- the releases list from GitHub's API and the head of +# this repo's `upstream` branch from Gitea's. Nothing of upstream's is fetched, +# so none of its history (which carries the Enterprise code) can land here. +# Importing is still by hand: tools/fork/strip.py onto `upstream`, then merge, +# as docs/spec/SPEC.md §2.2 and §2.2a describe. +# +# The imported base is the tag in the `upstream` branch's head commit subject +# ("Import upstream v0.16.22, stripped"). Drafts and pre-releases are ignored. +# An issue is opened once per release: an existing one with the same title, +# open or closed, stops a second. +# +# Daily 06:17 UTC; run it by hand with workflow_dispatch. +name: upstream-watch + +on: + schedule: + - cron: '17 6 * * *' + workflow_dispatch: + +concurrency: + group: upstream-watch + cancel-in-progress: false + +jobs: + upstream-watch: + runs-on: light + container: + image: python:3.13-slim@sha256:8d9d0b8bcf6506481eae4907c18f5e3e7902e629f5f6d684f9e7c32e85e3ddf0 # 3.13-slim + env: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + steps: + - shell: bash + run: | + python3 - <<'PY' + import json, os, re, sys, urllib.request + + api = f"{os.environ['CI_SERVER_INTERNAL']}/api/v1/repos/{os.environ['REPO']}" + def call(method, url, body=None, token=os.environ["TOKEN"]): + headers = {"Content-Type": "application/json", "User-Agent": "inbuxa-upstream-watch"} + if token: + headers["Authorization"] = f"token {token}" + req = urllib.request.Request(url, method=method, headers=headers, + data=json.dumps(body).encode() if body is not None else None) + with urllib.request.urlopen(req, timeout=30) as r: + return json.load(r) + SEMVER = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$") + def key(tag): + return tuple(int(x) for x in SEMVER.match(tag).groups()) + + subject = call("GET", f"{api}/branches/upstream")["commit"]["message"].splitlines()[0] + m = re.search(r"\bupstream (v\d+\.\d+\.\d+)\b", subject) + if not m: + print(f"Can't read the imported base from the upstream branch: {subject!r}", file=sys.stderr); sys.exit(1) + base = m.group(1) + + # Unauthenticated: a public repo, once a day, well inside the limit. + rels = call("GET", "https://api.github.com/repos/stalwartlabs/stalwart/releases?per_page=30", token=None) + newer = sorted((r for r in rels + if not r["draft"] and not r["prerelease"] and SEMVER.match(r["tag_name"]) + and key(r["tag_name"]) > key(base)), + key=lambda r: key(r["tag_name"])) + if not newer: + print(f"Up to date: {base} is the newest upstream release."); sys.exit(0) + + # Titles and bodies stay free of the upstream project's name, as the + # rest of the fork's user-visible text does. + existing = {i["title"] for i in call("GET", f"{api}/issues?state=all&type=issues&q=Import+upstream&limit=50")} + for r in newer: + tag = r["tag_name"] + title = f"Import upstream {tag}" + if title in existing: + print(f"{tag}: issue already exists."); continue + body = (f"Upstream published {tag} on {r['published_at'][:10]}. " + f"The fork's imported base is {base}.\n\n" + "Import it as tools/fork/README.md describes:\n\n" + "```bash\n" + "git -C \"$UPSTREAM_CLONE\" fetch --tags\n" + f"tools/fork/strip.py --upstream \"$UPSTREAM_CLONE\" --ref {tag} --out /tmp/strip-{tag}\n" + "```\n\n" + "Commit the stripped tree to `upstream` with the strip report in the message, " + "add any new third-party notices to `THIRD-PARTY.md`, then merge `upstream` into `main`.") + issue = call("POST", f"{api}/issues", {"title": title, "body": body}) + print(f"{tag}: opened #{issue['number']}.") + PY -- 2.54.0