The container job picked `sh -e {0}`, and sh on eclipse-temurin:jammy is
dash. Every `set -euo pipefail` step failed on line 2 with "Illegal option
-o pipefail" before running a command, so unit-tests and vectors-head went
red without compiling anything.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
204 lines
9.4 KiB
YAML
204 lines
9.4 KiB
YAML
# CI for the Android app on Gitea Actions. Translated from .gitlab-ci.yml on
|
|
# 2026-10-13; every gate it had is kept. Instrumented tests stay manual: they
|
|
# need an emulator and a local relay.
|
|
#
|
|
# Runs on mac1/mac2 (`ubuntu-latest`) inside the pinned toolchain image built
|
|
# from ci/android-ci.Dockerfile. Never on pauls-imac: Android unit tests hang
|
|
# there (2026-10-09).
|
|
name: ci
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
|
|
env:
|
|
# Bump when ci/android-ci.Dockerfile changes.
|
|
IMAGE: git.extant2000.com/extant2000/flamenet-android-ci:jdk21.0.5-api35-r1
|
|
GITEA_BASE: https://git.extant2000.com/extant2000
|
|
|
|
# Inside a container job the runner picks `sh -e {0}`, which is dash on this
|
|
# image, and dash has no `pipefail`. Every `set -euo pipefail` step died on
|
|
# line 2 before it ran anything. Named here once rather than per step.
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
|
|
jobs:
|
|
# The repository is public, so this sweep is the last thing between a pasted
|
|
# key and the internet. It was red for nine days in 2026-09 because nobody
|
|
# ran it.
|
|
secret-sweep:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- run: bash scripts/secret-sweep.sh
|
|
|
|
# Builds and pushes the toolchain image when its tag is not in the registry
|
|
# yet. A no-op on every other run.
|
|
image:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: Build and push the toolchain image if missing
|
|
env:
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
JOB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
ACTOR: ${{ gitea.actor }}
|
|
run: |
|
|
set -euo pipefail
|
|
curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz \
|
|
| tar -xz -C /usr/local/bin --strip-components=1 docker/docker
|
|
if [ -n "${REGISTRY_TOKEN:-}" ]; then
|
|
echo "$REGISTRY_TOKEN" | docker login git.extant2000.com -u extant2000git --password-stdin
|
|
else
|
|
echo "$JOB_TOKEN" | docker login git.extant2000.com -u "$ACTOR" --password-stdin
|
|
fi
|
|
if docker manifest inspect "$IMAGE" >/dev/null 2>&1; then
|
|
echo "image $IMAGE exists"; exit 0
|
|
fi
|
|
DOCKER_BUILDKIT=0 docker build -t "$IMAGE" -f ci/android-ci.Dockerfile ci
|
|
docker push "$IMAGE"
|
|
|
|
unit-tests:
|
|
needs: image
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
container:
|
|
image: git.extant2000.com/extant2000/flamenet-android-ci:jdk21.0.5-api35-r1
|
|
env:
|
|
# The interop tests FAIL rather than skip when unconfigured, on purpose.
|
|
# They need a real lyrebird from the flamenet-elligator2 checkout, so they
|
|
# are opted out HERE and nowhere else. Run scripts/obfs4-interop.sh
|
|
# locally before merging any obfs4 change.
|
|
OBFS4_INTEROP_OPTOUT: "1"
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: Fetch the pinned protocol vectors
|
|
run: |
|
|
set -euo pipefail
|
|
# Pinned to .e2e-ref, not tip (flamenet-relay#56 is what tip cost).
|
|
# `vectors-head` below is the advisory half that watches HEAD.
|
|
FLAMENET_E2E_PIN=$(cat .e2e-ref)
|
|
rm -rf .ci-vectors
|
|
git clone -q "$GITEA_BASE/flamenet-e2e.git" .ci-vectors
|
|
git -C .ci-vectors checkout -q "$FLAMENET_E2E_PIN"
|
|
- name: Unit tests
|
|
run: |
|
|
set -euo pipefail
|
|
V="$GITHUB_WORKSPACE/.ci-vectors/test/vectors"
|
|
export FLAMENET_VECTORS="$V/protocol-v1.json"
|
|
export FLAMENET_PQ_VECTORS="$V/pqx3dh-v2.json"
|
|
export FLAMENET_ACCOUNT_VECTORS="$V/account-kdf-v1.json"
|
|
export FLAMENET_BACKUP_VECTORS="$V/backup-v1.json"
|
|
export FLAMENET_BACKUP_PAYLOAD_VECTORS="$V/backup-payload-v1.json"
|
|
export FLAMENET_CONTENT_VECTORS="$V/content-v1.json"
|
|
export FLAMENET_POLL_VECTORS="$V/poll-tally-v1.json"
|
|
export FLAMENET_PADDING_VECTORS="$V/padding-v1.json"
|
|
export FLAMENET_SYNC_VECTORS="$V/sync-merge-v1.json"
|
|
export FLAMENET_HANDLE_CERT_VECTORS="$V/handle-cert-v1.json"
|
|
export FLAMENET_LINK_VECTORS="$V/links-v1.json"
|
|
export FLAMENET_BRIDGE_VECTORS="$V/bridge-offer-v1.json"
|
|
export FLAMENET_CONTENT_KINDS_VECTORS="$V/content-kinds-v1.json"
|
|
export FLAMENET_TODAY_VECTORS="$V/today-v1.json"
|
|
export FLAMENET_HELLO_LINK_VECTORS="$V/hello-link-v1.json"
|
|
# A path that silently does not exist would read as a protocol bug.
|
|
for f in "$FLAMENET_VECTORS" "$FLAMENET_PQ_VECTORS" "$FLAMENET_ACCOUNT_VECTORS" \
|
|
"$FLAMENET_BACKUP_VECTORS" "$FLAMENET_BACKUP_PAYLOAD_VECTORS" \
|
|
"$FLAMENET_CONTENT_VECTORS" "$FLAMENET_POLL_VECTORS" \
|
|
"$FLAMENET_PADDING_VECTORS" "$FLAMENET_SYNC_VECTORS" \
|
|
"$FLAMENET_HANDLE_CERT_VECTORS" "$FLAMENET_LINK_VECTORS" \
|
|
"$FLAMENET_BRIDGE_VECTORS" "$FLAMENET_CONTENT_KINDS_VECTORS" \
|
|
"$FLAMENET_TODAY_VECTORS" "$FLAMENET_HELLO_LINK_VECTORS"; do
|
|
[ -f "$f" ] || { echo "GATE: vector file missing: $f"; exit 1; }
|
|
done
|
|
# Clear old results so the gate never counts ghosts.
|
|
rm -rf ./*/build/test-results
|
|
# --rerun-tasks: a FROM-CACHE pass prints the same BUILD SUCCESSFUL
|
|
# as a real one. Both app variants: release is the one that ships.
|
|
./gradlew --no-daemon --rerun-tasks :engine:test :client:test :obfs4:test \
|
|
:games:test :app:testDebugUnitTest :app:testReleaseUnitTest
|
|
- name: Gate on the test results
|
|
run: |
|
|
# Assert on the XML, never on Gradle's exit status: `:app` prints no
|
|
# test names, and an up-to-date task is also "BUILD SUCCESSFUL".
|
|
python3 - <<'PY'
|
|
import glob, re, sys
|
|
|
|
# Floors near 90% of the suite (663 / 453 / 75 / 572, games 15).
|
|
# `app` counts both variants.
|
|
MINIMUMS = {"engine": 595, "client": 405, "obfs4": 68, "games": 13, "app": 515}
|
|
totals = {}
|
|
for module in MINIMUMS:
|
|
tests = failures = skipped = 0
|
|
for path in glob.glob(f"{module}/build/test-results/*/TEST-*.xml"):
|
|
text = open(path).read()
|
|
m = re.search(r'tests="(\d+)" skipped="(\d+)" failures="(\d+)" errors="(\d+)"', text)
|
|
if not m:
|
|
continue
|
|
tests += int(m.group(1)); skipped += int(m.group(2))
|
|
failures += int(m.group(3)) + int(m.group(4))
|
|
totals[module] = (tests, failures, skipped)
|
|
|
|
bad = False
|
|
for module, (tests, failures, skipped) in totals.items():
|
|
print(f"GATE: {module}: {tests} tests, {failures} failures, {skipped} skipped")
|
|
if tests < MINIMUMS[module]:
|
|
print(f"GATE: {module} reported {tests} tests, the suite did not run")
|
|
bad = True
|
|
if failures:
|
|
bad = True
|
|
grand = sum(t for t, _, _ in totals.values())
|
|
print(f"GATE: {grand} tests in total")
|
|
if grand < 1580:
|
|
print(f"GATE: only {grand} tests in the whole build; something is not running")
|
|
bad = True
|
|
sys.exit(1 if bad else 0)
|
|
PY
|
|
|
|
# The pin's conscience: the vector suites against flamenet-e2e HEAD.
|
|
# Allowed to fail. A red run means an update is waiting; see .e2e-ref.md.
|
|
vectors-head:
|
|
needs: image
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
continue-on-error: true
|
|
container:
|
|
image: git.extant2000.com/extant2000/flamenet-android-ci:jdk21.0.5-api35-r1
|
|
env:
|
|
OBFS4_INTEROP_OPTOUT: "1"
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: Vector suites at flamenet-e2e HEAD
|
|
run: |
|
|
set -euo pipefail
|
|
rm -rf .ci-vectors-head
|
|
git clone --depth 1 "$GITEA_BASE/flamenet-e2e.git" .ci-vectors-head
|
|
echo "pinned: $(cat .e2e-ref)"
|
|
echo "at HEAD: $(git -C .ci-vectors-head rev-parse HEAD)"
|
|
V="$GITHUB_WORKSPACE/.ci-vectors-head/test/vectors"
|
|
export FLAMENET_VECTORS="$V/protocol-v1.json"
|
|
export FLAMENET_PQ_VECTORS="$V/pqx3dh-v2.json"
|
|
export FLAMENET_ACCOUNT_VECTORS="$V/account-kdf-v1.json"
|
|
export FLAMENET_BACKUP_VECTORS="$V/backup-v1.json"
|
|
export FLAMENET_BACKUP_PAYLOAD_VECTORS="$V/backup-payload-v1.json"
|
|
export FLAMENET_CONTENT_VECTORS="$V/content-v1.json"
|
|
export FLAMENET_POLL_VECTORS="$V/poll-tally-v1.json"
|
|
export FLAMENET_PADDING_VECTORS="$V/padding-v1.json"
|
|
export FLAMENET_SYNC_VECTORS="$V/sync-merge-v1.json"
|
|
export FLAMENET_HANDLE_CERT_VECTORS="$V/handle-cert-v1.json"
|
|
export FLAMENET_LINK_VECTORS="$V/links-v1.json"
|
|
export FLAMENET_BRIDGE_VECTORS="$V/bridge-offer-v1.json"
|
|
export FLAMENET_CONTENT_KINDS_VECTORS="$V/content-kinds-v1.json"
|
|
export FLAMENET_TODAY_VECTORS="$V/today-v1.json"
|
|
export FLAMENET_HELLO_LINK_VECTORS="$V/hello-link-v1.json"
|
|
# Only the tests that read vectors: the engine, :obfs4 (the
|
|
# bridge-offer vectors, SPEC section 24), and the three 0.32 loaders
|
|
# in :client and :app.
|
|
./gradlew --no-daemon --rerun-tasks :engine:test :obfs4:test \
|
|
:client:test --tests io.flamenet.client.SharedVectorsTodayTest \
|
|
:app:testDebugUnitTest --tests io.flamenet.messenger.app.SharedVectorsContentKindsTest \
|
|
--tests io.flamenet.messenger.app.SharedVectorsHelloLinkTest
|