Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

AWS CodePipeline — Ionic Android APK Build

Auto-builds both debug and release APKs for the apps/ionic Capacitor project when a PR is merged to main, then distributes each variant to its respective Firebase App Distribution project.

VariantApplication IDFirebase ProjectDistribution Group
Debugcom.hb.hungryhub.debugDebug/Staging Firebasetester-android
Releasecom.hb.hungryhubProduction Firebasehungryhub-dev

The hh-android native repo’s GitHub Actions workflow is untouched and continues to handle its own builds (Capacitor is intended to replace it later).


Architecture

┌────────────────────────────────────────────────────────────────────────┐
│               CodePipeline: hh-ionic-android                            │
├──────────────────┬───────────────────────────┬─────────────────────────┤
│  Source          │  Build                    │  Deploy                 │
│  GitHub (main)   │  CodeBuild                │  CodeBuild              │
│                  │  pnpm install             │  Firebase CLI           │
│                  │  pnpm build:ionic:staging │  → debug APK → tester   │
│                  │  npx cap sync             │  → release APK → dev    │
│                  │  ./gradlew assembleDebug  │                         │
│                  │  pnpm build:ionic         │                         │
│                  │  npx cap sync             │                         │
│                  │  ./gradlew assembleRelease│                         │
└──────────────────┴───────────────────────────┴─────────────────────────┘

Trigger: Any push/merge to main (PR merge included).


Files

PathPurpose
apps/ionic/android/deployment/buildspec-build.ymlCodeBuild spec for the build stage
apps/ionic/android/deployment/buildspec-deploy.ymlCodeBuild spec for the deploy stage
apps/ionic/android/deployment/construct_release_notes.shGenerates Firebase release notes from git info
apps/ionic/android/app/build.gradleModified — release signing now respects CI_RELEASE_KEYSTORE_* env vars

The pipeline itself is configured directly in AWS CodePipeline (console or CLI), not via CloudFormation. Buildspecs are the only artefacts the dev team maintains.


Environment

SettingValue
AWS Regionap-southeast-1 (Singapore)
AWS Accountexisting account (DevOps handles credentials + role)
Build sizeBUILD_GENERAL1_MEDIUM (4 vCPU + 8 GB RAM)
Deploy sizeBUILD_GENERAL1_MEDIUM
NotificationsSlack (via AWS Chatbot) + email failure notifications
TriggerPR merged to main
Source formatCODE_ZIP (default — see source layout section)

CodePipeline Setup (DevOps — Console or CLI)

The pipeline is configured manually. Three stages:

Source

  • Provider: CodeStarSourceConnection
  • Connection ARN: from Step 2 below
  • Repository: hungryhub-team/hh-pegasus-monorepo
  • Branch: main
  • Output artifact format: CODE_ZIP (default)
  • Detect changes: false (CodePipeline polls on schedule)

Build

  • Provider: CodeBuild
  • Project name: hh-ionic-android-build
  • Buildspec: apps/ionic/android/deployment/buildspec-build.yml
  • Input artifact: SourceArtifact
  • Output artifact: BuildArtifact
  • Environment variables (mapped in CodeBuild project):
NameTypeValue
DEBUG_KEYSTORE_FILESecrets Managerhh-android/DEBUG_KEYSTORE_FILE
HUNGRYHUB_KEYSTORE_FILESecrets Managerhh-android/HUNGRYHUB_KEYSTORE_FILE
STAGE_FIREBASE_APP_IDParameter Store/hh-android/STAGE_FIREBASE_APP_ID
RELEASE_FIREBASE_APP_IDParameter Store/hh-android/RELEASE_FIREBASE_APP_ID

Deploy

  • Provider: CodeBuild
  • Project name: hh-ionic-android-deploy
  • Buildspec: apps/ionic/android/deployment/buildspec-deploy.yml
  • Input artifact: BuildArtifact
  • Environment variables (mapped in CodeBuild project):
NameTypeValue
FIREBASE_TOKENSecrets Managerhh-android/FIREBASE_TOKEN
STAGE_FIREBASE_APP_IDParameter Store/hh-android/STAGE_FIREBASE_APP_ID
RELEASE_FIREBASE_APP_IDParameter Store/hh-android/RELEASE_FIREBASE_APP_ID

What DevOps Needs to Provision

1. S3 Bucket (Artifact Store)

aws s3api create-bucket \
  --bucket hungryhub-ionic-android-artifacts \
  --region ap-southeast-1 \
  --create-bucket-configuration LocationConstraint=ap-southeast-1

aws s3api put-bucket-versioning \
  --bucket hungryhub-ionic-android-artifacts \
  --versioning-configuration Status=Enabled

aws s3api put-bucket-encryption \
  --bucket hungryhub-ionic-android-artifacts \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}
    }]
  }'

2. CodeStar Connection to GitHub

  1. AWS Console → Developer Tools → Settings → Connections → Create connection
  2. Provider: GitHub
  3. Name: hh-pegasus-github
  4. Click Install a new GitHub App → authorise on the hh-pegasus-monorepo repo
  5. Copy the connection ARN (format: arn:aws:codestar-connections:ap-southeast-1:ACCOUNT:connection/UUID)

3. AWS Secrets Manager Secrets

Base64-encode the keystores first:

base64 -i /path/to/debug.keystore | tr -d '\n' > debug.keystore.b64
base64 -i /path/to/HungryHub.keystore | tr -d '\n' > release.keystore.b64

Then create secrets. Use --secret-string (not --secret-binary):

⚠️ CodeBuild SDK quirk — Secrets stored via --secret-binary fail to resolve in CodeBuild SECRETS_MANAGER env vars with DOWNLOAD_SOURCE Secrets Manager Error Message: The result was nil when trying to get the secret value.

Use --secret-string file://debug.keystore.b64 (the .b64 text file) and let the buildspec do base64 -d at runtime. The keystore binary on disk is then byte-identical to the source.

aws secretsmanager create-secret \
  --name hh-android/DEBUG_KEYSTORE_FILE \
  --secret-string file://debug.keystore.b64

aws secretsmanager create-secret \
  --name hh-android/HUNGRYHUB_KEYSTORE_FILE \
  --secret-string file://release.keystore.b64

For the Firebase token, generate one locally:

firebase login:ci
# Copy the printed token
aws secretsmanager create-secret \
  --name hh-android/FIREBASE_TOKEN \
  --secret-string "PASTE_TOKEN_HERE"

4. SSM Parameter Store — Firebase App IDs

Extract app IDs from google-services.json:

# Debug app id (com.hb.hungryhub.debug)
DEBUG_APP_ID=$(jq -r '.client[0].client_info.mobilesdk_app_id' \
  apps/ionic/android/app/src/debug/google-services.json)
aws ssm put-parameter \
  --name /hh-android/STAGE_FIREBASE_APP_ID \
  --value "$DEBUG_APP_ID" \
  --type String

# Release app id (com.hb.hungryhub)
RELEASE_APP_ID=$(jq -r '.client[0].client_info.mobilesdk_app_id' \
  apps/ionic/android/app/src/release/google-services.json)
aws ssm put-parameter \
  --name /hh-android/RELEASE_FIREBASE_APP_ID \
  --value "$RELEASE_APP_ID" \
  --type String

5. Slack Notification Setup (optional)

  1. AWS Console → Chatbot → Slack → Configure client → copy the Workspace ID.
  2. In Slack, right-click the target channel → View channel details → copy the Channel ID (bottom of the “About” panel).
  3. Create the AWS Chatbot Slack channel configuration in the console pointing at an SNS topic hh-ionic-android-pipeline-notifications.
  4. Add a CodeStar Notifications rule on the pipeline targeting that SNS topic for events: pipeline-execution-started, pipeline-execution-succeeded, pipeline-execution-failed, pipeline-execution-canceled.

Behaviour Notes

Source layout — CODE_ZIP wraps the repo in src/

When OutputArtifactFormat: CODE_ZIP is used (the default for CodeStarSourceConnection), the GitHub repo is downloaded as a ZIP whose top-level directory is src/. After extraction:

$CODEBUILD_SRC_DIR/
  └── src/                 ← actual repo root
      ├── pnpm-workspace.yaml
      ├── apps/
      ├── packages/
      └── ...

The buildspecs detect this layout at runtime by walking up to find pnpm-workspace.yaml and exporting the resolved path as WORKSPACE_ROOT. All subsequent commands use $WORKSPACE_ROOT instead of $CODEBUILD_SRC_DIR, so they work regardless of whether the source lands at the root or under src/.

Build stage

  • Runs pnpm build:ionic:staging (staging Vite build) → npx cap sync android → ./gradlew assembleDebug
  • Then runs pnpm build:ionic (production Vite build) → npx cap sync android → ./gradlew assembleRelease
  • The release sub-build is skipped if hh-android/HUNGRYHUB_KEYSTORE_FILE is missing or empty (the keystore binary is required for packageRelease). Provision the release keystore secret to re-enable it; the build does NOT fail the pipeline just because the release variant is unavailable.
  • Both APKs are uploaded as build artifacts (output/debug/app-debug.apk + output/release/app-release.apk when present).
  • post_build bundles the Deploy stage’s buildspec + helper script into the same build_output artifact, at apps/ionic/android/deployment/buildspec-deploy.yml + apps/ionic/android/deployment/construct_release_notes.sh. This is the whole reason the artifact’s discard-paths is no — the deploy buildspec needs to be reachable by its original path inside the artifact.

Artifact layout — Deploy stage reads from build_output, not the source repo

The Deploy stage’s input_artifacts is ["build_output"] (set by the Codepipeline Terraform module’s deploy_input_artifacts variable; default ["build_output"] for the ionic_android_* modules). That makes build_output the primary CodeBuild source for the Deploy stage (${CODEBUILD_SRC_DIR} = root of the unzipped build_output artifact).

Why this matters: the Deploy stage’s BuildspecOverride path (apps/ionic/android/deployment/buildspec-deploy.yml) is resolved relative to ${CODEBUILD_SRC_DIR}. If the file is not bundled into build_output, the Deploy stage fails with YAML_FILE_ERROR: stat .../apps/ionic/android/deployment/buildspec-deploy.yml: no such file or directory.

The Deploy buildspec therefore reads everything directly from ${CODEBUILD_SRC_DIR} (no WORKSPACE_ROOT detection, no pnpm-workspace.yaml lookup — neither exists in the artifact). The construct_release_notes.sh script is also robust to .git being absent (the artifact does not include .git), falling back to a placeholder commit message.

Runtime versions

The CodeBuild install phase pins versions to match the repo exactly:

ToolPinned VersionSource
Node.js24package.json → engines.node: "^24.0.0"
Javacorretto21required by capgo-capacitor-social-login plugin (Java 21 source release)
pnpm11.8.0package.json → packageManager: "pnpm@11.8.0"
Android SDKcompileSdk=36, build-tools=36.0.0variables.gradle

Why this matters: pnpm 9 (the default CodeBuild pnpm install) cannot read the pnpm 11 lockfile’s overrides block and fails the install with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH even though the override values are semantically identical. Pin to 11.8.0 to match the lockfile.

pnpm 11 refuses to run on Node 18, so Node must be bumped to 24.

Android SDK is NOT pre-installed in aws/codebuild/standard:7.0. The build downloads the command-line tools, accepts licenses, and installs platform-tools + platforms;android-36 + build-tools;36.0.0 during the install phase. The full SDK is cached under /opt/android-sdk/ for fast subsequent builds.

Signing

The release signing config in apps/ionic/android/app/build.gradle now reads CI_RELEASE_KEYSTORE_* env vars when present, otherwise falls back to the hard-coded local path. Local development is unaffected.

Deploy stage

Each APK is uploaded to its own Firebase project using appdistribution:distribute:

  • output/debug/app-debug.apk → STAGE_FIREBASE_APP_ID → group tester-android
  • output/release/app-release.apk → RELEASE_FIREBASE_APP_ID → group hungryhub-dev

google-services.json placement

Already wired by Capacitor at:

  • apps/ionic/android/app/src/debug/google-services.json (→ staging Firebase)
  • apps/ionic/android/app/src/release/google-services.json (→ production Firebase)

The applicationIdSuffix '.debug' in buildTypes.debug ensures the debug APK uses the staging Firebase config at build time.

Caching

The build spec caches .gradle/, root node_modules/, and apps/ionic/node_modules/ across builds (~50% time reduction on incremental runs). Cache paths are relative so they resolve correctly regardless of whether WORKSPACE_ROOT is $CODEBUILD_SRC_DIR or $CODEBUILD_SRC_DIR/src.


Verification Checklist

After deploying the pipeline:

  • S3 bucket exists with versioning + encryption
  • GitHub connection status = Available
  • All 3 Secrets Manager secrets exist and are retrievable
  • Both SSM parameters exist and return valid Firebase app IDs
  • CodePipeline state = Available
  • Build project hh-ionic-android-build exists with buildspec pointing at the right path
  • Deploy project hh-ionic-android-deploy exists with buildspec pointing at the right path
  • Merge a test PR to main → pipeline runs end-to-end
  • WORKSPACE_ROOT env var is set correctly (printed in CodeBuild logs)
  • Debug APK appears in the tester-android Firebase group
  • Release APK appears in the hungryhub-dev Firebase group
  • Both APKs install and launch on an Android emulator/device

Troubleshooting

Rollup failed to resolve import "@hh-pegasus/pages-<domain>/..."

Build the missing package first:

pnpm install
pnpm --filter @hh-pegasus/locales build

ERR_PNPM_LOCKFILE_CONFIG_MISMATCH on pnpm install --frozen-lockfile

CodeBuild is using a different pnpm version than the one that generated the lockfile. The buildspecs must match the package.json#packageManager:

# apps/ionic/android/deployment/buildspec-build.yml
runtime-versions:
  nodejs: 24
commands:
  - npm install -g pnpm@11.8.0 firebase-tools

Use pnpm 11.8.0 (matches packageManager in repo). Node 18 is too old for pnpm 11.

ENOTDIR: not a directory, mkdir '/codebuild/output/src.../src/node_modules'

The Source action’s OutputArtifactFormat is CODE_ZIP, which wraps the repo in src/. The buildspecs handle this automatically by exporting WORKSPACE_ROOT, but if you see this error, the issue is almost certainly stale CodeBuild cache from a previous build attempt restoring an incompatible node_modules layout under the new workspace path.

Quickest fix (DevOps): Switch the Source action from CODE_ZIP to CODEBUILD_CLONE_REF in the CodePipeline console. With a git clone, the source lands at $CODEBUILD_SRC_DIR/ directly — no src/ wrapper, no caching surprises. No buildspec changes needed; the defensive WORKSPACE_ROOT detection still handles both layouts.

If keeping CODE_ZIP: invalidate the CodeBuild project’s local cache (CodeBuild console → project → Action → Invalidate local cache), then re-run. The new buildspec also aggressively rm -rf any stale node_modules and prints a source-layout debug block at the top of pre_build so you can verify the actual filesystem state.

Build fails on assembleRelease with “Keystore was tampered with”

Confirm the keystore secret contains a valid base64-encoded JKS. Re-create with --secret-string (NOT --secret-binary — see the quirk note in §Provisioning §3):

aws secretsmanager update-secret \
  --secret-id hh-android/HUNGRYHUB_KEYSTORE_FILE \
  --secret-string file://release.keystore.b64

firebase: command not found

The deploy stage installs Firebase CLI via npm install -g firebase-tools. If this fails, bump nodejs runtime in buildspec-deploy.yml.

Deploy stage fails with YAML_FILE_ERROR: stat .../apps/ionic/android/deployment/buildspec-deploy.yml: no such file or directory

The Deploy stage’s primary input is the Build stage’s build_output artifact, not the source repo. The buildspec at the BuildspecOverride path must be present inside the artifact.

The Build stage’s post_build is responsible for copying buildspec-deploy.yml + construct_release_notes.sh into the artifact at their original paths. If that step is missing or the artifact’s discard-paths: yes is in effect, the file is unreachable.

Quickest fix (DevOps): confirm the apps/ionic/android/deployment/buildspec-build.yml in your branch has the mkdir -p output/apps/ionic/android/deployment block in post_build and that artifacts: discard-paths: no is set. Push and re-run.

SDK location not found / Define a valid SDK location with an ANDROID_HOME environment variable

CodeBuild’s aws/codebuild/standard:7.0 image does not include the Android SDK. The install phase of buildspec-build.yml downloads the Android command-line tools, accepts licenses, and installs the required platforms;android-36 + build-tools;36.0.0 packages. ANDROID_HOME is set to /opt/android-sdk in the build’s env.variables so Gradle picks it up automatically.

If the install fails (e.g., download blocked by corporate proxy), either:

  1. Whitelist https://dl.google.com/android/repository/ on the egress proxy.
  2. Or bake the SDK into a custom CodeBuild image and switch the project to use it.

Pipeline “fails” on assembleRelease but debug succeeds

Verify the CI_RELEASE_KEYSTORE_* env vars are mapped correctly in the Build project’s Environment Variables section of CodeBuild.


Cost Estimate

ResourceCost
CodeBuild BUILD_GENERAL1_MEDIUM (~15-25 min/build)~$0.01/min × 20 min = $0.20/build
S3 storage (artifacts)< $1/month
CodePipeline (1 active pipeline)$1/month
Estimated~$5–10/month for ~20 builds