Make the release process script-driven and documented

The release flow, install policy, and on-device rules previously lived in
session context. Now:

- scripts/release.sh: the executable release — gate 1 static checks,
  gate 2 full go test (incl. TestNoFramesWhileIdle), gate 3 frame-regression
  profile on the emulator, build (SKIP_CHECK=1 avoids double static checks),
  then install to EVERY connected device (emulators and phones; the
  wireless-debugging serial changes per reconnect, the loop sidesteps it).
  Any gate failure aborts before install; dirty tree is warned, not fatal.
- doc/release.md: the process, the install-to-all-devices default, the rule
  that a phone is INSTALL-ONLY during a release (on-device
  profiling/testing is diagnostic and needs explicit approval per run),
  failure-handling table, versioning gap (still gogio default 1.0.0.1), and
  emulator requirements.
- doc/README.md: release.md added to the doc table.
- architecture.md §11: pointer to the release flow.

Validated end-to-end: release.sh ran all three gates (PASS), built the APK,
and installed it on both the phone and the emulator in one command.
This commit is contained in:
Greg Pomerantz 2026-08-20 14:54:48 -04:00
parent 8bdf916d8a
commit f31f849665
5 changed files with 150 additions and 3 deletions

View File

@ -1,6 +1,6 @@
# Pad documentation
Three documents, kept at the level of *what, why, and invariants* — not
Four documents, kept at the level of *what, why, and invariants* — not
line-by-line code — so they stay true as the implementation evolves.
| Doc | What it is |
@ -8,6 +8,7 @@ line-by-line code — so they stay true as the implementation evolves.
| [`spec.md`](./spec.md) | What the app **actually does**, measured performance, the code layout, and an explicit list of deferred features. |
| [`architecture.md`](./architecture.md) | How it works: single-owner concurrency model, channel topology, Frame handoff contract, ownership rules, editor/browser/render internals. |
| [`development_plan.md`](./development_plan.md) | The active plan: completed phases, remaining work, and the on-device observation loop. |
| [`release.md`](./release.md) | The release process (`scripts/release.sh`): the gates, the install-to-all-devices policy, and the rule that on-device profiling/testing is diagnostic and needs explicit approval. |
## Package inventory

View File

@ -586,6 +586,10 @@ cannot measure this app because it renders into a `SurfaceView`).
app and seeds/removes a file in its storage, and the developer may be using
the phone while a release runs. It exists to investigate a suspected
frame/battery problem, not to certify a release.
The full release flow (gates → build → install to all connected devices,
and the policy that a phone is install-only during a release) is
`doc/release.md` + `scripts/release.sh`.
- The profiler is owned by the goroutine that creates it and is single-goroutine
(no locks). It does **not** `Sync()` the CSV per flush (only per row batch) to
avoid periodic fsync hitches in the logic path.

74
doc/release.md Normal file
View File

@ -0,0 +1,74 @@
# Release process
A release is **`./scripts/release.sh`** — that one command is the
executable form of this document. If this document and the script ever
disagree, fix both in the same change.
## What a release does
1. **Gate 1 — static checks**: `scripts/check.sh` (go vet + staticcheck).
2. **Gate 2 — test suite**: `go test -count=1 ./...` — includes
`TestNoFramesWhileIdle` (internal/test/e2e), the headless
frame-regression guard.
3. **Gate 3 — frame-regression profile, EMULATOR only**:
`scripts/profile_emulator.sh` (auto-selects an emulator; it never
auto-selects a phone). See architecture.md §11 for what it measures and
why the budgets are shaped the way they are.
4. **Build**: `scripts/build_phone.sh --no-install` (arm64+arm APK →
`cmd/pad/pad-phone.apk`; static checks are skipped here, already gate 1).
5. **Install to every connected device** — emulators **and phones**.
Any gate failure aborts before the install. The working tree may be dirty;
the script warns, but the gates then certify the *current* (possibly
uncommitted) state — commit the release work before shipping.
## Install policy
- Pushing a release build to the developer's phone is the **default and
expected** behavior — `release.sh` installs to all `adb devices` entries,
which is what makes the phone just work without knowing its serial
(the wireless-debugging `IP:port` changes on every reconnect; the
install loop sidesteps that entirely).
- On a phone that has never had the app, grant **"All files access"**
(MANAGE_EXTERNAL_STORAGE) in system settings after the first install.
## On-device profiling/testing: NOT part of a release
The release touches a phone **only to install the APK**. It never
profiles, tests, force-stops, or otherwise drives a phone:
- On-device runs (e.g. `scripts/profile_emulator.sh -s <phone serial>`)
are **diagnostics**: they force-stop the app, seed and remove a file in
its storage, and read its profiler output. The developer may be using the
phone while a release runs.
- Therefore an on-device run requires **explicit approval in the moment**
(ask first, name the serial), and its results are informational, never a
gate. The frame-regression *gate* is tier 2 (headless) + tier 3
(emulator) only.
## Failure handling
| Gate | Fails on | First things to check |
|---|---|---|
| 1 static | vet/staticcheck findings | The findings; don't waive without a reason in the commit message |
| 2 tests | any test | `go test -count=1 -run <Name> ./...` to isolate; e2e tests are deterministic — a flake is a bug in the test |
| 3 profile | a phase over its frame budget | The printed phases + `PERF`/`PERF-PRESENT` logcat lines (a spinner is usually obvious); confirm the emulator wasn't under host load (rerun once before investigating) |
| build | gogio/apktool/sign | Toolchain notes in `scripts/build_phone.sh` header |
If the profile gate is in question, a diagnostic run **on the phone**
(requires approval) is the escalation path — not a replacement for the gate.
## Versioning (known gap)
The APK is currently built with the gogio-default version
(`1.0.0.1`); releases do **not** bump a version yet. When release
distribution starts mattering, add a version step here (and to
`release.sh`) — until then, "which release" is identified by the git
commit the APK was built from.
## Emulator requirements
Gate 3 needs a running emulator (`adb devices` shows `emulator-*`).
`scripts/emu.sh` manages the AVD (see its header); the AVD must have had
"All files access" granted once (it has). No phone needs to be connected
for a release to succeed — the install step simply reports no devices.

View File

@ -31,8 +31,14 @@ command -v apksigner >/dev/null 2>&1 || die "apksigner not found (need $ANDROID_
command -v adb >/dev/null 2>&1 || die "adb not found (need $ANDROID_HOME/platform-tools)"
[ -f "$HOME/.android/debug.keystore" ] || die "debug keystore missing: $HOME/.android/debug.keystore"
echo "=== static checks (go vet + staticcheck) ==="
"$REPO/scripts/check.sh"
# SKIP_CHECK=1 skips the static checks (scripts/release.sh already runs them
# as gate 1); a standalone build always runs them.
if [ "${SKIP_CHECK:-0}" = "1" ]; then
echo "=== static checks (skipped: SKIP_CHECK=1) ==="
else
echo "=== static checks (go vet + staticcheck) ==="
"$REPO/scripts/check.sh"
fi
APKTOOL="$ANDROID_HOME/tools/apktool.jar"
if [ ! -f "$APKTOOL" ]; then

62
scripts/release.sh Executable file
View File

@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Pad release: run the release gates, build the phone APK, and install it to
# every connected device.
#
# The full process, policies, and failure handling are documented in
# doc/release.md — this script is the executable form of that document.
#
# Gates (ALL must pass; any failure aborts before the install):
# 1. static checks: go vet + staticcheck (scripts/check.sh)
# 2. go test -count=1 ./... (includes TestNoFramesWhileIdle)
# 3. frame-regression profile on the EMULATOR (scripts/profile_emulator.sh)
#
# Install policy (doc/release.md):
# - The built APK is installed to EVERY connected device — emulators and
# phones. Pushing a release build to the developer's phone is the
# DEFAULT, expected behavior.
# - This script installs ONLY. It never profiles or tests a phone.
# On-device profiling/testing (e.g. scripts/profile_emulator.sh -s
# <phone serial>) is diagnostic and requires explicit developer approval
# per run.
#
# Usage:
# ./scripts/release.sh # gates + build + install to all devices
# ./scripts/release.sh --no-install
set -euo pipefail
REPO=$(cd "$(dirname "$0")/.." && pwd)
INSTALL=1
[ "${1:-}" = "--no-install" ] && INSTALL=0
if [ -n "$(git -C "$REPO" status --porcelain 2>/dev/null)" ]; then
echo "NOTE: working tree is dirty — the gates run on the current (possibly"
echo " uncommitted) state. Commit the release work before shipping."
fi
echo "=== gate 1/3: static checks ==="
"$REPO/scripts/check.sh"
echo "=== gate 2/3: go test (includes TestNoFramesWhileIdle) ==="
(cd "$REPO" && go test -count=1 ./...)
echo "=== gate 3/3: frame-regression profile (emulator only) ==="
"$REPO/scripts/profile_emulator.sh"
echo "=== build phone APK ==="
SKIP_CHECK=1 "$REPO/scripts/build_phone.sh" --no-install
if [ "$INSTALL" = "1" ]; then
echo "=== install to all connected devices ==="
serials=$(adb devices | awk 'NR>1 && $2=="device" {print $1}')
if [ -z "$serials" ]; then
echo "no connected devices — APK built at cmd/pad/pad-phone.apk;"
echo "install later with: adb -s <serial> install -r cmd/pad/pad-phone.apk"
else
for s in $serials; do
echo "-- installing on $s"
adb -s "$s" install -r "$REPO/cmd/pad/pad-phone.apk"
done
fi
fi
echo "=== RELEASE COMPLETE ==="