From 52d53e00440da5ccd01f5cc2cc345319cc808625 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 30 Jul 2026 15:28:10 -0400 Subject: [PATCH 1/2] ci: swap artifact upload+download to the mirrored actions (issue 2270) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit android.yml and release.yml uploaded via actions/upload-artifact@v3, which reports success while Gitea stores the result in a format its v4-only artifact API will never serve back — 72 artifacts on this repo are on disk, have valid DB rows, and are invisible to every retrieval path. Green jobs producing nothing retrievable. release.yml is a producer/consumer pair: android-release uploads minstrel-apk and image-release downloads it to bundle into the container. Swapping only the upload would have left download-artifact@v3 reading the v1/v3 listing and finding nothing, so mirror the download side too — bvandeusen/download-artifact, pull mirror of forgejo/download-artifact, pinned at its v5 tag to match the upload pin's major. Not actions/{upload,download}-artifact@v4: isGhes() throws on the hostname before opening a connection, so no server-side change reaches it. Upload steps also set if-no-files-found: error — image-release hard-depends on minstrel-apk existing, so an empty upload must fail where it happens. Co-Authored-By: Claude Opus 5 (1M context) --- .gitea/workflows/android.yml | 14 ++++++++++---- .gitea/workflows/release.yml | 22 ++++++++++++++++++---- ci-requirements.md | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/.gitea/workflows/android.yml b/.gitea/workflows/android.yml index 9e7a3f02..fcce50e6 100644 --- a/.gitea/workflows/android.yml +++ b/.gitea/workflows/android.yml @@ -80,10 +80,16 @@ jobs: - name: Upload debug APK if: github.event_name == 'push' && github.ref == 'refs/heads/main' - # Gitea Actions runs in GHES-emulation mode; @actions/artifact v2+ - # (i.e. upload-artifact@v4+) errors with "GHESNotSupportedError". - # Pin to @v3 until act_runner or the artifact backend catches up. - uses: actions/upload-artifact@v3 + # Mirrored action, never actions/upload-artifact. @v4+ throws + # GHESNotSupportedError client-side on the hostname (no server setting + # reaches that check), and @v3 is worse — it reports success while Gitea + # serves artifacts back only through the v4 API, so the upload is stored + # and invisible to every retrieval path. @v3 is what left 72 unreachable + # artifacts on this repo. Pinned by SHA because the mirror auto-syncs; + # full URL because DEFAULT_ACTIONS_URL sends bare owner/repo to github.com. + # See Scribe issues 2255 / 2270. + uses: https://git.fabledsword.com/bvandeusen/upload-artifact@cb8afe72b42edc798abfb8fcb556cf660d894245 with: name: minstrel-android-debug-${{ github.sha }} path: android/app/build/outputs/apk/debug/app-debug.apk + if-no-files-found: error diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index e7b39993..e6a5c9a4 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -131,12 +131,19 @@ jobs: -PMINSTREL_VERSION_CODE=${{ steps.ver.outputs.code }} - name: Upload APK as workflow artifact - # @v3 because Gitea Actions emulates GHES and the v2 artifact - # backend used by upload-artifact@v4 errors with GHESNotSupportedError. - uses: actions/upload-artifact@v3 + # Mirrored action, never actions/upload-artifact — @v4+ refuses on the + # hostname, @v3 uploads something Gitea will never serve back. This is + # the producing half of a pair: image-release downloads `minstrel-apk` + # below with the matching download-artifact mirror. Both must stay on + # the v4 protocol — mixing a v3 upload with a v4 download (or the + # reverse) yields an empty listing, not an error. See Scribe 2255 / 2270. + uses: https://git.fabledsword.com/bvandeusen/upload-artifact@cb8afe72b42edc798abfb8fcb556cf660d894245 with: name: minstrel-apk path: android/app/build/outputs/apk/release/app-release.apk + # error, not the default warn: image-release hard-depends on this + # artifact existing, so an empty upload must fail here, not there. + if-no-files-found: error - name: Attach APK to gitea Release shell: bash @@ -238,7 +245,14 @@ jobs: # Tag pushes only — android-release just produced this. Non-tag # builds take the "Bundle latest release APK" path below instead. if: steps.guard.outputs.ready == 'true' && startsWith(github.ref, 'refs/tags/v') - uses: actions/download-artifact@v3 + # Consuming half of the pair — must match the upload's protocol, so it + # uses the download mirror, never actions/download-artifact. Same fork, + # same reason: upstream's client-side GHES check rejects this hostname + # before it connects. bvandeusen/download-artifact mirrors + # code.forgejo.org/forgejo/download-artifact; SHA below is its `v5` tag, + # the major matching the upload pin. Pinned, not tagged — the mirror + # auto-syncs every 8h and a moved tag would change what runs. + uses: https://git.fabledsword.com/bvandeusen/download-artifact@1314311ddb542af343a82d478ac786ceada4143a with: name: minstrel-apk path: client/ diff --git a/ci-requirements.md b/ci-requirements.md index 05e9ebbc..7d34d5e6 100644 --- a/ci-requirements.md +++ b/ci-requirements.md @@ -44,4 +44,36 @@ None. - **Go toolchain pin.** `go.mod` is on `go 1.25.0` because `golang.org/x/crypto v0.51.0` declares 1.25 as its minimum. `ci-go:1.26` satisfies this with headroom. Future `x/crypto` bumps that move the Go floor should be paired with an image-tag bump in this file + the workflows. - **In-app update channel polling.** `release.yml` polls Gitea's release-asset API for up to 15 min on tag pushes to fetch the APK that `flutter.yml` is concurrently attaching to the same release. The asset eventually appears because `flutter.yml` and `release.yml` run in parallel on the same tag; if the polling times out, the server image ships without the bundled update channel (graceful degradation, not a build failure). - **Cache server reachability.** `test-web.yml` does NOT use `cache: 'npm'` on `actions/setup-node` — the Gitea Actions cache server isn't reachable from this runner's container network and `setup-node` was burning ~4m41s on ETIMEDOUT before failing open. With the migration to `ci-go:1.26`, `setup-node` is removed entirely (Node is in the image). The cache concern reappears if a future change re-introduces a network-dependent action. +- **Artifacts — use the mirrored actions, never `actions/{upload,download}-artifact`.** + ```yaml + uses: https://git.fabledsword.com/bvandeusen/upload-artifact@cb8afe72b42edc798abfb8fcb556cf660d894245 + uses: https://git.fabledsword.com/bvandeusen/download-artifact@1314311ddb542af343a82d478ac786ceada4143a + ``` + Upstream's `@v4+` cannot work against this instance and no server-side change + will help: `isGhes()` rejects any hostname that isn't `github.com` / + `*.ghe.com` / `*.localhost` and throws before it opens a connection, so the + server is never asked what it supports. `@v3` is worse — it reports success, + and Gitea then serves artifacts back only through the v4 API + (`content_encoding = application/zip`), so a v3 upload is stored but invisible + to every retrieval path. A green job producing nothing retrievable; that is how + 72 unreachable artifacts accumulated on this repo. Scribe issues 2255 / 2270. + + Both are pull mirrors of the Forgejo project's forks + (`code.forgejo.org/forgejo/{upload,download}-artifact`, one commit on upstream + disabling that check), mirrored so CI depends on commits we hold and pinned by + SHA because the mirrors auto-sync every 8h — a moved upstream tag would + otherwise silently change what runs. The pins are upstream's `v5` tags on both + sides; **keep the majors matched.** `release.yml` is a producer/consumer pair — + `android-release` uploads `minstrel-apk`, `image-release` downloads it — and + mixing protocol versions across that pair yields an empty listing rather than + an error, which is the failure mode this whole entry exists to prevent. + + Upload steps set `if-no-files-found: error` rather than the default `warn`, so + an upload that matches nothing fails its own job instead of failing the + consumer later. + + Retrieval: `GET /api/v1/repos/{owner}/{repo}/actions/runs/{run_id}/artifacts` + for the id (global run id, not the repo-scoped run number), then + `…/actions/artifacts/{id}/zip`. The workstation has no `unzip` — use + `python3 -m zipfile -e`. - **Friction asks.** None pending. The two images cover everything Minstrel needs. From fa0827f6687e6d6886367993ab9ba8c61871fbee Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 30 Jul 2026 15:37:43 -0400 Subject: [PATCH 2/2] =?UTF-8?q?ci:=20pin=20the=20download=20mirror=20to=20?= =?UTF-8?q?v6,=20not=20v5=20=E2=80=94=20match=20on=20@actions/artifact?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous pin matched the two actions by their own version numbers, which is meaningless: upload-artifact and download-artifact release on unrelated cadences. upload v5 bundles @actions/artifact ^4.0.0; download v5 bundles ^2.3.2. "v5 and v5" was in fact a mismatched pair. download v6 is the tag that puts ^4.0.0 on both sides — and ^4.0.0 is the library major just proven against this instance by the upload side (thoughtsync run 3094: two artifacts listed, downloaded and extracted intact). ^2.3.2 has never been exercised here. Not v7: that major is a runner requirement rather than a feature change. It moves to runs.using: node24 and upstream requires runner >= 2.327.1 for it, which act_runner does not claim to satisfy. Everything pinned stays node20. ci-requirements.md now carries the version/runtime table and the reasoning, so the next person matches on the library instead of the tag number. Co-Authored-By: Claude Opus 5 (1M context) --- .gitea/workflows/release.yml | 18 ++++++++++------ ci-requirements.md | 40 ++++++++++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index e6a5c9a4..577b5b45 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -245,14 +245,20 @@ jobs: # Tag pushes only — android-release just produced this. Non-tag # builds take the "Bundle latest release APK" path below instead. if: steps.guard.outputs.ready == 'true' && startsWith(github.ref, 'refs/tags/v') - # Consuming half of the pair — must match the upload's protocol, so it - # uses the download mirror, never actions/download-artifact. Same fork, + # Consuming half of the pair — never actions/download-artifact. Same fork, # same reason: upstream's client-side GHES check rejects this hostname # before it connects. bvandeusen/download-artifact mirrors - # code.forgejo.org/forgejo/download-artifact; SHA below is its `v5` tag, - # the major matching the upload pin. Pinned, not tagged — the mirror - # auto-syncs every 8h and a moved tag would change what runs. - uses: https://git.fabledsword.com/bvandeusen/download-artifact@1314311ddb542af343a82d478ac786ceada4143a + # code.forgejo.org/forgejo/download-artifact. + # + # SHA below is that fork's `v6` tag. Match on @actions/artifact, NOT on + # the action's own version number — the two actions release on unrelated + # cadences, and download v5 would pair a ^2.3.2 client with this file's + # ^4.0.0 uploader. v6 is the tag whose bundled library major (^4.0.0) is + # the same one proven against this instance by the upload side. + # Deliberately NOT v7: it moves to node24 and upstream requires runner + # >= 2.327.1 for it, which act_runner does not claim to satisfy. + # Pinned, not tagged — the mirror auto-syncs every 8h. + uses: https://git.fabledsword.com/bvandeusen/download-artifact@8d4e9521a5f7e5f8b6351f341f719f9f45a92a3a with: name: minstrel-apk path: client/ diff --git a/ci-requirements.md b/ci-requirements.md index 7d34d5e6..712ccda0 100644 --- a/ci-requirements.md +++ b/ci-requirements.md @@ -47,7 +47,7 @@ None. - **Artifacts — use the mirrored actions, never `actions/{upload,download}-artifact`.** ```yaml uses: https://git.fabledsword.com/bvandeusen/upload-artifact@cb8afe72b42edc798abfb8fcb556cf660d894245 - uses: https://git.fabledsword.com/bvandeusen/download-artifact@1314311ddb542af343a82d478ac786ceada4143a + uses: https://git.fabledsword.com/bvandeusen/download-artifact@8d4e9521a5f7e5f8b6351f341f719f9f45a92a3a ``` Upstream's `@v4+` cannot work against this instance and no server-side change will help: `isGhes()` rejects any hostname that isn't `github.com` / @@ -62,11 +62,39 @@ None. (`code.forgejo.org/forgejo/{upload,download}-artifact`, one commit on upstream disabling that check), mirrored so CI depends on commits we hold and pinned by SHA because the mirrors auto-sync every 8h — a moved upstream tag would - otherwise silently change what runs. The pins are upstream's `v5` tags on both - sides; **keep the majors matched.** `release.yml` is a producer/consumer pair — - `android-release` uploads `minstrel-apk`, `image-release` downloads it — and - mixing protocol versions across that pair yields an empty listing rather than - an error, which is the failure mode this whole entry exists to prevent. + otherwise silently change what runs. + + **Match the pins on `@actions/artifact`, not on the actions' own version + numbers.** The two actions release on unrelated cadences, so equal version + numbers do NOT mean a compatible pair — upload `v5` bundles `@actions/artifact` + ^4.0.0 while download `v5` bundles ^2.3.2. The pins above are upload **v5** and + download **v6**, which is the pairing that puts ^4.0.0 on both sides. This + matters because `release.yml` is a producer/consumer pair — `android-release` + uploads `minstrel-apk`, `image-release` downloads it — and a protocol mismatch + across it yields an empty listing rather than an error, exactly the silent + failure this entry exists to prevent. + + | tag | `@actions/artifact` | runtime | + |---|---|---| + | upload v4 | ^2.1.1 | node20 | + | **upload v5** ← pinned | **^4.0.0** | node20 | + | download v4 | ^2.1.1 | node20 | + | download v5 | ^2.3.2 | node20 | + | **download v6** ← pinned | **^4.0.0** | node20 | + | download v7 | ^5.0.0 | **node24** | + + The only true protocol break in this history was **v3 → v4** (upstream: + "Downloading artifacts that were created from `actions/upload-artifact@v3` and + below are not supported"); v4-and-up are one family. Later majors are mostly + ergonomics and runtime — upload v4 forbids re-uploading a name and caps a job + at 500 artifacts; download v5 made by-ID extraction match by-name. + + **Do not jump the download pin to v7.** That major is a runner requirement, not + a feature change: it moves to `runs.using: node24` and upstream states it + "requires a minimum Actions Runner version of 2.327.1 … if you are using + self-hosted runners, ensure they are updated before upgrading." act_runner is + not GitHub's runner and makes no such version claim, so node24 is unverified + here. Everything currently pinned is node20. Upload steps set `if-no-files-found: error` rather than the default `warn`, so an upload that matches nothing fails its own job instead of failing the