Skip to content

The Mutant Report™ #5

The Mutant Report™

The Mutant Report™ #5

Workflow file for this run

name: The Mutant Report™
on:
schedule:
- cron: "0 0 * * 0" # Runs every Sunday at midnight (UTC)
workflow_dispatch: # Allow manual trigger
jobs:
cargo_mutants:
runs-on: ubuntu-latest
permissions:
issues: write
contents: write
steps:
- name: Check out repository
uses: actions/checkout@v3
- name: Install cargo-mutants
run: cargo install cargo-mutants
- name: Run cargo-mutants
run: |
# Don't fail the entire job if missed mutants exist.
cargo mutants || true
- name: Generate markdown report
id: gen_report
run: |
# Current commit SHA
HEAD_SHA=$(git rev-parse HEAD)
X=$(grep -v '^[[:space:]]*$' mutants.out/missed.txt 2>/dev/null | wc -l)
Y=$(( \
$(grep -v '^[[:space:]]*$' mutants.out/caught.txt 2>/dev/null | wc -l) + \
$(grep -v '^[[:space:]]*$' mutants.out/missed.txt 2>/dev/null | wc -l) + \
$(grep -v '^[[:space:]]*$' mutants.out/timeout.txt 2>/dev/null | wc -l) + \
$(grep -v '^[[:space:]]*$' mutants.out/unviable.txt 2>/dev/null | wc -l) \
))
REPORT=$'$X uncaught mutants found (out of $Y total mutants) in commit https://github.com/icorbrey/jjj/blob/${HEAD_SHA}.\n\n'
if [ "$X" -gt "0" ]; then
DETAILS=$(cat mutants.out/missed.txt 2>/dev/null \
| sort -t: -k1,1 \
| awk -v HEAD="$HEAD_SHA" -F: '
BEGIN {
current_file = ""
}
{
file = $1
line = $2
column = $3
mutation = $4
for (i = 5; i <= NF; i++) {
mutation = mutation ":" $i
}
sub(/^ +/, "", mutation)
if (file != current_file) {
current_file = file
if (NR > 1) {
print ""
}
print "## `" file "`\n"
}
print "- [Line " line ", column " column "](https://github.com/icorbrey/jjj/blob/" HEAD "/" file "#L" line "): `" mutation "`"
}
')
REPORT+="$DETAILS"
else
REPORT+="No missed mutants found!"
fi
# Print the final report to stdout (for debugging/log)
echo -e "$REPORT"
# Pass data on to the next step
echo "uncaught_count=$X" >> "$GITHUB_OUTPUT"
ENCODED_REPORT="$(echo "$REPORT" | base64 -w0)"
echo "report_body=$ENCODED_REPORT" >> "$GITHUB_OUTPUT"
# Always unpin and close old "mutants" issues, even if none are missed this week
- name: Unpin and close old "mutants" issues
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// 1) Grab repo node ID (needed for unpinning)
const repoData = await github.graphql(
`
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
id
}
}
`,
{
owner: context.repo.owner,
repo: context.repo.repo,
}
);
const repositoryId = repoData.repository.id;
// 2) List all open issues labeled "mutants"
const oldMutantsIssues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: "mutants",
state: "open",
per_page: 100
});
// 3) Unpin (if pinned) and close each old "mutants" issue
for (const issue of oldMutantsIssues.data) {
const issueNumber = issue.number;
const issueNodeId = issue.node_id;
// Unpin with GraphQL. It's safe to attempt, even if not pinned.
try {
await github.graphql(
`
mutation($repositoryId: ID!, $issueId: ID!) {
unpinIssue(input: { issueId: $issueId }) {
issue {
id
}
}
}
`,
{
issueId: issueNodeId
}
);
core.info(`Unpinned issue #${issueNumber}.`);
} catch (err) {
core.warning(`Issue #${issueNumber} may not have been pinned. Error: ${err.message}`);
}
// Now close the issue via REST
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
state: "closed"
});
core.info(`Closed old mutants issue #${issueNumber}.`);
}
- name: Create + pin a new "mutants" issue (only if any uncaught)
if: ${{ steps.gen_report.outputs.uncaught_count != '0' }}
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// 1) Decode the base64-encoded report
const decodedBody = Buffer.from(
`${{ steps.gen_report.outputs.report_body }}`,
'base64'
).toString('utf8');
// 2) Build a custom title
const count = ${{ steps.gen_report.outputs.uncaught_count }};
const issueTitle = `The Mutant Report™: ${count} mutants running amok.`;
// 3) Create the new issue with the "mutants" label
const newIssue = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: issueTitle,
body: decodedBody,
labels: ["mutants"]
});
core.info(`Issue #${newIssue.data.number} created for missed mutants!`);
// 4) Pin the new issue using GraphQL
const newIssueNodeId = newIssue.data.node_id;
// Get the repository node ID again
const repoData = await github.graphql(
`
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
id
}
}
`,
{
owner: context.repo.owner,
repo: context.repo.repo,
}
);
const repositoryId = repoData.repository.id;
// Pin it
await github.graphql(
`
mutation($repositoryId: ID!, $issueId: ID!) {
pinIssue(input: { issueId: $issueId }) {
issue {
id
}
}
}
`,
{
issueId: newIssueNodeId
}
);
core.info(`Pinned issue #${newIssue.data.number} successfully!`);