🐳 Android image / Build and push (push) Successful in 2s
Build and test / android-image (push) Successful in 3s
Build and test / Desktop (Linux) (push) Failing after 1h12m2s
Build and test / Layer separation (push) Successful in 36s
Traceability / Requirement traces (push) Failing after 31s
Build and test / Android (aarch64) (push) Failing after 36m17s
The model fetch has been failing on every run with LFS: Client error: .../info/lfs/objects/0672d7a7... which reads like a rejected credential and is not one. The object is on the server and downloads fine; what fails is the shape of the request. `git lfs pull` makes two calls. The first, to `/info/lfs/objects/batch`, succeeds -- and Gitea answers it with a short-lived `Bearer` JWT scoped to that one object, for git-lfs to use on the second. git-lfs sends that JWT *and* the `Authorization` header this step had installed in git config, and two `Authorization` headers is a 400 from Gitea. Hence a client error on the object one step after the batch call it just made successfully, which is what made this look like an auth problem rather than a duplication. Confirmed directly against the server: the JWT alone on that URL is a 200, the JWT plus any second `Authorization` is a 400, and a lone token header that is merely wrong is a 401 -- so the scheme was never the issue. `lfs: true` on the checkout fails the same way and for the same reason, because actions/checkout persists a header of its own; the comment here blaming a credential the endpoint would not accept was wrong on both counts. So the headers are stripped -- checkout's included, since nothing later in either job talks to the remote -- and the token is handed to git-lfs as an ordinary credential instead. It authenticates the batch call and leaves the per-object JWT alone. This is what fails the Android job today: the build script sees a 133-byte pointer and panics by design, which is the message it is supposed to give and the one nobody could act on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
284 lines
12 KiB
YAML
284 lines
12 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
|
|
|
|
# The model, which is in LFS and is not optional.
|
|
#
|
|
# `core/dr-segment/models/*.onnx` is tracked in LFS (.gitattributes), so a
|
|
# plain checkout writes a ~130-byte pointer where 11 MB should be, and
|
|
# `dr-segment`'s build script panics by design rather than embedding a
|
|
# pointer and failing at inference. That failure reads like a broken build
|
|
# instead of a missing fetch, which is how it went unnoticed.
|
|
#
|
|
# Not `lfs: true` on the checkout above, and no `Authorization` header
|
|
# here either. Both install a blanket header for every request to this
|
|
# host, and the object download is the one request that already carries
|
|
# one: `git lfs pull` asks `/info/lfs/objects/batch` first, and Gitea
|
|
# answers with a short-lived `Bearer` JWT scoped to that object. git-lfs
|
|
# then sends the JWT *and* the configured header, and two `Authorization`
|
|
# headers is a 400 from Gitea — reported as
|
|
# LFS: Client error: .../info/lfs/objects/<oid>
|
|
# one step after the batch call that had just succeeded, which reads like
|
|
# a rejected credential rather than a duplicated one. A lone token header
|
|
# is understood fine; it is only the collision that fails.
|
|
#
|
|
# So: strip the headers and hand the token to git-lfs as an ordinary
|
|
# credential instead. It authenticates the batch call and leaves the
|
|
# per-object JWT untouched. Gitea authenticates on the password, so the
|
|
# username is a placeholder. Nothing later in this job talks to the
|
|
# remote, so dropping checkout's header costs us nothing.
|
|
#
|
|
# `continue-on-error` deliberately: if this cannot authenticate, the build
|
|
# below still runs and fails with the build script's own message, which
|
|
# names the real problem. A checkout that dies here says nothing.
|
|
- name: Fetch the segmentation model
|
|
continue-on-error: true
|
|
env:
|
|
LFS_TOKEN: ${{ secrets.GITEA_TOKEN || github.token }}
|
|
run: |
|
|
set -e
|
|
git lfs install --local
|
|
git config --local --get-regexp '^http\..*extraheader$' \
|
|
| cut -d' ' -f1 | sort -u \
|
|
| while read -r key; do git config --local --unset-all "$key"; done || true
|
|
git config --local lfs.url \
|
|
"https://x-access-token:${LFS_TOKEN}@gitea.tourolle.paris/dtourolle/DarkRoom.git/info/lfs"
|
|
git lfs pull
|
|
ls -l core/dr-segment/models/
|
|
|
|
- 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
|
|
|
|
# The model, which is in LFS and is not optional.
|
|
#
|
|
# `core/dr-segment/models/*.onnx` is tracked in LFS (.gitattributes), so a
|
|
# plain checkout writes a ~130-byte pointer where 11 MB should be, and
|
|
# `dr-segment`'s build script panics by design rather than embedding a
|
|
# pointer and failing at inference. That failure reads like a broken build
|
|
# instead of a missing fetch, which is how it went unnoticed.
|
|
#
|
|
# Not `lfs: true` on the checkout above, and no `Authorization` header
|
|
# here either. Both install a blanket header for every request to this
|
|
# host, and the object download is the one request that already carries
|
|
# one: `git lfs pull` asks `/info/lfs/objects/batch` first, and Gitea
|
|
# answers with a short-lived `Bearer` JWT scoped to that object. git-lfs
|
|
# then sends the JWT *and* the configured header, and two `Authorization`
|
|
# headers is a 400 from Gitea — reported as
|
|
# LFS: Client error: .../info/lfs/objects/<oid>
|
|
# one step after the batch call that had just succeeded, which reads like
|
|
# a rejected credential rather than a duplicated one. A lone token header
|
|
# is understood fine; it is only the collision that fails.
|
|
#
|
|
# So: strip the headers and hand the token to git-lfs as an ordinary
|
|
# credential instead. It authenticates the batch call and leaves the
|
|
# per-object JWT untouched. Gitea authenticates on the password, so the
|
|
# username is a placeholder. Nothing later in this job talks to the
|
|
# remote, so dropping checkout's header costs us nothing.
|
|
#
|
|
# `continue-on-error` deliberately: if this cannot authenticate, the build
|
|
# below still runs and fails with the build script's own message, which
|
|
# names the real problem. A checkout that dies here says nothing.
|
|
- name: Fetch the segmentation model
|
|
continue-on-error: true
|
|
env:
|
|
LFS_TOKEN: ${{ secrets.GITEA_TOKEN || github.token }}
|
|
run: |
|
|
set -e
|
|
git lfs install --local
|
|
git config --local --get-regexp '^http\..*extraheader$' \
|
|
| cut -d' ' -f1 | sort -u \
|
|
| while read -r key; do git config --local --unset-all "$key"; done || true
|
|
git config --local lfs.url \
|
|
"https://x-access-token:${LFS_TOKEN}@gitea.tourolle.paris/dtourolle/DarkRoom.git/info/lfs"
|
|
git lfs pull
|
|
ls -l core/dr-segment/models/
|
|
|
|
- name: Cache cargo
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
/opt/cargo/registry
|
|
target-android
|
|
key: android-${{ hashFiles('**/Cargo.lock') }}
|
|
|
|
# A fast gate on the crates most likely to break the cross-compile, run
|
|
# before the expensive part. It is `cargo check`, so it type-checks
|
|
# without linking and returns in a fraction of the time the step below
|
|
# takes.
|
|
#
|
|
# Not a statement that only these crates cross-compile — `darkroom-android`
|
|
# and the whole UI stack beneath it build for aarch64 too, which is what
|
|
# the API-level step below does. This one exists to fail fast and name a
|
|
# smaller suspect when it does.
|
|
- 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
|
|
# `darkroom-android`, not a core crate: this step reads the API level
|
|
# out of a *linked* object, and only that crate produces one. It is
|
|
# the workspace's single `crate-type = ["cdylib"]`; a library crate
|
|
# builds an rlib, which is an archive of object files that no linker
|
|
# has yet touched and that `file` therefore has nothing to say about.
|
|
# Asking for `-p dr-gpu` here could only ever reach the "no aarch64
|
|
# .so was produced" branch below, whatever the linker did.
|
|
#
|
|
# It is also the honest artefact to check: the .so this names is the
|
|
# one that ships in the APK, so the API level verified here is the
|
|
# API level a device will refuse to install against.
|
|
cargo ndk -t arm64-v8a build -p darkroom-android --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
|