docs/release-gate-runbook.md
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.
android-current.The validator rejects known identifying fields and values before a passing bundle can be created.
Prepare:
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.
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.
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.
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:
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.
The canonical scenario list is
tools/release_gate/scenarios.json. It contains 27 mandatory scenarios:
For each scenario:
pass, fail, blocked, or unsupported.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.
END-001 requires at least 240 minutes.END-002 requires at least 50 large-transfer/cancellation cycles.The validator rejects shorter durations and cycle counts.
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:
pass;unsupported, blocked, and pending never satisfy 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.
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.For day-to-day development there is a lighter-weight harness that drives a
debug-only broadcast receiver (app/src/debug/, never shipped in release)
exposing mesh operations over ADB: scan, connect, Noise handshake, DMs,
public broadcast, live push-to-talk, announce, file send/receive, BLE toggle, state dumps, and
raw packet injection. Results are JSON files in the app sandbox polled by the
host (cache/testhook/results/<id>.json, also logged under tag TestHook).
JAVA_HOME) and the
Android SDK platform-tools. adb must be on PATH or ANDROID_HOME set.adb devices → note the serials.Keep both phones unlocked with the screen on for the whole run. A locked
or dozing device forces the app into POWER_SAVER (1 s BLE scan per 60 s),
which makes discovery and handshakes take minutes and will flake every
scenario. The harness runs wake() (dismiss keyguard, stretch screen
timeout) during setup, but it cannot defeat a secure lock screen — unlock
the devices manually first. Note that svc power stayon only helps while a
device is actually charging.
./gradlew assembleDebug
python3 tools/release_gate/mesh_lab.py setup \
--serial-a <serial-1> --serial-b <serial-2> \
--apk app/build/outputs/apk/debug/app-arm64-v8a-debug.apk
setup cycles Bluetooth, installs the APK, clears app data, grants all
runtime permissions, wakes and launches the app, sets deterministic nicknames
(alice/bob), and waits for mutual peer discovery. It is safe (and
recommended) to rerun setup before each scenario batch; --apk may be
omitted if the current build is already installed.
python3 tools/release_gate/mesh_lab.py scenario all \
--serial-a <serial-1> --serial-b <serial-2> --out /tmp/meshlab-evidence
| Scenario | What it asserts |
|---|---|
dm | Noise handshake both ways, encrypted DM round trips with content match |
favorite_verification | favorite signal, orange-outline/filled mutual state, and peer fingerprint verification |
broadcast | public mesh message A→B |
ptt_dm | Noise-encrypted 440 Hz PTT in both directions; asserts real-time capture, zero sequence gaps, decoded PCM duration/energy/continuity, and finalized-note absorption |
ptt_broadcast | signed public 440 Hz PTT with the same bidirectional packet and decoded-audio quality assertions |
file | 1 KB broadcast file, receiver SHA-256 matches fixture |
file_oversize | >256-fragment broadcast file is rejected sender-side, receiver sees nothing |
file_private | Noise-encrypted private file, digest match |
media_private | private-chat contact ID resolves to the live mesh peer; voice, image, and generic-file digests match |
raw | raw packet injection is accepted by the mesh |
session_recovery | force-stop B mid-session: identity persists, re-handshake, DMs flow again |
identity_reset | pm clear B mid-session: new identity, rediscovery, handshake, DMs |
all | every scenario above in sequence |
Each run writes <scenario>-evidence.json to --out (digests, timings,
session states, logcat excerpts on failure) and exits non-zero on failure.
Evidence is a local diagnostic artifact; it may contain lab peer IDs and is
not privacy-checked like release-gate bundles — do not publish it.
Any hook command can be sent to one device directly:
python3 tools/release_gate/mesh_lab.py cmd --serial <serial> scan --extra timeout_ms=30000
python3 tools/release_gate/mesh_lab.py cmd --serial <serial> handshake --extra peer=<peer-id>
python3 tools/release_gate/mesh_lab.py cmd --serial <serial> state # full mesh dump
See TestHookDriver.kt for the full command set (ping, start, stop,
whoami, set_nickname, scan, peers, connect, handshake, session,
announce, broadcast_msg, dm_send, dm_recv, msg_recv, favorite_set,
favorite_status, verification_set, verification_status, file_send,
file_recv, file_cancel, ptt_send, ptt_recv, raw_send, ble, state,
clear_results).
setup. cmd ... state shows
App In Background: true and the BLE duty cycle when this is the cause.svc bluetooth disable/enable
on both devices (done automatically by setup) clears zombie GATT links.adb -s <serial> logcat -s TestHook MessageHandler FragmentManager BitchatFilePacket shows commands, results, decrypt
failures, fragment rejects, and saved incoming files in real time.run-as com.bitchat.droid cat cache/testhook/results/<id>.json.Unlike the release gate, this harness is a development aid: it prints raw diagnostics and does not produce a privacy-checked approval bundle.