Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9605ab7
fix(ci): measure a stacked unit against its parent pull request
Oct 5, 2026
a42df42
fix(gate): pass the pull request head and keep the resolved stacked base
Oct 6, 2026
1d7384d
fix(gate): restore the upload step header dropped while editing the g…
Oct 6, 2026
a0a26cc
fix(gate): paginate the open pull request map
Oct 6, 2026
6d90293
fix(gate): paginate without --slurp
Oct 6, 2026
134a184
fix(ci): keep the API token out of the step that runs the mutation gate
Oct 6, 2026
d444279
fix(ci): read the stacked map in a job that never runs pull-request code
Oct 6, 2026
3da494b
fix(ci): give the map step the id its job output reads
Oct 6, 2026
6e1ba47
test(gate): cover the multi-parent head fallback
Oct 6, 2026
a1a5fe9
refactor(gate): drop the duplicated comment above resolveCiInvocation
Oct 6, 2026
9dea54e
fix(gate): align selection, source reads, and mutation to one tree; r…
Oct 6, 2026
d67d4a6
Merge branch 'main' into fix/mutation-gate-stacked-base
easonLiangWorldedtech Oct 9, 2026
2034a0b
fix(ci): refuse an ungated draft head as a stacked base
easonliang28 Oct 28, 2026
fb6cfa7
style: format the mutation-gate scripts for the compile job's format …
easonliang28 Oct 28, 2026
fcdfbf1
Merge branch 'main' into fix/mutation-gate-stacked-base
easonliang28 Oct 28, 2026
69c57b3
fix(ci): bind a stacked unit to its parent's base branch and tree
easonliang28 Nov 3, 2026
780eec4
Merge upstream/main into fix/mutation-gate-stacked-base
easonliang28 Nov 3, 2026
1833535
fix(ci): pass the stacked map as a file and keep a failed reinstall v…
easonliang28 Nov 3, 2026
e8de314
fix(ci): tolerate a failed stacked-map upload and require a confirmed…
easonLiangWorldedtech Nov 3, 2026
f133c58
Merge upstream/main into fix/mutation-gate-stacked-base
easonliang28 Nov 4, 2026
fc17bc4
fix(mutation-gate): keep the stacked map out of the pull request's ow…
easonliang28 Nov 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 79 additions & 1 deletion .github/workflows/mutation-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,69 @@ concurrency:
cancel-in-progress: true

jobs:
# The open pull request map is read in a job that never checks out or runs pull-request
# code, so the token cannot reach anything the contributor controls. Only the filtered map
# crosses into the job that runs the gate. Each entry carries the draft flag and the base ref: a
# draft pull request is not gated, and a pull request whose base is another branch is not the
# branch this unit lands on, so either one makes the gate measure against the event base instead.
stacked_map:
name: Resolve the stacked parent
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
# The map is published as an artifact for the gate job, which is the only artifact this
# job writes; it never sees pull-request code because this job checks out nothing.
actions: write
steps:
- name: Read the open pull request heads
id: read_map
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# The fallback must itself be a command: this step runs bash with -e, so a bare '[]'
# is executed as a program name and an API failure would abort the job instead of
# measuring against the event base.
# --jq runs once per page, so a paginated call emits one JSON array per page; the
# gate reads that stream, not a single array.
# The base ref travels with every entry: the gate only measures a unit against a parent
# that targets the branch the unit itself targets.
STACKED_MAP="$(gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls?state=open&per_page=100" --jq '[.[] | {number: .number, headSha: .head.sha, draft: (.draft == true), baseRef: .base.ref}]')" || STACKED_MAP="$(printf '[]')"
# Publish the map as a file in an artifact rather than as a job output that the gate
# step then receives as an argument: Linux caps one argument or environment string at
# 128 KiB (MAX_ARG_STRLEN), and a map past that cap makes the gate step unstartable
# instead of merely large. A failed write aborts this job: a truncated or missing map
# would leave the gate measuring against the event base while still reporting success.
case "$STACKED_MAP" in
"["*) ;;
*) echo "::error title=Stacked unit base::The stacked map is not a JSON array" >&2; exit 1 ;;
esac
printf '%s\n' "$STACKED_MAP" > stacked-map.json

# A failed upload must not skip the gate: the download side already treats a missing
# artifact as "measure against the event base" (stricter, never looser), and that
# fallback can only run if this job still succeeds.
- name: Publish the stacked map
continue-on-error: true
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: mutation-stacked-map
path: stacked-map.json
if-no-files-found: error
retention-days: 1

mutation-diff:
name: mutation-diff
needs: stacked_map
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
if: github.event_name == 'merge_group' || github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 30
# Only the filtered map crosses into this job, and it crosses as a file: the token stays in the
# job that read it. actions:write covers reading that artifact and publishing the reports below.
permissions:
contents: read
actions: write
steps:
- name: Record merge-queue enforcement
if: github.event_name == 'merge_group'
Expand Down Expand Up @@ -46,14 +104,34 @@ jobs:
if: github.event_name == 'pull_request'
run: pnpm test:mutation-ci

- name: Read the stacked map
if: github.event_name == 'pull_request'
# Downloaded outside the checkout on purpose: a pull request can commit .mutation/stacked-map.json
# itself, and with continue-on-error a missing artifact would leave that committed file in place for
# the gate to read as if it were the map. Under RUNNER_TEMP a missing artifact means no file, which
# degrades to the event base - stricter than the intended measurement, never looser - and the gate
# logs it.
continue-on-error: true
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: mutation-stacked-map
path: ${{ runner.temp }}/mutation-stacked-map

- name: Enforce executable-line scope and run advisory mutation testing
if: github.event_name == 'pull_request'
env:
HEAD_SHA: ${{ github.sha }}
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
BASE_SHA="$(git rev-parse "$HEAD_SHA^1")"
node scripts/stryker-diff.mjs ci --base "$BASE_SHA" --head "$HEAD_SHA"
# The merge commit's second parent is the pull request head. When that head's own
# parent is another open pull request's head, this PR is a stacked unit, so the gate
# measures only this unit's delta instead of every unmerged ancestor.
# The event already carries that head for provenance, so it wins here; deriving it from
# the merge commit stays as the fallback for a manual run with no pull_request payload.
PR_HEAD_SHA="${PR_HEAD_SHA:-$(git rev-parse "$HEAD_SHA^2" 2>/dev/null || git rev-parse "$HEAD_SHA")}"
node scripts/stryker-diff.mjs ci --base "$BASE_SHA" --head "$HEAD_SHA" --pr-head "$PR_HEAD_SHA" --pr-base-ref "$PR_BASE_REF" --stacked-map-file "${RUNNER_TEMP}/mutation-stacked-map/stacked-map.json"

- name: Upload mutation reports
id: mutation_report
Expand Down
Loading
Loading