Skip to main content

CI/CD Pipeline

Drop CI/CD Pipeline

Last Updated: 2026-03-04 Source: .github/workflows/ (11 workflow files) Decision: ADR-012 — AWS App Runner


Overview

Drop uses GitHub Actions for CI/CD. All pipeline logic lives in .github/workflows/. There are 11 workflow files covering quality gates, staging and production deployment, hotfix path, mobile CI/OTA/store releases, and Terraform infrastructure.

Deployment targets:

Service Target Registry
drop-app (Next.js BFF) AWS App Runner (eu-west-1) Amazon ECR
drop-api (Hono mobile API) AWS App Runner (eu-west-1) Amazon ECR
drop-mobile (Expo React Native) Expo EAS (OTA + store builds) EAS / App Store / Play Store
Landing page Vercel

Workflow Files

File Name Trigger
ci.yml CI — Quality Gate push/PR to main, develop
deploy-staging.yml Deploy to Staging push to main, manual dispatch
deploy.yml Deploy to Production push to main
hotfix.yml Hotfix Deploy manual dispatch only
dast.yml DAST — OWASP ZAP Security Scan schedule (Sun 02:00 UTC), manual dispatch
mobile-ci.yml Mobile CI — Lint + Test push/PR to main, develop (paths: src/drop-mobile/**)
mobile-deploy.yml Mobile Deploy — OTA Update push to main (paths: src/drop-mobile/**)
mobile-release.yml Mobile Release — Store Build + Submit tag push (mobile-v*)
terraform-ci.yml Terraform Plan PR touching infrastructure/terraform/**, manual dispatch
terraform-plan.yml Terraform Plan PR touching infrastructure/terraform/**, manual dispatch
terraform-drift.yml Terraform Drift Detection schedule (Mon 06:00 UTC), manual dispatch

CI Quality Gate (ci.yml)

Triggers on push or PR to main or develop. Uses path filtering (dorny/paths-filter) so jobs only run when their package has changed (or always on main).

Jobs

1. detect-changes Detects which packages changed: drop-app, drop-api, drop-mobile, infrastructure. Outputs boolean flags used by downstream jobs.

2. drop-app-test — Lint + Test + Coverage

  • npm ci in src/drop-app
  • ESLint (npm run lint)
  • TypeScript type-check (npx tsc --noEmit)
  • Vitest with coverage (npx vitest run --coverage --reporter=verbose)
  • Coverage threshold gate (HARD FAIL if below): statements 80%, branches 70%, functions 80%, lines 80%
  • Coverage ratchet on PRs: blocks merge if coverage drops below main baseline
  • PR comment with coverage table
  • npm audit --omit=dev --audit-level=high

3. drop-app-e2e — Playwright E2E (depends on drop-app-test)

  • Builds app (npm run build)
  • Installs Playwright with Chromium
  • Runs all E2E tests (npx playwright test --reporter=html)
  • Uploads playwright-report/ artifact on failure (14-day retention)

4. drop-app-mutation — Stryker Mutation Testing (PR only, depends on drop-app-test)

  • Runs only on files changed in the PR (not test files)
  • Mutation score warning threshold: 60%

5. drop-api-test — Lint + Test for src/drop-api

  • TypeScript type-check
  • npm run test

6. drop-mobile-test — Lint + Test for src/drop-mobile

  • npx jest --passWithNoTests --ci
  • Fails if no Jest config found (tests required before production launch per PSD2 requirement)

7. docker-scan-app — Docker Build + Trivy Scan for drop-app (depends on drop-app-test + drop-app-e2e)

  • Builds Docker image tagged with commit SHA
  • Trivy scan: exits 1 on HIGH/CRITICAL unfixed vulnerabilities
  • Uploads SARIF results to GitHub Security tab

8. docker-scan-api — Docker Build + Trivy Scan for drop-api (depends on drop-api-test)

  • Builds drop-api Docker image
  • Trivy scan: exits 1 on HIGH/CRITICAL unfixed vulnerabilities

9. accessibility — Axe-core WCAG 2.1 AA audit (depends on drop-app-test)

  • Builds app and runs Playwright accessibility tests
  • Uses tests/accessibility/ config

10. sonarcloud — SonarCloud static analysis (depends on drop-app-test + drop-api-test)

  • Runs after any test job succeeds
  • Requires SONAR_TOKEN secret

11. security — Snyk dependency scan (runs always)

  • Scans all projects (--all-projects)
  • Severity threshold: high
  • Requires SNYK_TOKEN secret

11b. semgrep — Semgrep SAST

  • Runs on push to main and PRs targeting main
  • Uploads SARIF to GitHub Security tab
  • Requires SEMGREP_APP_TOKEN secret

11c. gitleaks — Secrets detection

  • Scans full git history (fetch-depth: 0)
  • Requires GITLEAKS_LICENSE secret

12. lighthouse — Lighthouse CI (depends on drop-app-test)

  • Builds app and runs npx lhci autorun
  • Requires LHCI_GITHUB_APP_TOKEN secret

13. quality-gate — Required status check (depends on all above)

  • Aggregates results from all jobs
  • Fails the pipeline if any required job failed
  • This job is configured as the required branch protection status check — PRs cannot merge unless it passes

Coverage Thresholds

Metric Threshold
Statements 80%
Branches 70%
Functions 80%
Lines 80%

Coverage is enforced by a hard-fail inline script in drop-app-test. PRs also run the ratchet check: coverage cannot decrease below the main branch baseline.


Staging Deployment (deploy-staging.yml)

Triggers on push to main or manual dispatch. Runs concurrently with (and independently of) the production deployment.

Flow

  1. Build + Push (staging) — Authenticate to AWS via OIDC, login to ECR, build both images with staging secrets, push with staging-{sha} tag and staging-latest floating tag.
  2. Deploy to Staging App Runneraws apprunner update-service pointing to new image, then aws apprunner start-deployment for both drop-app and drop-api staging services.
  3. Health check — Polls $STAGING_APP_URL/api/health every 10 seconds, up to 30 attempts (5 minutes). Fails the deployment if health check does not return HTTP 200.

ECR image tags: staging-{sha} and staging-latest

Required secrets: AWS_ACCOUNT_ID, AWS_ROLE_ARN, APP_RUNNER_STAGING_APP_ARN, APP_RUNNER_STAGING_API_ARN, STAGING_JWT_SECRET, STAGING_APP_URL


Production Deployment (deploy.yml)

Triggers on push to main. Does NOT cancel in-progress deploys (cancel-in-progress: false). Runs in the production GitHub environment (can require manual approvals if configured).

Flow

  1. Build + Push

    • Authenticate to AWS via OIDC (no long-lived credentials)
    • Record previous ECR image tags (for rollback)
    • Build drop-app with production secrets — Docker build physically includes test stage, so test failure blocks push
    • Build drop-api
    • Push both images to ECR with {sha} and latest tags
    • Sign images with cosign (Sigstore)
  2. Deploy to App Runner

    • aws apprunner start-deployment for both drop-app and drop-api services
  3. Health check + Automatic rollback

    • Polls $NEXT_PUBLIC_APP_URL/api/health every 10 seconds, up to 30 attempts (5 minutes)
    • On health check failure: automatically runs aws apprunner update-service with previous image tag and triggers redeployment of both services
    • Pipeline exits with failure regardless (alerts on failure)
  4. k6 Smoke Test (post-deploy, depends on successful deploy)

    • Installs k6 and runs tests/k6/smoke.js against the production API

ECR image tags: {sha} and latest

Required secrets: AWS_ACCOUNT_ID, AWS_ROLE_ARN, APP_RUNNER_APP_ARN, APP_RUNNER_API_ARN, JWT_SECRET, NEXT_PUBLIC_APP_URL


Full Pipeline: Staging then Production

Both deploy-staging.yml and deploy.yml trigger on push to main. They run in parallel. The staging workflow is designed to catch issues before production traffic is affected.

git push main
  │
  ├── ci.yml (Quality Gate)
  │   └── detect-changes → tests → docker scan → security scans → lighthouse → quality-gate
  │
  ├── deploy-staging.yml
  │   └── build-and-push → deploy staging → health check
  │
  └── deploy.yml
      └── build-and-push (with cosign signing)
          → deploy production
          → health check (+ auto-rollback on failure)
          → k6 smoke test

Hotfix Path (hotfix.yml)

Manual dispatch only. Used when a critical fix must bypass the staging step.

Inputs

Input Required Description
reason Yes Reason for hotfix (min 10 chars, logged permanently in Actions log)
skip_e2e No Skip E2E tests — emergency only, requires Alem approval

Flow

  1. audit-log — Logs actor, SHA, branch, timestamp, reason. Fails if reason is fewer than 10 characters.
  2. test (depends on audit-log, cannot skip) — Lint, type-check, Vitest unit tests
  3. e2e (depends on test, skippable via skip_e2e input) — Full Playwright E2E suite
  4. build-and-push (depends on test + e2e, runs if e2e was success or skipped) — Builds both images with hotfix-{sha} tag, pushes to ECR, signs with cosign
  5. deploy — Updates App Runner services directly (no staging step), health check with automatic rollback on failure

DAST — OWASP ZAP (dast.yml)

Scheduled weekly (Sunday 02:00 UTC) and available for manual dispatch.

  • Runs OWASP ZAP full scan against $STAGING_URL (staging, not production)
  • Uses .zap/rules.tsv for custom rules
  • Uploads HTML report artifact (30-day retention)
  • Uploads SARIF to GitHub Security tab

Mobile CI (mobile-ci.yml)

Triggers on push or PR to main / develop when src/drop-mobile/** changes.

Jobs

lint-and-test

  • Jest tests with coverage threshold enforcement (70%)
  • Posts PR comment with test results

maestro-e2e-ios (runs on macos-14)

  • Builds Expo dev client via npx expo prebuild --platform ios
  • Builds iOS app with xcodebuild targeting iPhone 15 simulator
  • Runs Maestro E2E flows from src/drop-mobile/e2e/flows/
  • Uploads JUnit XML results artifact (14-day retention)

maestro-e2e-android (runs on ubuntu-latest)

  • Builds Expo dev client via npx expo prebuild --platform android
  • Runs in Android emulator (API 33, x86_64, Pixel 6 profile)
  • Runs Maestro E2E flows
  • Uploads JUnit XML results artifact (14-day retention)

Mobile OTA Deploy (mobile-deploy.yml)

Triggers on push to main when src/drop-mobile/** changes.

Flow

  1. Auto-bumps patch version in app.json and package.json (e.g. 1.0.51.0.6)
  2. Commits version bump with [skip ci] to avoid re-trigger
  3. Runs eas update --channel production — pushes JavaScript bundle OTA to all installed apps on the production channel

Required secrets: EXPO_TOKEN


Mobile Store Release (mobile-release.yml)

Triggers on tag push matching mobile-v* (e.g. mobile-v2.0.0).

Flow

  1. Extracts version from tag name
  2. Updates app.json version
  3. eas build --platform all --profile production — builds native iOS and Android binaries via EAS Build
  4. eas submit --platform ios — submits to Apple TestFlight (continues on error if Apple Developer not enrolled)
  5. eas submit --platform android — submits to Google Play Internal Track (continues on error if Play Console not set up)
  6. Updates src/drop-app/src/config/app-versions.json with new version numbers
  7. Creates GitHub Release with summary of submission status

Required secrets: EXPO_TOKEN, APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD


Terraform — Infrastructure (terraform-ci.yml, terraform-plan.yml, terraform-drift.yml)

terraform-ci.yml and terraform-plan.yml

Both files trigger on PRs touching infrastructure/terraform/** and on manual dispatch. They run the same steps:

  1. Terraform format check (terraform fmt -check -recursive, non-blocking)
  2. terraform init
  3. terraform validate
  4. terraform plan -var-file=environments/production/terraform.tfvars
  5. Posts plan output as PR comment (format/init/validate/plan status table + collapsible plan output)
  6. Fails if plan exits with error

Note: terraform-ci.yml and terraform-plan.yml are effectively duplicates. Both are currently active and will both comment on infrastructure PRs. This should be consolidated to a single file.

Required secrets: AWS_ROLE_ARN Terraform version: 1.7.0 Working directory: infrastructure/terraform

terraform-drift.yml

Scheduled weekly (Monday 06:00 UTC) and available for manual dispatch. Detects drift between Terraform state and actual AWS resources.

  • terraform init --backend-config="key=drop/production/terraform.tfstate"
  • terraform plan -detailed-exitcode — exit code 2 means drift was detected
  • On drift: creates a GitHub Issue titled [Drift] Infrastructure drift detected — YYYY-MM-DD tagged infrastructure, drift, with plan output and cc @alem
  • On no drift: logs "No infrastructure drift detected."

Security Checks Summary

Check Tool When Fails Pipeline
Dependency audit npm audit Every CI run Yes (high severity)
Dependency scan Snyk Every CI run Yes (high severity)
SAST Semgrep Push to main + PRs to main Yes
Container scan Trivy Every CI run (after tests pass) Yes (HIGH/CRITICAL unfixed)
Secrets detection GitLeaks Every CI run Yes
DAST OWASP ZAP Weekly (Sunday) No (report only)
Code quality SonarCloud After tests pass Via quality-gate job

Branch Protection / Required Status Checks

The quality-gate job in ci.yml is the single required status check for merging PRs to main. It aggregates:

  • drop-app-test (lint, type-check, unit tests, coverage)
  • drop-app-e2e (Playwright)
  • drop-api-test
  • docker-scan-app (Trivy)
  • sonarcloud
  • security (Snyk)
  • lighthouse
  • semgrep
  • gitleaks

A PR cannot merge to main if any of these fail.


Required GitHub Secrets

Secret Used by
AWS_ACCOUNT_ID ECR registry URL construction
AWS_ROLE_ARN OIDC authentication to AWS
APP_RUNNER_APP_ARN Production drop-app App Runner service
APP_RUNNER_API_ARN Production drop-api App Runner service
APP_RUNNER_STAGING_APP_ARN Staging drop-app App Runner service
APP_RUNNER_STAGING_API_ARN Staging drop-api App Runner service
JWT_SECRET Production JWT signing key
STAGING_JWT_SECRET Staging JWT signing key
NEXT_PUBLIC_APP_URL Production app URL (health check + env)
STAGING_APP_URL Staging app URL (health check)
STAGING_URL DAST scan target
SONAR_TOKEN SonarCloud analysis
SNYK_TOKEN Snyk dependency scan
SEMGREP_APP_TOKEN Semgrep SAST
GITLEAKS_LICENSE GitLeaks secrets detection
LHCI_GITHUB_APP_TOKEN Lighthouse CI
EXPO_TOKEN EAS CLI authentication
APPLE_ID Apple App Store submission
APPLE_APP_SPECIFIC_PASSWORD Apple App Store submission

Local Development

# Start PostgreSQL via Docker
docker compose up -d

# Apply schema
make db-push

# Run unit tests
cd src/drop-app && npm test

# Run E2E tests (starts dev server automatically)
cd src/drop-app && npx playwright test

# Lint
cd src/drop-app && npm run lint

# Type-check
cd src/drop-app && npx tsc --noEmit

References