Skip to content

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

Merged
ohnorobo merged 5 commits into
laplante/capacitor-build-refactorfrom
laplante/capacitor-android-release
Oct 12, 2026
Merged

ohnorobo merged 5 commits into
laplante/capacitor-build-refactorfrom
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. Stacked on #2889, which holds the build-tooling groundwork: the shared APK packaging helper and parsing the build parameters once.

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. Web bundle. The web bundle build (web_bundle.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.
  2. 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 from refactor(client): share the Android APK packaging and parse Capacitor build parameters once #2889.
  3. Parity with the Cordova release configuration. Pins the NDK so that AGP strips the native libraries, and enables R8 and resource shrinking.
  4. 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 3, the universal APK goes from 60 MB to 43 MB, and libgojni.so from 12.7 MB to 8.9 MB per ABI.
  • The web bundle and the native app carry one build number: a release build with --buildNumber=424242 produced an APK whose versionCode and bundled environment.json both read 424242.

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 impact] The PR appears safe to merge; no new actionable defect was found.

Summary

Adds signed Android release builds for the Capacitor app.

  • Capacitor Android builds now produce signed release files.
  • Capacitor release builds now use production web assets.
  • Android release builds shrink code and package aligned native libraries.

Review used the full PR diff because the previous-review ancestry check did not pass. No new actionable issues were found. No builds or runtime tests were run during this review.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Parse build parameters once] --> B[Check release inputs]
  B --> C[Build production web bundle]
  C --> D[Sync Android assets and plugins]
  D --> E[Write temporary signing keystore]
  E --> F[Gradle bundleRelease]
  F --> G[Build universal APK]
  G --> H[Check 16 KB alignment]
  H --> I[Keep AAB, universal APK and Outline.zip]
  E --> J[Remove temporary keystore when finished]
Loading

Reviews (5) · Last reviewed commit: "build(client/android): leave the univers..." · 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 3 times, most recently from 1bbd6a8 to c7b2f25 Compare October 12, 2026 12:14
ohnorobo and others added 5 commits October 12, 2026 14:31
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>
@ohnorobo
ohnorobo removed this pull request from stack #2870 October 12, 2026 12:34
@ohnorobo
ohnorobo force-pushed the laplante/capacitor-android-release branch from c7b2f25 to 07f2ef8 Compare October 12, 2026 12:35
@ohnorobo
ohnorobo changed the base branch from master to laplante/capacitor-build-refactor October 12, 2026 12:35
@github-actions github-actions Bot added size/M and removed size/XL labels Oct 12, 2026
@ohnorobo
ohnorobo added this pull request to stack #2890 October 12, 2026 12:36
@ohnorobo

Copy link
Copy Markdown
Contributor Author

I decided this was too big, so I broke #2889 out of this change into a new PR.

@ohnorobo
ohnorobo merged commit 2a93ee6 into master Oct 12, 2026
54 of 59 checks passed
@ohnorobo
ohnorobo deleted the laplante/capacitor-android-release branch October 12, 2026 14:09
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.

2 participants