Forgejo Actions refusals lost their tailored guidance; restore it in cli-guard rationale() #1653

Closed
opened 2026-08-09 03:04:28 +00:00 by coilyco-ops · 1 comment
Owner

Context

Branch remove-format-exec-gate-refusal removes formatExecGateRefusal from cmd/ward/exec_gate.go. That function sniffed state.Reason prefixes and dispatched to five hand-written refusal messages. Since cli-guard v0.130.0 grew its own rationale() switch inside gittree.State.FormatRefusal (ward#1129), the ward-side dispatch was a second copy of the same conceptual check sitting a layer too high, so it was removed and gittree now owns the classification.

Deferred consequence

gittree.rationale() has three arms - dirty tree, detached HEAD, and a default of "Repo verbs require the branch to be pushed and synced with its upstream". It has no arm for a Forgejo Actions evidence failure, so those refusals now fall to the default and cite the wrong rule. An operator whose CI checkout failed event-payload validation is told to push and sync their branch, which does not diagnose the failure.

The removed ward-side text read:

Forgejo Actions pull-request repo verbs require a clean merge checkout
whose environment, event payload, origin, workspace, HEAD, and merge parents agree.

Fix

Add a strings.HasPrefix(s.Reason, "Forgejo Actions") arm to rationale() in cli-guard/cli/gittree/gittree.go, then bump ward's go.mod. This is the config-placement rule applied properly: the classification belongs at the layer that owns the refusal, not in each consumer.

Worth reviewing at the same time whether the git fetch <remote> and git push -u origin <branch> recovery lines are fully carried by s.Recovery in every arm, since CheckClean populates Recovery but checkCleanReadOnly leaves it empty on the CI path.

Fixed in the same branch: FormatRefusal renders nothing for a clean State, and the read-only pre-check marks a clean named branch Clean before CI evidence is validated. Without clearing Clean, an invalid-CI refusal on a named branch rendered as a completely empty error. Covered now by TestRunExecGateIntegration/named_Forgejo_checkout_refuses_with_a_rendered_message.

## Context Branch `remove-format-exec-gate-refusal` removes `formatExecGateRefusal` from `cmd/ward/exec_gate.go`. That function sniffed `state.Reason` prefixes and dispatched to five hand-written refusal messages. Since cli-guard v0.130.0 grew its own `rationale()` switch inside `gittree.State.FormatRefusal` (ward#1129), the ward-side dispatch was a second copy of the same conceptual check sitting a layer too high, so it was removed and `gittree` now owns the classification. ## Deferred consequence `gittree.rationale()` has three arms - dirty tree, detached HEAD, and a default of "Repo verbs require the branch to be pushed and synced with its upstream". It has no arm for a Forgejo Actions evidence failure, so those refusals now fall to the default and cite the wrong rule. An operator whose CI checkout failed event-payload validation is told to push and sync their branch, which does not diagnose the failure. The removed ward-side text read: ``` Forgejo Actions pull-request repo verbs require a clean merge checkout whose environment, event payload, origin, workspace, HEAD, and merge parents agree. ``` ## Fix Add a `strings.HasPrefix(s.Reason, "Forgejo Actions")` arm to `rationale()` in `cli-guard/cli/gittree/gittree.go`, then bump ward's `go.mod`. This is the config-placement rule applied properly: the classification belongs at the layer that owns the refusal, not in each consumer. Worth reviewing at the same time whether the `git fetch <remote>` and `git push -u origin <branch>` recovery lines are fully carried by `s.Recovery` in every arm, since `CheckClean` populates `Recovery` but `checkCleanReadOnly` leaves it empty on the CI path. ## Related Fixed in the same branch: `FormatRefusal` renders nothing for a clean `State`, and the read-only pre-check marks a clean named branch `Clean` before CI evidence is validated. Without clearing `Clean`, an invalid-CI refusal on a named branch rendered as a completely empty error. Covered now by `TestRunExecGateIntegration/named_Forgejo_checkout_refuses_with_a_rendered_message`.
Author
Owner

Obsoleted by the follow-up commit on the same branch, 05702831.

The premise here was that the tailored Forgejo Actions refusal text was worth restoring upstream. That assumed the gate keeps those refusal causes. It does not. The gate now refuses exactly one thing, an uncommitted .ward/ward.yaml, and the detached-HEAD, missing-upstream, fetch-failure, and behind-upstream causes no longer exist. There is no wrong-rationale message left to fix, because there is no refusal left to render for those cases.

Forgejo Actions attribution also stopped being a gate. Incomplete or inconsistent evidence now leaves the audit row unattributed instead of refusing, so the message this issue wanted to restore has no call site.

The rationale() observation still holds as a description of cli-guard, but ward no longer exercises any arm of it except the dirty-tree one. Closing rather than leaving a stale ask against cli-guard.

Two things from this issue that survive and are worth tracking separately if you want them:

  • the Recovery field is empty on any State that ward constructs itself rather than getting from CheckClean. Now moot in ward, still a sharp edge for another cli-guard consumer.
  • the real replacement for the whole binding, hashing the resolved command spec into the audit row. That gives exact reconstruction with no gate and no network, and it closes the ahead-leak that the removed checks never covered. Not filed yet, deliberately - it is a design change, not a cleanup.
Obsoleted by the follow-up commit on the same branch, `05702831`. The premise here was that the tailored Forgejo Actions refusal text was worth restoring upstream. That assumed the gate keeps those refusal causes. It does not. The gate now refuses exactly one thing, an uncommitted `.ward/ward.yaml`, and the detached-HEAD, missing-upstream, fetch-failure, and behind-upstream causes no longer exist. There is no wrong-rationale message left to fix, because there is no refusal left to render for those cases. Forgejo Actions attribution also stopped being a gate. Incomplete or inconsistent evidence now leaves the audit row unattributed instead of refusing, so the message this issue wanted to restore has no call site. The `rationale()` observation still holds as a description of cli-guard, but ward no longer exercises any arm of it except the dirty-tree one. Closing rather than leaving a stale ask against cli-guard. Two things from this issue that survive and are worth tracking separately if you want them: * the `Recovery` field is empty on any `State` that ward constructs itself rather than getting from `CheckClean`. Now moot in ward, still a sharp edge for another cli-guard consumer. * the real replacement for the whole binding, hashing the resolved command spec into the audit row. That gives exact reconstruction with no gate and no network, and it closes the ahead-leak that the removed checks never covered. Not filed yet, deliberately - it is a design change, not a cleanup.
Commenting is not possible because the repository is archived.
No milestone
No project
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/ward#1653
No description provided.