Previous design: android.yml's release job and release.yml ran in
parallel on every tag push. release.yml polled the gitea release-
download URL for up to 15 min waiting for the APK to appear. In
practice the polling step was completing in 11s — either following a
gitea redirect to a 200 page and writing HTML into the bundled APK
file, or returning empty content silently. Either way the resulting
image shipped without a working APK and the web Settings page's
in-app-update section never rendered.
Fix the race architecturally:
- Move the signed-release-build + Attach-APK steps out of android.yml
and into a new android-release job in release.yml.
- release.yml's image-release job declares needs: [android-release],
so on tag pushes the image cannot start building until the APK is
guaranteed-attached.
- Pass the APK between jobs via actions/upload-artifact@v3 +
download-artifact@v3 (the v2 backend isn't supported on Gitea).
This removes the polling loop entirely — the image-release job just
downloads the artifact, renames to minstrel.apk, writes the
.version sidecar, and continues to docker buildx.
- For main pushes android-release is skipped via its if: condition.
image-release uses so it
still runs (the skipped predecessor doesn't poison the chain) and
the download/stage steps gate themselves on the tag context. Main
images ship without an APK by design, same as before.
android.yml is now testing-only: lint + detekt + unit tests on every
push, debug APK artifact on main. Independent of release CI as
requested.
The previous version of the Attach APK to release step ran without
set -x and without capturing the upload's HTTP response. Run 118
(v2026.06.01 tag-build) shows the step reporting success but the
release ended up with zero assets — i.e. the upload silently failed
without surfacing a non-zero exit.
New version:
- set -euxo pipefail so every command is echoed and any failure
surfaces.
- ls -lh the APK before upload so we can see if Gradle even produced
it at the expected path.
- curl -w '%{http_code}' captures the actual HTTP status into a var
and writes the response body to a temp file; both are printed.
- Explicit if check on the HTTP code (200-299 = success) bails
loudly when it isn't.
No behavioral change in success path; on failure the cause is
visible in the log instead of being eaten.
android.yml run 116 (main push on 2534384e) failed the debug-APK
upload step with:
::error::@actions/artifact v2.0.0+, upload-artifact@v4+ and
download-artifact@v4+ are not currently supported on GHES.
Gitea Actions emulates GHES; the v2 backend used by upload-artifact
v4 isn't implemented there yet. Pinning to @v3 keeps the main-push
debug-APK channel working without affecting the tag-push release
path (which doesn't use this action).
Every commit on dev that's part of an open PR fires each workflow
TWICE: once for the push event and once for the pull_request event
(observable in run #43 + #44 for the same SHA in android.yml, same
shape across test-web.yml and test-go.yml). The two runs check the
exact same SHA — pure waste.
Drop the pull_request trigger from test-web.yml, test-go.yml, and
android.yml. The dev push already exercises every check that a PR
would re-run. This repo has no fork PRs to cover, which is the only
case pull_request would catch that push doesn't.
Add a concurrency group keyed on workflow + ref with
cancel-in-progress: true to every workflow including release.yml.
Rapid re-pushes (or force-moving the per-day tag) now cancel the
older in-flight run instead of stacking.
Drops the trailing patch digit from the CalVer model. Same-day
re-releases force-move the tag (and overwrite both the docker image
and the release asset of the same name). :latest is now updated by
every main push AND every tag push, so it always reflects the
newest blessed image without a separate cut step.
Comment-only changes — the workflow glob is already 'v*', so the
existing pipeline accepts the new format with no code changes.
Two related cutover changes so the rolling stable channel works end
to end after the Flutter sunset.
release.yml:
- Main pushes now tag both :main and :latest. Main is the protected
post-PR-merge branch and the team's stable channel; pinning to
:latest gets the newest main automatically while :vX.Y.Z stays
available for pinned consumers.
android.yml:
- Asset attach renamed from minstrel-android-<tag>.apk to
minstrel-<tag>.apk (M8 phase 14.4 cutover). Without this, the
server image's bundled in-app-update fetcher in release.yml polls
a filename that no workflow produces, so /api/client/version
returns 404 and the web UI's MobileAppDownload silently renders
nothing. With this rename the existing fetcher resolves to the
native APK with no further plumbing.
No code paths change; both fixes are workflow rewires.
Last push failed both android and flutter jobs at workflow-setup time
because the runner couldn't resolve forgejo/upload-artifact@v3
(github.com/forgejo/upload-artifact does not exist; the Forgejo
project hosts on Codeberg and our Gitea runner falls through to
github by default). The canonical Gitea Actions form is
actions/upload-artifact@v4, which act_runner resolves cleanly.
Flutter pipeline is being retired in favor of the native Android
client, so flutter.yml is deleted outright rather than fixed. The
container-image build's release-asset polling (release.yml) will now
graceful-degrade when chasing the legacy minstrel-<TAG>.apk name,
which the comment in ci-requirements.md already documents as
acceptable. Renaming the native APK to drop the -android- infix is
deferred to a follow-up so the cutover is reviewable in isolation.
- rename .forgejo/workflows/ to .gitea/workflows/ (git mv preserves
history); Gitea Actions reads either path, but the directory now
matches the active platform
- collapse ANDROID_STORE_PASSWORD + ANDROID_KEY_PASSWORD into a
single ANDROID_KEYSTORE_PASSWORD secret (PKCS12 keystores require
the two passwords to be identical, so the duplication carried no
information); build.gradle.kts still reads two env vars to stay
format-agnostic, both now sourced from the same secret in CI
- ignore android/*.keystore, android/*.keystore.*, android/*.jks,
android/keystore.properties so the regenerated signing material
never reaches a remote on accident
- update prose references from Forgejo to Gitea in CLAUDE.md and
the docs; the historical "migrated from Forgejo" note in CLAUDE.md
is kept intentionally; forgejo/upload-artifact@v3 action refs are
left untouched (canonical artifact action, resolves cleanly under
Gitea Actions)
Operator side: ANDROID_KEY_ALIAS, ANDROID_KEYSTORE_PASSWORD, and
ANDROID_KEYSTORE_B64 secrets registered in Gitea before this lands.