Files
flamenet-messenger-android/.gitlab-ci.yml
T
Paul HittandClaude Opus 5.5 9f4cb84009 Bridges: a screen before sign-in, several lines with failover, §24 receive rules
- "Connect through a bridge" on the sign-in and sign-up screen opens the
  bridge panel before any account exists: paste or scan a line, the live
  status, remove a line.
- Up to 8 stored lines (migrated from the one-line store), tried one at a
  time with the one that last worked first. OkHttp's own route loop does the
  trying: while lines are stored the resolver answers every name with one
  placeholder per line, so no name is looked up on the local network, and a
  refused connection, no handshake within 15 s, or TLS failing inside the
  tunnel moves to the next line. accounts.flamenet.io and the relay keep
  their own names through the tunnel, so the edge behind the bridge routes
  each to its real service. Every line failing never connects directly, and
  the error says no bridge could be reached.
- Offers: a gid, a line over 1024 bytes, or a line that is not a string is
  dropped; an oversized or blank line is never built. Ports are digits only.
- Held to bridge-offer-v1.json in :engine and :obfs4; .e2e-ref moves to
  flamenet-e2e c75b861 and CI exports FLAMENET_BRIDGE_VECTORS.
- Citations of §6.19 for the bridge offer now say §24.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:56:30 -04:00

227 lines
13 KiB
YAML

# T4: the Mac is in the merge path. Engine, client, obfs4 and app unit suites on
# the project's own runner (tag mac-local). Instrumented tests stay manual:
# they need an emulator and a local relay.
stages: [test]
# The repository is public, so this sweep is the last thing between a pasted
# key and the internet. Until 2026-09-17 it only ran when somebody remembered
# to, and it had been red for nine days without anyone noticing, because a
# formatter carried an allow marker off the line it covered. A check nobody
# runs protects nothing; here it runs on every merge request and every push
# to main, and it finishes in seconds.
secret-sweep:
stage: test
tags: [mac-local]
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
GIT_DEPTH: "1"
script:
- bash scripts/secret-sweep.sh
timeout: 5m
unit-tests:
stage: test
tags: [mac-local]
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
GIT_DEPTH: "1"
# The interop tests FAIL rather than skip when unconfigured, on purpose,
# a silent skip is how a transport suite goes green while proving nothing.
# They need a real lyrebird from the flamenet-elligator2 checkout, which is
# not in this build directory, so they are opted out HERE and nowhere else.
# Run `scripts/obfs4-interop.sh` locally before merging any obfs4 change:
# every other test in that module compares against recorded vectors or
# against itself, and two identically-wrong ends agree perfectly.
OBFS4_INTEROP_OPTOUT: "1"
script:
- export JAVA_HOME=/opt/homebrew/opt/openjdk@21
- export ANDROID_HOME=$HOME/Library/Android/sdk
# The protocol vectors are not in this repo, and CI never had them.
# `AccountKdfTest`, `PQVectorTest` and `PublishedVectorsTest` look for a
# sibling `flamenet-e2e` checkout. That exists on a developer's machine and
# has never existed in a build directory, so all twenty of them have been
# failing here since they were written, invisibly, because the build cache
# replayed a local pass. They fail rather than skip when the file is
# missing, exactly so this could not hide; it took `--rerun-tasks` to let
# them say so.
#
# Pinned to a commit, NOT cloned at tip. Tip was the first instinct,
# "if the protocol changes this SHOULD go red", and flamenet-relay#56 is
# the write-up of what that actually costs. There, a job clones
# `flamenet-e2e` at HEAD, so merging anything in that repo turned relay's
# `main` red in a pipeline nobody was watching, blocked six MRs sitting on
# merge-when-pipeline-succeeds, and was misdiagnosed as runner starvation.
# It fired twice in one evening. The person who merged was working in a
# different project and had no reason to look.
#
# A pin makes the same divergence a deliberate, reviewable bump here instead
# of a surprise failure in a repo nobody was thinking about. The commit
# lives in `.e2e-ref`; `.e2e-ref.md` says how to take an update.
#
# A pin ALONE is worse than HEAD, and that is what the first version
# of this got wrong. Pinning silently means the vectors can move and this
# suite goes on checking last month's protocol, reporting green, the exact
# "tested against a pinned old spec" failure the relay's own note warns
# about. `vectors-head` below is the other half: it runs the same checks at
# HEAD and is allowed to fail, so being behind is LOUD and non-blocking
# while being broken stays a hard stop.
#
# `flamenet-e2e` is public, so no credentials are needed.
- FLAMENET_E2E_PIN=$(cat "$CI_PROJECT_DIR/.e2e-ref")
- rm -rf "$CI_PROJECT_DIR/.ci-vectors"
- git clone -q https://gitlab.com/extant2000/flamenet-e2e.git "$CI_PROJECT_DIR/.ci-vectors"
- git -C "$CI_PROJECT_DIR/.ci-vectors" checkout -q "$FLAMENET_E2E_PIN"
- export FLAMENET_VECTORS="$CI_PROJECT_DIR/.ci-vectors/test/vectors/protocol-v1.json"
- export FLAMENET_PQ_VECTORS="$CI_PROJECT_DIR/.ci-vectors/test/vectors/pqx3dh-v2.json"
- export FLAMENET_ACCOUNT_VECTORS="$CI_PROJECT_DIR/.ci-vectors/test/vectors/account-kdf-v1.json"
- export FLAMENET_BACKUP_VECTORS="$CI_PROJECT_DIR/.ci-vectors/test/vectors/backup-v1.json"
- export FLAMENET_BACKUP_PAYLOAD_VECTORS="$CI_PROJECT_DIR/.ci-vectors/test/vectors/backup-payload-v1.json"
- export FLAMENET_CONTENT_VECTORS="$CI_PROJECT_DIR/.ci-vectors/test/vectors/content-v1.json"
- export FLAMENET_POLL_VECTORS="$CI_PROJECT_DIR/.ci-vectors/test/vectors/poll-tally-v1.json"
- export FLAMENET_PADDING_VECTORS="$CI_PROJECT_DIR/.ci-vectors/test/vectors/padding-v1.json"
- export FLAMENET_SYNC_VECTORS="$CI_PROJECT_DIR/.ci-vectors/test/vectors/sync-merge-v1.json"
- export FLAMENET_HANDLE_CERT_VECTORS="$CI_PROJECT_DIR/.ci-vectors/test/vectors/handle-cert-v1.json"
- export FLAMENET_LINK_VECTORS="$CI_PROJECT_DIR/.ci-vectors/test/vectors/links-v1.json"
- export FLAMENET_BRIDGE_VECTORS="$CI_PROJECT_DIR/.ci-vectors/test/vectors/bridge-offer-v1.json"
# Checked before the suite runs. A path that silently does not exist puts
# the tests straight back where they were, and the failure would read as a
# protocol bug rather than a missing file.
- |
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"; do
[ -f "$f" ] || { echo "GATE: vector file missing: $f"; exit 1; }
done
# Clear old results before running, or the gate counts ghosts. The
# runner reuses its build directory, so XML from a previous pipeline
# survives, and a suite that stopped running entirely would still be
# counted, by the very check meant to notice that it had stopped.
- rm -rf ./*/build/test-results
# Keep the build output out of Spotlight.
#
# The module build directories are rewritten every job on a host that is
# also the mac-local runner for both app repos. Indexing them drives the
# machine to a load average in the hundreds, with `mds_stores` and
# `mdworker_shared` at the top of CPU while no build process is near it.
# Measured on the iPhone repo: the same from-scratch build peaks at load
# ~12 with its output excluded and well over a hundred without.
#
# Written here rather than committed, because the directories these belong
# in are created by the build.
- |
for m in app engine client obfs4; do
mkdir -p "$m/build" && touch "$m/build/.metadata_never_index"
done
# `:obfs4:test` was missing from this list. The module shipped in !77
# with ~100 tests, the Elligator2 port, the framing, the handshake, the
# socket factory, and not one of them ran in CI. A break in the transport
# would have merged green.
#
# `--rerun-tasks` because this runner is the developer's own Mac.
# Gradle's build cache is shared with local runs, so a task whose inputs a
# local run already covered comes back FROM-CACHE and CI replays a result it
# never computed. That is not independent verification, and it is invisible:
# a cached pass and a real pass print the same "BUILD SUCCESSFUL".
# Both app variants, not just debug. The release variant was never run
# here, so four tests that die on it -- the merged manifest has no
# ComponentActivity, which is a debug-only dependency -- were red from the
# day they landed and CI never said a word. A variant nobody tests is a
# variant nobody knows about, and release is the one that ships.
- ./gradlew --no-daemon --rerun-tasks :engine:test :client:test :obfs4:test :app:testDebugUnitTest :app:testReleaseUnitTest
- |
# Assert on the RESULTS, never on Gradle's exit status. `:app` prints
# no per-test names at all, so a filter matching nothing and a full green
# run are indistinguishable in the log, and a skipped-as-up-to-date task
# is likewise just "BUILD SUCCESSFUL". The XML is the only thing that says
# what actually executed.
python3 - <<'PY'
import glob, re, sys
# Floors, not exact counts. Set around 90% of the suite as it stands
# (663 / 453 / 75 / 572) so a module that loses most of its
# tests to a bad filter is caught, without a gate that has to be edited
# every time somebody adds one.
#
# `app` counts both variants, because the glob takes every
# `test-results/*` directory and both now run.
MINIMUMS = {"engine": 595, "client": 405, "obfs4": 68, "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
# A floor across the whole build as well as per module: a module can be
# present and still have lost most of its suite to a bad filter.
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
timeout: 30m
# The pin's conscience. Runs the vector suites against flamenet-e2e's
# actual HEAD, and is allowed to fail. Without it, `.e2e-ref` is permission to
# fall behind forever with a green board, which is the failure mode a pin
# introduces and the reason the relay pairs its pin with the same job.
#
# A red run here means an update is waiting. Take it per `.e2e-ref.md`.
vectors-head:
stage: test
tags: [mac-local]
allow_failure: true
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
GIT_DEPTH: "1"
OBFS4_INTEROP_OPTOUT: "1"
script:
- export JAVA_HOME=/opt/homebrew/opt/openjdk@21
- export ANDROID_HOME=$HOME/Library/Android/sdk
- rm -rf "$CI_PROJECT_DIR/.ci-vectors-head"
- git clone --depth 1 https://gitlab.com/extant2000/flamenet-e2e.git "$CI_PROJECT_DIR/.ci-vectors-head"
- |
echo "pinned: $(cat "$CI_PROJECT_DIR/.e2e-ref")"
echo "at HEAD: $(git -C "$CI_PROJECT_DIR/.ci-vectors-head" rev-parse HEAD)"
- export FLAMENET_VECTORS="$CI_PROJECT_DIR/.ci-vectors-head/test/vectors/protocol-v1.json"
- export FLAMENET_PQ_VECTORS="$CI_PROJECT_DIR/.ci-vectors-head/test/vectors/pqx3dh-v2.json"
- export FLAMENET_ACCOUNT_VECTORS="$CI_PROJECT_DIR/.ci-vectors-head/test/vectors/account-kdf-v1.json"
- export FLAMENET_BACKUP_VECTORS="$CI_PROJECT_DIR/.ci-vectors-head/test/vectors/backup-v1.json"
- export FLAMENET_BACKUP_PAYLOAD_VECTORS="$CI_PROJECT_DIR/.ci-vectors-head/test/vectors/backup-payload-v1.json"
- export FLAMENET_CONTENT_VECTORS="$CI_PROJECT_DIR/.ci-vectors-head/test/vectors/content-v1.json"
- export FLAMENET_POLL_VECTORS="$CI_PROJECT_DIR/.ci-vectors-head/test/vectors/poll-tally-v1.json"
- export FLAMENET_PADDING_VECTORS="$CI_PROJECT_DIR/.ci-vectors-head/test/vectors/padding-v1.json"
- export FLAMENET_SYNC_VECTORS="$CI_PROJECT_DIR/.ci-vectors-head/test/vectors/sync-merge-v1.json"
- export FLAMENET_HANDLE_CERT_VECTORS="$CI_PROJECT_DIR/.ci-vectors-head/test/vectors/handle-cert-v1.json"
- export FLAMENET_LINK_VECTORS="$CI_PROJECT_DIR/.ci-vectors-head/test/vectors/links-v1.json"
- export FLAMENET_BRIDGE_VECTORS="$CI_PROJECT_DIR/.ci-vectors-head/test/vectors/bridge-offer-v1.json"
# Only the modules that read vectors. The rest of the suite has nothing
# to say about whether flamenet-e2e moved, and running it here would double
# the board's cost to answer a question it cannot answer. `:obfs4` reads
# the bridge-offer vectors (SPEC section 24).
- ./gradlew --no-daemon --rerun-tasks :engine:test :obfs4:test
timeout: 20m