Merge branch 'maint'

Carries today's maint batch: the local CI image-scoping work, which gives
each run its own build context and test image tag instead of writing the
shared mutable one, and the retirement of the bloom-storm chaos scenario.

Both apply unchanged here — the compose files, suite lists and matrix legs
they touch are identical on the two lines, so no branch adaptation was
needed. Parity stays symmetric across runners at 21 legs a side on this
branch and 24 on maint, the gap being the three rekey Docker suites maint
keeps by design.
This commit is contained in:
Johnathan Corgan
2026-07-26 17:16:26 +00:00
36 changed files with 423 additions and 485 deletions
+74 -13
View File
@@ -31,7 +31,6 @@
# nat-lan, nostr-publish-consume, stun-faults,
# chaos-churn-mixed-10, chaos-ethernet-mesh,
# chaos-ethernet-only, chaos-tcp-mesh, chaos-congestion-stress,
# chaos-bloom-storm,
# sidecar, dns-resolver, deb-install
#
# Opt-in (require --with-tor; depend on live Tor network):
@@ -152,7 +151,6 @@ CHAOS_SUITES=(
"ethernet-only ethernet-only"
"tcp-mesh tcp-mesh"
"congestion-stress congestion-stress"
"bloom-storm bloom-storm"
)
# Scenarios retired 2026-07-23 because their subject was a pure decision the
# Docker harness could not test reliably, now covered by sans-IO unit tests:
@@ -175,6 +173,24 @@ CHAOS_SUITES=(
# plus end-to-end datagram delivery (src/node/tests/forwarding.rs). Real-
# UDP convergence smoke still runs via static-mesh and the other chaos
# scenarios' baseline assertions, so no Docker coverage is lost.
#
# Retired 2026-07-26 for a third reason — the guard's power was never
# established, and unlike the two blocks above this one DOES lose coverage:
# - bloom-storm: guarded the regression in 0caef2a (fixed by 4cdf382), where
# a mid-chain tree update changing neither root nor depth leaked downstream
# as a sustained bloom announce storm. It was never once run against that
# regressed binary; its ceiling was inferred from a separate post-mortem
# harness that no longer exists in the tree. On the only regressed
# measurement that survives, the tail node's rate scales to ~7 sends per
# 30s, well under the scenario's ceiling of 40 — so it is not established
# that the assertion could ever have fired on its own bug class. The
# ceiling is also uniform per node, calibrated against the flap target
# (legitimately ~24) while the storm's actual signature is at the tail,
# which sits at 0 on fixed code. COVERAGE GAP: nothing now exercises
# downstream containment of a mid-chain ancestor swap. The scenario, its
# README, the link_swap sim primitive and the mesh-lab dispatch all stay
# on disk and it remains runnable by hand via
# testing/chaos/scripts/chaos.sh bloom-storm.
GATEWAY_SUITES=(gateway)
SIDECAR_SUITES=(sidecar)
FIREWALL_SUITES=(firewall)
@@ -288,8 +304,9 @@ record() {
#
# This script may be preempted (a CI worker sends SIGTERM, waits ~30s, then
# SIGKILL) so it can restart on a newer tip. To make that safe:
# * every docker resource is namespaced to THIS run (compose project prefix
# + per-run image tags) so a restart never collides with a dying run;
# * every docker resource is namespaced to THIS run (compose project prefix,
# per-run image tags, per-run build context) so a restart never collides
# with a dying run, and neither does a concurrent one;
# * a trap tears down everything this run created on signal/exit, bounded by
# `timeout` so a stuck `down` cannot wedge the trap (SIGKILL is the backstop).
@@ -326,6 +343,14 @@ CI_LABEL_RUN="com.corganlabs.fips-ci.run=${CI_RUN_ID}"
export FIPS_CI_RUN_ID="$CI_RUN_ID"
export FIPS_TEST_IMAGE="$CI_IMAGE_TEST"
export FIPS_TEST_APP_IMAGE="$CI_IMAGE_APP"
# The build context is this run's too. testing/docker/ is a single directory in
# the working tree, so two runs racing on its CONTENTS produce a per-run-tagged
# image built from the other run's binaries — scoping the tag alone does not
# close that. Absolute, and it has to be: compose interpolates this into
# build.context but resolves a relative result against the compose FILE's
# directory, not the working directory.
CI_BUILD_CONTEXT="$SCRIPT_DIR/docker-${CI_RUN_ID}"
export FIPS_BUILD_CONTEXT="$CI_BUILD_CONTEXT"
# Docker container names are GLOBAL — a compose project name does not scope
# them — so the suite compose files append this suffix to every explicit
# container_name, and the suite scripts append it wherever they address a
@@ -413,6 +438,15 @@ ci_teardown() {
rm -rf "$SCRIPT_DIR/static/generated-configs${CI_RUN_NAME_SUFFIX}"
rm -rf "$SCRIPT_DIR/firewall/generated-configs${CI_RUN_NAME_SUFFIX}"
fi
# 4. This run's build context. Unlike the generated configs it is removed
# on a red run too: it holds the binaries and the Dockerfiles, both
# reproducible from the commit, so it is never the evidence of a
# failure. Guarded on the path having been derived at all, so an early
# exit cannot turn this into `rm -rf $SCRIPT_DIR/docker-`.
if [[ -n "${CI_BUILD_CONTEXT:-}" && "$CI_BUILD_CONTEXT" != "$SCRIPT_DIR/docker-" ]]; then
rm -rf "$CI_BUILD_CONTEXT"
fi
}
on_signal() {
@@ -786,24 +820,39 @@ run_tor_directory() {
run_integration() {
stage "Stage 3: Integration Tests"
# Install binaries to shared docker context
# Populate THIS run's build context, then install the binaries into it.
# Everything but the binaries is copied from the tracked context directory;
# the binaries are installed fresh, and a previous run's are deliberately
# not carried over, since inheriting them is the failure this scoping
# exists to prevent.
info "Preparing build context $CI_BUILD_CONTEXT"
rm -rf "$CI_BUILD_CONTEXT"
mkdir -p "$CI_BUILD_CONTEXT" || { record "docker-build" 1; return; }
local _f
for _f in "$SCRIPT_DIR"/docker/*; do
case "$(basename "$_f")" in
fips|fipsctl|fipstop|fips-gateway) continue ;;
esac
cp -a "$_f" "$CI_BUILD_CONTEXT/" || { record "docker-build" 1; return; }
done
info "Installing release binaries"
install_binaries testing/docker
install_binaries "$CI_BUILD_CONTEXT"
# Build unified test image once (used by all harnesses). Tag per-run
# (fips-test:${run}) so a build killed mid-flight never wedges the next
# run's rebuild, and concurrent runs never clobber each other's image.
# Then retag :latest for the compose files / harness scripts that still
# reference fips-test:latest directly; the retag happens only after BOTH
# builds succeed, so :latest never points at a half-built image.
info "Building $CI_IMAGE_TEST Docker image"
docker build -t "$CI_IMAGE_TEST" --label "$CI_LABEL" --label "$CI_LABEL_RUN" testing/docker --quiet \
docker build -t "$CI_IMAGE_TEST" --label "$CI_LABEL" --label "$CI_LABEL_RUN" "$CI_BUILD_CONTEXT" --quiet \
|| { record "docker-build" 1; return; }
docker build -t "$CI_IMAGE_APP" --label "$CI_LABEL" --label "$CI_LABEL_RUN" \
-f testing/docker/Dockerfile.app testing/docker --quiet \
-f "$CI_BUILD_CONTEXT/Dockerfile.app" "$CI_BUILD_CONTEXT" --quiet \
|| { record "docker-build-app" 1; return; }
docker tag "$CI_IMAGE_TEST" fips-test:latest
docker tag "$CI_IMAGE_APP" fips-test-app:latest
# Deliberately NOT retagged to fips-test:latest. Every consumer reads
# FIPS_TEST_IMAGE, and a bridge back to the shared mutable name would let a
# consumer that does not keep working silently — resolving whichever
# concurrent run wrote the tag last, and recording that run's binaries under
# this run's commit. Without the bridge a missed consumer fails loudly.
# Single suite mode
if [[ -n "$ONLY_SUITE" ]]; then
@@ -1011,6 +1060,17 @@ run_ci_parity() {
record "ci-parity" $rc
}
# Nothing may resolve the shared mutable test image. This run does not write
# fips-test:latest, so a consumer naming it fails loudly here and now — but only
# on a host with no hand-built copy lying around, which is not a property to
# rely on. This is the static half of that.
run_image_scoping() {
local rc=0
info "[image-scoping] Checking that nothing names the shared test image"
"$SCRIPT_DIR/check-image-scoping.sh" || rc=$?
record "image-scoping" $rc
}
# Every daemon log string a test matches on must still be emitted by src/.
# A stale one does not fail — it stops observing, and an expect-zero assertion
# built on it then passes for the wrong reason.
@@ -1064,6 +1124,7 @@ main() {
run_ci_parity
run_log_strings
run_trailing_log
run_image_scoping
run_wait_converge
if [[ "$TEST_ONLY" == true ]]; then