ci(server): bundle Android APK into image on tag releases (#397 phase 3)

Final phase of the in-app update flow. release.yml on tag pushes
fetches the APK that flutter.yml is attaching to the same release,
drops it into client/ in the build context, and the Dockerfile's
COPY client/ /app/client/ bakes it into the image.

### How it sequences

flutter.yml and release.yml both trigger on tag pushes and run in
parallel on different runners (flutter-ci vs go-ci). flutter.yml
typically finishes APK build + release attachment in 2-5 min.
release.yml polls the release page for up to 15 min for the APK to
appear, then proceeds. If the APK never lands (flutter.yml failure,
network hiccup), release.yml emits a warning and ships the image
without the bundled APK — server returns 404 from /api/client/version,
banner stays hidden, manual download from the release page still
works. Graceful degradation, not a blocker.

### Repo shape

- client/.gitkeep + client/README.md so the directory exists in git
  and the COPY in the Dockerfile always finds something to copy
- .gitignore excludes client/minstrel.apk + .version so accidental
  commits don't bloat the repo
- Dockerfile: COPY --chown=minstrel:minstrel client/ /app/client/

### Why polling vs cross-workflow trigger

Forgejo Actions' workflow_run support varies by runner version; the
polling approach is universally compatible. If/when we standardize on
a runner that handles workflow_run cleanly, the polling step can
become a `needs:` dependency.

Closes #397.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 19:44:08 -04:00
parent 02d9f39845
commit e57a53a92e
5 changed files with 73 additions and 8 deletions
+28
View File
@@ -63,6 +63,34 @@ jobs:
echo "${{ secrets.REGISTRY_TOKEN }}" \
| docker login git.fabledsword.com -u "${{ github.actor }}" --password-stdin
# In-app update flow (#397): on tag pushes only, fetch the APK
# that flutter.yml is attaching to this same release. flutter.yml
# runs in parallel; poll up to 15 min for the asset to appear.
# On main pushes (or if the APK never lands), client/ stays empty
# and the endpoints return 404 — graceful degradation.
- name: Fetch APK for bundled in-app update channel
if: steps.guard.outputs.ready == 'true' && startsWith(github.ref, 'refs/tags/v')
shell: bash
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
TAG="${GITHUB_REF#refs/tags/}"
REPO="${GITHUB_REPOSITORY}"
APK_URL="https://git.fabledsword.com/${REPO}/releases/download/${TAG}/minstrel-${TAG}.apk"
echo "Polling ${APK_URL} (up to 15 min for flutter.yml to attach)..."
for i in $(seq 1 30); do
if curl -fsSL -H "Authorization: token ${RELEASE_TOKEN}" \
-o client/minstrel.apk "$APK_URL"; then
echo "Got APK on attempt $i"
echo "${TAG}" > client/minstrel.apk.version
ls -lh client/
exit 0
fi
echo "APK not ready yet (attempt $i/30), sleeping 30s..."
sleep 30
done
echo "::warning::APK never appeared at ${APK_URL} after 15 min — image will ship without bundled update channel"
- name: Build and push
if: steps.guard.outputs.ready == 'true'
run: docker buildx build --push ${{ steps.tags.outputs.args }} .