M12 — the Android client, end to end #2
@@ -52,6 +52,11 @@ jobs:
|
|||||||
# Android ABIs + cargo-ndk + SDK/NDK + JDK 25 + ktlint + detekt.
|
# Android ABIs + cargo-ndk + SDK/NDK + JDK 25 + ktlint + detekt.
|
||||||
image: git.fabledsword.com/bvandeusen/ci-rust-android:1.97
|
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:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: android
|
working-directory: android
|
||||||
@@ -222,3 +227,31 @@ jobs:
|
|||||||
name: thoughtsync-android-${{ steps.build.outputs.label }}-${{ github.sha }}
|
name: thoughtsync-android-${{ steps.build.outputs.label }}-${{ github.sha }}
|
||||||
path: ${{ steps.build.outputs.apk }}
|
path: ${{ steps.build.outputs.apk }}
|
||||||
if-no-files-found: error
|
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"
|
- "alembic.ini"
|
||||||
- "Dockerfile"
|
- "Dockerfile"
|
||||||
- ".forgejo/workflows/ci.yml"
|
- ".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
|
# Cancel older runs on the same branch when a newer push lands. Tag runs get their
|
||||||
# own group implicitly and are never cancelled.
|
# own group implicitly and are never cancelled.
|
||||||
@@ -42,6 +46,98 @@ env:
|
|||||||
IMAGE: git.fabledsword.com/bvandeusen/thoughtsync
|
IMAGE: git.fabledsword.com/bvandeusen/thoughtsync
|
||||||
|
|
||||||
jobs:
|
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:
|
typecheck:
|
||||||
name: TypeScript typecheck
|
name: TypeScript typecheck
|
||||||
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
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
|
# Build gates on lint + typecheck. The `test` job runs in parallel for
|
||||||
# visibility but does not block dev image builds (DB-backed integration
|
# visibility but does not block dev image builds (DB-backed integration
|
||||||
# testing happens against the dev image manually, not on every push).
|
# testing happens against the dev image manually, not on every push).
|
||||||
needs: [typecheck, lint]
|
needs: [gate, typecheck, lint]
|
||||||
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
if: needs.gate.outputs.build == 'true'
|
||||||
runs-on: python-ci
|
runs-on: python-ci
|
||||||
container:
|
container:
|
||||||
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||||
|
|||||||
Reference in New Issue
Block a user