ci: Stand up multi-package mutation health — nightly passes + mutant-* skills (no-changelog) (#31356)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Declan Carroll
2026-06-01 08:57:25 +00:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 1b8235ef76
commit ecfc39b3e5
14 changed files with 413 additions and 239 deletions
+62 -22
View File
@@ -2,38 +2,76 @@ name: 'Mutation Health (nightly)'
on:
schedule:
# 03:30 UTC daily — outside CI rush, before EU morning.
# 03:30 UTC daily — outside CI rush, before EU morning. Runs both passes.
- cron: '30 3 * * *'
workflow_dispatch:
inputs:
package:
description: 'Workspace package to mutate'
mode:
description: 'Which pass to run'
type: choice
options:
- n8n-workflow
default: n8n-workflow
- both
- baseline # score files with no result yet (the `new` bucket)
- coverage # revisit the weakest scored files (`red`/`stale`, lowest first)
default: both
permissions:
contents: read
# Prevent overlapping scheduled + manual runs from racing on the same
# ledger row. The writer MERGE is idempotent, but two concurrent runs
# would emit duplicate event rows for the same picked file.
concurrency:
group: mutation-health-${{ github.event.inputs.package || 'n8n-workflow' }}
cancel-in-progress: false
env:
PACKAGE_NAME: ${{ github.event.inputs.package || 'n8n-workflow' }}
PKG_DIR: packages/workflow # 1-to-1 mapping today; generalise when more packages are wired up
REPORTS_DIR: packages/workflow/reports/mutation
READER_URL: https://internal.users.n8n.cloud/webhook/mutation-health-ledger
jobs:
run:
name: Mutate one file, ledger writeback
# Build the (package × pass) matrix once. The package list lives here only —
# onboarding a vitest package is a one-line addition below. The requested
# `mode` filters which passes run (scheduled runs default to both).
# baseline → score files with no result yet (picker `new` bucket)
# coverage → revisit the weakest scored files (picker `red`/`stale`, lowest first)
setup:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.build.outputs.matrix }}
steps:
- name: Build matrix
id: build
env:
REQUESTED_MODE: ${{ github.event.inputs.mode || 'both' }}
# vitest packages only. A package with its own stryker.config.mjs overrides
# the shared default (scripts/mutation-health/stryker.default.mjs) — n8n-workflow
# does this to run the legacy expression engine and dodge the isolated-vm
# dry-run crash (see its config + DEVP-257). Jest packages and
# @n8n/expression-runtime are intentionally absent.
run: |
node -e '
const packages = [
{ name: "n8n-workflow", dir: "packages/workflow", slug: "workflow" },
{ name: "@n8n/crdt", dir: "packages/@n8n/crdt", slug: "crdt" },
{ name: "@n8n/decorators", dir: "packages/@n8n/decorators", slug: "decorators" },
];
const req = process.env.REQUESTED_MODE;
const modes = req === "both" ? ["baseline", "coverage"] : [req];
const include = packages.flatMap((p) => modes.map((mode) => ({ ...p, mode })));
console.log("matrix=" + JSON.stringify({ include }));
' >> "$GITHUB_OUTPUT"
mutate:
needs: setup
name: ${{ matrix.mode }} · ${{ matrix.name }}
runs-on: blacksmith-4vcpu-ubuntu-2204
timeout-minutes: 60
strategy:
fail-fast: false # one leg's failure must not skip the others
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
# Per package + pass: a scheduled run and a manual run won't double-write the
# same ledger row, but different packages/passes still run in parallel.
concurrency:
group: mutation-health-${{ matrix.mode }}-${{ matrix.slug }}
cancel-in-progress: false
env:
PACKAGE_NAME: ${{ matrix.name }}
PKG_DIR: ${{ matrix.dir }}
MODE: ${{ matrix.mode }}
REPORTS_DIR: ${{ matrix.dir }}/reports/mutation
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -46,18 +84,20 @@ jobs:
- name: Fetch live ledger from BigQuery
run: |
mkdir -p "$REPORTS_DIR"
curl --fail -sS "$READER_URL?package=$PACKAGE_NAME" -o "$REPORTS_DIR/live-ledger.json"
curl --fail -sS --get --data-urlencode "package=$PACKAGE_NAME" \
"$READER_URL" -o "$REPORTS_DIR/live-ledger.json"
- name: Pick next source file
id: pick
run: |
picked_json=$(node scripts/mutation-health/pick-next.mjs \
--package-dir "$PKG_DIR" \
--ledger-file "$REPORTS_DIR/live-ledger.json")
--ledger-file "$REPORTS_DIR/live-ledger.json" \
--mode "$MODE")
echo "$picked_json"
src_repo=$(echo "$picked_json" | jq -r '.picked.source_file_path // ""')
if [ -z "$src_repo" ]; then
echo "::notice::Picker returned no work (all-green / empty ledger). Exiting cleanly."
echo "::notice::Picker returned no work for mode=$MODE (bucket empty). Exiting cleanly."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
@@ -76,7 +116,7 @@ jobs:
# "Stryker crashed." continue-on-error: true would collapse them.
run: |
set +e
pnpm --filter "$PACKAGE_NAME" mutate "${{ steps.pick.outputs.source-rel }}"
node scripts/mutation-health/mutate.mjs "${{ steps.pick.outputs.source-rel }}" --package-dir "$PKG_DIR"
rc=$?
set -e
if [ "$rc" -gt 1 ]; then
@@ -107,7 +147,7 @@ jobs:
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: mutation-health-${{ env.PACKAGE_NAME }}-${{ github.run_id }}
name: mutation-health-${{ matrix.mode }}-${{ matrix.slug }}-${{ github.run_id }}
path: |
${{ env.REPORTS_DIR }}/raw.json
${{ env.REPORTS_DIR }}/raw.html