Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Debug APK

<!-- CI posts a "Debug APK" comment with the download link once the build
finishes. Paste that link here, or write "see the Debug APK comment". -->

## Manual test checklist

<!-- 3-6 steps someone can follow on a phone, each with the expected result. -->

1.
2.
3.

## What changed

<!-- What the user will notice, then the main code changes. Link the issue or
Discord report this fixes. -->

## Risks

<!-- What could break, and on which radios, devices, or Android versions. Write
"Low" and say why if it really is low. -->

## Tested automatically

<!-- New or changed unit tests and what they cover. Note anything that only CI
ran (instrumented tests, lint). -->
142 changes: 142 additions & 0 deletions .github/workflows/pr-debug-apk.yml
Original file line number Diff line number Diff line change
@@ -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 = '<!-- ft8af-debug-apk -->';
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 });
}
26 changes: 26 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,32 @@ Remove the worktree when the branch is merged: `git worktree remove <path>`.
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

`<type>/<short-slug>`, where `<type>` 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<N>-<sha>-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
Expand Down
Loading