Skip to content
Merged
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
119 changes: 119 additions & 0 deletions .github/workflows/release-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: Build unsigned GitHub release APK

on:
push:
tags:
- '[0-9]*'
workflow_dispatch:
inputs:
ref:
description: Tag or commit to build
required: true
type: string

permissions:
contents: read

concurrency:
group: release-build-${{ inputs.ref || github.ref }}
cancel-in-progress: false

jobs:
build-unsigned:
name: Build unsigned APK on Linux
runs-on: ubuntu-22.04
timeout-minutes: 60

steps:
- name: Check out release source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: ${{ inputs.ref || github.ref }}
submodules: recursive
persist-credentials: false

- name: Set up JDK 21
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5
with:
java-version: '21'
distribution: temurin

- name: Install pinned Android build tools
run: |
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" \
"platforms;android-37" \
"build-tools;36.1.0" \
"cmake;3.22.1" \
"ndk;27.2.12479018"

- name: Set up pinned Rust toolchain
run: ./scripts/setup_rust_android.sh

- name: Set up Gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
with:
cache-read-only: true

- name: Prefetch Gradle dependencies
run: |
./gradlew :app:dependencies :app:prefetchAndroidLintDependencies \
--no-daemon --no-configuration-cache

- name: Build unsigned GitHub release APK offline
env:
CARGO_TARGET_DIR: ${{ runner.temp }}/tc-wgbridge-target
run: |
./gradlew :app:assembleGithubRelease --offline \
--no-build-cache --no-configuration-cache --no-daemon

- name: Verify and collect unsigned APK
id: artifact
env:
RELEASE_REF: ${{ inputs.ref || github.ref_name }}
run: |
BUILD_TOOLS="$ANDROID_HOME/build-tools/36.1.0"
mapfile -t apks < <(find app/build/outputs/apk/github/release \
-type f -name '*.apk' -print)
if (( ${#apks[@]} != 1 )); then
printf 'Expected exactly one release APK, found %d\n' "${#apks[@]}" >&2
printf '%s\n' "${apks[@]}" >&2
exit 1
fi

apk=${apks[0]}
if "$BUILD_TOOLS/apksigner" verify "$apk" >/dev/null 2>&1; then
echo 'Release build was unexpectedly signed.' >&2
exit 1
fi
"$BUILD_TOOLS/zipalign" -c -P 16 -v 4 "$apk"

commit=$(git rev-parse HEAD)
mkdir -p release-artifact
cp "$apk" release-artifact/TrackerControl-githubRelease-unsigned.apk
(
cd release-artifact
sha256sum TrackerControl-githubRelease-unsigned.apk \
> SHA256SUMS-unsigned.txt
)

{
printf 'commit=%s\n' "$commit"
printf 'tag_or_ref=%s\n' "$RELEASE_REF"
printf 'runner_image=%s\n' "$ImageOS-$ImageVersion"
printf 'android_build_tools=36.1.0\n'
printf 'android_ndk=27.2.12479018\n'
printf 'cargo_ndk='; cargo ndk --version
rustc -vV
java -version 2>&1
} > release-artifact/BUILD-INFO.txt

printf 'commit=%s\n' "$commit" >> "$GITHUB_OUTPUT"

- name: Upload unsigned release artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: trackercontrol-github-release-${{ steps.artifact.outputs.commit }}
path: release-artifact/
if-no-files-found: error
compression-level: 0
retention-days: 30
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.env
/output/

.claude/
fdroidserver/
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ You need:

Build from within Android Studio, or use the provided gradle wrapper — see the [Android developer documentation](https://developer.android.com/studio/build/building-cmdline). If you find any problems with these instructions, please file an issue.

Maintainers publishing the GitHub APK should follow the [release build and local signing procedure](docs/RELEASING.md).

## Contributing

TrackerControl is community-driven and welcomes contributions of all kinds — programming skills are not required.
Expand Down
98 changes: 98 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Publishing a GitHub APK release

TrackerControl builds release APK payloads on Linux so that external rebuilders
can reproduce the native Rust libraries. The long-lived Android signing key
stays on the release maintainer's Mac and is never uploaded to GitHub Actions.

The release has two stages:

1. A numeric tag triggers `.github/workflows/release-build.yml`. It performs a
clean, unsigned `githubRelease` build on Ubuntu 22.04 with pinned Android,
Rust, cargo-ndk, and Java toolchains.
2. `scripts/build_and_sign.sh` downloads that exact workflow artifact, checks
that its commit matches the tag, signs it locally, verifies the certificate,
and creates a draft GitHub release.

## One-time local setup

Install and authenticate the GitHub CLI, and ensure Android SDK Build Tools are
installed:

```bash
gh auth login
"$HOME/Library/Android/sdk/cmdline-tools/latest/bin/sdkmanager" \
'build-tools;36.1.0'
```

Place the release keystore at `trackercontrol.jks` in the repository root. The
path is ignored by Git. A different path can be supplied with
`RELEASE_STORE_FILE`.

The script expects the alias `trackercontrol`. Override it with
`RELEASE_KEY_ALIAS` if necessary.

Store both passwords in macOS Keychain under the service names used by the
existing release process:

```bash
security add-generic-password -U -a "$USER" \
-s android_keystore_password -w
security add-generic-password -U -a "$USER" \
-s android_key_password -w
```

Keep an offline backup of the keystore and its passwords. The expected public
certificate SHA-256 is hard-coded in the script so that the wrong keystore
cannot accidentally sign a release.

## Publish a release

First update and commit `versionCode` and `versionName` in `app/build.gradle`.
Create and push the numeric release tag:

```bash
git tag 2026071301
git push origin 2026071301
```

Wait for **Build unsigned GitHub release APK** to finish successfully. Then run:

```bash
./scripts/build_and_sign.sh 2026071301
```

The script writes the signed APK and verification metadata to
`output/release-2026071301/` and creates a draft GitHub release containing:

- `TrackerControl-githubRelease-latest.apk`
- `SHA256SUMS.txt`
- `BUILD-INFO.txt`

Install and test the APK from the draft, then publish the release in GitHub.

If more than one successful build exists for the same commit, pass the desired
Actions run ID explicitly:

```bash
./scripts/build_and_sign.sh 2026071301 1234567890
```

## Manually rebuild a tag or commit

The workflow can also be started from the Actions UI with **Run workflow** and
a tag or commit in the `ref` input. It still produces only an unsigned artifact;
the signing key remains local.

## Overrides

The local script supports these environment variables:

- `GH_REPO`
- `RELEASE_WORKFLOW`
- `RELEASE_STORE_FILE`
- `RELEASE_STORE_PASSWORD`
- `RELEASE_KEY_ALIAS`
- `RELEASE_KEY_PASSWORD`
- `RELEASE_CERT_SHA256`
- `RELEASE_BUILD_TOOLS_VERSION`
- `RELEASE_OUTPUT_DIR`
Loading