Skip to content

🧬 Mutation Tests (Stryker) #39

🧬 Mutation Tests (Stryker)

🧬 Mutation Tests (Stryker) #39

Workflow file for this run

# ============================================================
# WorldScript Studio – Mutation Testing (Stryker) β€” Parallel + Incremental
# QNBS-v3: Matrix-parallel Stryker with per-module caches and fail-safe aggregation.
# Run manually via workflow_dispatch. Each matrix job targets one curated module;
# runtime and score remain measured outputs rather than undocumented promises.
# ============================================================
name: 🧬 Mutation Tests (Stryker)
on:
workflow_dispatch:
inputs:
mode:
description: 'Run mode'
required: true
default: 'incremental'
type: choice
options:
- incremental
- force
module:
description: 'Scope selector: all, tier-a, or one module name'
required: false
default: 'all'
per_job_concurrency:
description: 'Concurrency per job (default: 2)'
required: false
default: '2'
concurrency:
group: mutation-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
scope:
name: 🧬 Validate Stryker Scope
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.scope.outputs.matrix }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- id: scope
shell: bash
run: |
SELECTOR="${{ github.event.inputs.module || 'all' }}"
node scripts/stryker-scope.mjs --selector "$SELECTOR"
echo "matrix=$(node scripts/stryker-scope.mjs --matrix --selector "$SELECTOR")" >> "$GITHUB_OUTPUT"
# ============================================
# PARALLEL MATRIX JOBS (one per module)
# ============================================
stryker:
name: 🧬 Stryker ${{ matrix.name }}
runs-on: ubuntu-latest
timeout-minutes: 45
needs: scope
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.scope.outputs.matrix) }}
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/setup
# === Incremental cache (per module) ===
- name: Restore Stryker incremental cache
id: cache-incremental
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: reports/stryker-incremental-${{ matrix.name }}.json
key: stryker-incremental-${{ matrix.name }}-${{ github.sha }}
restore-keys: |
stryker-incremental-${{ matrix.name }}-
- name: Prepare incremental file
run: |
mkdir -p reports
MODE="${{ github.event.inputs.mode || 'incremental' }}"
if [ "$MODE" = "force" ]; then
echo "🧹 Force mode β†’ clearing incremental cache for ${{ matrix.name }}"
rm -f reports/stryker-incremental-${{ matrix.name }}.json
elif [ -f "reports/stryker-incremental-${{ matrix.name }}.json" ]; then
echo "♻️ Using existing incremental cache for ${{ matrix.name }}"
else
echo "πŸ†• No incremental cache found β€” will create new one"
fi
- name: Run Stryker (${{ matrix.name }})
run: |
MODE="${{ github.event.inputs.mode || 'incremental' }}"
FORCE_FLAG=""
if [ "$MODE" = "force" ]; then
FORCE_FLAG="--force"
fi
CONCURRENCY="${{ github.event.inputs.per_job_concurrency || '2' }}"
echo "▢️ Module: ${{ matrix.name }}"
echo " Pattern: ${{ matrix.mutate }}"
echo " Concurrency: $CONCURRENCY"
echo " Mode: $MODE"
# QNBS-v3: pass the supported CLI option; arbitrary env vars are not Stryker config.
INCREMENTAL_FILE="reports/stryker-incremental-${{ matrix.name }}.json"
pnpm exec stryker run \
stryker.config.mjs \
--incremental \
--incrementalFile "$INCREMENTAL_FILE" \
$FORCE_FLAG \
--concurrency "$CONCURRENCY" \
--mutate "${{ matrix.mutate }}" \
--reporters progress,json,html
env:
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
- name: Upload Stryker report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: stryker-report-${{ matrix.name }}
path: reports/mutation/
if-no-files-found: error
retention-days: 30
# ============================================
# AGGREGATE JOB β€” combined summary
# ============================================
aggregate:
name: πŸ“Š Stryker Aggregate Report
runs-on: ubuntu-latest
needs: [scope, stryker]
if: always()
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download all Stryker reports
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
path: all-reports
pattern: stryker-report-*
merge-multiple: false
- name: Build combined summary
run: |
if [ "${{ needs.stryker.result }}" != "success" ]; then
echo "Stryker matrix did not complete successfully: ${{ needs.stryker.result }}" >&2
exit 1
fi
node scripts/aggregate-stryker-reports.mjs all-reports | tee -a "$GITHUB_STEP_SUMMARY"
echo "> Mode: \`${{ github.event.inputs.mode || 'incremental' }}\` | Concurrency: \`${{ github.event.inputs.per_job_concurrency || '2' }}\`" >> "$GITHUB_STEP_SUMMARY"
echo "> Threshold: break β‰₯ 75 | low β‰₯ 70 | high β‰₯ 85" >> "$GITHUB_STEP_SUMMARY"
echo "> Individual HTML/JSON reports are available as artifacts." >> "$GITHUB_STEP_SUMMARY"