Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/workflows/coverage-report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Backend Coverage Report

on:
workflow_run:
workflows:
- Unit Tests
types:
- completed

permissions:
actions: read
contents: read
pull-requests: write

concurrency:
group: >-
coverage-report-${{ github.event.workflow_run.head_repository.id }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true

jobs:
report:
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
# Keep the privileged workflow on trusted default-branch code. The action
# documents workflow_run support, but changed-file rows still require an
# end-to-end check on the first PR after this workflow reaches main.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Download pull request coverage
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
github-token: ${{ github.token }}
run-id: ${{ github.event.workflow_run.id }}
name: backend-coverage-node-24
path: coverage

- name: Download latest main coverage baseline
id: baseline
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
with:
workflow: test.yml
branch: main
event: push
workflow_conclusion: success
name: backend-coverage-baseline
path: coverage-main
search_artifacts: true
if_no_artifact_found: warn

- name: Comment backend coverage trend
if: steps.baseline.outputs.found_artifact == 'true'
uses: davelosert/vitest-coverage-report-action@8b157684c6a6b259b97d45e72b44242865c0f6a5 # v2
with:
name: Backend
github-token: ${{ github.token }}
comment-on: pr
json-summary-path: coverage/coverage-summary.json
json-final-path: coverage/coverage-final.json
json-summary-compare-path: coverage-main/coverage-summary.json
file-coverage-mode: changes
sort-by: lines-asc

- name: Comment backend coverage without baseline
if: steps.baseline.outputs.found_artifact != 'true'
uses: davelosert/vitest-coverage-report-action@8b157684c6a6b259b97d45e72b44242865c0f6a5 # v2
with:
name: Backend
github-token: ${{ github.token }}
comment-on: pr
json-summary-path: coverage/coverage-summary.json
json-final-path: coverage/coverage-final.json
file-coverage-mode: changes
sort-by: lines-asc
70 changes: 70 additions & 0 deletions .github/workflows/mutation-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Backend Mutation PR

on:
pull_request:
types:
- opened
- synchronize
- reopened
- ready_for_review
paths:
- lambda/**/*.js
- '!lambda/**/test/**'
- '!lambda/**/.build/**'

permissions:
contents: read

concurrency:
group: mutation-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
mutation:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: npm

- name: Install dependencies
run: npm ci

- name: Mutate changed backend files
id: mutation
continue-on-error: true
timeout-minutes: 30
env:
MUTATION_WORKFLOW_URL: >-
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: >-
npm run test:mutation:changed --
--base=${{ github.event.pull_request.base.sha }}

- name: Finalize mutation summary
if: always()
env:
MUTATION_EXECUTION_INCOMPLETE: ${{ steps.mutation.outcome != 'success' }}
MUTATION_WORKFLOW_URL: >-
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: >-
npm run test:mutation:changed --
--report-only
--base=${{ github.event.pull_request.base.sha }}

- name: Upload mutation reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: backend-mutation-pr
path: reports/mutation/pr/
if-no-files-found: warn
retention-days: 7
59 changes: 59 additions & 0 deletions .github/workflows/mutation-report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Backend Mutation Report

on:
workflow_run:
workflows:
- Backend Mutation PR
types:
- completed

permissions:
actions: read
contents: read
pull-requests: write

jobs:
report:
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- name: Find pull request
id: pull-request
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
OWNER_REPO: ${{ github.repository }}
WORKFLOW_PR: ${{ github.event.workflow_run.pull_requests[0].number }}
run: |
number="$WORKFLOW_PR"
if [ -z "$number" ]; then
number=$(gh api "repos/$OWNER_REPO/commits/$HEAD_SHA/pulls" --jq '.[0].number')
fi
if ! [[ "$number" =~ ^[0-9]+$ ]]; then
echo "Unable to identify the pull request for $HEAD_SHA" >&2
exit 1
fi
echo "number=$number" >> "$GITHUB_OUTPUT"
draft=$(gh api "repos/$OWNER_REPO/pulls/$number" --jq '.draft')
echo "draft=$draft" >> "$GITHUB_OUTPUT"

- name: Download mutation report
if: steps.pull-request.outputs.draft == 'false'
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
github-token: ${{ github.token }}
run-id: ${{ github.event.workflow_run.id }}
name: backend-mutation-pr
path: mutation-report

- name: Comment mutation findings
if: steps.pull-request.outputs.draft == 'false'
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3
with:
GITHUB_TOKEN: ${{ github.token }}
number_force: ${{ steps.pull-request.outputs.number }}
header: backend-mutation
path: mutation-report/summary.md
skip_unchanged: true
26 changes: 26 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,33 @@ jobs:
run: npm ci

- name: Run unit tests
if: matrix.node-version == 22
run: npm test

- name: Run unit tests with coverage
if: matrix.node-version == 24
run: npm run test:coverage

- name: Upload backend coverage report
if: matrix.node-version == 24
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: backend-coverage-node-24
path: coverage/
if-no-files-found: warn
retention-days: 3

- name: Publish backend coverage baseline
if: >-
matrix.node-version == 24 &&
github.event_name == 'push' &&
github.ref == 'refs/heads/main'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: backend-coverage-baseline
path: coverage/coverage-summary.json
if-no-files-found: error
retention-days: 30

- name: Run release tooling tests
run: npm run test:release
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ node_modules

# Test coverage output
coverage/
reports/
.stryker-tmp/

# Dev screenshots and scratch files
*.png
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,35 @@ Run the unit tests and generate a coverage report:
```bash
npm test # run all unit tests
npm run test:coverage # run tests with a coverage report (HTML in coverage/)
npm run test:mutation:dry # validate the mutation-testing harness without running mutants
npm run test:mutation # mutate all backend JavaScript sources
npm run test:mutation:changed -- --base=origin/main # mutate changed backend files
```

Backend coverage includes untested Lambda source files and produces text, HTML,
LCOV, JSON summary, and JSON detail reports. CI retains the full HTML report as
a workflow artifact. Every successful push to `main` also publishes the latest
coverage summary as the comparison baseline. Pull requests receive a
non-blocking coverage comment showing the trend for changed backend files. The
first PR after these workflows reach `main` must confirm that the third-party
report action correctly resolves changed files from its `workflow_run` context.
Mutation testing uses StrykerJS with the existing Vitest tests across all Lambda
projects. It runs each Lambda domain sequentially to keep the instrumented
process bounded, writes per-domain HTML and JSON reports under
`reports/mutation/`, then generates `reports/mutation/summary.json`. Pass a
single domain when needed, for example
`npm run test:mutation -- --scope=v2-orchestrator`. Static mutants are ignored
by default because they require reloading the test environment and dominate the
runtime on the largest domains. Pass `--include-static` for an exhaustive run.
Mutation scores are initially informational so the first runs establish a
baseline before any blocking threshold is chosen. Pull requests that change
backend JavaScript also run an informational mutation campaign over each
changed production file. The PR comment uses the official Stryker metrics and
highlights surviving or uncovered mutants without blocking the pull request.
Draft pull requests are skipped, static mutants are excluded, previous runs are
cancelled after a new push, and the pilot stops after 30 minutes. A timeout
produces a partial informational report rather than failing the pull request.

Lint, format, and security checks:

```bash
Expand Down
Loading
Loading