Post

Forter — Building an Android APK Without a Laptop

Forter — Building an Android APK Without a Laptop

Goal

Edit, build, and install the Forter APK using only a phone. Android apps cannot be compiled on a phone, so the build runs on GitHub Actions.

Loop:

  1. Edit code on GitHub web or the GitHub mobile app
  2. Commit — this triggers the workflow
  3. Build completes in about 3 minutes
  4. Tap the APK in Releases and install

Workflow

.github/workflows/build.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
name: Build APK

on:
  push:
    branches: [dev, main]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - uses: actions/checkout@v4

      - name: JDK 21
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '21'

      - name: Gradle cache
        uses: gradle/actions/setup-gradle@v4

      - name: Make gradlew executable
        run: chmod +x ./gradlew

      - name: Build
        run: ./gradlew assembleDebug --no-daemon --stacktrace

      - name: Version tag
        id: tag
        run: echo "name=build-$(TZ=Asia/Seoul date +%Y%m%d-%H%M)" >> $GITHUB_OUTPUT

      - name: Upload release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ steps.tag.outputs.name }}
          name: Forter ${{ steps.tag.outputs.name }}
          files: app/build/outputs/apk/debug/app-debug.apk

Runner

  • Each run gets a fresh Ubuntu VM: 2+ cores, 16 GB RAM, SSD. Discarded when the job ends.
  • Android SDK, JDK and other toolchains are preinstalled on the runner image.
  • Public repositories: unlimited minutes. Private: 2,000 minutes per month.
  • gradle/actions/setup-gradle@v4 caches dependencies between runs, so later builds are faster than the first.

Release instead of artifact

actions/upload-artifact delivers a ZIP. On a phone that requires a file manager and an extraction step before installing. Attaching the APK to a release makes it a single tap.

Publishing a release requires permissions: contents: write on the job. The injected token is read-only by default; without it the build succeeds and the final step fails with Resource not accessible by integration.

Executable bit on gradlew

A repository pushed from Windows does not carry the POSIX executable permission, and ./gradlew fails on the Linux runner with Permission denied.

Two fixes:

  • chmod +x ./gradlew as a workflow step
  • or fix it once in Git: git update-index --chmod=+x gradlew

JDK version

Forter uses AGP 9.0.1 and Gradle 9.2.1, which require JDK 21. JDK 17, used in most Android CI examples, fails here. Check gradle/wrapper/gradle-wrapper.properties and the version catalog before copying a snippet.

Timestamps

Runners are on UTC. The first release was tagged build-20260912-1133 at 20:33 KST. TZ=Asia/Seoul in front of date produces a local tag.

Publishing the workflow on Jekyll

Liquid processes $ inside code blocks. Wrap the YAML in and , otherwise those lines render empty.

Installation

  • Android warns “This app may be dangerous” for any APK not from the Play Store.
  • Debug builds are signed with the SDK’s shared debug key, so there is no verified developer to display.
  • The signing key is identical across builds, so each APK upgrades the previous install.
  • APK size: 23 MB (debug build, Compose + Nordic BLE library).

Limitation

A compile error takes 3 minutes to surface instead of appearing immediately in the IDE. This pipeline fits small changes — polling intervals, log lines, flags to flip before a test drive. Writing new classes is still faster on a laptop.

Next: connecting to the OBD2 adapter over Bluetooth SPP and reading the first PID.

This post is licensed under CC BY 4.0 by the author.