diff --git a/.gitea/workflows/upstream-watch.yml b/.gitea/workflows/upstream-watch.yml deleted file mode 100644 index da6e215..0000000 --- a/.gitea/workflows/upstream-watch.yml +++ /dev/null @@ -1,152 +0,0 @@ -# Watch the upstream web UI for new versions, and open an issue for each one -# so it can be read for features worth writing here. -# -# The console no longer merges upstream (README › Upstream): later versions -# include code under a proprietary license. So the issue lists what changed, -# file by file, grouped by each file's license header, for a reader to know -# which files may be opened at all. Enterprise-only files, and files holding -# Enterprise snippets, are named but must never be opened. -# -# Reads metadata only: the tag list and the compare between two tags from -# GitHub's API. Each changed file is fetched just to read its license header -# lines; nothing of upstream's code is kept, printed, or put in the issue, only -# paths, change counts and licenses. Titles and bodies stay free of the -# upstream project's name, as the rest of the fork's user-visible text does. -# -# Each version since the fork point (v1.0.11) gets one issue, compared with -# the version before it. An existing issue with the same title, open or -# closed, stops a second. Tags are watched rather than releases: upstream has -# tagged versions before publishing them as releases. -# -# Daily 06:47 UTC; run it by hand with workflow_dispatch. DRY_RUN=1 prints -# the issues instead of opening them. -name: upstream-watch - -on: - schedule: - - cron: '47 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.parse, urllib.request - - FORK_BASE = "v1.0.11" - UPSTREAM = "stalwartlabs/webui" - DRY_RUN = os.environ.get("DRY_RUN") == "1" - - api = f"{os.environ.get('CI_SERVER_INTERNAL', '')}/api/v1/repos/{os.environ.get('REPO', '')}" - def call(method, url, body=None, token=None, raw=False): - headers = {"User-Agent": "inbuxa-admin-upstream-watch"} - if body is not None: - headers["Content-Type"] = "application/json" - 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 r.read() if raw else 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()) - def plural(n, word): - return f"{n} {word}" if n == 1 else f"{n} {word}s" - - # Unauthenticated: a public repo, once a day, well inside the limit. - tags = call("GET", f"https://api.github.com/repos/{UPSTREAM}/tags?per_page=100") - newer = sorted((t["name"] for t in tags - if SEMVER.match(t["name"]) and key(t["name"]) > key(FORK_BASE)), key=key) - if not newer: - print(f"Nothing new: {FORK_BASE}, the fork point, is the newest version."); sys.exit(0) - - if DRY_RUN: - existing = set() - else: - existing = {i["title"] for i in call("GET", f"{api}/issues?state=all&type=issues" - "&q=Review+upstream+web+UI&limit=50", token=os.environ["TOKEN"])} - - SPDX = re.compile(r"SPDX-License-Identifier:\s*(.+?)\s*(?:\*/|-->)?\s*$", re.M) - def license_of(tag, path): - """One of: enterprise, snippets, dual, other:, none. Reads only the license lines.""" - url = f"https://raw.githubusercontent.com/{UPSTREAM}/{urllib.parse.quote(tag)}/{urllib.parse.quote(path)}" - text = call("GET", url, raw=True).decode("utf-8", "replace") - ids = SPDX.findall(text) - if not ids: - return "none" - head = ids[0] - if head == "LicenseRef-SEL": - return "enterprise" - if "SPDX-SnippetBegin" in text or "LicenseRef-SEL" in ids[1:]: - return "snippets" - if head == "AGPL-3.0-only OR LicenseRef-SEL": - return "dual" - return f"other:{head}" - - GROUPS = [ - ("enterprise", "Enterprise only: never open"), - ("snippets", "Dual, holding Enterprise snippets: never open"), - ("removed", "Removed"), - ("dual", "Dual (AGPL-3.0-only OR LicenseRef-SEL): may be read"), - ("none", "No license header: check before reading"), - ] - - prev = FORK_BASE - for tag in newer: - title = f"Review upstream web UI {tag}" - if title in existing: - print(f"{tag}: issue already exists."); prev = tag; continue - cmp = call("GET", f"https://api.github.com/repos/{UPSTREAM}/compare/{prev}...{tag}") - files = cmp.get("files", []) - groups = {} - for f in files: - if f["status"] == "removed": - group = "removed" - else: - group = license_of(tag, f["filename"]) - line = f"- `{f['filename']}` ({f['status']}, +{f['additions']} −{f['deletions']})" - if f["status"] == "renamed": - line += f", from `{f['previous_filename']}`" - groups.setdefault(group, []).append(line) - - date = cmp["commits"][-1]["commit"]["committer"]["date"][:10] if cmp.get("commits") else "?" - parts = [ - f"Upstream tagged the web UI {tag} ({date}): {plural(cmp.get('total_commits', 0), 'commit')} and " - f"{plural(len(files), 'changed file')} since {prev}. The console was forked at {FORK_BASE} and " - "no longer merges upstream (README › Upstream).", - "Read what changed and note here anything worth writing ourselves, in our own code. " - "Open only files marked *may be read*. Never open a file marked *never open*, " - "and don't hand one to anyone who writes console code.", - ] - if len(files) >= 300: - parts.append("**The compare stops at 300 files, so this list is incomplete.**") - labels = dict(GROUPS) - order = [g for g, _ in GROUPS] + sorted(g for g in groups if g not in labels) - for group in order: - if group not in groups: - continue - label = labels.get(group, f"Other license ({group[6:]}): check before reading") - parts.append(f"
{label}: {len(groups[group])}\n\n" - + "\n".join(sorted(groups[group])) + "\n\n
") - body = "\n\n".join(parts) - - if DRY_RUN: - print(f"=== {title}\n{body}\n") - else: - issue = call("POST", f"{api}/issues", {"title": title, "body": body}, token=os.environ["TOKEN"]) - print(f"{tag}: opened #{issue['number']}.") - prev = tag - PY diff --git a/README.md b/README.md index a96c287..5994598 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,9 @@ npm run build INBUXA Admin was forked from upstream at v1.0.11 and no longer takes upstream's changes. Later upstream releases include code under a proprietary license, so -nothing after v1.0.11 is merged or copied in. Upstream releases are still -followed, and a feature worth having is written here, in this fork's own code. +nothing after v1.0.11 is merged or copied in. The console has diverged too far +to take anything from upstream, so its releases aren't followed either: new +features are this fork's own. ## Versions diff --git a/tools/notice-check.mjs b/tools/notice-check.mjs index 54b4d4d..a6ed401 100644 --- a/tools/notice-check.mjs +++ b/tools/notice-check.mjs @@ -20,7 +20,7 @@ * built on, so the list is what actually differs rather than a guess. A file * counts as upstream's when its header names Stalwart Labs as a copyright * holder; files the fork wrote carry their own copyright and need nothing. - * When a newer upstream release is merged, move BASE to it. + * Upstream is no longer merged, so BASE stays at v1.0.11. */ import { execFileSync } from 'node:child_process'; import { readFileSync, writeFileSync } from 'node:fs';