Files
agentic-dev/.github/workflows/mutation-nightly.yml
Danijel Martinek c0dbadf1c2 ci: honest audit gate, real Socket CLI, reachable mutation issue step
- 'pnpm audit signatures' is an npm-only feature; pnpm ignored the
  word and ran a plain full audit. Replace with an explicit
  'pnpm audit --prod --audit-level=critical' (documented rationale)
- socket-cli is a 0.0.1 stub on npm; use the real 'socket' CLI and
  fail loudly instead of silently passing
- mutation-nightly's issue-opening step was gated on if: failure(),
  unreachable under continue-on-error — gate on steps.mutate.outcome
- wire the ADR-023 renovate major-bump gate
  (scripts/library-decisions/check.mjs --renovate-pr) on PRs; verified
  it no-ops on non-renovate branches

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:43:32 +02:00

115 lines
3.9 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
id: mutate
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
# `continue-on-error: true` means the JOB never reports failure — gate
# the tracking issue on the STEP outcome instead (if: failure() was
# unreachable and the issue could never open).
- name: Open tracking issue on >5% score drop
if: steps.mutate.outcome == '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'],
});