CI/CD Pipeline
Drop CI/CD Pipeline
Last Updated:updated: 2026-03-0402-13
Source: , .github/workflows/src/drop-app/package.json(11Dockerfile, workflowfly.toml, files)vitest.config.ts, Decision: ADR-012 — AWS App Runnerplaywright.config.ts
OverviewCurrent State
Drop usesis in MVP/pre-production stage. Core CI/CD infrastructure exists including a GitHub Actions forworkflow.
What Allexists:
- GitHub
livesActionsinCI workflow (.github/workflows/ci.yml.)Therewithare511jobs:workflowlint-and-typecheck,filestest,coveringbuild,qualitye2e,gates,docker-build - Dockerfile with multi-stage build (
Dockerfile:1-63) - docker-compose for local and production
deployment,(docker-compose.yml,hotfixdocker-compose.production.yml) - Fly.io
mobiledeploymentCI/OTA/storeconfigreleases,(fly.toml) - Vitest
Terraformunit/integrationinfrastructure.test framework (vitest.config.ts) - Playwright E2E test framework (
playwright.config.ts) - Health check endpoint (
/api/health) - QA report generation via
scripts/qa-report.js(automated in CI)
DeploymentWhat targets:does not exist yet:
- Automated deployment pipeline (CI builds but does not deploy)
- Container registry integration
- Automated security scanning (npm audit, Snyk)
- Test coverage reporting
- Staging 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
|
| |
|
| |
|
tests/setup.ts |
|
./src |
Playwright Workflow(E2E)
Config: src/drop-app/playwright.config.ts:1-39
Test dir |
|
|
Parallel |
false | -- |
Workers |
||
| Retries (CI) | ||
| Timeout | 30,000ms | |
| Base URL | |
|
Reporter |
||
Trace |
||
| | |
| | |
| | |
| | |
| | |
|
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-appESLint (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 belowmainbaselinePR comment with coverage tablenpm 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 ChromiumRuns all E2E tests (npx playwright test --reporter=html)Uploadsplaywright-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-checknpm run test
6. drop-mobile-test — Lint + Test for src/drop-mobile
npx jest --passWithNoTests --ciFails 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 SHATrivy scan: exits 1 on HIGH/CRITICAL unfixed vulnerabilitiesUploads SARIF results to GitHub Security tab
8. docker-scan-api — Docker Build + Trivy Scan for drop-api (depends on drop-api-test)
Buildsdrop-apiDocker imageTrivy 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 testsUsestests/accessibility/config
10. sonarcloud — SonarCloud static analysis (depends on drop-app-test + drop-api-test)
Runs after any test job succeedsRequiresSONAR_TOKENsecret
11. security — Snyk dependency scan (runs always)
Scans all projects (--all-projects)Severity threshold: highRequiresSNYK_TOKENsecret
11b. semgrep — Semgrep SAST
Runs on push tomainand PRs targetingmainUploads SARIF to GitHub Security tabRequiresSEMGREP_APP_TOKENsecret
11c. gitleaks — Secrets detection
Scans full git history (fetch-depth: 0)RequiresGITLEAKS_LICENSEsecret
12. lighthouse — Lighthouse CI (depends on drop-app-test)
Builds app and runsnpx lhci autorunRequiresLHCI_GITHUB_APP_TOKENsecret
13. quality-gate — Required status check (depends on all above)
Aggregates results from all jobsFails the pipeline if any required job failedThis job is configured as the required branch protection status check — PRs cannot merge unless it passes
Coverage Thresholds
CoverageTest is enforced by a hard-fail inline script in drop-app-test. PRs also run the ratchet checkprojects:: 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
Builduser-flows+--PushBasic user journey tests (staging)— Authenticate to AWS via OIDC, login to ECR, build both images with staging secrets, push withstaging-{sha}user-flows.spec.tstag andstaging-latestfloating tag.)Deploy to Staging App Runner—aws apprunner update-servicefull-flowspointing--toCompletenewfeatureimage,journeysthen(aws apprunner start-deploymentfull-flows.spec.tsfor bothdrop-appanddrop-apistaging services.)Healthinput-chaoscheck--—Malicious/edge-casePollsinput testing (input-chaos.spec.ts). Depends on$STAGING_APP_URL/api/healthuser-flowsevery 10 seconds, up to 30 attempts (5 minutes).Fails the deployment if health check does not return HTTP 200.
ECRWeb imageserver tags:config: Auto-starts staging-{sha}npm run devandfor staging-latest
Requiredtests. secrets:Reuses AWS_ACCOUNT_ID,existing AWS_ROLE_ARN,server APP_RUNNER_STAGING_APP_ARN,if APP_RUNNER_STAGING_API_ARN,running. STAGING_JWT_SECRET,30s timeout.STAGING_APP_URL
Production Deployment (deploy.yml)Targets
Triggers on push to main. Does NOT cancel in-progress deploys
Fly.io (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-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)
Deploy to App Runner
aws apprunner start-deployment for both drop-app and drop-api services
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)
k6 Smoke Test (post-deploy, depends on successful deploy)
Installs k6 and runs tests/k6/smoke.js against the production API
Build + Push
Authenticate to AWS via OIDC (no long-lived credentials)Record previous ECR image tags (for rollback)Builddrop-appwith production secrets — Docker build physically includes test stage, so test failure blocks pushBuilddrop-apiPush both images to ECR with{sha}andlatesttagsSign 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 runsaws apprunner update-servicewith previous image tag and triggers redeployment of both servicesPipeline exits with failure regardless (alerts on failure)
k6 Smoke Test (post-deploy, depends on successful deploy)
Installs k6 and runstests/k6/smoke.jsagainst the production API
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
| ||
|
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 testse2e(depends on test, skippable viaskip_e2einput) — Full Playwright E2E suitebuild-and-push(depends on test + e2e, runs if e2e was success or skipped) — Builds both images withhotfix-{sha}tag, pushes to ECR, signs with cosigndeploy— 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 rulesUploads 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.
JobsStaging)
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 vianpx expo prebuild --platform iosBuilds iOS app with xcodebuild targeting iPhone 15 simulatorRuns Maestro E2E flows fromsrc/drop-mobile/e2e/flows/Uploads JUnit XML results artifact (14-day retention)
maestro-e2e-android (runs on ubuntu-latest)
Builds Expo dev client vianpx expo prebuild --platform androidRuns in Android emulator (API 33, x86_64, Pixel 6 profile)Runs Maestro E2E flowsUploads 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 inapp.jsonandpackage.json(e.g.1.0.5→1.0.6)Commits version bump with[skip ci]to avoid re-triggerRunseas update --channel production— pushes JavaScript bundle OTA to all installed apps on the production channel
Required secrets:Config: EXPO_TOKENfly.toml:1-28
Mobile Store Release (mobile-release.yml)
Triggers on tag push matching mobile-v* (e.g. mobile-v2.0.0).
Flow
Extracts version from tag nameUpdatesapp.jsonversioneas 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)Updatessrc/drop-app/src/config/app-versions.jsonwith new version numbersCreates 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.tfvarsPosts 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 detectedOn drift: creates a GitHub Issue titled[Drift] Infrastructure drift detected — YYYY-MM-DDtaggedinfrastructure,drift, with plan output andcc @alemOn no drift: logs "No infrastructure drift detected."
Security Checks Summary
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
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
Local Development
# StartDeploy 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 DockerDocker)
docker compose up -d
# Apply schema
make db-push
#
Existing testsGitHub cdActions src/drop-appCI &&Workflow
File: .github/workflows/ci.yml
Triggers on push/PR to main or master:
Jobs:
1. lint-and-typecheck — npm ci, npm run lint, tsc --noEmit
2. test — npm ci, npm test # Run E2E tests--if-present (startsdepends devon serverlint-and-typecheck)
automatically)3. cdbuild src/drop-app— &&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 Lint+ cdfull-flows, src/drop-appgenerate &&QA npmreport, runupload lintartifacts #(depends Type-checkon cdbuild)
src/drop-app5. &&docker-build npx— tscdocker build --noEmitt drop-app:ci (depends on test + build + e2e)
Artifacts
uploaded:References
ADR-012playwright-report/—AWSPlaywrightAppHTMLRunnerreport (7 day retention)Securityqa-report.htmlHardening—ChecklistQA Vitestmetricsconfigreport Playwright(pass/fail,configexecution time)
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).