ci: move CI to Gitea Actions
ci / vectors-head (pull_request) Successful in 11s
ci / secret-sweep (pull_request) Successful in 22s
ci / parity-scrub (pull_request) Failing after 13s
ci / unit-tests (pull_request) Failing after 30m2s
ci / ui-tests (pull_request) Failing after 30m2s

Translates .gitlab-ci.yml to .gitea/workflows/ci.yml and deletes it.
Every gate is kept: secret sweep, unit tests with the vector pin check and
the 700-test floor, UI tests with the 10-ran floor, vectors-head (advisory),
parity scrub.

Xcode jobs run on the pauls-imac runner; the sweep, vectors-head and the
scrub run on mac1/mac2. Builds and tests only, no signing.

The Obfs4 submodule and the sibling clones now come from
git.extant2000.com/extant2000, as do the README links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-10-14 11:03:09 -04:00
co-authored by Claude Opus 5.5
parent b3c47c9926
commit 9099b6866c
6 changed files with 211 additions and 404 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# Why this repository pins flamenet-e2e
`.e2e-ref` is the commit of [flamenet-e2e](https://gitlab.com/extant2000/flamenet-e2e)
`.e2e-ref` is the commit of [flamenet-e2e](https://git.extant2000.com/extant2000/flamenet-e2e)
that the copies in `Tests/Vectors/` are checked against. See
`Tests/Vectors/PROVENANCE.md` for why they are copies at all.
@@ -16,7 +16,7 @@ the vectors move and nothing says so, `ProtocolVectorTests` quietly goes on
checking last month's protocol and reports green, which is precisely the
failure that suite exists to prevent, reintroduced one level up.
That is what `vectors-head` is for, and why it is `allow_failure: true`.
That is what `vectors-head` is for, and why it is `continue-on-error: true` in `.gitea/workflows/ci.yml`.
Being behind is LOUD and NON-BLOCKING. Being broken is a hard stop.
## Taking an update
+201
View File
@@ -0,0 +1,201 @@
# CI for the iOS app on Gitea Actions. Translated from .gitlab-ci.yml on
# 2026-10-13; every gate it had is kept.
#
# Builds and tests only. TestFlight and App Store uploads stay manual on the
# MacBook Air and the iMac; no signing material is in CI.
#
# Runners: the Xcode jobs need `pauls-imac` (host mode, Xcode 27, capacity 1,
# so they run one after the other). The three jobs that only read source run
# on mac1/mac2 (`ubuntu-latest`) to keep the iMac free.
name: ci
on:
push:
branches: [main]
pull_request:
env:
GITEA_BASE: https://git.extant2000.com/extant2000
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. No submodule fetch: a sweep of this repo is about this repo.
secret-sweep:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- run: bash Scripts/secret-sweep.sh
unit-tests:
runs-on: pauls-imac
timeout-minutes: 30
steps:
# The obfs4 transport is the Packages/Obfs4 submodule
# (flamenet-elligator2, public on Gitea). Without it package resolution
# fails with "the package manifest at Packages/Obfs4/Package.swift
# cannot be accessed".
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Unit tests
shell: bash
run: |
set -euo pipefail
# The selected Xcode, never a hardcoded beta path (see
# xcode-is-the-release-now). Printed so the log says what built.
export DEVELOPER_DIR="$(xcode-select -p)"
# A cancelled run can leave a wedged booted `iPhone 17`; the next job
# then waits until the timeout. Shut down only this device: the iMac
# is a workstation and other simulators are not this job's to close.
xcrun simctl shutdown "iPhone 17" 2>/dev/null || true
xcodebuild -version
# Vectors are copied into Tests/Vectors and pinned by .e2e-ref.
# Check the copy against the pinned flamenet-e2e commit.
# `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"
for f in protocol-v1 pqx3dh-v2 account-kdf-v1 backup-v1 backup-payload-v1 content-v1 \
poll-tally-v1 padding-v1 sync-merge-v1 handle-cert-v1 links-v1 bridge-offer-v1 \
content-kinds-v1 today-v1 hello-link-v1; do
if ! diff -q "ci-vectors/test/vectors/$f.json" "Tests/Vectors/$f.json" >/dev/null; then
echo "GATE: Tests/Vectors/$f.json differs from flamenet-e2e@$FLAMENET_E2E_PIN."
echo "GATE: the vectors moved. Read what changed, then refresh the copy:"
echo "GATE: cp ci-vectors/test/vectors/*.json Tests/Vectors/"
diff "ci-vectors/test/vectors/$f.json" "Tests/Vectors/$f.json" | head -20 || true
exit 1
fi
done
echo "GATE: protocol vectors match flamenet-e2e@$FLAMENET_E2E_PIN"
# Scripts/generate.sh, never bare `xcodegen generate`: only the script
# copies the pinned Package.resolved into the generated project.
Scripts/generate.sh
# Keep the ~700 MB build output out of Spotlight; indexing it drove
# the runner host to a load average in the hundreds.
mkdir -p ci-build && touch ci-build/.metadata_never_index
# `|| true`: the log is the gate, checked below.
xcodebuild -project FlamenetMessenger.xcodeproj -scheme FlamenetMessenger \
-sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 17" \
-derivedDataPath ci-build test -only-testing:FlamenetMessengerTests 2>&1 \
| tee ci-test.log \
| grep -E "Executed|TEST (SUCCEEDED|FAILED)" || true
# Assert on the LOG, never on grep's exit status: a failing run
# prints "TEST FAILED", grep matches it and exits 0.
grep -q '\*\* TEST SUCCEEDED \*\*' ci-test.log || {
echo "GATE: xcodebuild did not report '** TEST SUCCEEDED **'"
tail -40 ci-test.log; exit 1; }
grep -qE 'Executed [1-9][0-9]* tests, with 0 failures' ci-test.log || {
echo "GATE: no run of at least one test with zero failures"
tail -40 ci-test.log; exit 1; }
# Floor below today's count, so a suite that drops from 1289 to 12
# cannot pass. Raise it when it stops meaning anything.
COUNT=$(grep -oE 'Executed [0-9]+ tests, with 0 failures' ci-test.log \
| tail -1 | grep -oE '[0-9]+' | head -1)
if [ "${COUNT:-0}" -lt 1150 ]; then
echo "GATE: only ${COUNT:-0} tests ran; the suite is not all there"
tail -40 ci-test.log; exit 1
fi
echo "GATE: Executed $COUNT tests, with 0 failures"
- if: failure()
uses: actions/upload-artifact@v3
with:
name: ci-test-log
path: ci-test.log
retention-days: 7
# The 14 UI tests that need no live relay (69 more skip without one).
# Gating since 2026-09-25. Its own job so a UI failure reads as one.
ui-tests:
runs-on: pauls-imac
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: UI tests
shell: bash
run: |
set -euo pipefail
export DEVELOPER_DIR="$(xcode-select -p)"
xcrun simctl shutdown "iPhone 17" 2>/dev/null || true
Scripts/generate.sh
mkdir -p ci-build && touch ci-build/.metadata_never_index
xcodebuild -project FlamenetMessenger.xcodeproj -scheme FlamenetMessenger \
-sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 17" \
-derivedDataPath ci-build test -only-testing:FlamenetMessengerUITests 2>&1 \
| tee ci-ui.log \
| grep -E "Executed|TEST (SUCCEEDED|FAILED)" || true
grep -q '\*\* TEST SUCCEEDED \*\*' ci-ui.log || {
echo "GATE: xcodebuild did not report '** TEST SUCCEEDED **'"
tail -60 ci-ui.log; exit 1; }
# A floor on what actually RAN. Both summary shapes are read:
# Executed 83 tests, with 69 tests skipped and 0 failures ...
# Executed 14 tests, with 0 failures ...
RAN=$(grep -oE 'Executed [0-9]+ tests' ci-ui.log | tail -1 | grep -oE '[0-9]+' || true)
SKIPPED=$(grep -oE '[0-9]+ tests skipped' ci-ui.log | tail -1 | grep -oE '[0-9]+' || true)
ACTUAL=$(( ${RAN:-0} - ${SKIPPED:-0} ))
if [ "$ACTUAL" -lt 10 ]; then
echo "GATE: only ${ACTUAL} UI test(s) actually ran; the rest skipped"
tail -60 ci-ui.log; exit 1
fi
echo "GATE: ${ACTUAL} UI tests ran, ${SKIPPED:-0} skipped for want of a live relay"
- if: failure()
uses: actions/upload-artifact@v3
with:
name: ci-ui-log
path: ci-ui.log
retention-days: 7
# The pin's conscience: compares the carried vectors with flamenet-e2e HEAD.
# Allowed to fail. A red run means an update is waiting; see .e2e-ref.md.
vectors-head:
runs-on: ubuntu-latest
timeout-minutes: 10
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: Compare vectors with flamenet-e2e HEAD
run: |
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)"
behind=0
for f in protocol-v1 pqx3dh-v2 account-kdf-v1 backup-v1 backup-payload-v1 content-v1 \
poll-tally-v1 padding-v1 sync-merge-v1 handle-cert-v1 links-v1 bridge-offer-v1 \
content-kinds-v1 today-v1 hello-link-v1; do
if ! diff -q "ci-vectors-head/test/vectors/$f.json" "Tests/Vectors/$f.json" >/dev/null; then
echo "ADVISORY: $f.json has moved at HEAD:"
diff "ci-vectors-head/test/vectors/$f.json" "Tests/Vectors/$f.json" | head -30 || true
behind=1
fi
done
if [ "$behind" = "1" ]; then
echo "ADVISORY: the published vectors have changed. This is not a failure of"
echo "ADVISORY: this branch; it means an update is waiting. See .e2e-ref.md."
exit 1
fi
echo "ADVISORY: vectors are current with flamenet-e2e HEAD"
# The weekly scrub, on every change. Its checks are the silent ones: an
# envelope kind one app sends and the other cannot parse shows nothing at
# either end. The siblings are public, so they clone with no credentials.
parity-scrub:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Parity scrub
run: |
rm -rf ci-scrub && mkdir -p ci-scrub
git clone --depth 1 "$GITEA_BASE/flamenet-messenger-android.git" ci-scrub/android
git clone --depth 1 "$GITEA_BASE/flamenet-relay.git" ci-scrub/relay
# Non-zero on a HIGH, which is the whole contract.
python3 Scripts/parity-scrub.py --ios . --android ci-scrub/android --relay ci-scrub/relay
-394
View File
@@ -1,394 +0,0 @@
# T4: the Mac is in the merge path. Runs on the project's own macOS runner
# (tag mac-local), the unit suite that used to run only when someone
# remembered to. UI tests stay manual: they need a local relay and a booted
# pair of simulators.
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.
# No submodule fetch: the obfs4 transport is a private repository with its
# own tree, and a sweep of this one is about this one.
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 obfs4 transport is a submodule, and the runner skips those by
# default. Without this the job fails at package resolution with
# "the package manifest at Packages/Obfs4/Package.swift cannot be
# accessed", which reads as a missing file rather than a missing clone.
GIT_SUBMODULE_STRATEGY: recursive
# And this, because the submodule is a PRIVATE repo. It rewrites the
# submodule's URL to use `CI_JOB_TOKEN`, which is the only credential this
# job has. It also requires flamenet-elligator2 to allow this project in its
# CI job-token allowlist, a setting on the *other* project, so a failure
# here looks like a permissions problem with no local cause.
GIT_SUBMODULE_FORCE_HTTPS: "true"
GIT_SUBMODULE_DEPTH: "1"
script:
# The selected Xcode, not a hardcoded beta path. The beta became the
# release on 2026-09-15 and /Applications/Xcode-beta.app went with it,
# and since `git` on a Mac is an xcrun shim, this job then died before its
# first real command, on every pipeline for a day, with nothing in the
# summary but "failed". The version is printed so the log says what built.
- export DEVELOPER_DIR="$(xcode-select -p)"
# Shut the target simulator down before xcodebuild asks for it.
#
# A cancelled pipeline can leave CoreSimulator holding a booted, wedged
# `iPhone 17`, and the NEXT job then waits on a device that never becomes
# usable. That is not a hypothetical: on 2026-09-25 this job sat for
# thirty minutes and was killed by the job timeout, having printed no test
# output at all, five minutes after a pipeline was cancelled mid-build.
# Its normal run is under four minutes.
#
# `shutdown all` would be the usual line and is deliberately NOT used: this
# runner is a workstation, and a simulator somebody is debugging in is not
# this job's to close. Only the device the job is about to drive.
- xcrun simctl shutdown "iPhone 17" 2>/dev/null || true
- xcodebuild -version
# `Scripts/generate.sh`, never bare `xcodegen generate`. `.gitignore`
# excludes the whole `.xcodeproj`, and SwiftPM keeps `Package.resolved`
# INSIDE it, so the pin git can actually see lives in `Scripts/`, and only
# that script copies it into the generated workspace. Running xcodegen
# directly here meant CI resolved every dependency fresh against the
# network while local builds were correctly pinned, which is why this only
# ever broke on CI. It broke when upstream MOVED the
# `151.0.0` tag of stasel/WebRTC, a prebuilt XCFramework, from
# 19aa8c1f to 3e43fa9b. The pin is what noticed.
# The protocol vectors are copied into `Tests/Vectors`, so check the
# copy is current. They are copied rather than resolved from a sibling
# checkout because that is exactly how Android lost twenty conformance tests
# its engine looks for `../../../flamenet-e2e/...`, which exists on a
# developer's machine and never in a build directory, so its suite failed
# here from the day it was written without anyone seeing
# it. A copy in the test bundle cannot be absent. What a copy CAN do is go
# stale, so this is the other half of that bargain.
#
# Pinned, not tip. flamenet-relay#56 is the write-up of what tip costs: a
# merge in flamenet-e2e turned that repo's `main` red in a pipeline nobody
# was watching and blocked six MRs, and was misdiagnosed as runner
# starvation. The commit lives in `.e2e-ref`; `.e2e-ref.md` says how to move
# it.
#
# A pin ALONE is worse than tip, which is what the first version of
# this got wrong. Pinning silently means the vectors can move and
# `ProtocolVectorTests` goes on checking last month's protocol, reporting
# green, the very failure that suite exists to prevent, reintroduced one
# level up. `vectors-head` below is the other half.
- FLAMENET_E2E_PIN=$(cat .e2e-ref)
- rm -rf ci-vectors
- git clone -q https://gitlab.com/extant2000/flamenet-e2e.git ci-vectors
- git -C ci-vectors checkout -q "$FLAMENET_E2E_PIN"
- |
for f in protocol-v1 pqx3dh-v2 account-kdf-v1 backup-v1 backup-payload-v1 content-v1 \
poll-tally-v1 padding-v1 sync-merge-v1 handle-cert-v1 links-v1 bridge-offer-v1 \
content-kinds-v1 today-v1 hello-link-v1; do
if ! diff -q "ci-vectors/test/vectors/$f.json" "Tests/Vectors/$f.json" >/dev/null; then
echo "GATE: Tests/Vectors/$f.json differs from flamenet-e2e@$FLAMENET_E2E_PIN."
echo "GATE: the vectors moved. Read what changed, then refresh the copy:"
echo "GATE: cp ci-vectors/test/vectors/*.json Tests/Vectors/"
diff "ci-vectors/test/vectors/$f.json" "Tests/Vectors/$f.json" | head -20
exit 1
fi
done
# Inside the block, not a bare `- echo "GATE: ..."` list item. An
# unquoted `: ` in a plain scalar makes YAML read the item as a MAPPING,
# and the whole file becomes invalid, which GitLab reports as a pipeline
# that "failed" with zero jobs and no yaml_errors on the pipeline object.
# Nothing in that says "your CI file is malformed". `ci/lint` does.
echo "GATE: protocol vectors match flamenet-e2e@$FLAMENET_E2E_PIN"
- Scripts/generate.sh
# Keep the build output out of Spotlight.
#
# `ci-build` reaches ~700 MB and is rewritten every job. Indexing it drives
# this host, which is the mac-local runner and therefore in the merge path
# for both app repos, to a load average in the hundreds: `mds_stores` and
# `mdworker_shared` sit at the top of CPU while no `xcodebuild` process is
# near it. That makes every job slower and the machine unpleasant to use
# while CI runs.
#
# The marker has to be written HERE rather than committed, because the
# directory it belongs in is created by the build and removed between runs.
- mkdir -p ci-build && touch ci-build/.metadata_never_index
- |
# `|| true`, deliberately: the log is the gate, not this pipeline's
# exit status, and the checks below need to run so they can print the
# tail of the log when something goes wrong.
set -o pipefail
xcodebuild -project FlamenetMessenger.xcodeproj -scheme FlamenetMessenger \
-sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 17" \
-derivedDataPath ci-build test -only-testing:FlamenetMessengerTests 2>&1 \
| tee ci-test.log \
| grep -E "Executed|TEST (SUCCEEDED|FAILED)" || true
- |
# Assert on the LOG, never on grep's exit status. This job used to
# end in `| grep -E "Executed|TEST (SUCCEEDED|FAILED)" || exit 1`, and a
# pipeline exits with the status of its LAST command. A failing test run
# prints " TEST FAILED ", grep matched it, grep exited 0, and the
# job went green on a red suite. Every pass this job ever reported was
# unverified. It only ever went red when the build died before printing
# anything at all, which is failing for the wrong reason.
grep -q '\*\* TEST SUCCEEDED \*\*' ci-test.log || {
echo "GATE: xcodebuild did not report '** TEST SUCCEEDED **'"
tail -40 ci-test.log; exit 1; }
# `[1-9][0-9]*`, so a suite that ran nothing cannot pass. A tree that
# silently stops building tests otherwise reports the same green as one
# that runs them all.
grep -qE 'Executed [1-9][0-9]* tests, with 0 failures' ci-test.log || {
echo "GATE: no run of at least one test with zero failures"
tail -40 ci-test.log; exit 1; }
# A floor, because "at least one test" is not a gate. Twice now a
# green pipeline here has had to be checked by hand against main's count
# ios!133 and ios!134 both printed a truncated log with no sign of the
# tests that had just been added. A suite that silently dropped from 741
# to 12 passes the check above and looks identical in the log.
#
# Set below today's count, not equal to it: this must not need editing
# every time somebody adds a test. Raise it when it stops meaning
# anything.
COUNT=$(grep -oE 'Executed [0-9]+ tests, with 0 failures' ci-test.log \
| tail -1 | grep -oE '[0-9]+' | head -1)
if [ "${COUNT:-0}" -lt 700 ]; then
echo "GATE: only ${COUNT:-0} tests ran; the suite is not all there"
tail -40 ci-test.log; exit 1
fi
echo "GATE: Executed $COUNT tests, with 0 failures"
artifacts:
when: on_failure
paths: [ci-test.log]
expire_in: 1 week
timeout: 30m
# The rules engines for the games in a chat (§6.33), a local SwiftPM package
# with its own tests and the vectors in games-v1.json, which Android's `:games`
# module runs too. `swift test` needs no simulator; a cold run here took 12
# seconds. Its own job, so a rules failure reads as one.
games-engine:
stage: test
tags: [mac-local]
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
GIT_DEPTH: "1"
script:
- export DEVELOPER_DIR="$(xcode-select -p)"
# Keep the build output out of Spotlight, as in `unit-tests`.
- mkdir -p Packages/FlamenetGames/.build && touch Packages/FlamenetGames/.build/.metadata_never_index
- swift test --package-path Packages/FlamenetGames
timeout: 15m
# The 83 tests in `UITests/` that nothing ran.
#
# `unit-tests` above passes `-only-testing:FlamenetMessengerTests`, so the UI
# target was compiled by nobody and run by nobody on the way to main. That is
# the surface a person actually touches: sign-up, placing a call, the files
# vault, the iPad layout.
#
# Most of it cannot run here and is not meant to. 69 of the 83 gate on a live
# relay and real credentials, `XCTSkipUnless(relayIsUp(...))`, and skip cleanly
# without them; those exist to be pointed at a real deployment by hand, which
# is what `Scripts/` has drivers for. The remaining 14 need none of that and
# take about three and a half minutes.
#
# Its own job rather than folded into `unit-tests`, so that a UI failure reads
# as a UI failure. A swipe that lands early is a different kind of news from a
# unit test going red, and the floor logic in that job counts XCTest cases,
# which UI runs would muddle.
#
# GATING, since 2026-09-25. It did not for a while, and the reason is worth
# keeping: two local runs of this target produced three failures, all of them
# `app.launch()` timing out with "Error getting main window", in tests nobody
# had touched, while the box sat at a load average of 115. That was the
# machine rather than the tests, and it is the same host CI runs on, so
# gating then would have handed the merge path to whatever else was running.
#
# The flag came off on evidence: three consecutive CI runs at 4.6, 5.0 and
# 5.2 minutes, the last of them immediately after an unrelated job on this
# host hung for thirty minutes, so it was not a quiet machine that earned it.
# Those three were BRANCH pipelines, which is all this job used to run in;
# see the note on `rules` below for why that matters.
#
# A caution for whoever reads this next, because it nearly bought the wrong
# fix: two mac-local runners are registered, which LOOKS like this job and
# `unit-tests` can collide over one simulator. Their start and end times say
# they have always run strictly one after the other. The thirty-minute hang
# above was a wedged simulator left by a cancelled pipeline, which is what the
# `simctl shutdown` line in both jobs now clears. Check the timestamps before
# believing in a race.
#
# One real flake WAS found and fixed on the way:
# `FilesVaultUITests.testAFileCanBeDeletedOnItsOwn` failed one run in three by
# swiping a row the list had not finished settling. A swipe that lands early
# does nothing and leaves no trace, so waiting longer cannot help. It now waits
# for the row and retries the gesture once.
ui-tests:
stage: test
tags: [mac-local]
# Without these, this job is not in a merge request pipeline AT ALL.
#
# A job with no `rules` and no `only` runs on branch pipelines and is left
# out of merge request ones, and every other job in this file carries the
# two conditions below. So for as long as it had neither, `allow_failure`
# on this job was decorative: removing the flag changed nothing, because
# the job was never in the pipeline that gates a merge. Its clean runs were
# all branch pipelines, main and hand-triggered, none of them in anybody's
# merge path.
#
# Caught by reading the job list of the merge request pipeline rather than
# the YAML: the flag was gone and the job was absent.
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
script:
- export DEVELOPER_DIR=$(xcode-select -p)
# Shut the target simulator down before xcodebuild asks for it.
#
# A cancelled pipeline can leave CoreSimulator holding a booted, wedged
# `iPhone 17`, and the NEXT job then waits on a device that never becomes
# usable. That is not a hypothetical: on 2026-09-25 this job sat for
# thirty minutes and was killed by the job timeout, having printed no test
# output at all, five minutes after a pipeline was cancelled mid-build.
# Its normal run is under four minutes.
#
# `shutdown all` would be the usual line and is deliberately NOT used: this
# runner is a workstation, and a simulator somebody is debugging in is not
# this job's to close. Only the device the job is about to drive.
- xcrun simctl shutdown "iPhone 17" 2>/dev/null || true
- Scripts/generate.sh
- mkdir -p ci-build && touch ci-build/.metadata_never_index
- |
set -o pipefail
xcodebuild -project FlamenetMessenger.xcodeproj -scheme FlamenetMessenger \
-sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 17" \
-derivedDataPath ci-build test -only-testing:FlamenetMessengerUITests 2>&1 \
| tee ci-ui.log \
| grep -E "Executed|TEST (SUCCEEDED|FAILED)" || true
- |
# Assert on the LOG, never on grep's exit status, for the reason spelled
# out in `unit-tests`: a failing run PRINTS " TEST FAILED ", grep matches
# it, grep exits 0, and the job goes green on a red suite.
grep -q '\*\* TEST SUCCEEDED \*\*' ci-ui.log || {
echo "GATE: xcodebuild did not report '** TEST SUCCEEDED **'"
tail -60 ci-ui.log; exit 1; }
# A floor on what actually RAN, not on what was scheduled. Every one of
# these can skip, so "0 executed, 0 failures" is a perfectly green log
# for a target that did nothing, and that is the failure this job exists
# to not have.
# Both shapes of the summary line, because the skipped clause is only
# there when something skipped:
# Executed 83 tests, with 69 tests skipped and 0 failures ...
# Executed 14 tests, with 0 failures ...
# Reading only the first shape would make a run with NOTHING skipped
# come out as zero and fail this job for being healthy.
RAN=$(grep -oE 'Executed [0-9]+ tests' ci-ui.log | tail -1 | grep -oE '[0-9]+')
SKIPPED=$(grep -oE '[0-9]+ tests skipped' ci-ui.log | tail -1 | grep -oE '[0-9]+')
ACTUAL=$(( ${RAN:-0} - ${SKIPPED:-0} ))
# Set below today's 14, not equal to it, so adding a test that needs a
# relay does not need this number edited. Raise it when it stops meaning
# anything.
if [ "$ACTUAL" -lt 10 ]; then
echo "GATE: only ${ACTUAL} UI test(s) actually ran; the rest skipped"
tail -60 ci-ui.log; exit 1
fi
echo "GATE: ${ACTUAL} UI tests ran, ${SKIPPED:-0} skipped for want of a live relay"
artifacts:
when: on_failure
paths: [ci-ui.log]
expire_in: 1 week
timeout: 30m
# The pin's conscience. Compares the vectors this repo carries 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.
#
# Deliberately does NOT build the app. The question it answers, "did the
# published vectors move?", needs a clone and a diff, and nothing else. A job
# that took twenty minutes to say "you are two commits behind" would be turned
# off within a week.
#
# 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"
script:
- rm -rf ci-vectors-head
- git clone --depth 1 https://gitlab.com/extant2000/flamenet-e2e.git ci-vectors-head
- |
echo "pinned: $(cat .e2e-ref)"
echo "at HEAD: $(git -C ci-vectors-head rev-parse HEAD)"
behind=0
for f in protocol-v1 pqx3dh-v2 account-kdf-v1 backup-v1 backup-payload-v1 content-v1 \
poll-tally-v1 padding-v1 sync-merge-v1 handle-cert-v1 links-v1 bridge-offer-v1 \
content-kinds-v1 today-v1 hello-link-v1; do
if ! diff -q "ci-vectors-head/test/vectors/$f.json" "Tests/Vectors/$f.json" >/dev/null; then
echo "ADVISORY: $f.json has moved at HEAD:"
diff "ci-vectors-head/test/vectors/$f.json" "Tests/Vectors/$f.json" | head -30
behind=1
fi
done
if [ "$behind" = "1" ]; then
echo "ADVISORY: the published vectors have changed. This is not a failure of"
echo "ADVISORY: this branch; it means an update is waiting. See .e2e-ref.md."
exit 1
fi
echo "ADVISORY: vectors are current with flamenet-e2e HEAD"
timeout: 10m
# The weekly scrub, on every merge request instead of weekly.
#
# `Scripts/SCRUB.md` describes it as something to run with the three checkouts
# side by side, and its own docstring says the checks it carries are the ones
# whose failure mode is SILENT: an envelope kind one app sends and the other
# cannot parse shows nothing at either end and tells the sender nothing.
#
# A check that only runs when remembered stops running. On 2026-09-20 it had
# been reporting four HIGHs that were all wrong -- it knew two of the four
# shapes iOS uses to decide what an envelope is -- which is the state its own
# documentation says is worse than not having it, because people learn to skim
# a scrub that cries wolf.
#
# All three repositories are public, so the two siblings clone with no
# credentials, the same way `vectors-head` above takes flamenet-e2e. Shallow:
# this reads source, not history.
parity-scrub:
stage: test
tags: [mac-local]
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
GIT_DEPTH: "1"
script:
- rm -rf ci-scrub
- mkdir -p ci-scrub
- git clone --depth 1 https://gitlab.com/extant2000/flamenet-messenger-android.git ci-scrub/android
- git clone --depth 1 https://gitlab.com/extant2000/flamenet-relay.git ci-scrub/relay
# Non-zero on a HIGH, which is the whole contract.
- python3 Scripts/parity-scrub.py --ios . --android ci-scrub/android --relay ci-scrub/relay
timeout: 10m
+1 -1
View File
@@ -44,7 +44,7 @@ Repeatable via `Scripts/android-signup.sh` and `RelayTypesUITests`.
## 3, cross-relay messaging
**The relays do it.** `test/federation.mjs` (in [flamenet-e2e](https://gitlab.com/extant2000/flamenet-e2e) since the split) against the live fleet:
**The relays do it.** `test/federation.mjs` (in [flamenet-e2e](https://git.extant2000.com/extant2000/flamenet-e2e) since the split) against the live fleet:
**11 passed, 0 failed**: relay1 fetches bob's devices from relay2, sends
across the boundary, bob's relay accepts exactly one envelope, he polls and it
is there, it carries no sender attribution across the boundary, a relay off
+4 -4
View File
@@ -27,9 +27,9 @@ pipeline:
| Repo | What it is |
|---|---|
| [flamenet-e2e](https://gitlab.com/extant2000/flamenet-e2e) | The protocol: JS engine, specification, published vectors, the interop driver |
| [flamenet-relay](https://gitlab.com/extant2000/flamenet-relay) | The server |
| [flamenet-messenger-android](https://gitlab.com/extant2000/flamenet-messenger-android) | The Android app |
| [flamenet-e2e](https://git.extant2000.com/extant2000/flamenet-e2e) | The protocol: JS engine, specification, published vectors, the interop driver |
| [flamenet-relay](https://git.extant2000.com/extant2000/flamenet-relay) | The server |
| [flamenet-messenger-android](https://git.extant2000.com/extant2000/flamenet-messenger-android) | The Android app |
those three lived here as subtrees, and the public
repos were rsync extracts of them, refreshed by hand. The extracts drifted for
@@ -97,7 +97,7 @@ tick.
The server is its own repository now:
```bash
git clone https://gitlab.com/extant2000/flamenet-relay.git
git clone https://git.extant2000.com/extant2000/flamenet-relay.git
cd flamenet-relay
cp .env.example .env # then fill in the secrets it asks for
docker compose up -d
+3 -3
View File
@@ -1,7 +1,7 @@
# Where these came from, and why they are copied
These are the normative protocol vectors from
[`flamenet-e2e`](https://gitlab.com/extant2000/flamenet-e2e), at
[`flamenet-e2e`](https://git.extant2000.com/extant2000/flamenet-e2e), at
`test/vectors/`. That repo is the authority; this is a copy.
**They are copied rather than resolved from a sibling checkout because
@@ -23,7 +23,7 @@ that has to agree about it.
Refresh:
```
git clone --depth 1 https://gitlab.com/extant2000/flamenet-e2e.git /tmp/e2e
git clone --depth 1 https://git.extant2000.com/extant2000/flamenet-e2e.git /tmp/e2e
cp /tmp/e2e/test/vectors/*.json Tests/Vectors/
```
@@ -31,7 +31,7 @@ cp /tmp/e2e/test/vectors/*.json Tests/Vectors/
Copied on 2026-10-07 from `flamenet-e2e` branch `privacy/sweep-2026-10`,
commit `7036115`, which is not yet on that repository's `main`. So it is
deliberately NOT in the CI diff list in `.gitlab-ci.yml`: the pinned commit in
deliberately NOT in the CI diff list in `.gitea/workflows/ci.yml`: the pinned commit in
`.e2e-ref` has no such file, and the gate would fail on a file that does not
exist there. When the e2e branch merges, move `.e2e-ref` to a commit that has
it, add `padding-v1` to the list, and refresh `backup-payload-v1.json` and