From f6dee78203a26d822d30e4b7256d4438f1e17c66 Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Mon, 19 Oct 2026 20:35:14 -0500 Subject: [PATCH 1/5] Add PR debug APK workflow, PR template, and PR conventions Every PR that touches ft8af/** now gets an installable debug APK, uploaded as an artifact and linked from a sticky PR comment. The PR build in android.yml is an unsigned release APK, which phones refuse to install. Also adds a PR description template (APK link, manual test checklist, what changed, risks, automated tests) and branch-name and PR rules in AGENTS.md. Co-Authored-By: Claude Opus 5.5 --- .github/pull_request_template.md | 27 ++++++ .github/workflows/pr-debug-apk.yml | 142 +++++++++++++++++++++++++++++ AGENTS.md | 26 ++++++ 3 files changed, 195 insertions(+) create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/pr-debug-apk.yml diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..c547bf0c0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,27 @@ +## Debug APK + + + +## Manual test checklist + + + +1. +2. +3. + +## What changed + + + +## Risks + + + +## Tested automatically + + diff --git a/.github/workflows/pr-debug-apk.yml b/.github/workflows/pr-debug-apk.yml new file mode 100644 index 000000000..ed695ac4d --- /dev/null +++ b/.github/workflows/pr-debug-apk.yml @@ -0,0 +1,142 @@ +name: PR debug APK + +# Builds an installable debug APK for every Android PR so it can be tested by +# hand on a phone. The PR build in android.yml is an UNSIGNED release APK, which +# Android refuses to install. This one is signed with the runner's debug key and +# installs on any device that allows sideloading. It is uploaded as an artifact, +# and a single sticky PR comment, updated on each push, links to the newest one. +# +# Tests and the release build still run in android.yml, whose `android-gate` is +# the required status check. Don't make this job required: it is path-filtered +# at the trigger, so on PRs that don't touch Android it never reports a status, +# and a required check would sit pending forever. + +on: + pull_request: + branches: [main, staging, dev] + paths: + - 'ft8af/**' + - '.github/workflows/pr-debug-apk.yml' + +permissions: + contents: read + pull-requests: write + +# A new push to the PR makes the previous APK stale, so cancel its build. +concurrency: + group: pr-debug-apk-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + debug-apk: + name: Debug APK for manual testing + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Cache Gradle dependencies + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('ft8af/**/*.gradle*', 'ft8af/gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + gradle-${{ runner.os }}- + + - name: Grant execute permission for gradlew + run: chmod +x ft8af/gradlew + + # Same retry as android.yml; keep NDK_VERSION in sync with it. + - name: Pre-install NDK (retry around flaky SDK downloads) + run: | + set -euo pipefail + NDK_VERSION=25.1.8937393 + SDKMANAGER="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" + MARKER="$ANDROID_HOME/ndk/$NDK_VERSION/source.properties" + for attempt in 1 2 3; do + echo "::group::NDK install attempt $attempt" + rm -rf "$ANDROID_HOME/ndk/$NDK_VERSION" "$HOME/.android/cache" + yes | "$SDKMANAGER" --install "ndk;$NDK_VERSION" || true + echo "::endgroup::" + if [[ -f "$MARKER" ]]; then + echo "NDK $NDK_VERSION ready at $ANDROID_HOME/ndk/$NDK_VERSION" + exit 0 + fi + echo "Attempt $attempt failed; retrying in 10s..." + sleep 10 + done + echo "::error::Failed to install NDK $NDK_VERSION after 3 attempts" + exit 1 + + - name: Build debug APK + working-directory: ft8af + run: ./gradlew assembleDebug --stacktrace + + - name: Name the APK after the PR and commit + id: apk + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + name="FT8AF-pr${PR_NUMBER}-${HEAD_SHA:0:7}-debug" + mv ft8af/app/build/outputs/apk/debug/app-debug.apk "ft8af/app/build/outputs/apk/debug/${name}.apk" + echo "name=$name" >> "$GITHUB_OUTPUT" + + - name: Upload debug APK artifact + id: upload + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.apk.outputs.name }} + path: ft8af/app/build/outputs/apk/debug/${{ steps.apk.outputs.name }}.apk + if-no-files-found: error + retention-days: 30 + + - name: Link the APK in the run summary + env: + ARTIFACT_URL: ${{ steps.upload.outputs.artifact-url }} + APK_NAME: ${{ steps.apk.outputs.name }} + run: | + echo "Debug APK: [${APK_NAME}.zip](${ARTIFACT_URL})" >> "$GITHUB_STEP_SUMMARY" + + # Fork PRs get a read-only token, so they can't comment. Their APK is + # still on the run page, through the summary link above. + - name: Post or update the APK comment on the PR + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + uses: actions/github-script@v7 + env: + ARTIFACT_URL: ${{ steps.upload.outputs.artifact-url }} + APK_NAME: ${{ steps.apk.outputs.name }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + with: + script: | + const marker = ''; + const { ARTIFACT_URL, APK_NAME, HEAD_SHA, RUN_URL } = process.env; + const body = [ + marker, + '### Debug APK', + '', + `**[Download ${APK_NAME}.zip](${ARTIFACT_URL})** (built from ${HEAD_SHA.slice(0, 7)}, [run](${RUN_URL}))`, + '', + 'Unzip it and install the `.apk` on the phone. The download needs a signed-in GitHub account, and the link expires after 30 days.', + 'This build is signed with a debug key. If the Play Store build is installed, uninstall it first; Android will not install over an app signed with a different key.', + ].join('\n'); + const { owner, repo } = context.repo; + const issue_number = context.payload.pull_request.number; + const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100 }); + const existing = comments.find(c => c.user.type === 'Bot' && c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + } diff --git a/AGENTS.md b/AGENTS.md index 59780008b..1047860f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,6 +68,32 @@ Remove the worktree when the branch is merged: `git worktree remove `. When nothing else is in flight in your checkout, a feature branch in place is fine, but a worktree per task is still the recommended default. +### Branch names + +`/`, where `` is `feat`, `fix`, `chore`, or `docs` +(e.g. `fix/civ-address-hex`). When the work comes from a tracked task, put its +id first in the slug, e.g. `fix/bur-12-cq-button-stuck`. + +### Pull requests + +- **Agents never merge.** Open the PR against `dev`, then stop. The maintainer + merges after testing the APK on a phone. +- **Fill in every section of the PR template** + (`.github/pull_request_template.md`): the debug APK link, a 3-6 step + manual test checklist with expected results, what changed, risks, and what was + tested automatically. +- **The debug APK comes from CI.** For any PR that touches `ft8af/**`, + `.github/workflows/pr-debug-apk.yml` builds `FT8AF-pr--debug.apk` + and posts (or updates) one "Debug APK" comment on the PR with the download + link. Before you hand off, wait for that comment and link it in the + description. If the job fails, the PR isn't ready. +- **CI must be green.** That means `android-gate` (unit tests and the release + build) plus `Debug APK for manual testing`. +- **Request a Copilot review** once CI is green, and address or answer each + comment it leaves. +- **Label PRs that come from Discord feedback** with `from-discord`, and link + the Discord message or the tracking task in "What changed". + ## Testing **Every new code path requires a new test.** Any branch, helper, or behavior From 81f8774e394a649b76bcd515323a69696d6cd203 Mon Sep 17 00:00:00 2001 From: patrickrb <6586559+patrickrb@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:22:38 -0500 Subject: [PATCH 2/5] Add Copilot-first, Claude code-review fallback to PR conventions Copilot review is enabled but can run out of credits. PRs now request Copilot first, fall back to Claude's code-review skill, address every finding before QA, and record which reviewer was used. Co-Authored-By: Claude Opus 5.5 --- .github/pull_request_template.md | 8 ++++++++ AGENTS.md | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c547bf0c0..3c9fc2328 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -25,3 +25,11 @@ Discord report this fixes. --> + +## Code review + + + +- Reviewer used: +- [ ] Every finding addressed or answered diff --git a/AGENTS.md b/AGENTS.md index 1047860f2..b18634c2d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,8 +89,11 @@ id first in the slug, e.g. `fix/bur-12-cq-button-stuck`. description. If the job fails, the PR isn't ready. - **CI must be green.** That means `android-gate` (unit tests and the release build) plus `Debug APK for manual testing`. -- **Request a Copilot review** once CI is green, and address or answer each - comment it leaves. +- **Get a code review before QA.** Once CI is green, request a Copilot review + first. If Copilot doesn't respond (it can run out of credits), run Claude's + `code-review` skill on the PR instead. Address or answer every finding before + handing off to QA, and fill in the "Code review" section of the template + with the reviewer you used. - **Label PRs that come from Discord feedback** with `from-discord`, and link the Discord message or the tracking task in "What changed". From 59a3b84d851db90b30a39428c66e1c6b06823c6b Mon Sep 17 00:00:00 2001 From: patrickrb <6586559+patrickrb@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:26:45 -0500 Subject: [PATCH 3/5] PR debug APK: shared debug key, build the PR head, retry on 429 From a Claude code-review pass (Copilot is out of credits): - Sign with a committed debug keystore so each new PR APK installs over the previous one instead of failing with a signature mismatch. - Check out the PR head SHA so the APK matches the commit it is named after. - Retry assembleDebug like android.yml, since Maven Central rate-limits runners. Co-Authored-By: Claude Opus 5.5 --- .github/debug.keystore | Bin 0 -> 2690 bytes .github/workflows/pr-debug-apk.yml | 30 ++++++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 .github/debug.keystore diff --git a/.github/debug.keystore b/.github/debug.keystore new file mode 100644 index 0000000000000000000000000000000000000000..6c5c50bf1675863d740c7356dae2998fc26752c1 GIT binary patch literal 2690 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 Date: Tue, 20 Oct 2026 14:30:58 -0500 Subject: [PATCH 4/5] PR debug APK: pass the shared keystore to Gradle directly Copying it to ~/.android/debug.keystore was ignored on the runner (the APK still carried a fresh runner key). Use AGP's injected signing properties. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-debug-apk.yml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pr-debug-apk.yml b/.github/workflows/pr-debug-apk.yml index 2867a0602..696be737c 100644 --- a/.github/workflows/pr-debug-apk.yml +++ b/.github/workflows/pr-debug-apk.yml @@ -40,14 +40,6 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - # Every runner would otherwise generate its own debug key, so each new APK - # would refuse to install over the last one. This shared key is public on - # purpose: it only signs test builds, never anything shipped. - - name: Use the shared debug keystore - run: | - mkdir -p ~/.android - cp .github/debug.keystore ~/.android/debug.keystore - - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -89,14 +81,24 @@ jobs: echo "::error::Failed to install NDK $NDK_VERSION after 3 attempts" exit 1 + # Sign with the shared .github/debug.keystore. Otherwise every runner makes + # its own debug key and each new APK refuses to install over the last one. + # The key is public on purpose: it only signs test builds, never releases. + # # Maven Central rate-limits GitHub runner IPs (HTTP 429); retry like # android.yml does. - name: Build debug APK working-directory: ft8af run: | set -euo pipefail + signing=( + -Pandroid.injected.signing.store.file="$GITHUB_WORKSPACE/.github/debug.keystore" + -Pandroid.injected.signing.store.password=android + -Pandroid.injected.signing.key.alias=androiddebugkey + -Pandroid.injected.signing.key.password=android + ) for attempt in 1 2 3; do - if ./gradlew assembleDebug --stacktrace; then + if ./gradlew assembleDebug "${signing[@]}" --stacktrace; then exit 0 fi echo "::warning::Gradle assemble failed (attempt ${attempt}/3) — retrying after backoff" From 9727f5a8feac09552ff6ead89a3554c881d5c135 Mon Sep 17 00:00:00 2001 From: patrickrb <6586559+patrickrb@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:14:07 -0500 Subject: [PATCH 5/5] PR conventions: device testing only for needs-device-test PRs, QA evidence The maintainer tests on a device once per release batch (the staging Play internal build) instead of installing every PR's APK. Per-PR device testing is limited to PRs labeled needs-device-test (transmit/PTT, CAT, USB audio or serial, audio routing, permissions, signing/release config). QA posts emulator screenshots or a recording as evidence before handoff. Co-Authored-By: Claude Opus 5.5 --- .github/pull_request_template.md | 19 ++++++++++++++++++- AGENTS.md | 31 ++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3c9fc2328..14dfee2e9 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,9 +3,20 @@ +## Device test + + + +- [ ] Needs device test (labeled `needs-device-test`): +- [ ] Emulator is enough + ## Manual test checklist - + 1. 2. @@ -33,3 +44,9 @@ ran (instrumented tests, lint). --> - Reviewer used: - [ ] Every finding addressed or answered + +## QA evidence + + + diff --git a/AGENTS.md b/AGENTS.md index b18634c2d..6aba5bf40 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,7 +77,9 @@ id first in the slug, e.g. `fix/bur-12-cq-button-stuck`. ### Pull requests - **Agents never merge.** Open the PR against `dev`, then stop. The maintainer - merges after testing the APK on a phone. + merges. They test on a phone only for `needs-device-test` PRs (see "Device + testing" below); for everything else they merge on the CI, code review, and + QA evidence. - **Fill in every section of the PR template** (`.github/pull_request_template.md`): the debug APK link, a 3-6 step manual test checklist with expected results, what changed, risks, and what was @@ -96,6 +98,33 @@ id first in the slug, e.g. `fix/bur-12-cq-button-stuck`. with the reviewer you used. - **Label PRs that come from Discord feedback** with `from-discord`, and link the Discord message or the tracking task in "What changed". +- **QA attaches evidence.** After code review, QA installs the debug APK on an + emulator, runs the PR's manual test checklist, and posts screenshots or a + short screen recording to the PR. A PR without that evidence isn't ready + for the maintainer. + +### Device testing + +The maintainer doesn't install every PR's APK. Device testing happens once per +release batch, on the Play internal build that `staging` produces, which holds +every PR merged into `dev` since the last batch. Per-PR device testing is only +for changes an emulator can't check. Label those PRs `needs-device-test`: + +- transmit, PTT, or anything that keys the radio +- CAT / rig control (CI-V, Hamlib, FlexRadio, etc.) +- USB audio or USB serial +- audio routing, input/output device selection, or sample-rate handling +- runtime permissions, or what's declared in `AndroidManifest.xml` +- signing, build types, `versionCode`, or other release config + +The author applies the label and says why in the template's "Device test" +section. QA checks the label against the diff and adds it if it's missing. +When in doubt, add it. Everything else (UI, decode/display logic, settings, +CI, docs) merges on CI + code review + QA evidence. + +`from-discord` fixes are confirmed by the reporter: once the fix reaches a Play +testing track, `discord-play-track-watch` posts to Discord and the reporter +checks it on their own setup. ## Testing