CI/CD Pipeline
Drop CI/CD Pipeline
Last updated:Updated: 2026-02-1303-04
Source: src/drop-app/package.json.github/workflows/, Dockerfile,(11 fly.toml,workflow vitest.config.ts,files)
Decision: ADR-012 — AWS App Runnerplaywright.config.ts
Current StateOverview
Drop is in MVP/pre-production stage. Core CI/CD infrastructure exists including auses GitHub Actions workflow.
WhatCI/CD. exists:
- pipeline
GitHublogicActionslivesCIinworkflow (.github/workflows/ci.yml).withThere5arejobs:11lint-and-typecheck,workflowtest,filesbuild,coveringe2e,qualitydocker-buildgates, Dockerfile with multi-stage build (Dockerfile:1-63)docker-compose for localstaging and production(docker-compose.yml,deployment,docker-compose.production.yml)hotfix Fly.iopath,deploymentmobileconfigCI/OTA/store(fly.toml)releases, Vitestandunit/integrationTerraformtest framework (vitest.config.ts)Playwright E2E test framework (playwright.config.ts)Health check endpoint (/api/health)QA report generation viascripts/qa-report.js(automated in CI)
WhatDeployment does not exist yet:targets:
Automated deployment pipeline (CI builds but does not deploy)Container registry integrationAutomated security scanning (npm audit, Snyk)Test coverage reportingStaging environment (Fly.io config exists but not deployed)
Build Pipeline
Step 1: Install Dependencies
npm ci
Installs exact versions from package-lock.json.
Step 2: Lint
npm run lint # eslint
Step 3: Type Check
npx tsc --noEmit
Step 4: Unit + Integration Tests
npm test # vitest run
Runs all tests in tests/**/*.test.ts (from vitest.config.ts:7). Test setup: tests/setup.ts sets NODE_ENV=test.
Step 5: Build
npm run build # next build
Produces standalone output for Docker deployment.
Step 6: Docker Build
docker build -t drop-app .
Multi-stage build: deps -> builder -> runner.
Step 7: E2E Tests (requires running server)
npx playwright test
Requires dev server on http://localhost:3000. Playwright auto-starts it via webServer config.
Test Framework Configuration
Vitest (Unit + Integration)
Config: src/drop-app/vitest.config.ts:1-15
| Registry | ||
|---|---|---|
drop-app (Next.js BFF) |
AWS App Runner () |
Amazon ECR |
drop-api (Hono mobile API) |
AWS App Runner () |
Amazon ECR |
drop-mobile (Expo React Native) |
Expo EAS (OTA + store builds) |
EAS / App Store / Play Store |
Vercel |
— |
Playwright
(E2E)
Workflow
Config: src/drop-app/playwright.config.ts:1-39
| Trigger | ||
|---|---|---|
ci.yml |
CI — Quality Gate | push/PR to , develop |
deploy-staging.yml |
Deploy to Staging | push to , |
deploy.yml |
||
|
||
hotfix.yml |
manual dispatch only | |
dast.yml |
DAST — OWASP ZAP Security Scan | schedule (Sun 02:00 UTC), manual dispatch |
|
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 ciinsrc/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
mainbaseline - 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-apiDocker 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_TOKENsecret
11. security — Snyk dependency scan (runs always)
- Scans all projects (
--all-projects) - Severity threshold: high
- Requires
SNYK_TOKENsecret
11b. semgrep — Semgrep SAST
- Runs on push to
mainand PRs targetingmain - Uploads SARIF to GitHub Security tab
- Requires
SEMGREP_APP_TOKENsecret
11c. gitleaks — Secrets detection
- Scans full git history (
fetch-depth: 0) - Requires
GITLEAKS_LICENSEsecret
12. lighthouse — Lighthouse CI (depends on drop-app-test)
- Builds app and runs
npx lhci autorun - Requires
LHCI_GITHUB_APP_TOKENsecret
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 Testratchet projects: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
- Build + Push (staging) — Authenticate to AWS via OIDC, login to ECR, build both images with staging secrets, push with
user-flowsstaging-{sha}--tagBasicanduserstaging-latestjourneyfloatingtests (user-flows.spec.ts)tag. - Deploy to Staging App Runner —
full-flowsaws apprunner update-service--pointingCompletetofeaturenewjourneysimage,(thenfull-flows.spec.tsaws apprunner start-deployment)for bothdrop-appanddrop-apistaging services. - Health check — Polls
input-chaos$STAGING_APP_URL/api/health--everyMalicious/edge-case10inputseconds,testingup to 30 attempts (input-chaos.spec.ts)5 minutes).DependsFailsontheuser-flows.deployment if health check does not return HTTP 200.
Required devsecrets: AWS_ACCOUNT_ID, forAWS_ROLE_ARN, E2EAPP_RUNNER_STAGING_APP_ARN, tests.APP_RUNNER_STAGING_API_ARN, ReusesSTAGING_JWT_SECRET, existing server if running. 30s timeout.STAGING_APP_URL
Production Deployment Targets(deploy.yml)
Fly.io
Triggers on push to main. Does NOT cancel in-progress deploys (Staging)cancel-in-progress: false). Runs in the production GitHub environment (can require manual approvals if configured).
Flow
-
Build + Push
- Authenticate to AWS via OIDC (no long-lived credentials)
- Record previous ECR image tags (for rollback)
- Build
drop-appwith production secrets — Docker build physically includes test stage, so test failure blocks push - Build
drop-api - Push both images to ECR with
{sha}andlatesttags - Sign images with cosign (Sigstore)
-
Deploy to App Runner
aws apprunner start-deploymentfor bothdrop-appanddrop-apiservices
-
Health check + Automatic rollback
- Polls
$NEXT_PUBLIC_APP_URL/api/healthevery 10 seconds, up to 30 attempts (5 minutes) - On health check failure: automatically runs
aws apprunner update-servicewith previous image tag and triggers redeployment of both services - Pipeline exits with failure regardless (alerts on failure)
- Polls
-
k6 Smoke Test (post-deploy, depends on successful deploy)
- Installs k6 and runs
tests/k6/smoke.jsagainst the production API
- Installs k6 and runs
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
- audit-log — Logs actor, SHA, branch, timestamp, reason. Fails if reason is fewer than 10 characters.
- test (depends on audit-log, cannot skip) — Lint, type-check, Vitest unit tests
- e2e (depends on test, skippable via
skip_e2einput) — Full Playwright E2E suite - 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 - 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.tsvfor 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
Config: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
- Auto-bumps patch version in
app.jsonandpackage.json(e.g.1.0.5→1.0.6) - Commits version bump with
[skip ci]to avoid re-trigger - Runs
eas update --channel production— pushes JavaScript bundle OTA to all installed apps on the production channel
Required secrets: fly.toml:1-28EXPO_TOKEN
Mobile Store Release (mobile-release.yml)
Triggers on tag push matching mobile-v* (e.g. mobile-v2.0.0).
Flow
- Extracts version from tag name
- Updates
app.jsonversion eas build --platform all --profile production— builds native iOS and Android binaries via EAS Buildeas submit --platform ios— submits to Apple TestFlight (continues on error if Apple Developer not enrolled)eas submit --platform android— submits to Google Play Internal Track (continues on error if Play Console not set up)- Updates
src/drop-app/src/config/app-versions.jsonwith new version numbers - 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:
- Terraform format check (
terraform fmt -check -recursive, non-blocking) terraform initterraform validateterraform plan -var-file=environments/production/terraform.tfvars- Posts plan output as PR comment (format/init/validate/plan status table + collapsible plan output)
- 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-DDtaggedinfrastructure,drift, with plan output andcc @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-testdocker-scan-app(Trivy)sonarcloudsecurity(Snyk)lighthousesemgrepgitleaks
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
# DeployStart to Fly.io staging
fly deploy
# Set secrets
fly secrets set JWT_SECRET="your-secret"
fly secrets set NEXT_PUBLIC_SERVICE_MODE="mock"
Region: arn (Stockholm)
Auto-scaling: Scales to 0 when idle, auto-starts on request.
Docker (Self-hosted)
# Local dev (PostgreSQL 16 via Docker)Docker
docker compose up -d
# Apply schema
make db-push
# Run
Existingunit GitHubtests
Actionscd CIsrc/drop-app Workflow
File: .github/workflows/ci.yml
Triggers on push/PR to main or master:
Jobs:
1. lint-and-typecheck —&& npm ci,test
# Run E2E tests (starts dev server automatically)
cd src/drop-app && npx playwright test
# Lint
cd src/drop-app && npm run lint,lint
# Type-check
cd src/drop-app && npx tsc --noEmit
2. test — npm ci, npm test --if-present (depends on lint-and-typecheck)
3. build — npm ci, npm run build with JWT_SECRET placeholder (depends on lint-and-typecheck)
4. e2e — npm ci, npx playwright install chromium, npm run build, npm run start (production mode), npx playwright test user-flows + full-flows, generate QA report, upload artifacts (depends on build)
5. docker-build — docker build -t drop-app:ci (depends on test + build + e2e)
Artifacts
uploaded:
References
ADR-012 —playwright-report/PlaywrightAWSHTMLAppreport (7 day retention)RunnerSecurityqa-report.html—HardeningQAChecklist- Vitest
reportconfig - Playwright
execution time)config
Not yet implemented:
Security scan (npm audit, Snyk)Deploy to staging (fly deploy)Deploy to production (manual approval gate)
Status: Full CI pipeline including E2E tests in place. CD deployment tracked in security hardening checklist (security/hardening-checklist.md:120-126).