diff --git a/doc/README.md b/doc/README.md index 68e1ed4..f4ebb8b 100644 --- a/doc/README.md +++ b/doc/README.md @@ -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 diff --git a/doc/architecture.md b/doc/architecture.md index 50976a6..3cca63e 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -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. diff --git a/doc/release.md b/doc/release.md new file mode 100644 index 0000000..d0ccbd8 --- /dev/null +++ b/doc/release.md @@ -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 `) + 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 ./...` 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. diff --git a/scripts/build_phone.sh b/scripts/build_phone.sh index 45a11a7..73e7a35 100755 --- a/scripts/build_phone.sh +++ b/scripts/build_phone.sh @@ -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 diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..253a308 --- /dev/null +++ b/scripts/release.sh @@ -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 +# ) 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 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 ==="