Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
186 changes: 186 additions & 0 deletions .github/workflows/report-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
name: Muster report check

# Reusable workflow (workflow_call). Data repos install this via the thin caller stub in
# kit/callers/muster-report.yml.
#
# IMPLEMENTATION NOTE (deviation from the original M3 plan draft): the muster image
# (ghcr.io/warhub/muster:latest, see Dockerfile) is dotnet-runtime + git ONLY — it has no
# `gh` CLI and no `python3`. An earlier draft of this workflow ran every step, including
# `gh`/label/PR steps, inside `container: ghcr.io/warhub/muster:latest`, which cannot work.
# Both jobs below instead run as plain `runs-on: ubuntu-latest` (gh CLI + jq preinstalled,
# GH_TOKEN available) and invoke the muster image explicitly via `docker run`, mounting the
# checkout the same way the docker-action (action.yml) itself does. Only the muster
# invocation itself runs inside the container; every gh/label/PR step runs on the runner.

on:
workflow_call:
inputs:
data-path:
type: string
default: "."
data-source:
type: string
description: dataSource URI matching the repo's fixtures (e.g. github:BSData/wh40k-11e)
required: true
engines:
type: string
default: "wham"
governing:
type: string
default: "newrecruit battlescribe wham"
fixtures-out:
type: string
default: "tests/rosters"
secrets:
pr-token:
description: Token used to open the promotion PR. Pass a PAT or GitHub App installation token so the PR triggers workflows; defaults to github.token (PR checks will NOT auto-run).
required: false

permissions:
issues: write
contents: write
pull-requests: write

jobs:
evaluate:
if: >
github.event_name == 'issues' ||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/muster check'))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Save issue body
env:
BODY: ${{ github.event.issue.body }}
run: |
mkdir -p .muster
printf '%s' "$BODY" > .muster/issue-body.md

- name: Find previous reply
uses: peter-evans/find-comment@v3
id: fc
with:
issue-number: ${{ github.event.issue.number }}
body-includes: "<!-- muster:report -->"

- name: Save previous reply body
# Sticky comment (Post reply, below) is posted with edit-mode: replace on this single
# comment. Handing the previous body to `muster report` (via --previous-reply) lets it
# carry the durable snapshot forward when this evaluation produces none of its own —
# otherwise a rotted NR link / re-edit would silently destroy the only stored snapshot,
# making promotion permanently impossible. Only written when a previous comment exists;
# entrypoint.sh treats a missing file as "no previous reply".
if: steps.fc.outputs.comment-id != ''
env:
PREV: ${{ steps.fc.outputs.comment-body }}
run: |
mkdir -p .muster
printf '%s' "$PREV" > .muster/previous-reply.md

- name: Evaluate report
run: |
docker run --rm \
-v "$PWD:/workspace" -w /workspace \
--entrypoint /entrypoint.sh \
ghcr.io/warhub/muster:latest \
report "${{ inputs.data-path }}" ".muster/issue-body.md" \
"${{ inputs.data-source }}" "${{ inputs.engines }}" "${{ inputs.governing }}" . \
.muster/previous-reply.md

- name: Notify reporter of harness error
# Evaluate report failing means the harness crashed outright (exit 1 from
# entrypoint.sh's report mode) -- silence otherwise, since no reply.md was ever
# produced for the sticky-comment step below to post. Plain issue comment, NEVER the
# sticky <!-- muster:report --> comment (that would need a real verdict to replace it
# with, which is exactly what we don't have here).
if: failure()
env:
GH_TOKEN: ${{ github.token }}
run: |
gh issue comment "${{ github.event.issue.number }}" -R "$GITHUB_REPOSITORY" \
-b "⚠ The muster harness hit an internal error evaluating this report. Maintainers have been notified — no action needed from you."

- name: Post reply
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.issue.number }}
body-path: reply.md
edit-mode: replace

- name: Apply labels
env:
GH_TOKEN: ${{ github.token }}
ISSUE: ${{ github.event.issue.number }}
run: |
for label in confirmed not-reproducible needs-info inconclusive engine-gap; do
color=$(case "$label" in
confirmed) echo d73a4a ;;
not-reproducible) echo 0e8a16 ;;
needs-info) echo fbca04 ;;
inconclusive) echo d4c5f9 ;;
engine-gap) echo 5319e7 ;;
esac)
gh label create "$label" --force --color "$color" -R "$GITHUB_REPOSITORY" || true
done

labels=$(jq -r '.labels | join(",")' report.json)
current=$(gh issue view "$ISSUE" -R "$GITHUB_REPOSITORY" --json labels \
-q '[.labels[].name] | map(select(. == "confirmed" or . == "not-reproducible" or . == "needs-info" or . == "inconclusive" or . == "engine-gap")) | join(",")')
[ -n "$current" ] && gh issue edit "$ISSUE" -R "$GITHUB_REPOSITORY" --remove-label "$current" || true
[ -n "$labels" ] && gh issue edit "$ISSUE" -R "$GITHUB_REPOSITORY" --add-label "$labels" || true

promote:
if: github.event_name == 'issue_comment' && contains(github.event.comment.body, '/muster promote')
runs-on: ubuntu-latest
steps:
- name: Check commenter permission
env:
GH_TOKEN: ${{ github.token }}
run: |
perm=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/${{ github.event.comment.user.login }}/permission" -q .permission)
case "$perm" in
admin|write|maintain) ;;
*)
gh issue comment "${{ github.event.issue.number }}" -R "$GITHUB_REPOSITORY" \
-b "Sorry @${{ github.event.comment.user.login }}, promotion needs write access."
exit 1
;;
esac

- uses: actions/checkout@v4

- name: Fetch comments and issue body
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p .muster
gh api "repos/$GITHUB_REPOSITORY/issues/${{ github.event.issue.number }}/comments" --paginate > .muster/comments.json
gh api "repos/$GITHUB_REPOSITORY/issues/${{ github.event.issue.number }}" -q .body > .muster/issue-body.md

- name: Promote
run: |
docker run --rm \
-v "$PWD:/workspace" -w /workspace \
--entrypoint /entrypoint.sh \
ghcr.io/warhub/muster:latest \
promote "${{ inputs.data-path }}" ".muster/issue-body.md" ".muster/comments.json" \
"${{ inputs.data-source }}" "${{ github.event.issue.number }}" "${{ inputs.fixtures-out }}" \
"${{ inputs.engines }}" "${{ inputs.governing }}"

- name: Open PR
env:
GH_TOKEN: ${{ secrets.pr-token || github.token }}
ISSUE: ${{ github.event.issue.number }}
run: |
git config user.name "muster-bot"
git config user.email "muster@users.noreply.github.com"
branch="muster/promote-issue-$ISSUE"
git checkout -b "$branch"
git add "${{ inputs.fixtures-out }}"
git commit -m "test: promote issue #$ISSUE roster to golden fixture"
git push origin "$branch"
gh pr create -R "$GITHUB_REPOSITORY" --head "$branch" \
--title "Promote issue #$ISSUE roster to golden fixture" \
--body "Promotes the reproducing roster from #$ISSUE to ${{ inputs.fixtures-out }}/. Review the pinned expected values — they were captured from the current (post-fix) engine evaluation. Closes #$ISSUE."
1 change: 1 addition & 0 deletions Muster.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
</Folder>
<Folder Name="/tests/">
<Project Path="tests/Muster.Cli.Tests/Muster.Cli.Tests.csproj" />
<Project Path="tests/Muster.TestAdapter/Muster.TestAdapter.csproj" />
</Folder>
</Solution>
16 changes: 16 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ inputs:
base-ref:
description: "Base git ref for blast-radius diff (empty = plain test)"
default: ""
fail-on-broke:
description: Fail the check when the governing engine classifies any fixture as broke/verdict-changed (diff mode).
default: "true"
fail-on-inconclusive:
description: Fail the check (exit 1) instead of neutral warning when the run is inconclusive.
default: "false"
engines:
description: Space-separated engine specs (e.g. "wham newrecruit=docker:ghcr.io/warhub/bsspec-adapter-newrecruit:latest").
default: "wham"
governing:
description: Space-separated governing precedence.
default: "newrecruit battlescribe wham"
outputs:
report-path:
description: "Path to muster-report.md"
Expand All @@ -20,3 +32,7 @@ runs:
- "${{ inputs.data-path }}"
- "${{ inputs.fixtures-path }}"
- "${{ inputs.base-ref }}"
- "${{ inputs.fail-on-broke }}"
- "${{ inputs.fail-on-inconclusive }}"
- "${{ inputs.engines }}"
- "${{ inputs.governing }}"
5 changes: 5 additions & 0 deletions docs/authoring-fixtures.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Authoring golden-roster fixtures

> Fixtures aren't only hand-authored — `/muster promote` on a confirmed bug report writes
> one for you, pinned to the engine's current values. See
> [`docs/executable-bug-reports.md`](executable-bug-reports.md) for the issue-form → report
> → promote flow that produces regression fixtures from real bug reports.

A **fixture** is a battlescribe-spec roster DSL YAML file that drives wham's
roster engine against real (or inline) game data and asserts on the
resulting roster state. `muster test` discovers every `*.yaml` file under a
Expand Down
Loading
Loading