Skip to main content
Engineering deep dive · Updated September 2026

Our CI/CD and Automated Testing Pipeline for Web and Mobile

The delivery pipeline our teams set up on most web, API and mobile projects: what runs on every pull request, how changes reach production safely, how mobile builds get signed and distributed, and what we measure to know whether it's working.

Published September 16, 2026 · 12 min read

Principles behind the pipeline

A pipeline exists to make releasing boring. When deploys are rare, large and manual, every release is a risk event and teams batch even more changes to avoid them. We design for the opposite: small changes, automated checks that people trust, and production releases you can undo quickly. Four principles shape the details:

  • Fast feedback first. The checks developers wait on must be quick. Slow suites run later or in parallel, never as a gate on every commit.
  • Build once, promote the artefact. The container image or app binary tested in staging is byte-for-byte what reaches production. Only configuration changes between environments.
  • Separate deploy from release. Code reaches production dark behind feature flags, and exposure to users is a separate, reversible decision.
  • Flaky tests are bugs. A test that fails randomly teaches people to click "re-run". We quarantine it within a day and fix or delete it.
CI/CD pipeline from pull request to production A short-lived branch opens a pull request, which triggers parallel checks: lint and type-check, unit tests, SAST, dependency scan and secrets scan, plus contract tests. A preview environment is deployed and Playwright end-to-end smoke tests run against it. After review and merge to main, one artefact is built, signed and pushed, deployed to staging with migrations and full E2E, then released to production through canary or blue/green with automated rollback on SLO breach. Feature flags control user exposure. PULL REQUEST (minutes) Short-livedbranch + PR Parallel checkslint · type-check · unitSAST · dependency scansecrets scan · contract testsmigration dry-run Preview envper PR, auto-destroyed E2E smokePlaywright on preview Review + mergeto main MAIN (build once, promote) Build + signimage, SBOM, provenance Stagingmigrations + full E2E Productioncanary / blue-green Release via flags% rollout, kill switch automated rollback on SLO breach
Pull request stage (top) and main-branch promotion (bottom). Deploying and releasing are separate steps.

Trunk-based flow

We use trunk-based development with short-lived branches. Branches normally merge within a day or two and are kept small enough to review properly. We avoid long-running develop and release branches. They delay integration, and merge conflicts pile up exactly when a release is due.

  • main is always releasable. Branch protection requires passing checks, at least one approving review and an up-to-date branch, and a merge queue handles the last point on busy repositories.
  • Unfinished features merge behind a flag, never on a branch that lives for weeks.
  • Conventional commit messages feed automated changelogs and semantic version bumps for libraries and mobile apps.
  • Mobile teams add a short-lived release branch at store submission time, because store review makes "deploy from main" impractical. Only fixes are cherry-picked into it.

Pull request checks

Everything below runs in parallel on each PR. The target is useful feedback within a few minutes, which we get with dependency caching, test sharding and running only affected projects in monorepos (Nx, Turborepo or path filters).

CheckTypical toolsBlocks merge?
Formatting and lintESLint / Biome, Prettier, dotnet format, ktlint, SwiftLint, flutter analyzeYes
Type-checktsc --noEmit, compiler warnings as errors, mypy/pyrightYes
Unit and component testsVitest/Jest, xUnit, pytest, flutter_test, JUnitYes
SASTCodeQL, SemgrepYes for high severity on changed code
Dependency (SCA) scanDependabot / Renovate alerts, OSV-Scanner, TrivyYes for known-exploitable critical issues; others tracked
Secrets scanGitleaks, GitHub push protectionYes, always
Container / IaC scanTrivy, Checkov, terraform plan posted as a commentYes for misconfigurations flagged critical
Contract testsPact, schema diff (OpenAPI, GraphQL)Yes when a breaking change has no versioning
Migration dry-runApply migrations to a disposable databaseYes

Security gates need to be tuned carefully. If every medium finding blocks merges, developers learn to suppress findings in bulk. We block on high-confidence, high-severity issues in new code and track the rest as a backlog with owners.

The test pyramid we aim for

The shape matters more than any coverage percentage:

  • Many unit tests: fast and deterministic, testing domain logic without I/O. This is where edge cases belong.
  • A solid middle of integration tests: real database, queue and cache in containers (Testcontainers), testing repositories, API handlers and message consumers against real dependencies instead of mocks.
  • Contract tests at service boundaries instead of big cross-service E2E suites.
  • A thin layer of E2E tests covering critical user journeys only: sign-up, login, checkout or the core workflow, and billing.

Opinion: the anti-pattern we meet most often is the "ice-cream cone", with hundreds of slow UI tests and few unit tests. It is slow, flaky and expensive to maintain. When we inherit one, we delete E2E tests that duplicate lower-level coverage before writing anything new.

For AI features, deterministic tests aren't enough. Model-driven behaviour needs evaluation datasets and scoring, which we cover in testing and evaluating AI agents before launch. Those eval runs slot into the same pipeline as a separate, non-blocking-by-default job with thresholds.

Contract tests between services

When a web front end, mobile apps and several services all depend on the same APIs, full-stack E2E environments become the bottleneck. Consumer-driven contract tests (Pact) let each consumer publish the interactions it relies on. The provider's pipeline then checks those contracts against its real implementation before merge.

  • A provider can't merge a change that breaks a published consumer expectation.
  • "Can I deploy?" checks against the contract broker stop a consumer version from reaching an environment where its provider version isn't compatible.
  • For public or schema-first APIs, OpenAPI or GraphQL schema diffing in CI (flagging removed fields, changed types and new required parameters) catches most breaking changes at lower cost.

Mobile apps make this matter more. Old app versions stay installed for months, so the API must stay compatible with every version still in use, not just the latest.

Ephemeral preview environments

Each pull request that touches the application gets its own short-lived environment with a unique URL posted on the PR. Product owners and QA review real behaviour before merge, and E2E smoke tests run against it.

  • Front ends: preview deployments on the hosting platform, pointing at a shared staging API or the PR's own backend.
  • Backends: a namespace or stack per PR (Kubernetes namespace, ECS service or Compose on a runner) created from the same IaC modules as production.
  • Data: a fresh database seeded with synthetic fixtures, never a copy of production data. Database branching features in managed Postgres providers make this cheap where available.
  • Cost control: destroy on merge or close, plus a time-to-live reaper for anything left behind.

End-to-end tests with Playwright

We use Playwright for web E2E. Its auto-waiting locators, isolated browser contexts and trace viewer make failures debuggable instead of mysterious. Practices that keep the suite reliable:

  • Select elements by role and accessible name (getByRole('button', { name: 'Place order' })), not CSS classes. This also nudges the UI towards accessibility.
  • Create test data through APIs in fixtures, not by clicking through setup screens. Log in once and reuse the saved storage state.
  • Every test owns its data, so tests can run in parallel and in any order.
  • Traces, videos and screenshots are kept on failure only and uploaded as CI artefacts.
  • A smoke subset runs on previews, the full suite runs on staging, and a handful of synthetic checks run against production on a schedule.

Sample GitHub Actions workflow

A simplified version of the PR workflow we start from for a TypeScript web and API repository. Real projects add caching of build outputs, sharding and environment-specific jobs.

name: pr-checks
on:
  pull_request:
    branches: [main]
permissions:
  contents: read
  security-events: write
concurrency:
  group: pr-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version-file: .nvmrc, cache: npm }
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test -- --coverage

  integration:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env: { POSTGRES_PASSWORD: test }
        ports: ["5432:5432"]
        options: >-
          --health-cmd "pg_isready" --health-interval 5s --health-retries 10
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version-file: .nvmrc, cache: npm }
      - run: npm ci
      - run: npm run db:migrate   # dry-run of real migrations
        env: { DATABASE_URL: postgres://postgres:test@localhost:5432/postgres }
      - run: npm run test:integration
        env: { DATABASE_URL: postgres://postgres:test@localhost:5432/postgres }

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: gitleaks/gitleaks-action@v2
        env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" }
      - uses: github/codeql-action/init@v3
        with: { languages: javascript-typescript }
      - uses: github/codeql-action/analyze@v3
      - uses: aquasecurity/trivy-action@master   # pin to a release tag or SHA in real use
        with: { scan-type: fs, severity: "CRITICAL,HIGH", exit-code: "1" }

  e2e-smoke:
    needs: [quality]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version-file: .nvmrc, cache: npm }
      - run: npm ci && npx playwright install --with-deps chromium
      - run: npx playwright test --grep @smoke
        env: { BASE_URL: "${{ vars.PREVIEW_BASE_URL }}" }   # set by the preview-deploy job
      - uses: actions/upload-artifact@v4
        if: failure()
        with: { name: playwright-report, path: playwright-report }

In production repositories, third-party actions are pinned to commit SHAs, workflow permissions are kept to a minimum, and cloud deploys authenticate with OIDC federation rather than long-lived keys.

Database migrations in the pipeline

Schema changes cause many avoidable incidents. Our rules:

  • Migrations are versioned files in the repository (Flyway, Liquibase, EF Core, Prisma, Alembic), and they run as a separate pipeline step before the new application version rolls out, never at application start-up across many replicas.
  • Expand and contract. Every change must work with both the old and new application versions, because during a rolling or canary deploy both run at once. To rename a column, add the new column, dual-write, backfill, switch reads, and drop the old column in a later release.
  • Large backfills run as throttled background jobs, not inside the migration transaction.
  • For PostgreSQL, the CI linter flags locking operations such as non-concurrent index creation or adding a volatile default to a big table. Staging runs migrations against a realistically sized dataset.
  • Down migrations are not the rollback plan. Rolling forward with a fix, supported by the expand/contract discipline, is safer in practice.

Multi-tenant systems add tenant-by-tenant rollout considerations, discussed in multi-tenant data isolation and scaling in PostgreSQL.

Feature flags, blue/green and canary releases

TechniqueWhat it controlsUse it forWatch out for
Feature flagsWhich users see a code pathUnfinished work, gradual rollout, per-tenant enablement, kill switchesFlag debt: every flag gets an owner and a removal date
Blue/greenWhich whole environment receives trafficInstant switch-back; releases with infrastructure changesDouble capacity during the switch; database must suit both versions
CanaryWhat share of traffic hits the new versionCatching regressions that only show under real trafficNeeds good metrics per version; low-traffic services give weak signals
RollingInstances replaced graduallyLow-risk services, simple setupsSlower rollback than switching traffic back

Our usual production setup is a canary (for example through Argo Rollouts, AWS CodeDeploy, or weighted target groups on the load balancer). Traffic moves in steps, and automated analysis compares the canary's error rate and latency with the stable version before each step. Feature flags then control user exposure on top of that. The flag platform can be a managed service or self-hosted with an OpenFeature-compatible SDK, so the vendor can be swapped later. Infrastructure these rollouts run on is covered in our AWS reference architecture for high-traffic SaaS.

Rollback

  • Application: redeploy the previous immutable artefact, or shift traffic back in a canary or blue/green setup. Either is one command or one click, and it is rehearsed.
  • Automatic triggers: SLO burn-rate alerts during a rollout abort it without waiting for a human.
  • Feature: turn off the flag. This is often the fastest mitigation and needs no deploy.
  • Data: expand/contract migrations mean the previous version still works with the current schema. Destructive contract steps wait until the release has been stable for a while.
  • Mobile: you cannot roll back an installed binary. Halt the staged rollout, disable the feature remotely, and ship a fix. This is why mobile releases lean heavily on remote flags.

The mobile pipeline

Mobile CI has its own constraints: macOS runners for iOS, code signing, store processing times and review. Our standard setup, for native, Flutter or React Native apps:

  1. PR: lint, unit and widget/component tests, and a debug build for both platforms to catch native build breaks early.
  2. Signing: Fastlane match (or the platform's managed signing) keeps certificates and provisioning profiles in an encrypted store. The App Store Connect API key and the Play service account JSON live in CI secrets. No developer's laptop is a single point of failure.
  3. Build numbers come from the CI run number, so every build is traceable to a commit.
  4. Distribution: merges to main upload to TestFlight (internal testers) and the Google Play internal testing track automatically, with release notes generated from commits.
  5. Device E2E: Maestro, Detox, Espresso/XCUITest or Flutter integration tests run on emulators for smoke tests, and a device farm covers wider coverage before promotion.
  6. Promotion: internal → closed testing / external TestFlight → production with a staged rollout, watching crash-free sessions and ANR rates in Crashlytics or Sentry before widening.
# fastlane/Fastfile (excerpt)
platform :ios do
  lane :beta do
    setup_ci
    match(type: "appstore", readonly: true)
    increment_build_number(build_number: ENV["GITHUB_RUN_NUMBER"])
    build_app(scheme: "App", export_method: "app-store")
    upload_to_testflight(skip_waiting_for_build_processing: true)
  end
end

platform :android do
  lane :internal do
    gradle(task: "bundle", build_type: "Release")
    upload_to_play_store(track: "internal", aab: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH])
  end
end

What we measure: DORA metrics

We track the DORA delivery metrics for every product we run a pipeline for. We use them as a team diagnostic, not as targets to game or to compare teams against each other:

MetricHow we measure itWhat a bad trend usually points to
Deployment frequencyProduction deploys per service from the CD systemBatching, manual approvals, fear of releasing
Change lead timeFirst commit (or PR open) to running in productionSlow reviews, slow or flaky CI, queued environments
Change failure rateDeploys that caused an incident, rollback or hotfix ÷ total deploysWeak test layers, missing canary analysis, risky migrations
Failed deployment recovery timeFrom failure detection to restored serviceSlow rollback, poor observability, no kill switches

We deliberately don't publish target numbers here. Sensible values depend on the product, its regulatory context and its release channel (a web API and an app-store app are very different). Our aim on each engagement is steady improvement from the team's own baseline. Alongside DORA we watch pipeline duration, the flaky-test rate and preview-environment cost.

How this fits into our wider delivery process is described in how we work. Our DevOps services team builds and runs pipelines like this one, and our QA and testing services team owns the test strategy and automation. For where the field is heading, including platform engineering and AI-assisted operations, see our article on DevOps trends for 2026. The same pipeline also carries legacy migrations safely, as in modernising a .NET Framework application.

Frequently asked questions

Why trunk-based development instead of GitFlow?

Short-lived branches merged to main at least every day or two integrate changes early, keep merge conflicts small and make continuous delivery possible. Unfinished work is hidden behind feature flags. Mobile teams still cut a short-lived release branch at store submission because of app review.

What checks should run on every pull request?

At minimum: formatting and lint, type-checking, unit tests, integration tests against real dependencies in containers, SAST, dependency and secrets scanning, and a migration dry-run. Contract tests and an E2E smoke run on a preview environment add confidence for multi-service and user-facing changes.

How do you run database migrations without downtime?

Migrations run as a separate pipeline step before the new version rolls out and follow the expand-and-contract pattern, so old and new application versions both work with the schema during a canary or rolling deploy. Large backfills run as throttled background jobs, and destructive steps ship in a later release.

Canary or blue/green deployment: which is better?

Canary releases expose a small share of real traffic to the new version and compare its metrics before widening, which catches regressions that only appear under production load. Blue/green switches all traffic between two environments and gives an instant switch-back. Many teams use canaries for services and blue/green for releases with infrastructure changes.

Which DORA metrics do you track and what values do you target?

We track deployment frequency, change lead time, change failure rate and failed deployment recovery time. We do not set universal targets because sensible values depend on the product, regulation and release channel. We measure each team's baseline and aim for steady improvement.

How is a mobile CI/CD pipeline different from a web pipeline?

Mobile pipelines need macOS runners for iOS, managed code signing, build numbering, and store distribution through TestFlight and Google Play testing tracks with staged rollouts. Installed binaries cannot be rolled back, so mobile releases rely on remote feature flags, halting staged rollouts and fast fix releases.

Want a delivery pipeline you can trust?

Whether you are starting fresh or fixing a flaky, slow pipeline, our engineers can review what you have and propose a practical path to safer, more frequent releases.

Talk to an engineer
© Next Olive Technologies · nextolive.com · sales@nextolive.com