Skip to main content

Deployment

Apps (static export)

npm run build tracking # or: npm run build portal

Each app's next.config.ts sets output: "export" in production, emitting a fully static site to the app's out/. Any static host can serve it — no Node server required.

TODO: there is no committed app hosting/CI deploy config yet. Document the chosen host and pipeline here once it exists.

Native (Capacitor)

The tracking app's static export is wrapped by Capacitor for iOS/Android (apps/tracking/capacitor.config.ts, apps/tracking/ios, apps/tracking/android). Build output is copied into the native shell via cap copy / Xcode; native build artefacts are git-ignored, the shells are committed.

TODO: document the exact cap command sequence and signing steps for an iOS release build. The Android release flow is documented below.

Android → Play Store (GitLab CI)

The Capacitor app ships to the existing Play listing of the former native appapplicationId is com.enode.enodeone (immutable on Play; the code namespace stays ai.enode.tracking, see apps/tracking/android/app/build.gradle).

The pipeline (.gitlab-ci.yml) has two manual jobs, both running the same chain npm run build tracking → source-map guard → npx cap sync android → Gradle:

JobBranchesEffect
build_androidanySigned AAB as CI artifact — no Play access. Dry run for the whole build chain.
publish_android_internalmain onlyBuilds and uploads the AAB to the Play internal-testing track.

Backend selection (which API server a native build talks to)

The backend base URL is frozen into the static web export at build time (NEXT_PUBLIC_API_SERVER / NEXT_PUBLIC_API_BASE_URL, default develop — single source: packages/core/src/api/servers.ts). Since the native shells only package the pre-built out/, each shell injects window.__ENODE_API_SERVER__ before any bundled JS runs, which overrides the baked default:

  • AndroidBuildConfig.ENODE_API_SERVER, set per build type in apps/tracking/android/app/build.gradle and injected by MainActivity via WebViewCompat.addDocumentStartJavaScript. Release builds (incl. the CI jobs below) always point at production; debug builds use the baked default (develop) unless repointed with -PenodeApiServer=<name> or the ENODE_API_SERVER env var at Gradle time — no web rebuild needed.
  • iOSENODE_API_SERVER from the Xcode run scheme (dev runs) or the per-configuration Info.plist value (Release = production), injected as a WKUserScript (ios/App/App/AppViewController.swift).

Publishing uses the Gradle Play Publisher plugin. versionCode is 3200 + CI_PIPELINE_IID, set in app/build.gradle; versionName is maintained by hand. The internal track needs no Google review; promotion to beta/production happens manually in the Play Console and goes through review from the closed track onward.

Why the versionCode is not resolved from Play

The plugin's resolutionStrategy = AUTO (highest code on Play + 1) was removed because it deadlocks against the DRAFT release status used below. Play burns a versionCode permanently the moment an artifact is uploaded with it — deleting the draft does not release it — but AUTO only sees codes currently held in a track. So once a publish fails after upload, every rerun resolves that same burned code and Play rejects it with:

Concurrent uploads for app com.enode.enodeone (version code already used).

Despite the wording, this is not a concurrency problem, and the failure repeats forever because AUTO cannot observe the code it needs to skip. Deriving the code from the pipeline counter instead makes it monotonic and independent of Play's state. If a draft is still open, clicking Start rollout also resolves it — deleting the draft does not.

The 3200 base clears every code used on the listing so far. Raise it only: lowering it would collide with an already-burned code.

Release flow

  1. Merge to main.
  2. Start publish_android_internal via the ▶ button in the pipeline.
  3. The upload lands as a draft on the internal track (releaseStatus = DRAFT in app/build.gradle) — nothing reaches the internal testers yet. Review it in the Play Console and click Start rollout to release it to the internal-testing track.
  4. Test via the internal-testing track, then promote the release in the Play Console (Releases → promote).
  5. Bump versionName when the next marketing version begins.

Signing & credentials

The AAB must be signed with the upload key of the existing listing. The keystore is not committed: CI decodes it from UPLOAD_KEYSTORE_BASE64 into apps/tracking/android/upload-keystore.jks (git-ignored); the passwords come from masked CI variables (UPLOAD_KEYSTORE_PASSWORD, UPLOAD_KEY_ALIAS, UPLOAD_KEY_PASSWORD). The Play service account JSON arrives base64-encoded in PLAY_SERVICE_ACCOUNT_JSON_BASE64 and is passed to the plugin via the ANDROID_PUBLISHER_CREDENTIALS env var. All five variables are protected (GitLab → Settings → CI/CD → Variables); main is a protected branch, so feature-branch pipelines never see them.

Local signed builds work the same way: copy the upload keystore to apps/tracking/android/upload-keystore.jks and set the three UPLOAD_* password/alias env vars before ./gradlew :app:bundleRelease. Without the file, release builds are unsigned (debug builds are unaffected).

Source maps

The CI aborts if apps/tracking/out contains *.map files: source maps in the APK would expose the readable app source. Next.js has productionBrowserSourceMaps off by default — the guard only protects against a future config change.

Documentation site → Cloudflare Pages

The docs website is built separately from the apps:

npm run docs:build # → docs-site/build
npm run docs:serve # preview at http://localhost:4000

The built site is a single-page app and must be served over HTTP — opening docs-site/build/index.html as a file:// URL will not work.

It deploys to Cloudflare Pages from main via the dashboard Git integration — Cloudflare builds the site itself on every push, with no secrets stored in the repo. Build settings come from wrangler.toml (project enode-tracking-docs, output docs-site/build) and .nvmrc (Node 22).

One-time setup

The deploy config (wrangler.toml, .nvmrc, the docs build) must be on main first — Cloudflare builds the branch you connect.

  1. Merge the documentation setup into main.
  2. Cloudflare dashboard → Workers & Pages → Create → Pages → Connect to Git, select this repository.
  3. Configure the build:
    • Production branch: main
    • Build command: npm run docs:build
    • Build output directory: docs-site/build
    • Root directory: / (repo root)
    • Environment variable: NODE_VERSION=22 (or rely on .nvmrc). If the build can't find the dev dependencies, also set NPM_FLAGS=--include=dev.
  4. Save and Deploy. Every push to main now builds and deploys automatically; pull requests get preview URLs.

After the first deploy

  • The site is live at https://enode-tracking-docs.pages.dev.
  • For a custom domain, attach it in Cloudflare and set the DOCS_URL env var to the final URL (baseUrl stays /).

Cloudflare builds TypeDoc + Storybook + Docusaurus in one npm run docs:build run (a few minutes). The build is not coupled to GitHub Actions — docs-check still runs on PRs as the quality gate, but it does not deploy.