M12 — the Android client, end to end #2
@@ -52,6 +52,11 @@ jobs:
|
||||
# Android ABIs + cargo-ndk + SDK/NDK + JDK 25 + ktlint + detekt.
|
||||
image: git.fabledsword.com/bvandeusen/ci-rust-android:1.97
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
# For the dispatch at the end: this lane starts the server image build.
|
||||
actions: write
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: android
|
||||
@@ -222,3 +227,31 @@ jobs:
|
||||
name: thoughtsync-android-${{ steps.build.outputs.label }}-${{ github.sha }}
|
||||
path: ${{ steps.build.outputs.apk }}
|
||||
if-no-files-found: error
|
||||
|
||||
# The server image bakes in whatever client the dev release holds, so it has
|
||||
# to be built AFTER this lane, not alongside it. `ci.yml` stands down on any
|
||||
# push that touches the Android app (its `gate` job) and waits to be called
|
||||
# from here — that is the other half of this.
|
||||
#
|
||||
# `always()`: a FAILED Android build must still let the server image through.
|
||||
# There is no new client in that case, so it bakes in the previous one, which
|
||||
# is exactly right — the alternative is a broken Android lane silently
|
||||
# blocking server delivery.
|
||||
#
|
||||
# Not `if: success()` and not skipped on tags either: every ref that builds an
|
||||
# image needs the call, or nothing builds one at all.
|
||||
- name: Build the server image now the client is published
|
||||
if: always() && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main')
|
||||
working-directory: .
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Loud on failure rather than `|| true`: if this call stops working, the
|
||||
# symptom is server images silently never being built for Android pushes,
|
||||
# which is invisible until someone wonders why the app never updates.
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"ref":"${{ github.ref_name }}"}' \
|
||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/workflows/ci.yml/dispatches"
|
||||
echo "Dispatched ci.yml on ${{ github.ref_name }}."
|
||||
|
||||
@@ -27,6 +27,10 @@ on:
|
||||
- "alembic.ini"
|
||||
- "Dockerfile"
|
||||
- ".forgejo/workflows/ci.yml"
|
||||
# Dispatched by the Android lane once it has published a client, so the image
|
||||
# that bakes it in is built AFTER the APK exists rather than racing it. See the
|
||||
# `gate` job below for the other half.
|
||||
workflow_dispatch:
|
||||
|
||||
# Cancel older runs on the same branch when a newer push lands. Tag runs get their
|
||||
# own group implicitly and are never cancelled.
|
||||
@@ -42,6 +46,98 @@ env:
|
||||
IMAGE: git.fabledsword.com/bvandeusen/thoughtsync
|
||||
|
||||
jobs:
|
||||
# Should this push build an image now, or is the Android lane about to publish a
|
||||
# client that the image ought to contain?
|
||||
#
|
||||
# A push touching the Android app runs BOTH workflows at once. Building here
|
||||
# would bake in the PREVIOUS client and then, when the new one landed, there
|
||||
# would be no second build — `:<sha>` is the immutable rollback unit (rule 46)
|
||||
# and rebuilding it with different content would make it neither.
|
||||
#
|
||||
# So on such a push this workflow stands down, and the Android lane dispatches it
|
||||
# when it is finished. Exactly one image per commit, containing the client from
|
||||
# that commit.
|
||||
#
|
||||
# The path list below MUST match android.yml's trigger. Two places holding one
|
||||
# decision is the recurring failure in this repo (issues 2181-2183); it is here
|
||||
# because a workflow cannot read another's filters, and it is a `git diff` rather
|
||||
# than a config so at least it is inspectable in the log.
|
||||
gate:
|
||||
name: Build now, or wait for Android?
|
||||
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: python-ci
|
||||
container:
|
||||
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||
outputs:
|
||||
build: ${{ steps.decide.outputs.build }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Full history: the diff below spans the whole PUSHED RANGE, not just the
|
||||
# tip. A push of three commits whose Android change sits in the first
|
||||
# would otherwise look Android-free, and the race this job exists to
|
||||
# prevent would happen anyway — silently, which is the worst version.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Decide
|
||||
id: decide
|
||||
run: |
|
||||
# A dispatched run IS the Android lane calling back. Always build.
|
||||
if [ "${{ github.event_name }}" != "push" ]; then
|
||||
echo "Dispatched by the Android lane — building."
|
||||
echo "build=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# A tag. The Android lane does not run on tags, so nothing would ever
|
||||
# call back — standing down here would mean a release tag that never
|
||||
# produces an image at all.
|
||||
case "${{ github.ref }}" in
|
||||
refs/tags/*)
|
||||
echo "Tag build — the Android lane does not run on tags. Building."
|
||||
echo "build=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# No parent (first commit, or a force-push that orphaned it) — nothing to
|
||||
# compare, so build rather than stall.
|
||||
if ! git rev-parse --verify -q HEAD^ >/dev/null; then
|
||||
echo "No parent commit to diff against — building."
|
||||
echo "build=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# The whole push, not just its tip. `before` is what the ref pointed at
|
||||
# beforehand; it is absent or all-zeros for a brand-new branch, and may
|
||||
# be unreachable after a force-push — fall back to the tip commit then.
|
||||
before="${{ github.event.before }}"
|
||||
if [ -n "$before" ] \
|
||||
&& [ "$before" != "0000000000000000000000000000000000000000" ] \
|
||||
&& git cat-file -e "$before^{commit}" 2>/dev/null; then
|
||||
range="$before..HEAD"
|
||||
else
|
||||
range="HEAD^..HEAD"
|
||||
fi
|
||||
echo "Comparing $range"
|
||||
|
||||
changed="$(git diff --name-only $range)"
|
||||
echo "Changed in this push:"
|
||||
echo "$changed" | sed 's/^/ /'
|
||||
|
||||
if echo "$changed" | grep -qE '^(android/|core/|Cargo\.toml$|Cargo\.lock$|\.forgejo/workflows/android\.yml$)'; then
|
||||
echo ""
|
||||
echo "This push also changes the Android client. Standing down: the"
|
||||
echo "Android lane will publish a new APK and dispatch this workflow,"
|
||||
echo "so the image is built once, with the client from this commit."
|
||||
echo "build=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo ""
|
||||
echo "No Android change — the newest published client is already the"
|
||||
echo "right one to bake in. Building."
|
||||
echo "build=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
typecheck:
|
||||
name: TypeScript typecheck
|
||||
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||
@@ -95,8 +191,8 @@ jobs:
|
||||
# Build gates on lint + typecheck. The `test` job runs in parallel for
|
||||
# visibility but does not block dev image builds (DB-backed integration
|
||||
# testing happens against the dev image manually, not on every push).
|
||||
needs: [typecheck, lint]
|
||||
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [gate, typecheck, lint]
|
||||
if: needs.gate.outputs.build == 'true'
|
||||
runs-on: python-ci
|
||||
container:
|
||||
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||
|
||||
Reference in New Issue
Block a user