Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
4121c9fb7This file is pinned to the selected release. Download the original to open it in your design tool.
---
name: Mutation Tests
on:
schedule:
- cron: '0 3 * * 1' # Weekly: Monday 03:00 UTC
workflow_dispatch:
workflow_call:
permissions: {}
env:
# Lets tests catch text I/O without an explicit encoding (see [tool.pytest.ini_options] in cmd/scadm/pyproject.toml and cmd/scadfmt/pyproject.toml).
PYTHONWARNDEFAULTENCODING: '1'
# Silences the same warning from mutmut and other tooling. pytest's own filter still turns it into an error inside tests.
PYTHONWARNINGS: 'ignore::EncodingWarning'
concurrency:
group: mutation-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
# Mutates only the functions a PR changed, one comment section per package. A change to a package's run setup gets a full run of it.
# Release PRs only bump versions and changelogs; their code was mutation-tested on its own PRs.
pr:
if: github.event_name == 'pull_request' && !startsWith(github.head_ref, 'release-please--')
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.14'
- name: Install dependencies
run: |
set -euo pipefail
pip install -e cmd/scadm -e cmd/scadfmt pytest -r requirements.txt
# Per package: pick the scope, run mutmut capped at 5 minutes so a growing suite can't stall PRs
# (results collected before the cap are kept), then render its section of the PR comment.
- name: Run mutation tests
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
sections=()
for package in scadm scadfmt; do
globs="$RUNNER_TEMP/$package-globs.txt"
python cmd/test/mutation_report.py scope --package "$package" --base "$BASE_SHA" > "$globs"
setup_changed=$(git diff --name-only "$BASE_SHA...HEAD" -- \
"cmd/$package/pyproject.toml" cmd/test/mutation_report.py requirements.txt .github/workflows/mutation-tests.yml)
args=()
names=()
if [[ -z "$setup_changed" ]]; then
args+=(--globs-file "$globs")
# grep exits 1 on an empty scope; any other failure stops the job.
scoped=$(grep . "$globs" || [[ $? -eq 1 ]])
if [[ -n "$scoped" ]]; then
mapfile -t names <<< "$scoped"
fi
fi
if [[ -n "$setup_changed" || ${#names[@]} -gt 0 ]]; then
rc=0
(cd "cmd/$package" && timeout --kill-after=30s 300 mutmut run "${names[@]}") || rc=$?
if [[ "$rc" -eq 124 || "$rc" -eq 137 ]]; then
echo "::warning title=Mutation run timed out ($package)::Results are partial. If this keeps happening, move per-PR mutation runs to a nightly job."
args+=(--timed-out)
elif [[ "$rc" -ne 0 ]]; then
exit "$rc"
fi
fi
python cmd/test/mutation_report.py report --package "$package" "${args[@]}" > "$RUNNER_TEMP/$package.md"
sections+=("$RUNNER_TEMP/$package.md")
done
python cmd/test/mutation_report.py combine "${sections[@]}" > "$RUNNER_TEMP/report.md"
cat "$RUNNER_TEMP/report.md" >> "$GITHUB_STEP_SUMMARY"
# Fork PRs get a read-only token, so they rely on the job summary above.
- name: Upsert PR comment
if: github.event.pull_request.head.repo.full_name == github.repository
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
ids=$(gh api "repos/$REPO/issues/$PR/comments" --paginate \
--jq '.[] | select(.body | startswith("<!-- mutation-report -->")) | .id')
existing=${ids%%$'\n'*}
if [[ -z "$existing" ]]; then
gh api -X POST "repos/$REPO/issues/$PR/comments" -F body=@"$RUNNER_TEMP/report.md" --silent
else
gh api -X PATCH "repos/$REPO/issues/comments/$existing" -F body=@"$RUNNER_TEMP/report.md" --silent
fi
# Whole package, to keep overall test debt visible: report to Discord, refresh the package's mutation badge.
full:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- package: scadm
badge: mutation.json
- package: scadfmt
badge: scadfmt-mutation.json
env:
PACKAGE: ${{ matrix.package }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: true # publish-badge pushes with these
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.14'
- name: Install dependencies
run: |
set -euo pipefail
pip install -e "cmd/$PACKAGE" pytest -r requirements.txt
- name: Run mutation tests
working-directory: cmd/${{ matrix.package }}
run: |
set -euo pipefail
mutmut run
mutmut export-cicd-stats
mutmut badge --output "$RUNNER_TEMP/mutation.json"
- name: Build report
run: |
set -euo pipefail
python cmd/test/mutation_report.py report --package "$PACKAGE" >> "$GITHUB_STEP_SUMMARY"
- name: Post to Discord
env:
DISCORD_CI_WEBHOOK_URL: ${{ secrets.DISCORD_CI_WEBHOOK_URL }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [[ -z "$DISCORD_CI_WEBHOOK_URL" ]]; then
echo "::warning::DISCORD_CI_WEBHOOK_URL is not set, skipping the Discord report"
exit 0
fi
python cmd/test/mutation_report.py discord --package "$PACKAGE" --run-url "$RUN_URL" > "$RUNNER_TEMP/discord.json"
curl -fsS -H "Content-Type: application/json" -d @"$RUNNER_TEMP/discord.json" "$DISCORD_CI_WEBHOOK_URL"
- name: Publish mutation badge
if: github.ref == 'refs/heads/main'
uses: ./.github/actions/publish-badge
with:
file: ${{ runner.temp }}/mutation.json
name: ${{ matrix.badge }}