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:
@@ -63,6 +63,34 @@ jobs:
|
|||||||
echo "${{ secrets.REGISTRY_TOKEN }}" \
|
echo "${{ secrets.REGISTRY_TOKEN }}" \
|
||||||
| docker login git.fabledsword.com -u "${{ github.actor }}" --password-stdin
|
| 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
|
- name: Build and push
|
||||||
if: steps.guard.outputs.ready == 'true'
|
if: steps.guard.outputs.ready == 'true'
|
||||||
run: docker buildx build --push ${{ steps.tags.outputs.args }} .
|
run: docker buildx build --push ${{ steps.tags.outputs.args }} .
|
||||||
|
|||||||
@@ -12,6 +12,11 @@
|
|||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
|
# Bundled Android APK + version sidecar (#397). Populated by CI for
|
||||||
|
# tag releases; never committed. README in client/ explains the flow.
|
||||||
|
client/minstrel.apk
|
||||||
|
client/minstrel.apk.version
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
|
|||||||
+5
-8
@@ -36,14 +36,11 @@ COPY config.example.yaml /etc/smartmusic/config.yaml
|
|||||||
RUN mkdir -p /app/data /app/client && chown -R minstrel:minstrel /app
|
RUN mkdir -p /app/data /app/client && chown -R minstrel:minstrel /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# In-app update channel (#397): bundled Android APK + sidecar version
|
# In-app update channel (#397). client/ in the build context holds
|
||||||
# file. CI populates these on tag releases via a build context COPY;
|
# minstrel.apk + minstrel.apk.version (populated by release.yml on tag
|
||||||
# leaving the COPY commented during scaffold means the endpoints return
|
# pushes; .gitkeep + README otherwise). Endpoints return 404 when the
|
||||||
# 404 until the CI sequencing lands. Operators can also drop these in
|
# APK files aren't present, so non-tag images degrade gracefully.
|
||||||
# at runtime via a volume mount on /app/client.
|
COPY --chown=minstrel:minstrel client/ /app/client/
|
||||||
#
|
|
||||||
# COPY minstrel.apk /app/client/minstrel.apk
|
|
||||||
# COPY minstrel.apk.version /app/client/minstrel.apk.version
|
|
||||||
|
|
||||||
USER minstrel
|
USER minstrel
|
||||||
EXPOSE 4533
|
EXPOSE 4533
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Bundled client assets
|
||||||
|
|
||||||
|
Holds the Android APK (`minstrel.apk` + `minstrel.apk.version`) that the
|
||||||
|
server serves via `/api/client/version` and `/api/client/apk` for the
|
||||||
|
in-app update flow (#397).
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
CI populates this directory on tag releases:
|
||||||
|
1. `flutter.yml` builds `app-release.apk` and attaches it to the
|
||||||
|
Forgejo release as `minstrel-<TAG>.apk`.
|
||||||
|
2. `release.yml` waits for that asset to appear, downloads it into
|
||||||
|
this directory as `minstrel.apk`, writes the tag string to
|
||||||
|
`minstrel.apk.version`, and `docker buildx build` includes both
|
||||||
|
via `COPY client/ /app/client/`.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Empty directory works fine — the endpoints return 404 and the Flutter
|
||||||
|
update banner stays hidden ("no update channel" graceful degradation).
|
||||||
|
|
||||||
|
To smoke-test the update flow locally, drop a real APK + a version
|
||||||
|
file into this directory:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp /path/to/app-release.apk client/minstrel.apk
|
||||||
|
echo "v0.2.0" > client/minstrel.apk.version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why a directory and not embed.FS?
|
||||||
|
|
||||||
|
The APK is 30-60 MB. Embedding bloats the Go binary and slows
|
||||||
|
`go build` for everyone, even when the APK isn't being changed.
|
||||||
|
File-on-disk also lets operators override at runtime via volume
|
||||||
|
mount on `/app/client/` if they want to ship their own build.
|
||||||
Reference in New Issue
Block a user