Skip to content

fix(action): pass --repo before the check subcommand - #109

Merged
derek-palmer merged 2 commits into
mainfrom
fix/108-action-repo-flag-order
Aug 26, 2026
Merged

derek-palmer merged 2 commits into
mainfrom
fix/108-action-repo-flag-order

Conversation

@derek-palmer

Copy link
Copy Markdown
Owner

Fixes #108.

The bug

action.yml invoked forerunner check --repo <path>, but --repo is a top-level flag, not an argument of the check subcommand. Argparse exited 2 before any drift check ran, so every run of the action failed on argument parsing:

  • fail-on-drift: "true" — job failed 100% of the time, reported as documentation drift. Nothing was checked.
  • fail-on-drift: "false" — the trailing || true swallowed exit 2 and the job went green. Also nothing was checked.

Changes

action.yml — forerunner --repo "<path>" check, invoked once, branching on the exit code. Only exit 1 (drift found) is downgraded to a ::warning:: under fail-on-drift: false; every other non-zero code fails the job, so a check that fails to run can never report green again.

--repo on every subcommand (cli.py, installer.py) — a shared parents= parser gives init, scan, doc, check, doctor, refresh and install the flag. Previously only mcp-server redefined it as its own flag, and that asymmetry is plausibly where the action's invocation shape came from. Both orders now parse; the subcommand's value wins when given on both sides. mcp-server keeps its own declaration — its help text differs, and a shared parent would conflict.

No change to cmd_check's exit codes. The issue floated making them distinguishable from an argparse failure; they already are — 0 clean, 1 drift, 2 invalid config. Argparse also uses 2, but both 2s mean "failed to run", which is the correct grouping. The distinction that matters (drift vs. everything else) held all along and is now pinned by tests rather than left implicit.

Tests

Asserting a non-zero exit would not have caught this — exit 2 is non-zero too. Every assertion here separates "found drift" from "failed to run".

  • tests/fixtures/drifting-repo/ — a config enabling exactly one rule (RI1-missing-cli) and a README that documents forerunner check while the fixture ships no CLI. Guaranteed one violation. Not a scanned doc path, so this repo's own forerunner check still exits 0.
  • tests/test_action_yml.py — extracts the step's real argv and: parses it with the live CLI parser; asserts --repo precedes the subcommand; runs it end-to-end against the fixture asserting exit 1 and RI1-missing-cli on stderr; and pins the step's shell logic across all six (exit code x fail-on-drift) combinations using a forerunner stub.
  • tests/test_cli.py — both flag orders parse for every subcommand, later-flag-wins, and main()'s args.repo = None backfill.
  • ci.yml action-smoke job — runs the local composite action against the fixture, ungated (must pass) then gated (must fail). A composite step exposes only pass/fail, never the exit code, so no single run can tell 1 from 2; the pair can, because exit 2 fails the ungated step too.

Mutation-checked both guards: restoring the || true swallow fails the [2-false-2] case, restoring the old flag order fails the ordering test.

324 tests pass. scripts/check_versions.py clean.

Note for consumers

The action installs the released CLI from PyPI, so the smoke job exercises action.yml's wiring against a real forerunner — which is the bug class here. Consuming repos still on a broken tag can keep calling the CLI directly (forerunner check from the workspace root) until this ships.

`--repo` is a top-level flag on the forerunner parser, not an argument of
the `check` subcommand, so `forerunner check --repo <path>` exited 2 on
argparse before any drift check ran. With `fail-on-drift: "true"` the job
failed every run and reported it as documentation drift; with `"false"`
the trailing `|| true` swallowed exit 2 and the job went green having
checked nothing.

Add a regression test that extracts the action's invocations and parses
them with the real CLI parser, since asserting a non-zero exit alone
cannot tell an argparse failure (2) from detected drift (1).

Fixes #108
Follow-ups from #108.

Only `mcp-server` redefined `--repo` as its own subcommand flag; every
other subcommand required the top-level position. That asymmetry is the
trap the action's broken invocation grew out of, so share one parent
parser and give every subcommand the flag. Both orders now parse; the
subcommand's value wins when given on both sides.

The action's `|| true` under `fail-on-drift: false` swallowed every
non-zero exit, so a check that failed to run reported green. Run the CLI
once, capture the code, and downgrade only exit 1 (drift found) to a
warning; anything else still fails the job.

Cover both with a drifting fixture repo: an end-to-end run asserting
exit 1 and the violation text, a table pinning the step's exit-code
mapping against a CLI stub, and a CI job running the local composite
action ungated (must pass) then gated (must fail) — the pair distinguishes
exit 1 from exit 2, which neither step can read on its own.

Refs #108
@derek-palmer derek-palmer self-assigned this Aug 26, 2026
@derek-palmer
derek-palmer merged commit 646fb83 into main Aug 26, 2026
13 checks passed
@derek-palmer
derek-palmer deleted the fix/108-action-repo-flag-order branch August 26, 2026 19:33
@derek-palmer derek-palmer mentioned this pull request Aug 26, 2026
6 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GitHub Action always fails: --repo passed after the subcommand, so forerunner check exits 2

1 participant