Skip to content

build(client/android): add a release build for the Capacitor app - #2867

Open
ohnorobo wants to merge 9 commits into
masterfrom
laplante/capacitor-android-release
Open

ohnorobo wants to merge 9 commits into
masterfrom
laplante/capacitor-android-release

Conversation

@ohnorobo

@ohnorobo ohnorobo commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

What

Adds a production release path for the Capacitor Android app, step 2 of the Capacitor migration plan. Until now the Capacitor build was debug-only.

npm run action client/capacitor/build android -- --buildMode=release --versionName=<version>

takes the same environment variables as the Cordova release build (SENTRY_DSN, ANDROID_KEY_STORE_CONTENTS, ANDROID_KEY_STORE_PASSWORD, JAVA_HOME) and leaves the signed app-release.aab and universal.apk in client/capacitor/android/app/build/outputs/bundle/release/.

iOS and macOS release builds are not part of this PR. Nothing in outline-release changes yet, so releases keep using Cordova.

Commits

  1. Shared helper. Moves the bundletool download, universal APK generation and 16 KB alignment check out of the Cordova build action into client/build/android_universal_apk.mjs, unchanged, so both build actions use the same code.
  2. Web bundle. web_build.action.mjs accepts release mode: webpack in production mode, a version and a Sentry DSN are required, and the DSN is written to environment.json. These are the same checks the Cordova web build does.
  3. Release build. app/build.gradle takes the version and the signing keystore as Gradle project properties, and build.action.mjs runs bundleRelease and then the shared helper.
  4. Parity with the Cordova release configuration. Pins the NDK so that AGP strips the native libraries, and enables R8 and resource shrinking.
  5. Universal APK as an output. The helper already extracted universal.apk to check its alignment; it now keeps it next to the AAB, so the release scripts copy artifacts instead of unpacking the bundletool archive. Outline.zip stays for the Cordova release path.

Differences from the Cordova release build

  • The keystore password reaches Gradle through the environment rather than a command-line argument, and the decoded keystore is written to a temporary directory that is removed after the build, rather than into the source tree.
  • The version is passed to Gradle as properties instead of being substituted into generated files.
  • The build needs JDK 21, where Cordova needs JDK 17.
  • The version code is the same "hours since the epoch" build number as Cordova, so a Capacitor release is always newer than any Cordova one.

Verified

Locally, with a throwaway keystore:

  • The release build succeeds. The universal APK and the AAB are signed with the test key, carry the requested version name and the build number as version code, contain the DSN in environment.json, and pass the 16 KB alignment check.
  • The keystore password does not appear in the build log.
  • With commit 4, the universal APK goes from 60 MB to 43 MB, and libgojni.so from 12.7 MB to 8.9 MB per ABI.
  • The Cordova release build still succeeds through the moved helper.

On an API 35 emulator, with release builds of both apps signed with the same test key:

  • Upgrade. A server added in the Cordova release is still there after an in-place upgrade to the Capacitor release, the privacy screen does not show again, and the server survives a relaunch.
  • VPN. The R8 Capacitor release connects through a local Shadowsocks server (tun0 comes up, and the server sees the TCP and UDP connectivity checks) and disconnects cleanly.

Full release build (1.21.6)

Built as a real release through the outline-release scripts (OutlineFoundation/outline-release#17, build client android) from #2869's head 4dde5a4, which includes this PR. 1.21.6 is a dummy version not intended for publishing, and the publish step has not been tested.

  • The release keystore signed the AAB and universal APK. Version name 1.21.6, version code 497604.
  • Play accepted the AAB on the internal testing track: App bundle explorer.
  • The universal APK and AAB are in s3://outline-release-candidates/client/android/1.21.6/1/, the native debug symbols went to Sentry, and the build is tagged client_android/v1.21.6-rc.1.

🤖 Generated with Claude Code

@ohnorobo
ohnorobo added this pull request to stack #2870 September 30, 2026 13:31
@ohnorobo
ohnorobo force-pushed the laplante/capacitor-android-release branch 2 times, most recently from fc8f8cc to 7a99ec7 Compare October 6, 2026 12:39
@ohnorobo
ohnorobo marked this pull request as ready for review October 7, 2026 12:44
@ohnorobo
ohnorobo requested a review from a team as a code owner October 7, 2026 12:44
@greptile-apps

greptile-apps Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High risk] Adds Android release build infrastructure and signing.

The PR appears safe to merge; no actionable defect was established.

What we checked:

  • Build numbers stay together: The parent computes the number once and passes it to both the web build and Gradle.

Summary

Adds signed Android release builds for the Capacitor client.

  • Capacitor Android builds signed releases with an installable APK.
  • Capacitor release builds include production web assets and Sentry settings.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Capacitor Android release action] --> B[Production web bundle]
  B --> C[Capacitor sync and native library build]
  C --> D[Gradle bundleRelease with version and signing inputs]
  D --> E[Signed app-release.aab]
  E --> F[Shared bundletool helper]
  F --> G[Signed universal.apk]
  G --> H[16 KB alignment check]
  H --> I[Release artifacts]
  J[Cordova Android release action] --> F
Loading

Reviews (4) · Last reviewed commit: "build(client): strip the space-separated..." · Reviewed by Greptile

Comment thread client/capacitor/build.action.mjs
@github-actions github-actions Bot added size/XL and removed size/L labels Oct 7, 2026
Comment thread client/capacitor/build.action.mjs Outdated
@ohnorobo
ohnorobo requested a review from angelodlfrtr October 7, 2026 13:52
@ohnorobo
ohnorobo force-pushed the laplante/capacitor-android-release branch from 934dda3 to f0804dd Compare October 7, 2026 13:52
ohnorobo and others added 9 commits October 8, 2026 12:10
…helper

Move the bundletool download, universal APK generation and 16 KB alignment
check out of the Cordova build action into client/build, unchanged, so the
Capacitor release build can use them too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Build the bundle with webpack in production mode, require a version and a
Sentry DSN, and write the DSN to environment.json, like the Cordova web
build does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`npm run action client/capacitor/build android -- --buildMode=release`
now builds the signed release AAB and the universal APK archive
(Outline.zip), taking the same environment variables as the Cordova
release build.

The version and the signing keystore reach Gradle as project properties.
The keystore password is read from the environment, so that it is never on
a command line, and the decoded keystore lives in a temporary directory
instead of the source tree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…pacitor app

Match the release configuration of the Cordova app: pin the NDK so that AGP
can strip the native libraries, package them uncompressed, and shrink the
code and resources with R8. This takes the universal APK from 60 MB to
43 MB, the size of the Cordova one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The helper already extracted universal.apk to check its 16 KB alignment,
then threw it away and shipped only the bundletool archive, which the
release scripts had to unpack again. Keep the APK in the output directory
so the release scripts copy artifacts instead of knowing about bundletool's
packaging. Outline.zip stays for the Cordova release path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
build.action.mjs computed the build number, then ran web_build, which
computed its own. Across an hour boundary the native version code and the
APP_BUILD_NUMBER in environment.json disagreed. getBuildParameters now
accepts --buildNumber, and the Capacitor build passes its number down.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`--buildNumber N` left N behind as a stray positional argument, which the
web build then took for the platform.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ohnorobo
ohnorobo force-pushed the laplante/capacitor-android-release branch from f0804dd to 1bbd6a8 Compare October 8, 2026 10:10

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant