Files
DarkRoom/.gitea/workflows/build-and-test.yml
T
dtourolleandClaude Opus 5 b044a8c067 Build the Android CI image in CI, not on a laptop
The Android job ran in gitea.tourolle.paris/dtourolle/darkroom-android:latest,
a tag that had never been pushed. The image existed only as a local
darkroom-android:latest on one machine, so every Android job died at docker
pull with "manifest unknown" before reaching a step. The registry API confirms
it: that manifest is a 404 while the other builder images answer 200.

android-image.yml now builds and pushes it, following KPN's docker.yaml — host
runner rather than a container, so it has the Docker daemon and the host's
cached registry credentials, and a plain-git checkout because that host has no
Node for actions/checkout.

Where it diverges from KPN: that workflow gates on dorny/paths-filter running
inside the builder image, which here would need the very image that is missing.
The tag is the git tree hash of docker/android instead, which changes when and
only when a file there changes. An unrelated push reuses the image, a Dockerfile
edit cannot keep serving a stale latest, and a missing tag rebuilds itself
without a manual step.

The presence probe is curl against the registry API, not `docker manifest
inspect`. The latter exits 1 on this registry even for tags that are plainly
there — jellytau-builder:latest answers HTTP 200 while docker reports "manifest
unknown" for it — and trusting it would have rebuilt 7 GB on every push. The
HEAD request also yields Docker-Content-Digest, so latest is repointed only when
the digests actually disagree, without pulling any layers.

A probe that cannot authenticate falls through to building. Rebuilding when it
was unnecessary costs minutes; skipping a build that was needed is the failure
this commit exists to remove.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 14:59:57 +02:00

178 lines
6.5 KiB
YAML

name: Build and test
# Desktop and Android are built on every push, per the v0.1 decision to carry
# both platforms from the first commit. An Android break is then caught the day
# it lands rather than at a porting milestone.
on:
push:
branches: [main, master, develop]
pull_request:
branches: [main, master, develop]
jobs:
# The Android job runs inside an image that this repo builds. Ensure it is in
# the registry before anything tries to pull it — see android-image.yml for
# why this is a job rather than a documented manual step. It is a no-op of a
# few seconds unless docker/android actually changed.
android-image:
uses: ./.gitea/workflows/android-image.yml
desktop:
runs-on: linux/amd64
name: Desktop (Linux)
# actions/checkout and actions/cache are JavaScript actions: the runner
# executes them with Node from inside this container. The bare runner image
# has none, so the job failed at checkout before reaching any build step.
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: desktop-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
# Slint and winit need these at build time; the runner image is minimal.
- name: Build dependencies
run: |
apt-get update -qq
apt-get install -y -qq pkg-config libfontconfig1-dev libxkbcommon-dev
# The act image ships Node but no Rust. Pinned to the workspace
# rust-version so CI, the Android image, and local builds agree — a
# floating toolchain turns an unrelated push into a mystery failure.
- name: Install Rust 1.92.0
run: |
set -e
curl -fsSL https://sh.rustup.rs | sh -s -- \
-y --no-modify-path --profile minimal \
--default-toolchain 1.92.0 --component rustfmt,clippy
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Format
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --workspace --all-targets -- -D warnings
# GPU tests skip themselves where no adapter is present rather than
# failing — CI runners generally have none, and a test that cannot run is
# not evidence either way.
- name: Test
run: cargo test --workspace
- name: Build
run: cargo build --workspace --release
android:
runs-on: linux/amd64
name: Android (aarch64)
# Waits for the image build. Without this the pull races the push and the
# job dies with "manifest unknown" before its first step, which is the
# failure mode this ordering exists to remove.
needs: android-image
container:
image: gitea.tourolle.paris/dtourolle/darkroom-android:latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
/opt/cargo/registry
target-android
key: android-${{ hashFiles('**/Cargo.lock') }}
# Only the core crates cross-compile today; the UI and app crates join
# once the Android shell exists (milestone v0.1, FR-PLAT-AND-*).
- name: Cross-compile core
env:
CARGO_TARGET_DIR: target-android
run: cargo check -p dr-types -p dr-gpu -p dr-sync --target aarch64-linux-android
# The linker targets MIN_API, not the compile SDK. cargo-ndk otherwise
# defaults to API 21, far below the Vulkan floor this app needs — and the
# mismatch is invisible until a device refuses to install.
#
# Look under the target triple, and fail on a mismatch. Searching the
# whole target dir for the first `*.so` found the host proc-macro
# libraries in target-android/debug/deps instead — x86-64 objects built
# by the runner's gcc, whose .comment section says nothing about Android
# and can never contradict the expected API. The step passed regardless
# of what the linker actually did, which is the one thing it exists to
# rule out.
- name: Verify minimum API level
env:
CARGO_TARGET_DIR: target-android
run: |
set -e
cargo ndk -t arm64-v8a build -p dr-gpu --release
MIN_API=$(sed -n 's/^ARG MIN_API=\([0-9]*\).*/\1/p' docker/android/Dockerfile)
# Empty on both sides would compare equal and pass, so neither side
# is allowed to be the result of a failed parse.
if [ -z "$MIN_API" ]; then
echo "no ARG MIN_API= in docker/android/Dockerfile"
exit 1
fi
SO=$(find target-android/aarch64-linux-android/release -maxdepth 1 -name '*.so' | head -1)
if [ -z "$SO" ]; then
echo "no aarch64 .so was produced"
exit 1
fi
echo "checking $SO"
file "$SO"
API=$(file "$SO" | sed -n 's/.*for Android \([0-9]*\).*/\1/p')
if [ -z "$API" ] || [ "$API" != "$MIN_API" ]; then
echo "FAIL: linked for Android '${API:-unknown}', expected $MIN_API"
exit 1
fi
layering:
runs-on: linux/amd64
name: Layer separation
# Node for the JS actions, as above. cargo comes from rustup below.
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# `cargo tree` resolves the dependency graph, so it needs the registry
# index but no system libraries — this job builds nothing.
- name: Install Rust 1.92.0
run: |
set -e
curl -fsSL https://sh.rustup.rs | sh -s -- \
-y --no-modify-path --profile minimal --default-toolchain 1.92.0
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# ARCH §6.5a: no core/ crate may depend on the UI toolkit. One stray
# `use slint::` costs headless golden-image testing and the
# one-operation-two-presentations property together, and nothing else
# would notice.
- name: Core crates must not depend on the UI
run: |
set -e
FAILED=0
for crate in dr-types dr-gpu dr-sync; do
if cargo tree -p "$crate" -e normal 2>/dev/null | grep -qE '\bslint\b|\bi-slint'; then
echo "FAIL: $crate depends on Slint (ARCH §6.5a)"
FAILED=1
else
echo "ok: $crate"
fi
done
exit $FAILED