Files
agentic-dev/.github/workflows/mutation-nightly.yml
Danijel Martinek f77e6ea881 chore(template): clean-slate template snapshot from bb4a0c7
Curated, product-agnostic snapshot of the post-story-04 tree: demo
content deleted, auth-only reference feature, web-next shell, all gates
green. Product-specific docs, ADRs 027-029, PRDs/epics/archive, editor
library traces, and product naming are curated out; generic template
repairs (coverage provider devDeps, root test:coverage script, live
lint fixes, root-only release-please) are kept. See TEMPLATE.md for
provenance, curation list, and usage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
2026-07-12 20:40:54 +02:00

111 lines
3.6 KiB
YAML

# Mutation testing (L3) — nightly run + on-demand. ADR-020.
#
# Stryker is slow (~minutes per feature) so it's NOT part of the default
# CI loop. This workflow runs nightly (and on manual dispatch) across every
# feature with a stryker.config.json, then uploads the HTML + JSON
# mutation reports as artifacts.
#
# On a meaningful score drop (>5%) it opens a tracking issue.
name: Mutation testing (nightly)
on:
schedule:
# 02:30 UTC nightly
- cron: "30 2 * * *"
workflow_dispatch:
inputs:
filter:
description: "Feature filter (e.g. @repo/auth). Empty = all features."
required: false
default: ""
permissions:
contents: read
issues: write
jobs:
mutate:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: cms_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Run mutation testing
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/cms_test
PAYLOAD_SECRET: test-secret-do-not-use-in-prod
run: |
if [ -n "${{ inputs.filter }}" ]; then
pnpm mutate -- --filter "${{ inputs.filter }}"
else
pnpm mutate
fi
continue-on-error: true
- name: Upload mutation reports
if: always()
uses: actions/upload-artifact@v4
with:
name: mutation-reports
path: packages/*/reports/mutation/
retention-days: 30
- name: Open tracking issue on >5% score drop
if: failure()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const reports = [];
const pkgsDir = path.join(process.cwd(), 'packages');
if (fs.existsSync(pkgsDir)) {
for (const pkg of fs.readdirSync(pkgsDir)) {
const json = path.join(pkgsDir, pkg, 'reports', 'mutation', 'mutation.json');
if (fs.existsSync(json)) {
try {
const data = JSON.parse(fs.readFileSync(json, 'utf8'));
const score = data.thresholds?.high && data.systemUnderTestMetrics?.metrics?.mutationScore;
if (typeof score === 'number') {
reports.push({ pkg, score: score.toFixed(2) });
}
} catch { /* skip */ }
}
}
}
if (reports.length === 0) return;
const body = [
'Nightly mutation testing run flagged failures. Latest scores:',
'',
...reports.map(r => `- **${r.pkg}**: ${r.score}%`),
'',
`Run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
].join('\n');
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Mutation score drop — ${new Date().toISOString().slice(0, 10)}`,
body,
labels: ['mutation-testing', 'automated'],
});