8.4 KiB
Physical-device and cross-client release gate
This runbook turns Milestone 10 into a repeatable release procedure. The gate uses a host-side CLI and USB/ADB as its control channel, so control traffic never shares BLE, Wi-Fi Aware, Nostr, or Tor with the system under test.
The gate cannot pass without the required physical devices and counterpart clients. A pending or blocked result is useful diagnostic evidence, but it is not release approval.
Safety and privacy rules
- Use only disposable lab app data, identities, nicknames, messages, and files.
- Never use a personal Nostr account or a production relay.
- Do not put device serials, UDIDs, Bluetooth/MAC/IP addresses, peer IDs, usernames, email addresses, local home paths, or message contents in a result, trace, filename, issue, commit, or release artifact.
- Device selectors may be supplied to ADB commands as ephemeral inputs. The
tooling emits only logical aliases such as
android-current. - Models, manufacturer classes, Android API levels, negotiated MTU classes, client versions, commit hashes, aggregate counts, durations, and stable failure reason codes are allowed.
- Do not archive raw logcat. Convert observations to the structured, privacy-checked trace format, and keep any raw diagnostic capture local until it has been reviewed and sanitized.
The validator rejects known identifying fields and values before a passing bundle can be created.
Required lab
Prepare:
- At least three physical Android devices for three-hop relay testing.
- At least two Android API levels and two manufacturer classes.
- BLE central and peripheral support on every Android device.
- At least one Android 13+ device with Wi-Fi Aware.
- One physical device running the current iOS client.
- The last supported Android client.
- The release-candidate APK built from one exact full Git commit.
- A local, disposable Nostr relay/Tor fixture with production network access blocked.
One physical handset may be reused for the legacy-client phase after the current-client evidence for that slot is complete, but the matrix must keep the logical aliases and installed client versions unambiguous.
1. Verify the deterministic gate
From the repository root:
./gradlew clientRewriteContractTest checkChangedLineCoverage lintDebug
python3 tools/release_gate/release_gate.py validate-manifest
Do not begin device work from a dirty tree or a build whose deterministic gate does not pass.
2. Create the device matrix
Copy tools/release_gate/device-matrix.example.json to an ignored working
directory under release-gate-results/. Replace every template value and set
both current-Android commit fields to the exact full commit under test.
Probe Android capabilities without storing the ADB selector:
python3 tools/release_gate/android_lab.py probe \
--serial "$BITCHAT_ADB_SELECTOR" \
--alias android-current
Copy only the returned logical metadata into the matrix. Validate it:
python3 tools/release_gate/release_gate.py validate-matrix \
--matrix release-gate-results/device-matrix.json \
--commit "$BITCHAT_RELEASE_COMMIT"
The matrix validator enforces physical devices, three Android participants, two API levels, two manufacturer classes, Wi-Fi Aware, BLE roles, iOS, and explicit current/legacy client versions.
3. Initialize disposable fixtures
python3 tools/release_gate/release_gate.py init \
--matrix release-gate-results/device-matrix.json \
--commit "$BITCHAT_RELEASE_COMMIT" \
--run-id rc-lab-01 \
--output release-gate-results/rc-lab-01
Initialization pins the scenario and fixture manifests, creates every scenario
as pending, and generates deterministic:
- zero-byte and small files;
- a Unicode-named medium file;
- sparse exact-maximum and oversized boundary files.
The fixture manifest records size and SHA-256. The final archive contains the manifest, not the large fixture bodies.
Clear only the disposable app data on each selected lab device:
python3 tools/release_gate/android_lab.py prepare \
--serial "$BITCHAT_ADB_SELECTOR" \
--confirm-disposable-app-data
This stops the app and runs package-data cleanup. The explicit confirmation is required because the operation is destructive to that app's local data.
4. Execute scenarios
The canonical scenario list is
tools/release_gate/scenarios.json. It contains 27 mandatory scenarios:
- the complete physical transport matrix;
- Android API/manufacturer/permission/background coverage;
- 11 Android-to-Android workflows;
- 8 cross-client/backward-compatibility workflows;
- 6 background and endurance workflows.
For each scenario:
- Confirm the listed participants and capabilities.
- Perform the corresponding steps in device-transport-test-matrix.md and the Milestone 10 checklist.
- Record connection, lifecycle, transport, receipt, resource, and terminal state as aggregate evidence.
- Append at least one structured trace event.
- Mark the scenario
pass,fail,blocked, orunsupported.
Record evidence with the exact keys declared by the scenario:
python3 tools/release_gate/release_gate.py record \
--run release-gate-results/rc-lab-01 \
--scenario A2A-001 \
--status pass \
--evidence connection-transitions=4 \
--evidence packet-correlation-count=6 \
--evidence failure-reasons=none
Append a privacy-safe trace event:
python3 tools/release_gate/release_gate.py trace \
--run release-gate-results/rc-lab-01 \
--scenario A2A-001 \
--source android-current \
--event reconnect-terminal \
--outcome pass \
--metric reconnect-count=1 \
--metric duplicate-delivery-count=0
Capture resource snapshots during endurance work:
python3 tools/release_gate/android_lab.py snapshot \
--serial "$BITCHAT_ADB_SELECTOR" \
--alias android-current \
--run release-gate-results/rc-lab-01 \
--scenario END-003
Use run-local sequential correlation labels while observing packets; archive only aggregate correlation counts. Record failures with a stable reason code, file a regression issue, and preserve the incomplete artifact.
5. Endurance requirements
END-001requires at least 240 minutes.END-002requires at least 50 large-transfer/cancellation cycles.- Sample memory, threads, file descriptors, wake locks, connection counts, and late callbacks at consistent intervals.
- A passing result requires bounded resource behavior and a clean terminal state; merely completing the time window is insufficient.
The validator rejects shorter durations and cycle counts.
6. Inspect progress and validate
During a run:
python3 tools/release_gate/release_gate.py validate \
--run release-gate-results/rc-lab-01 \
--allow-incomplete
python3 tools/release_gate/release_gate.py summary \
--run release-gate-results/rc-lab-01
The release validator, without --allow-incomplete, requires:
- every scenario to be
pass; - every declared evidence field;
- at least one structured trace per scenario;
- the pinned scenario and fixture manifests;
- the exact client commit and complete device matrix;
- endurance minimums;
- a completion timestamp;
- no detected identifying fields or values.
unsupported, blocked, and pending never satisfy release approval.
7. Archive release approval
After the complete validator passes:
python3 tools/release_gate/release_gate.py bundle \
--run release-gate-results/rc-lab-01 \
--output release-gate-results/rc-lab-01.zip
The deterministic archive contains the scenario manifest, device/client matrix,
results, structured trace, fixture manifest, Markdown summary, and
SHA256SUMS. Attach it to the release approval record without renaming fields
or adding raw diagnostics.
Finally, clean the disposable app data with the same confirmed cleanup
command and stop the local relay/Tor fixture.
Failure handling
fail: behavior violated a contract. Record a stable reason code, file a bug, add a deterministic regression where possible, fix it, and rerun the affected scenario plus dependent scenarios.blocked: required lab infrastructure or counterpart client was unavailable. Preserve the artifact and do not approve release.unsupported: the selected device lacks a capability. Because the defined matrix requires Wi-Fi Aware, replace the device or matrix; unsupported does not waive a mandatory scenario.- A flaky result is a failure until its cause is understood. Never average retries into a pass.