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.
| Variant | Application ID | Firebase Project | Distribution Group |
|---|---|---|---|
| Debug | com.hb.hungryhub.debug | Debug/Staging Firebase | tester-android |
| Release | com.hb.hungryhub | Production Firebase | hungryhub-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
| Path | Purpose |
|---|---|
apps/ionic/android/deployment/buildspec-build.yml | CodeBuild spec for the build stage |
apps/ionic/android/deployment/buildspec-deploy.yml | CodeBuild spec for the deploy stage |
apps/ionic/android/deployment/construct_release_notes.sh | Generates Firebase release notes from git info |
apps/ionic/android/app/build.gradle | Modified — 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
| Setting | Value |
|---|---|
| AWS Region | ap-southeast-1 (Singapore) |
| AWS Account | existing account (DevOps handles credentials + role) |
| Build size | BUILD_GENERAL1_MEDIUM (4 vCPU + 8 GB RAM) |
| Deploy size | BUILD_GENERAL1_MEDIUM |
| Notifications | Slack (via AWS Chatbot) + email failure notifications |
| Trigger | PR merged to main |
| Source format | CODE_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):
| Name | Type | Value |
|---|---|---|
DEBUG_KEYSTORE_FILE | Secrets Manager | hh-android/DEBUG_KEYSTORE_FILE |
HUNGRYHUB_KEYSTORE_FILE | Secrets Manager | hh-android/HUNGRYHUB_KEYSTORE_FILE |
STAGE_FIREBASE_APP_ID | Parameter Store | /hh-android/STAGE_FIREBASE_APP_ID |
RELEASE_FIREBASE_APP_ID | Parameter 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):
| Name | Type | Value |
|---|---|---|
FIREBASE_TOKEN | Secrets Manager | hh-android/FIREBASE_TOKEN |
STAGE_FIREBASE_APP_ID | Parameter Store | /hh-android/STAGE_FIREBASE_APP_ID |
RELEASE_FIREBASE_APP_ID | Parameter 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
- AWS Console → Developer Tools → Settings → Connections → Create connection
- Provider: GitHub
- Name:
hh-pegasus-github - Click Install a new GitHub App → authorise on the
hh-pegasus-monoreporepo - 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-binaryfail to resolve in CodeBuildSECRETS_MANAGERenv vars withDOWNLOAD_SOURCE Secrets Manager Error Message: The result was nil when trying to get the secret value.Use
--secret-string file://debug.keystore.b64(the.b64text file) and let the buildspec dobase64 -dat 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)
- AWS Console → Chatbot → Slack → Configure client → copy the Workspace ID.
- In Slack, right-click the target channel → View channel details → copy the Channel ID (bottom of the “About” panel).
- Create the AWS Chatbot Slack channel configuration in the console pointing
at an SNS topic
hh-ionic-android-pipeline-notifications. - 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_FILEis missing or empty (the keystore binary is required forpackageRelease). 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.apkwhen present). post_buildbundles the Deploy stage’s buildspec + helper script into the samebuild_outputartifact, atapps/ionic/android/deployment/buildspec-deploy.yml+apps/ionic/android/deployment/construct_release_notes.sh. This is the whole reason the artifact’sdiscard-pathsisno— 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
BuildspecOverridepath (apps/ionic/android/deployment/buildspec-deploy.yml) is resolved relative to${CODEBUILD_SRC_DIR}. If the file is not bundled intobuild_output, the Deploy stage fails withYAML_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:
| Tool | Pinned Version | Source |
|---|---|---|
| Node.js | 24 | package.json → engines.node: "^24.0.0" |
| Java | corretto21 | required by capgo-capacitor-social-login plugin (Java 21 source release) |
| pnpm | 11.8.0 | package.json → packageManager: "pnpm@11.8.0" |
| Android SDK | compileSdk=36, build-tools=36.0.0 | variables.gradle |
Why this matters: pnpm 9 (the default CodeBuild pnpm install) cannot read the pnpm 11 lockfile’s
overridesblock and fails the install withERR_PNPM_LOCKFILE_CONFIG_MISMATCHeven 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 installsplatform-tools+platforms;android-36+build-tools;36.0.0during theinstallphase. 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→ grouptester-androidoutput/release/app-release.apk→RELEASE_FIREBASE_APP_ID→ grouphungryhub-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-buildexists with buildspec pointing at the right path - Deploy project
hh-ionic-android-deployexists with buildspec pointing at the right path - Merge a test PR to
main→ pipeline runs end-to-end -
WORKSPACE_ROOTenv 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:
- Whitelist
https://dl.google.com/android/repository/on the egress proxy. - 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
| Resource | Cost |
|---|---|
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 |