AbsoluteJS

Release

Build signed store artifacts, prove the exact artifact works, and publish it to Google Play and TestFlight from your machine or from a generated GitHub Actions workflow.

#Path to the stores

1
Check the release
Checks the native projects, config and dependencies for anything that must not reach a store build, such as development servers, debugging flags or cleartext traffic.
absolute mobile doctor release
2
Build and sign
Builds your server and pages, syncs the native project, runs the release doctor again and produces a signed Android App Bundle or iOS IPA in its own content-addressed directory.
absolute mobile build android|ios
3
Certify
Installs that exact artifact, runs the acceptance checks, and binds the report to the artifact’s digest.
absolute mobile test --release · mobile certify
4
Publish
Hands the certified artifact to your release module, which uploads it to Google Play or TestFlight. Version codes and build numbers are allocated for you.
absolute mobile publish android|ios

Run every command from your app's root, the directory with package.json and absolute.config.ts. Each release lands in its own directory under .absolutejs/mobile/releases, named by the hash of its contents, so the artifact you certify is the artifact you publish.

#Release doctor

The release doctor runs on its own and as part of every build. A failed check stops the build; a warning is reported and does not.

BASH
bunx absolute mobile doctor release           # every configured platform
bunx absolute mobile doctor release ios --json # one platform, machine-readable
App-wideThe production origin is HTTPS, deep-link identities are complete, dependency versions are locked, and branding is configured.
Each platformApp identity, bundle integrity, Content Security Policy, deep links, and the update watchdog matching your updates config.
Development residueNo HMR assets, development journal, dev certificate authority, cleartext traffic, android:debuggable, WebView debugging or iOS get-task-allow.
Platform specificsExported Android components are deliberate, and iOS App Transport Security and the marketing version are set.
Device featuresNative packages, permissions, usage strings, the privacy manifest and push setup match the device features your code imports.
Data and updatesThe offline Sync schema is valid, and the update server uses durable storage that it can actually write to.

Some things no tool can check for you. The doctor lists them as manual review on every run:

Test on a physical device
Complete the store privacy questionnaires
Publish a privacy policy
Keep signing keys in your custody
Review the data practices of third-party native SDKs

#Build and sign

BASH
bunx absolute mobile build android src/backend/server.ts
bunx absolute mobile build ios src/backend/server.ts

Android. Configure a release signingConfig in the Android project in mobile/android, or set all four of these variables and AbsoluteJS signs the bundle with your upload key. The bundle must pass signature verification before it is kept.

BASH
export ABSOLUTE_ANDROID_KEYSTORE_PATH="$HOME/.config/absolutejs/upload.jks"
export ABSOLUTE_ANDROID_KEYSTORE_PASSWORD="..."
export ABSOLUTE_ANDROID_KEY_ALIAS="upload"
export ABSOLUTE_ANDROID_KEY_PASSWORD="..."

iOS. Builds use Xcode automatic signing and export for App Store Connect. Set your team, and set mobile.ios.version to the marketing version you are shipping. On Linux or Windows, the build runs on a Mac you have paired over SSH.

BASH
export ABSOLUTE_IOS_DEVELOPMENT_TEAM="ABCDE12345"

# From Linux or Windows, build on a paired Mac
bunx absolute mobile pair mac studio builder@studio.local
bunx absolute mobile build ios src/backend/server.ts --remote studio
Unsigned builds
A build that ends up unsigned stops with an error. Pass --unsigned only for a build you will not publish.

#Certify

A certification records that a specific artifact passed its acceptance checks. It is tied to the artifact's digest, so it cannot be reused for a different build. Production releases require one by default: installed evidence for Android and store-delivered evidence for iOS.

BASH
# Install the exact release on a device and run the acceptance checks
bunx absolute mobile test android \
  --release .absolutejs/mobile/releases/android/amobile_android_RELEASE \
  --report

# Bind that evidence to the release
bunx absolute mobile certify .absolutejs/mobile/releases/android/amobile_android_RELEASE \
  --evidence path/to/report-dir \
  --require installed
EvidencePlatformWhat it proves
installedAndroidThe exact AAB is installed through Bundletool and launched, offline and online
simulatoriOSThe exact archive runs in the iOS Simulator
deviceiOSInstalled and run on a physical iPhone or iPad
storeiOSDelivered through TestFlight and run on a device

For iOS, stronger evidence satisfies weaker requirements: store covers device, and device covers simulator. Set the requirement per release channel and per Google Play track, or set certification: false to turn the gate off.

TS
mobile: {
  // ...
  release: {
    certification: {
      channels: {
        production: { android: 'installed', ios: 'store' },
        beta: { android: 'installed', ios: 'simulator' }
      },
      googlePlayTracks: { production: 'installed', internal: false }
    }
  }
}

#Publish

Publishing goes through a release module in your project, mobile.release.ts by default. It keeps every release and its certification in your storage, then uploads to the store with @absolutejs/deploy. Credentials stay in the module's environment and are never passed as command-line flags. Google Play uses Application Default Credentials.

TS
// mobile.release.ts
import { S3Client } from '@aws-sdk/client-s3';
import { awsS3BlobStore } from '@absolutejs/blob/aws-s3';
import { createGooglePlayReleasePublisher } from '@absolutejs/deploy/google-play';
import { createNativeReleaseRegistry } from '@absolutejs/deploy/native-release';

const store = awsS3BlobStore({
  bucket: process.env.RELEASE_BUCKET ?? '',
  client: new S3Client({ region: process.env.RELEASE_REGION })
});

export default createGooglePlayReleasePublisher({
  receiptStore: store,
  registry: createNativeReleaseRegistry({ store })
});

TestFlight uses an App Store Connect API key. Keep it in a second module and pick it with --registry.

TS
// mobile.release.ios.ts
import { S3Client } from '@aws-sdk/client-s3';
import { awsS3BlobStore } from '@absolutejs/blob/aws-s3';
import { createAppStoreConnectReleasePublisher } from '@absolutejs/deploy/app-store-connect';
import { createNativeReleaseRegistry } from '@absolutejs/deploy/native-release';

const store = awsS3BlobStore({
  bucket: process.env.RELEASE_BUCKET ?? '',
  client: new S3Client({ region: process.env.RELEASE_REGION })
});

export default createAppStoreConnectReleasePublisher({
  auth: {
    issuerId: process.env.APP_STORE_CONNECT_ISSUER_ID ?? '',
    keyId: process.env.APP_STORE_CONNECT_KEY_ID ?? '',
    privateKey: await Bun.file(
      process.env.APP_STORE_CONNECT_PRIVATE_KEY_PATH ?? 'AuthKey.p8'
    ).text()
  },
  receiptStore: store,
  registry: createNativeReleaseRegistry({ store })
});
BASH
# Google Play: 10% of production, with release notes
bunx absolute mobile publish android src/backend/server.ts \
  --certification path/to/certification-dir \
  --play-track production \
  --play-rollout 0.1 \
  --play-notes 'en-US=Faster checkout'

# TestFlight: an external group, submitted for beta review
bunx absolute mobile publish ios src/backend/server.ts \
  --registry mobile.release.ios.ts \
  --testflight-group 'External Beta' \
  --testflight-notes 'en-US=Faster checkout' \
  --testflight-submit-review
FlagMeaning
--play-trackinternal, alpha, beta, production or a custom track
--play-rolloutFraction of users for a staged rollout, such as 0.1
--play-statuscompleted, draft, halted or in-progress
--play-notesRelease notes as language=text; repeatable
--play-update-priorityIn-app update priority from 0 to 5
--testflight-groupBeta group name or ID
--testflight-notesWhat to Test, as locale=text; repeatable
--testflight-submit-reviewSubmit the build for beta app review
--releasePublish an existing release directory instead of building
--registryRelease module to use; defaults to mobile.release.ts

The Play version code and the Apple build number are allocated during publishing and recorded, so a retried publish reuses them rather than uploading twice.

#GitHub Actions

One command writes .github/workflows/absolute-mobile.yml with validate, Android and iOS jobs. Pull requests build without any secrets. Release jobs run in a protected absolute-mobile-release environment, sign their certifications through GitHub's OIDC identity, and clean up every credential file when they finish. Add --secret-env for each variable your release module reads.

BASH
bunx absolute mobile ci github src/backend/server.ts \
  --publish \
  --secret-env RELEASE_BUCKET \
  --secret-env RELEASE_REGION

Create the absolute-mobile-release environment under Settings › Environments, require a reviewer, and store these secrets there:

Android secretPurpose
ABSOLUTE_ANDROID_KEYSTORE_BASE64Base64 Android upload keystore
ABSOLUTE_ANDROID_KEYSTORE_PASSWORDKeystore password
ABSOLUTE_ANDROID_KEY_ALIASUpload key alias
ABSOLUTE_ANDROID_KEY_PASSWORDUpload key password
ABSOLUTE_GOOGLE_CREDENTIALS_BASE64Google service-account JSON; only to publish to a Play track
iOS secretPurpose
ABSOLUTE_IOS_CERTIFICATE_BASE64Base64 Apple Distribution .p12
ABSOLUTE_IOS_CERTIFICATE_PASSWORD.p12 password
ABSOLUTE_IOS_PROVISIONING_PROFILE_BASE64Base64 App Store provisioning profile
ABSOLUTE_IOS_KEYCHAIN_PASSWORDPassword for the temporary keychain
ABSOLUTE_IOS_DEVELOPMENT_TEAMTen-character Apple team ID
APP_STORE_CONNECT_ISSUER_IDApp Store Connect API issuer
APP_STORE_CONNECT_KEY_IDApp Store Connect API key ID
APP_STORE_CONNECT_PRIVATE_KEY_BASE64Base64 .p8 key; only to publish to TestFlight

#Promote without rebuilding

Promotion publishes the artifact a CI run already built and certified. Every step is recorded locally, so an interrupted promotion resumes where it stopped and is never dispatched twice.

BASH
# Publish the exact artifact a CI run built and certified, without rebuilding
bunx absolute mobile ci promote android \
  --run-id 1234567890 \
  --certification path/to/certification-dir \
  --play-track production \
  --watch --audit

bunx absolute mobile ci promotions   # every promotion and its state
bunx absolute mobile ci promote --resume DISPATCH_ID

Fixes that only change your pages do not need a store release at all. Over-the-air updates ship them to installed apps directly.