Path-filter selection (GitHub Actions, using `dorny/paths-filter`)
This determines which service's test suite should even run, based on what the PR actually touches:
name: pr-test-selection
on:
pull_request:
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
backend:
- 'services/payments/**'
- 'services/checkout/**'
- 'shared/lib/**'
frontend:
- 'apps/web/**'
- 'packages/ui/**'
backend-tier1:
needs: detect-changes
if: needs.detect-changes.outputs.backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright test --grep @tier1 --project=backend
frontend-tier1:
needs: detect-changes
if: needs.detect-changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright test --grep @tier1 --project=frontend
Note `shared/lib/**` is deliberately mapped to the backend filter here, not excluded: a change to shared code is exactly the case where path-based inference alone under-selects, which is why the escape hatch in the next example matters.
Tag-based selection plus the escape-hatch label
Building on the `@tier1` / `@tier2` tagging convention from the regression-suite article, add an explicit override for PRs where the author knows path/tag inference won't judge risk correctly:
full-suite-override:
if: contains(github.event.pull_request.labels.*.name, 'run-full-suite')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright test
import { test } from "@playwright/test";
test("checkout completes with valid discount code @tier1", async ({ page }) => {
// path: services/checkout, risk: critical, runs on every PR touching checkout
});
test("shared currency formatter handles negative amounts @tier1", async ({ page }) => {
// path: shared/lib, risk: critical because it's used everywhere,
// this is why shared/lib maps to the backend PR filter above, not a separate low-priority lane
});
The combination of the two steps above is deliberately simple to read: a PR touching only `apps/web/` never triggers backend tests, a PR touching `shared/lib/` triggers the full backend Tier 1 set because shared code is treated as high-risk by default, and any PR can force the complete suite with one label when the author has reason to believe the automatic selection isn't enough. That inspectability, an engineer can read the YAML and know exactly why a given test did or didn't run, is the actual gap in a purely coverage-diff-driven approach: those tools work, but the selection logic lives inside a third-party service rather than in a file the team owns and can read.