ci: add parameterized build workflow (env x target x platform)
Manually-triggered, build-only GitHub Actions workflow on GitHub-hosted runners. Inputs: environment (staging/prod), target (all/customer/mitra/ backend/control_center), platform (android/ios/both). Runner split: iOS app jobs run on macos-latest, all else on ubuntu-latest. Apps build debug-signed APKs; control_center bakes VITE_API_BASE_URL; backend exports a docker-save tarball. iOS jobs await per-flavor Xcode schemes + signing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
258
.github/workflows/build.yml
vendored
Normal file
258
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,258 @@
|
||||
name: Build
|
||||
|
||||
# Manually triggered, parameterized build pipeline (build-only — no push/deploy).
|
||||
#
|
||||
# Trigger: GitHub → Actions tab → "Build" → "Run workflow" → pick:
|
||||
# environment: staging | prod (which flavor/config to build)
|
||||
# target: all | customer | mitra | backend | control_center
|
||||
# platform: android | ios | both (only affects the customer/mitra apps)
|
||||
#
|
||||
# Runner split: iOS app jobs run on macOS runners (iOS can't build on Linux);
|
||||
# everything else (Android, backend, control_center) runs on Linux.
|
||||
#
|
||||
# Outputs land as downloadable artifacts on the run summary page.
|
||||
# See .github/workflows/README.md for details.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: "Build environment"
|
||||
type: choice
|
||||
required: true
|
||||
default: staging
|
||||
options: [staging, prod]
|
||||
target:
|
||||
description: "What to build"
|
||||
type: choice
|
||||
required: true
|
||||
default: all
|
||||
options: [all, customer, mitra, backend, control_center]
|
||||
platform:
|
||||
description: "Mobile platform (apps only; macOS runners cost ~10x)"
|
||||
type: choice
|
||||
required: true
|
||||
default: android
|
||||
options: [android, ios, both]
|
||||
|
||||
run-name: "Build ${{ inputs.target }} (${{ inputs.environment }}, ${{ inputs.platform }})"
|
||||
|
||||
# Cancel an in-flight run of the same target+env+platform when a new one starts.
|
||||
concurrency:
|
||||
group: build-${{ inputs.target }}-${{ inputs.environment }}-${{ inputs.platform }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.41.9" # keep in sync with the team's local Flutter
|
||||
JAVA_VERSION: "17" # AGP 8 requires JDK 17
|
||||
NODE_VERSION: "20" # matches backend engines + control_center
|
||||
|
||||
jobs:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Expand the `target` input into a JSON list the build jobs gate on.
|
||||
# `all` → every component; otherwise just the one chosen.
|
||||
# ---------------------------------------------------------------------------
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
targets: ${{ steps.expand.outputs.targets }}
|
||||
steps:
|
||||
- id: expand
|
||||
run: |
|
||||
if [ "${{ inputs.target }}" = "all" ]; then
|
||||
echo 'targets=["customer","mitra","backend","control_center"]' >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo 'targets=["${{ inputs.target }}"]' >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- run: echo "Building → ${{ steps.expand.outputs.targets }} (env=${{ inputs.environment }}, platform=${{ inputs.platform }})"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# customer app — ANDROID (Linux runner). Debug-signed (no keystore in repo).
|
||||
# ---------------------------------------------------------------------------
|
||||
customer-android:
|
||||
needs: prepare
|
||||
if: contains(fromJSON(needs.prepare.outputs.targets), 'customer') && (inputs.platform == 'android' || inputs.platform == 'both')
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: client_app
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
- run: flutter pub get
|
||||
- name: Build APK
|
||||
run: |
|
||||
flutter build apk \
|
||||
--flavor ${{ inputs.environment }} \
|
||||
-t lib/main_${{ inputs.environment }}.dart \
|
||||
--dart-define-from-file=env/${{ inputs.environment }}.json
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: customer-${{ inputs.environment }}-android-apk
|
||||
path: client_app/build/app/outputs/flutter-apk/app-${{ inputs.environment }}-release.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# customer app — iOS (macOS runner). Unsigned build (--no-codesign).
|
||||
# PREREQ: per-flavor Xcode schemes must exist (staging/prod). Until then this
|
||||
# fails with "The Xcode project does not define custom schemes". Distributable
|
||||
# IPAs additionally need signing certs/profiles as secrets — not set up yet.
|
||||
# ---------------------------------------------------------------------------
|
||||
customer-ios:
|
||||
needs: prepare
|
||||
if: contains(fromJSON(needs.prepare.outputs.targets), 'customer') && (inputs.platform == 'ios' || inputs.platform == 'both')
|
||||
runs-on: macos-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: client_app
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
- run: flutter pub get
|
||||
- name: Build iOS (unsigned)
|
||||
run: |
|
||||
flutter build ios --no-codesign \
|
||||
--flavor ${{ inputs.environment }} \
|
||||
-t lib/main_${{ inputs.environment }}.dart \
|
||||
--dart-define-from-file=env/${{ inputs.environment }}.json
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: customer-${{ inputs.environment }}-ios-app
|
||||
path: client_app/build/ios/iphoneos/Runner.app
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# mitra app — ANDROID (Linux runner).
|
||||
# ---------------------------------------------------------------------------
|
||||
mitra-android:
|
||||
needs: prepare
|
||||
if: contains(fromJSON(needs.prepare.outputs.targets), 'mitra') && (inputs.platform == 'android' || inputs.platform == 'both')
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mitra_app
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
- run: flutter pub get
|
||||
- name: Build APK
|
||||
run: |
|
||||
flutter build apk \
|
||||
--flavor ${{ inputs.environment }} \
|
||||
-t lib/main_${{ inputs.environment }}.dart \
|
||||
--dart-define-from-file=env/${{ inputs.environment }}.json
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mitra-${{ inputs.environment }}-android-apk
|
||||
path: mitra_app/build/app/outputs/flutter-apk/app-${{ inputs.environment }}-release.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# mitra app — iOS (macOS runner). Same PREREQ caveats as customer-ios.
|
||||
# ---------------------------------------------------------------------------
|
||||
mitra-ios:
|
||||
needs: prepare
|
||||
if: contains(fromJSON(needs.prepare.outputs.targets), 'mitra') && (inputs.platform == 'ios' || inputs.platform == 'both')
|
||||
runs-on: macos-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mitra_app
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
- run: flutter pub get
|
||||
- name: Build iOS (unsigned)
|
||||
run: |
|
||||
flutter build ios --no-codesign \
|
||||
--flavor ${{ inputs.environment }} \
|
||||
-t lib/main_${{ inputs.environment }}.dart \
|
||||
--dart-define-from-file=env/${{ inputs.environment }}.json
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mitra-${{ inputs.environment }}-ios-app
|
||||
path: mitra_app/build/ios/iphoneos/Runner.app
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# control_center — Vite SPA static build (Linux). VITE_API_BASE_URL is baked
|
||||
# in at build time. Override defaults via repo Variables CC_API_BASE_URL_PROD
|
||||
# / CC_API_BASE_URL_STAGING (Settings → Secrets and variables → Actions).
|
||||
# ---------------------------------------------------------------------------
|
||||
control_center:
|
||||
needs: prepare
|
||||
if: contains(fromJSON(needs.prepare.outputs.targets), 'control_center')
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: control_center
|
||||
env:
|
||||
VITE_API_BASE_URL: >-
|
||||
${{ inputs.environment == 'prod'
|
||||
&& (vars.CC_API_BASE_URL_PROD || 'https://internal.halobestie.com')
|
||||
|| (vars.CC_API_BASE_URL_STAGING || 'https://staging-internal.halobestie.com') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: npm
|
||||
cache-dependency-path: control_center/package-lock.json
|
||||
- run: npm ci
|
||||
- name: Build (VITE_API_BASE_URL=${{ env.VITE_API_BASE_URL }})
|
||||
run: npm run build
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: control-center-${{ inputs.environment }}-dist
|
||||
path: control_center/dist
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# backend — Docker image (Linux). Env-AGNOSTIC (config is runtime env vars),
|
||||
# built once and exported as a loadable tarball. Deploy without a registry:
|
||||
# gunzip -c halobestie-backend-*.tar.gz | docker load
|
||||
# ---------------------------------------------------------------------------
|
||||
backend:
|
||||
needs: prepare
|
||||
if: contains(fromJSON(needs.prepare.outputs.targets), 'backend')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build image
|
||||
run: docker build -t halobestie-backend:${{ inputs.environment }} ./backend
|
||||
- name: Export image as tarball
|
||||
run: docker save halobestie-backend:${{ inputs.environment }} | gzip > halobestie-backend-${{ inputs.environment }}.tar.gz
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: backend-${{ inputs.environment }}-image
|
||||
path: halobestie-backend-${{ inputs.environment }}.tar.gz
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
Reference in New Issue
Block a user