Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7966c6f
ci: use WarHub bot app token for private submodule checkout
amis92 Jul 13, 2026
ca5eaac
ci: full-history checkout for Nerdbank.GitVersioning in submodules
amis92 Jul 13, 2026
f365d7c
docs: M3 design spec — executable bug reports
amis92 Jul 13, 2026
317b533
docs: M3 spec amendment — per-engine evaluation everywhere
amis92 Jul 13, 2026
627c22d
docs: M3 spec — NR as default governing engine, configurable precedence
amis92 Jul 13, 2026
f85318e
docs: M3 spec — NR adapter ships as public Docker image, docker: adap…
amis92 Jul 13, 2026
7c95d1c
docs: M3 implementation plan (14 tasks) + committed NR list sample
amis92 Jul 13, 2026
44beec3
test: shared FakeRosterEngine + NDJSON TestAdapter host
amis92 Jul 13, 2026
c0cb518
feat: engine registry — builtin/exec/docker engine specs, governing r…
amis92 Jul 13, 2026
fa356cd
fix: delegate Cleanup() in CompositeDisposableEngine (default-interfa…
amis92 Jul 13, 2026
b084336
feat: multi-engine test — engine-parameterized RunFixtures, MultiRunR…
amis92 Jul 13, 2026
9b75907
feat: per-engine diff, --fail-on-broke governing gate, engine-gap sur…
amis92 Jul 13, 2026
0581538
feat: NR share-link parser (strict allowlist) + open_share_link clien…
amis92 Jul 13, 2026
da11be5
fix: NrClient — degrade mid-body IO failures gracefully, propagate re…
amis92 Jul 13, 2026
58c343c
feat: NR list parser + fixture-DSL spec emitter with observed-value pins
amis92 Jul 13, 2026
2caf8fa
fix: SpecEmitter.Quote — escape control characters for YAML round-tri…
amis92 Jul 13, 2026
b10b097
feat: .ros/.rosz to ReplayRoster converter with per-selection observe…
amis92 Jul 13, 2026
88da923
fix: RosterFileConverter — skip unmappable forces with Unmapped note,…
amis92 Jul 13, 2026
adf472d
feat: muster convert — roster file or NR link to fixture-DSL spec
amis92 Jul 13, 2026
a5a85c6
fix: convert — any failure exits 2, never an unhandled crash
amis92 Jul 13, 2026
4fe1346
feat: hostile-input issue body parser + allowlisted attachment client
amis92 Jul 13, 2026
ab53142
feat: muster report — issue body to verdict, labels, per-engine reply…
amis92 Jul 13, 2026
56bcaf7
feat: muster promote — snapshot extraction, re-pin to current values,…
amis92 Jul 13, 2026
df0c4d1
fix: line-anchor promotion marker strip; add render->extract->repin c…
amis92 Jul 13, 2026
1201907
feat: issue-form kit + reusable report/promote workflows + docs
amis92 Jul 13, 2026
3e2b771
fix: optional pr-token secret so promotion PRs can trigger CI checks
amis92 Jul 13, 2026
78e56dc
chore: bump wham (battlescribe-spec NR adapter host)
amis92 Jul 13, 2026
22ddd18
fix: F1/F2/F3 from final whole-branch review of M3
amis92 Jul 13, 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
29 changes: 24 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,23 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
# NOTE (publishing-phase ordering): submodule fetch below requires
# wham's `feat/yaml-reader` branch and battlescribe-spec's
# `feat/muster-support` branch to be pushed upstream first. Until
# those land, this checkout step will fail to resolve the pinned
# submodule commits on a fresh clone.
# battlescribe-spec (nested submodule) is a private repo: mint a
# WarHub bot GitHub App token for checkout, same pattern as
# WarHub/wham's CI. Requires the org-level WARHUB_BOT_APP_ID var and
# WARHUB_BOT_PRIVATE_KEY secret to be visible to this repo.
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ vars.WARHUB_BOT_APP_ID }}
private-key: ${{ secrets.WARHUB_BOT_PRIVATE_KEY }}
owner: WarHub
repositories: battlescribe-spec
- uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
submodules: recursive
fetch-depth: 0 # full history: Nerdbank.GitVersioning in the wham submodule needs version height

- uses: actions/setup-dotnet@v4
with:
Expand All @@ -35,9 +44,19 @@ jobs:
needs: test
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ vars.WARHUB_BOT_APP_ID }}
private-key: ${{ secrets.WARHUB_BOT_PRIVATE_KEY }}
owner: WarHub
repositories: battlescribe-spec
- uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
submodules: recursive
fetch-depth: 0 # full history: Nerdbank.GitVersioning in the wham submodule needs version height

- name: Build image
run: docker build -t "$IMAGE_NAME:ci" .
Expand Down
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