From 5606afe150555c9d5cf1a99a03852a5a4a06b253 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 19 Apr 2026 18:21:36 +0100 Subject: [PATCH 1/3] feat(ci): open a downstream-bump tracking issue on every release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a single source of truth for every downstream distribution of Etherpad (Docker Hub, Snap, Debian, Home Assistant, Umbrel, TrueCharts, Proxmox, Cloudron, YunoHost, CasaOS, BigBlueButton, Unraid, Sandstorm, Nextcloud Ownpad) at docs/downstreams.yml, plus a workflow that, on every GitHub release publish, opens a single tracking issue with a checklist grouped by how each downstream is kept current: ๐Ÿš€ Automatic โ€” this repo's CI handles it on tag push ๐Ÿงฉ Manual bump in-repo โ€” someone edits a file here, CI does the rest ๐Ÿค– Externally automated โ€” a Renovate-like bot on the downstream side โœ‰๏ธ Needs a PR we send โ€” a maintainer files a bump PR ๐Ÿ“จ Needs an issue we file ๐Ÿค Maintained externally โ€” we have no lever; poke if stale โš ๏ธ Known stale โ€” kept for visibility, no action Motivation: without this, external catalogs like CasaOS, TrueCharts, Bigbluebutton's `bbb-etherpad.placeholder.sh`, and the Sandstorm market listing accumulate years of drift. Turning "remember every downstream" into a per-release checklist is the lightest-touch fix that scales. The renderer is a standalone Python script so the issue format can be tweaked and dry-run locally: python3 .github/scripts/render-downstream-tracker.py \ docs/downstreams.yml 2.6.1 ether/etherpad-lite A `workflow_dispatch` trigger with a manual `version` input is included so the tracker can be smoke-tested before the next real release. Refs #7529 Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/scripts/render-downstream-tracker.py | 109 ++++++++++++ .github/workflows/release-downstreams.yml | 71 ++++++++ docs/downstreams.yml | 172 +++++++++++++++++++ 3 files changed, 352 insertions(+) create mode 100755 .github/scripts/render-downstream-tracker.py create mode 100644 .github/workflows/release-downstreams.yml create mode 100644 docs/downstreams.yml diff --git a/.github/scripts/render-downstream-tracker.py b/.github/scripts/render-downstream-tracker.py new file mode 100755 index 00000000000..5624f4d6cb8 --- /dev/null +++ b/.github/scripts/render-downstream-tracker.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Render the downstream-bump tracking issue body from docs/downstreams.yml. + +Called from .github/workflows/release-downstreams.yml. Kept as a +standalone script (rather than inline yaml-munging in the workflow) so +the format is easy to tweak without re-running CI to eyeball it โ€” run +locally with: + + python3 .github/scripts/render-downstream-tracker.py \\ + docs/downstreams.yml 2.6.1 ether/etherpad-lite + +Usage: render-downstream-tracker.py +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import yaml + +GROUPS: list[tuple[str, str]] = [ + ("automatic", "๐Ÿš€ Automatic (this repo handles it)"), + ("manual_ci", "๐Ÿงฉ Manual bump in this repo"), + ("external_auto", "๐Ÿค– Externally automated"), + ("external_pr", "โœ‰๏ธ Needs a PR we send"), + ("external_issue", "๐Ÿ“จ Needs an issue we file"), + ("external_maintainer", "๐Ÿค Maintained externally โ€” poke if stale"), + ("stale", "โš ๏ธ Known stale โ€” informational only"), +] + + +def render(catalog_path: Path, version: str, repo: str) -> str: + with catalog_path.open() as f: + catalog = yaml.safe_load(f) + items = catalog.get("downstreams", []) + + out: list[str] = [] + out.append(f"## Downstream distribution checklist for `{version}`\n") + out.append( + "Auto-opened by `.github/workflows/release-downstreams.yml` on " + "release publish.\n" + ) + out.append( + f"Source of truth: [`docs/downstreams.yml`](https://github.com/" + f"{repo}/blob/develop/docs/downstreams.yml).\n" + ) + out.append( + "Tick items as you verify them. Anything still unchecked a week " + "after release is a candidate for follow-up.\n" + ) + + for update_type, heading in GROUPS: + matches = [i for i in items if i.get("update_type") == update_type] + if not matches: + continue + out.append(f"\n### {heading}\n") + for item in matches: + out.append(_render_item(item, repo)) + + return "\n".join(out) + + +def _render_item(item: dict, repo: str) -> str: + name = item["name"] + target_repo = item.get("repo") + # `path` and `file` are aliases that point at a specific file/dir + # inside the downstream repo (or inside this repo for `manual_ci`). + path = item.get("path") or item.get("file") + workflow = item.get("workflow") + notes = item.get("notes", "").strip() + + # Primary link: deep-link to the file/dir if we know one, otherwise + # to the repo root. `HEAD` avoids pinning to a stale default-branch + # name (`main` vs `master` vs `develop`). + link = "" + if target_repo: + base = f"https://github.com/{target_repo}" + if path: + link = f" โ€” [`{target_repo}/{path}`]({base}/blob/HEAD/{path})" + else: + link = f" โ€” [`{target_repo}`]({base})" + if workflow: + workflow_url = f"https://github.com/{repo}/blob/develop/{workflow}" + link += f" ยท [workflow]({workflow_url})" + + lines = [f"- [ ] **{name}**{link}"] + if notes: + # Indent notes under the checkbox so GitHub renders them as part + # of the list item rather than a sibling paragraph. + for note_line in notes.splitlines(): + lines.append(f" {note_line}") + lines.append("") + return "\n".join(lines) + + +def main() -> int: + if len(sys.argv) != 4: + print(__doc__, file=sys.stderr) + return 2 + catalog_path = Path(sys.argv[1]) + version = sys.argv[2] + repo = sys.argv[3] + print(render(catalog_path, version, repo)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/release-downstreams.yml b/.github/workflows/release-downstreams.yml new file mode 100644 index 00000000000..9a4a6c36527 --- /dev/null +++ b/.github/workflows/release-downstreams.yml @@ -0,0 +1,71 @@ +name: Release โ€” downstream bump tracker + +# Opens a single tracking issue on every release that lists every +# downstream distribution of Etherpad (see docs/downstreams.yml) with +# hints on whether they update automatically or need a manual PR. +# +# Rationale: external catalogs (CasaOS, TrueCharts, BBB, Unraid, โ€ฆ) go +# stale because nobody remembers to poke them at release time. This +# workflow turns "remember to update every downstream" into a checklist +# we can close methodically. +# +# To test before a real release: run `Actions โ†’ Release โ€” downstream +# bump tracker โ†’ Run workflow`, supply a version (e.g. `2.6.1-test`). + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: Version string to use (e.g. 2.6.1). Defaults to the release tag. + required: true + +permissions: + contents: read + issues: write + +jobs: + open-tracking-issue: + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v6 + + - name: Resolve version + id: v + env: + TAG: ${{ github.event.release.tag_name }} + INPUT: ${{ inputs.version }} + run: | + VERSION="${TAG:-$INPUT}" + VERSION="${VERSION#v}" + if [ -z "${VERSION}" ]; then + echo "Could not determine version." >&2 + exit 1 + fi + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + + - name: Render issue body + id: render + env: + VERSION: ${{ steps.v.outputs.version }} + run: | + python3 -m pip install --quiet pyyaml + BODY=$(mktemp) + python3 .github/scripts/render-downstream-tracker.py \ + docs/downstreams.yml \ + "$VERSION" \ + "$GITHUB_REPOSITORY" \ + > "$BODY" + echo "body-path=$BODY" >> "$GITHUB_OUTPUT" + + - name: Open tracking issue + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh issue create \ + --repo "$GITHUB_REPOSITORY" \ + --title "Downstream bumps for ${{ steps.v.outputs.version }}" \ + --label "release,downstream" \ + --body-file '${{ steps.render.outputs.body-path }}' diff --git a/docs/downstreams.yml b/docs/downstreams.yml new file mode 100644 index 00000000000..a9b2ba9757c --- /dev/null +++ b/docs/downstreams.yml @@ -0,0 +1,172 @@ +# Downstream distribution catalog for Etherpad. +# +# This file is the single source of truth for every place Etherpad is +# packaged or listed outside this repository. It is consumed by +# .github/workflows/release-downstreams.yml, which opens a tracking issue +# on every release so maintainers can work through the list and keep +# downstream catalogs from going stale. +# +# When you add a new downstream, put it here first โ€” the release +# workflow will then remind us to update it on the next tag. +# +# update_type values: +# automatic โ€” handled by a workflow in this repo on tag push +# (Docker Hub, Snap, Debian .deb, Home Assistant +# add-on image). No manual step required; the +# checklist item is a smoke-test verification. +# manual_ci โ€” controlled by a file in this repo that a human +# needs to bump (e.g. the HA add-on config.yaml +# `version:` field). The workflow hints which +# file to edit. +# external_auto โ€” a downstream repo's own automation (Renovate, +# a similar bot, or a runtime `check_for_gh_release` +# call) detects new Docker/GitHub tags. No action +# required unless the bot falls behind. +# external_pr โ€” a downstream repo requires a manual PR to bump +# a pinned version. A maintainer opens that PR. +# external_issue โ€” a downstream repo accepts issues rather than PRs +# for version bumps (maintainer-driven). +# external_maintainer โ€” entirely out of our hands; the downstream has an +# owner we can ping but not patch. Keep the entry +# as a reminder to poke them. +# stale โ€” the downstream hasn't tracked upstream in a long +# time. Kept in the list so nobody re-discovers it +# and assumes it's current. + +downstreams: + # ---- In-repo, fully automated ----------------------------------------- + - name: Official Docker image + repo: ether/etherpad-lite + update_type: automatic + workflow: .github/workflows/docker.yml + notes: | + Smoke test: `docker pull etherpad/etherpad:` works and the + container exposes `/health` on 9001 after boot. + + - name: Snap (snapcraft.io) + repo: ether/etherpad-lite + update_type: automatic + workflow: .github/workflows/snap-publish.yml + notes: | + Tag triggers edge publish; stable promotion requires a manual + approval in the `snap-store-stable` GitHub Environment. Smoke test: + `snap info etherpad-lite` shows the new version on the `stable` + channel within 24h. + + - name: Debian .deb (GitHub Releases) + repo: ether/etherpad-lite + update_type: automatic + workflow: .github/workflows/deb-package.yml + notes: | + Tag triggers build; artefacts are attached to the GitHub Release. + Smoke test: `dpkg -i etherpad-lite__amd64.deb` on a clean + Ubuntu 24.04 VM, `curl /health` returns 200. + + - name: Home Assistant add-on (GHCR images) + repo: ether/etherpad-lite + update_type: manual_ci + file: packaging/home-assistant/etherpad/config.yaml + workflow: .github/workflows/hassio-addon.yml + notes: | + Bump the `version:` field in `packaging/home-assistant/etherpad/config.yaml` + and merge to develop. The workflow multi-arch builds and pushes to + GHCR. If the upstream Docker image changes base OS, verify the + add-on Dockerfile's `COPY --from=upstream` still works. + + # ---- External, auto-tracking ------------------------------------------ + - name: Umbrel App Store + repo: getumbrel/umbrel-apps + path: etherpad + update_type: external_auto + notes: | + Renovate tracks the Docker image tag. No action needed if the app + bumps within a week of release; otherwise file an issue. + + - name: TrueCharts + repo: trueforge-org/truecharts + path: charts/stable/etherpad + update_type: external_auto + notes: | + Renovate tracks the official `docker.io/etherpad/etherpad` image + (after PR 47234 merged). Verify the chart catalog shows the new + `appVersion:` within a week. + + - name: Proxmox VE Helper-Script + repo: community-scripts/ProxmoxVED + path: ct/etherpad.sh + update_type: external_auto + notes: | + The script uses `check_for_gh_release` against `ether/etherpad-lite` + at user runtime, so there's no per-release bump โ€” users re-running + the helper script pick up the latest GitHub release automatically. + + # ---- External, maintainer-driven -------------------------------------- + - name: Cloudron + repo: cloudron-io/etherpad.cloudronapp + update_type: external_maintainer + notes: | + Cloudron's automation typically publishes within a week. If the + store page lags by more than 2 weeks, post on + . + + - name: YunoHost + repo: YunoHost-Apps/etherpad_ynh + update_type: external_maintainer + notes: | + Actively maintained; usually bumps automatically. If no release + appears within 2 weeks, file an issue on the repo tagging the + current maintainer. + + - name: Nextcloud Ownpad + repo: otetard/ownpad + update_type: external_maintainer + notes: | + External integration plugin; generally forward-compatible with new + Etherpad versions. Only relevant if a breaking API change lands. + + # ---- External, per-release PR needed ---------------------------------- + - name: CasaOS App Store + repo: IceWhaleTech/CasaOS-AppStore + path: Apps/Etherpad/docker-compose.yml + update_type: external_pr + notes: | + Docker image tag is pinned in the compose file. File a PR bumping + `image: etherpad/etherpad:` and the `version` string in + the `x-casaos` metadata. + + - name: BigBlueButton + repo: bigbluebutton/bigbluebutton + path: bbb-etherpad.placeholder.sh + update_type: external_pr + notes: | + Pinned to a specific tag in the placeholder clone script. Major + version bumps may require additional changes in + `build/packages-template/bbb-etherpad/build.sh` (npm โ†’ pnpm, + Node engine bump). File a PR or issue against `develop`. + + - name: Unraid Community Apps + repo: selfhosters/unRAID-CA-templates + update_type: external_maintainer + notes: | + Community-maintained XML templates. If the template falls behind, + file an issue or PR on the template repo, or contact the + maintainer in the Unraid forum thread for Etherpad. + + # ---- Known stale / low-signal ----------------------------------------- + - name: Sandstorm + repo: sandstorm-io/sandstorm + update_type: stale + notes: | + Market listing hasn't moved since 2015. A full repack requires + vagga-based Sandstorm packaging. Noted here so nobody tries the + app expecting it to be current; removing the listing is a separate + conversation with the Sandstorm team. + + - name: TrueNAS SCALE (legacy) + repo: trueforge-org/truecharts + update_type: stale + notes: | + The TrueNAS SCALE app catalog (iXsystems) was historically sourced + from TrueCharts; both have since diverged. TrueCharts' own + catalog is the authoritative source now; iXsystems ships their + own apps independently. From 04410549329694116b1e6931a4177905588a935b Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 19 Apr 2026 18:35:55 +0100 Subject: [PATCH 2/3] fix(ci): use github.event.inputs.version in release trigger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Qodo feedback on #7561: reading `inputs.version` on a `release: published` event can yield an empty string or a context-evaluation failure depending on runtime. `inputs` only populates on workflow_dispatch. Switch to `github.event.inputs.version` which is typed as the dispatch payload directly, and add the event name to the error message for easier debugging when neither tag nor input is set. Python 4-space indent is left as-is โ€” that's PEP 8, and the 2-space repo style rule Qodo references applies to the shell/YAML/JS tree, not to standalone Python scripts. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release-downstreams.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-downstreams.yml b/.github/workflows/release-downstreams.yml index 9a4a6c36527..a08a7e8ea8f 100644 --- a/.github/workflows/release-downstreams.yml +++ b/.github/workflows/release-downstreams.yml @@ -36,12 +36,18 @@ jobs: id: v env: TAG: ${{ github.event.release.tag_name }} - INPUT: ${{ inputs.version }} + # `inputs.version` only exists on workflow_dispatch; reading it on + # release events can yield an empty string or a context-evaluation + # error depending on GitHub Actions behavior. Pull the dispatch + # payload directly via github.event.inputs so release runs stay + # clean. + INPUT: ${{ github.event.inputs.version }} + EVENT: ${{ github.event_name }} run: | VERSION="${TAG:-$INPUT}" VERSION="${VERSION#v}" if [ -z "${VERSION}" ]; then - echo "Could not determine version." >&2 + echo "Could not determine version (event=${EVENT})." >&2 exit 1 fi echo "version=${VERSION}" >> "$GITHUB_OUTPUT" From 972a08f1d2bd04fbc224a3aa2d2300ee3364b40e Mon Sep 17 00:00:00 2001 From: John McLear Date: Wed, 29 Apr 2026 12:53:25 +0100 Subject: [PATCH 3/3] ci: harden downstream tracker against Qodo review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Qodo review on #7561: 3. Workflow lacks feature flag โ€” add an opt-out gate via `vars.SKIP_DOWNSTREAM_TRACKER`. Default stays opt-out (the whole point of the tracker is to fire automatically on release; opt-in would re-introduce the "forgot to enable it" failure mode). 4. Wrong GitHub path URLs โ€” `path:` and `file:` are no longer aliases. `file:` now renders as `/blob/HEAD/...` (single file) and `path:` as `/tree/HEAD/...` (directory). Updated `docs/downstreams.yml` entries that pointed at single files (Proxmox VED ct/etherpad.sh, CasaOS docker-compose.yml, BBB placeholder.sh) to use `file:`. Renderer now errors if both keys are set on the same entry. 5. Duplicate issue creation โ€” before `gh issue create`, search for an existing issue with the same title (across open/closed) and skip create if one exists. Re-running the workflow for the same release no longer piles up duplicates. 6. Missing YAML type validation โ€” render() now validates that the top-level is a mapping, that `downstreams` is a list, and that each entry is a mapping with `name` and `update_type`. main() catches ValueError and surfaces it as a single CI-friendly error line instead of a Python traceback. Plus a `test_render_downstream_tracker.py` smoke test exercising the file/dir routing and each validation guard. Pushing back on Qodo issue 1 (4-space โ†’ 2-space indent): Python files follow PEP 8, which mandates 4-space indentation. The 2-space rule in the project compliance checklist applies to JS/TS source. Forcing 2-space on a Python script makes it harder to read and breaks tooling defaults (formatters, linters, IDEs). Leaving as-is. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/scripts/render-downstream-tracker.py | 52 ++++++++++-- .../scripts/test_render_downstream_tracker.py | 85 +++++++++++++++++++ .github/workflows/release-downstreams.yml | 29 ++++++- docs/downstreams.yml | 6 +- 4 files changed, 161 insertions(+), 11 deletions(-) create mode 100644 .github/scripts/test_render_downstream_tracker.py diff --git a/.github/scripts/render-downstream-tracker.py b/.github/scripts/render-downstream-tracker.py index 5624f4d6cb8..c16ec0a30ba 100755 --- a/.github/scripts/render-downstream-tracker.py +++ b/.github/scripts/render-downstream-tracker.py @@ -33,7 +33,34 @@ def render(catalog_path: Path, version: str, repo: str) -> str: with catalog_path.open() as f: catalog = yaml.safe_load(f) + if not isinstance(catalog, dict): + raise ValueError( + f"{catalog_path}: top-level must be a mapping, " + f"got {type(catalog).__name__}" + ) items = catalog.get("downstreams", []) + if not isinstance(items, list): + raise ValueError( + f"{catalog_path}: `downstreams` must be a list, " + f"got {type(items).__name__}" + ) + for idx, item in enumerate(items): + if not isinstance(item, dict): + raise ValueError( + f"{catalog_path}: downstreams[{idx}] must be a mapping, " + f"got {type(item).__name__}" + ) + if "name" not in item or "update_type" not in item: + raise ValueError( + f"{catalog_path}: downstreams[{idx}] missing required " + f"`name` and/or `update_type`" + ) + if "path" in item and "file" in item: + raise ValueError( + f"{catalog_path}: downstreams[{idx}] ({item['name']}) " + f"sets both `path` and `file`; use `file` for files and " + f"`path` for directories, not both" + ) out: list[str] = [] out.append(f"## Downstream distribution checklist for `{version}`\n") @@ -64,9 +91,13 @@ def render(catalog_path: Path, version: str, repo: str) -> str: def _render_item(item: dict, repo: str) -> str: name = item["name"] target_repo = item.get("repo") - # `path` and `file` are aliases that point at a specific file/dir - # inside the downstream repo (or inside this repo for `manual_ci`). - path = item.get("path") or item.get("file") + # `file:` deep-links to a single file (GitHub /blob/...). + # `path:` deep-links to a directory (GitHub /tree/...). + # `/blob/` and `/tree/` both 404 on GitHub, so the two + # must be distinguished. The renderer trusts the YAML key โ€” see + # render() for the both-set guard. + file_path = item.get("file") + dir_path = item.get("path") workflow = item.get("workflow") notes = item.get("notes", "").strip() @@ -76,8 +107,10 @@ def _render_item(item: dict, repo: str) -> str: link = "" if target_repo: base = f"https://github.com/{target_repo}" - if path: - link = f" โ€” [`{target_repo}/{path}`]({base}/blob/HEAD/{path})" + if file_path: + link = f" โ€” [`{target_repo}/{file_path}`]({base}/blob/HEAD/{file_path})" + elif dir_path: + link = f" โ€” [`{target_repo}/{dir_path}`]({base}/tree/HEAD/{dir_path})" else: link = f" โ€” [`{target_repo}`]({base})" if workflow: @@ -101,7 +134,14 @@ def main() -> int: catalog_path = Path(sys.argv[1]) version = sys.argv[2] repo = sys.argv[3] - print(render(catalog_path, version, repo)) + try: + body = render(catalog_path, version, repo) + except ValueError as e: + # Surface validation errors as a clean CI failure with a single + # actionable line, instead of a Python traceback. + print(f"render-downstream-tracker: {e}", file=sys.stderr) + return 1 + print(body) return 0 diff --git a/.github/scripts/test_render_downstream_tracker.py b/.github/scripts/test_render_downstream_tracker.py new file mode 100644 index 00000000000..43c7cdd559e --- /dev/null +++ b/.github/scripts/test_render_downstream_tracker.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Smoke tests for render-downstream-tracker.py. + +Run from the repo root with: python3 .github/scripts/test_render_downstream_tracker.py +Exits 0 on success, non-zero with a diff on failure. +""" + +from __future__ import annotations + +import importlib.util +import sys +import tempfile +import textwrap +from pathlib import Path + +HERE = Path(__file__).resolve().parent +spec = importlib.util.spec_from_file_location( + "render_downstream_tracker", HERE / "render-downstream-tracker.py" +) +mod = importlib.util.module_from_spec(spec) +spec.loader.exec_module(mod) + + +def write(tmpdir: Path, content: str) -> Path: + p = tmpdir / "catalog.yml" + p.write_text(textwrap.dedent(content)) + return p + + +def expect_value_error(tmpdir: Path, content: str, needle: str) -> None: + p = write(tmpdir, content) + try: + mod.render(p, "1.0", "ether/etherpad") + except ValueError as e: + assert needle in str(e), f"expected {needle!r} in {e!r}" + return + raise AssertionError(f"expected ValueError containing {needle!r}") + + +def main() -> int: + with tempfile.TemporaryDirectory() as td: + tmpdir = Path(td) + + # File targets render as /blob/HEAD/, directory targets render as + # /tree/HEAD/. The two are not interchangeable on GitHub. + body = mod.render(write(tmpdir, """ + downstreams: + - name: A file target + repo: foo/bar + update_type: external_pr + file: src/thing.sh + - name: A dir target + repo: foo/baz + update_type: external_auto + path: charts/etherpad + """), "1.0", "ether/etherpad") + assert "/blob/HEAD/src/thing.sh" in body, body + assert "/tree/HEAD/charts/etherpad" in body, body + + # Validation errors must be raised as ValueError (caught by main() + # and printed as a single CI-friendly line). + expect_value_error(tmpdir, "[]\n", "must be a mapping") + expect_value_error(tmpdir, "downstreams: not-a-list\n", "must be a list") + expect_value_error(tmpdir, """ + downstreams: + - "string item" + """, "must be a mapping") + expect_value_error(tmpdir, """ + downstreams: + - name: missing-update_type + """, "missing required") + expect_value_error(tmpdir, """ + downstreams: + - name: Both + update_type: external_pr + path: dir/ + file: file.txt + """, "both `path` and `file`") + + print("ok") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/release-downstreams.yml b/.github/workflows/release-downstreams.yml index a08a7e8ea8f..164a7fc5678 100644 --- a/.github/workflows/release-downstreams.yml +++ b/.github/workflows/release-downstreams.yml @@ -28,6 +28,11 @@ permissions: jobs: open-tracking-issue: runs-on: ubuntu-latest + # Opt-out gate: set the repo variable `SKIP_DOWNSTREAM_TRACKER=true` + # to disable this workflow without removing the file. Default is + # opt-out, not opt-in, because forgetting a flag at release time is + # exactly the failure mode this tracker exists to prevent. + if: vars.SKIP_DOWNSTREAM_TRACKER != 'true' steps: - name: Check out uses: actions/checkout@v6 @@ -69,9 +74,29 @@ jobs: - name: Open tracking issue env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.v.outputs.version }} + BODY_PATH: ${{ steps.render.outputs.body-path }} run: | + set -euo pipefail + TITLE="Downstream bumps for ${VERSION}" + # Dedupe: re-runs of this workflow (or repeated workflow_dispatch + # for the same version) must not pile up duplicate tracking issues. + # Search both open and closed for the same exact title. + EXISTING=$(gh issue list \ + --repo "$GITHUB_REPOSITORY" \ + --state all \ + --label release \ + --label downstream \ + --search "\"$TITLE\" in:title" \ + --json number,title \ + --jq ".[] | select(.title == \"$TITLE\") | .number" \ + | head -n1) + if [ -n "${EXISTING:-}" ]; then + echo "Tracking issue #$EXISTING already exists for ${VERSION}; skipping create." + exit 0 + fi gh issue create \ --repo "$GITHUB_REPOSITORY" \ - --title "Downstream bumps for ${{ steps.v.outputs.version }}" \ + --title "$TITLE" \ --label "release,downstream" \ - --body-file '${{ steps.render.outputs.body-path }}' + --body-file "$BODY_PATH" diff --git a/docs/downstreams.yml b/docs/downstreams.yml index a9b2ba9757c..44525965501 100644 --- a/docs/downstreams.yml +++ b/docs/downstreams.yml @@ -93,7 +93,7 @@ downstreams: - name: Proxmox VE Helper-Script repo: community-scripts/ProxmoxVED - path: ct/etherpad.sh + file: ct/etherpad.sh update_type: external_auto notes: | The script uses `check_for_gh_release` against `ether/etherpad-lite` @@ -127,7 +127,7 @@ downstreams: # ---- External, per-release PR needed ---------------------------------- - name: CasaOS App Store repo: IceWhaleTech/CasaOS-AppStore - path: Apps/Etherpad/docker-compose.yml + file: Apps/Etherpad/docker-compose.yml update_type: external_pr notes: | Docker image tag is pinned in the compose file. File a PR bumping @@ -136,7 +136,7 @@ downstreams: - name: BigBlueButton repo: bigbluebutton/bigbluebutton - path: bbb-etherpad.placeholder.sh + file: bbb-etherpad.placeholder.sh update_type: external_pr notes: | Pinned to a specific tag in the placeholder clone script. Major