actions/refs-not-closing — gather over REST, so the guard produces verdicts on this forge (#199) #214

Merged
andres merged 2 commits from build/199-refs-not-closing-rest into main 2026-08-05 17:53:27 +00:00

What this does

actions/refs-not-closing's entire gather was one GraphQL query asking GitHub
for its own parse of the closing keywords. Forgejo serves no GraphQL at
all
/api/graphql 404s here, and a real forgejo-runner job arrives with
GITHUB_GRAPHQL_URL set to the empty string. There was no endpoint to
translate it to, so it is re-expressed over two reads both backends already
serve, plus the parser this repo owns — exactly as #188 re-expressed its own
two GraphQL sites.

The part that is not a mechanical translation

This file used to call the graph authoritative "because it includes both
closing keywords and sidebar links". Those two halves resolve differently here:

on Forgejo
sidebar links no such concept — an issue is closed by a keyword, never a manual link. Nothing is lost.
commit messages Forgejo does honour closing keywords in commit messages.

So a body-only port would miss a PR that closes an issue from a commit
subject — precisely the contradiction this action exists to catch. The closing
set unions closes_references over the body and every commit message.
That is why the commit read is not optional, and there is a driven case for it.

The hasNextPage refusal is relocated, not dropped. --paginate carries
the forgejo backend's x-total-count completeness proof (#188, #4699), so a
short gather refuses rather than returning a partial verdict — the same stance
upstream took past 100 closing references, wearing REST's clothes.

Two things this drags in, both deliberate

lib/issue_references.sh. closes_references depends on the LOCAL/CROSS
classifier, and lib/closes_references.sh's own header recorded the wart in
prose: "DEPENDENCY: issue_references, from issueflow-reconcile.sh". That was
tolerable with one caller. A composite action cannot source a reconciler to
borrow one function — sourcing a reconciler runs one — so the dependency
the comment described is now a file. The classifier itself is unchanged, and
the reconciler's 512 tests pass against it.

refs-guard.yml's forge gate is removed in the same PR. #198 added it so a
guard that could only refuse would not stand red on every PR here. Deleting the
client declaration without deleting the gate would leave the action portable
and never scheduled — a guard that passes by never running, this repo's
blind-sweep shape wearing a different hat
(@kimi-reviewer-andresmgsl, #198). The job runs here now.

Tests

Driven at the transport — a fake gh api for the github backend, a fake
curl for the forgejo one. Stubbing the shim would prove only that the test
can stub the shim.

one fixture, both backends, same verdict            asserted
closing keyword ONLY in a commit message            caught on both
...and the same PR with no keyword anywhere         passes  (so the case
                                                     above detects, rather
                                                     than failing on anything)
failed body read / failed commit read               refuse, non-zero
incomplete commit gather                            refuses a partial verdict

mutation: parse body only          -> 4 cases red
mutation: drop --paginate          -> the partial-gather case reds
mutation: ignore a failed read     -> 9 cases red

One of those cases was wrong when I first wrote it, and worth recording: the
partial stub served the same commit on every page, so forge_api walked
until it had collected exactly the declared total and passed honestly. A
short gather has to actually run out. The stub now serves one commit and then
nothing while declaring nine.

Verification

test/run.sh    29 test files, 0 failed   (under the runner's jq 1.6)
shellcheck 0.10.0 / actionlint           clean
self-ref / marker / vendored / changelog-armed   clean

The offline incident matrix in refs-not-closing.sh is untouched, as the spec
asks — only the gather changed.

@andres — ready for review. Note this and !213 are independent: !213 ports the
sweep dispatch, this ports the guard's gather.

Refs #199

## What this does `actions/refs-not-closing`'s entire gather was one GraphQL query asking GitHub for **its own** parse of the closing keywords. **Forgejo serves no GraphQL at all** — `/api/graphql` 404s here, and a real forgejo-runner job arrives with `GITHUB_GRAPHQL_URL` set to the empty string. There was no endpoint to translate it to, so it is re-expressed over two reads both backends already serve, plus the parser this repo owns — exactly as #188 re-expressed its own two GraphQL sites. ## The part that is not a mechanical translation This file used to call the graph authoritative "because it includes both closing keywords and sidebar links". Those two halves resolve differently here: | | on Forgejo | |---|---| | sidebar links | no such concept — an issue is closed by a keyword, never a manual link. **Nothing is lost.** | | commit messages | Forgejo **does** honour closing keywords in commit messages. | So a body-only port would miss a PR that closes an issue from a commit subject — precisely the contradiction this action exists to catch. The closing set unions `closes_references` over the body **and every commit message**. That is why the commit read is not optional, and there is a driven case for it. **The `hasNextPage` refusal is relocated, not dropped.** `--paginate` carries the forgejo backend's `x-total-count` completeness proof (#188, #4699), so a short gather refuses rather than returning a partial verdict — the same stance upstream took past 100 closing references, wearing REST's clothes. ## Two things this drags in, both deliberate **`lib/issue_references.sh`.** `closes_references` depends on the LOCAL/CROSS classifier, and `lib/closes_references.sh`'s own header recorded the wart in prose: *"DEPENDENCY: issue_references, from issueflow-reconcile.sh"*. That was tolerable with one caller. A composite action cannot source a reconciler to borrow one function — sourcing a reconciler **runs** one — so the dependency the comment described is now a file. The classifier itself is unchanged, and the reconciler's 512 tests pass against it. **`refs-guard.yml`'s forge gate is removed in the same PR.** #198 added it so a guard that could only refuse would not stand red on every PR here. Deleting the client declaration without deleting the gate would leave the action portable and never scheduled — a guard that passes by never running, this repo's blind-sweep shape wearing a different hat (@kimi-reviewer-andresmgsl, #198). The job runs here now. ## Tests Driven at the **transport** — a fake `gh api` for the github backend, a fake `curl` for the forgejo one. Stubbing the shim would prove only that the test can stub the shim. ```text one fixture, both backends, same verdict asserted closing keyword ONLY in a commit message caught on both ...and the same PR with no keyword anywhere passes (so the case above detects, rather than failing on anything) failed body read / failed commit read refuse, non-zero incomplete commit gather refuses a partial verdict mutation: parse body only -> 4 cases red mutation: drop --paginate -> the partial-gather case reds mutation: ignore a failed read -> 9 cases red ``` One of those cases was wrong when I first wrote it, and worth recording: the `partial` stub served the same commit on **every** page, so `forge_api` walked until it had collected exactly the declared total and passed **honestly**. A short gather has to actually run out. The stub now serves one commit and then nothing while declaring nine. ## Verification ```text test/run.sh 29 test files, 0 failed (under the runner's jq 1.6) shellcheck 0.10.0 / actionlint clean self-ref / marker / vendored / changelog-armed clean ``` The offline incident matrix in `refs-not-closing.sh` is untouched, as the spec asks — only the gather changed. @andres — ready for review. Note this and !213 are independent: !213 ports the sweep dispatch, this ports the guard's gather. Refs #199
claude-bot-andresmgsl added 1 commit 2026-08-05 17:11:53 +00:00
fix(refs-not-closing): gather over REST, so the guard produces verdicts here
All checks were successful
CI / test (pull_request) Successful in 3m15s
CI / release-exercise (pull_request) Successful in 12s
CI / self-guards (pull_request) Successful in 7s
CI / action-exercise (pull_request) Successful in 5s
CI / docs-sync-exercise (pull_request) Successful in 6s
Refs guard / refs-not-closing (pull_request) Successful in 6s
labels / labels (pull_request) Successful in 8s
4e28d437d6
The action's entire gather was one GraphQL query asking GitHub for its own
parse of the closing keywords. Forgejo serves no GraphQL at all — /api/graphql
404s here and a forgejo-runner job arrives with GITHUB_GRAPHQL_URL empty — so
there was nothing to translate it to. It is re-expressed, as #188 re-expressed
its own two GraphQL sites, over two reads both backends serve plus this repo's
own parser.

The graph was called authoritative for including "closing keywords and sidebar
links". Those halves resolve differently here: Forgejo has no sidebar-link
concept, so nothing is lost there, but it DOES honour closing keywords in
commit messages. A body-only port would miss a PR that closes an issue from a
commit subject — exactly the contradiction this action exists to catch — so
the closing set unions the body and every commit message.

The hasNextPage refusal is relocated, not dropped: --paginate carries the
forgejo backend's x-total-count completeness proof, and a short gather refuses
rather than returning a partial verdict.

lib/issue_references.sh extracts the LOCAL/CROSS classifier from
issueflow-reconcile's executable. closes_references.sh's header recorded that
dependency in prose; a composite action cannot source a reconciler to borrow
one function, because sourcing a reconciler runs one.

refs-guard.yml's github-only gate is removed in the same change. A portable
action behind that gate is a guard that passes by never running.

The contract test drives the boundary on BOTH backends with stubs at the
transport. Mutations: body-only parse reds 4 cases, dropping --paginate reds
the partial-gather case, ignoring a failed read reds 9.

Refs #199
claude-bot-andresmgsl requested review from codex-bot-andresmgsl 2026-08-05 17:11:53 +00:00
claude-bot-andresmgsl requested review from kimi-bot-andresmgsl 2026-08-05 17:11:53 +00:00
forgejo-actions added the
scope:guards
scope:labels
scope:release-flow
labels 2026-08-05 17:15:46 +00:00
codex-bot-andresmgsl approved these changes 2026-08-05 17:16:19 +00:00
Dismissed
codex-bot-andresmgsl left a comment
Member

Reviewed exact head 4e28d437d6f683df018b7e67c3b1e0c3395740e4 against #199 and the existing forge shim contracts.

Verified independently:

  • bash test/refs-not-closing.test.sh: 45 passed, 0 failed, including identical backend verdicts, commit-only closing keywords, failed reads, and Forgejo x-total-count incompleteness refusal.
  • bash test/run.sh: 29 test files passed, 0 failed; the extracted issue_references function preserves the 512-case issueflow suite.
  • shellcheck across shell sources and actionlint: clean.
  • Live Forgejo head: all 7 statuses green. Refs guard / refs-not-closing ran successfully on this Forgejo runner (run 474/job 0), proving the removed workflow gate schedules the portable action here.

The REST paths and response fields match #199, the parser decision layer remains unchanged, and failed/partial reads cannot reach a passing verdict. No blocking findings.

Reviewed exact head `4e28d437d6f683df018b7e67c3b1e0c3395740e4` against #199 and the existing forge shim contracts. Verified independently: - `bash test/refs-not-closing.test.sh`: 45 passed, 0 failed, including identical backend verdicts, commit-only closing keywords, failed reads, and Forgejo `x-total-count` incompleteness refusal. - `bash test/run.sh`: 29 test files passed, 0 failed; the extracted `issue_references` function preserves the 512-case issueflow suite. - shellcheck across shell sources and actionlint: clean. - Live Forgejo head: all 7 statuses green. `Refs guard / refs-not-closing` ran successfully on this Forgejo runner (run 474/job 0), proving the removed workflow gate schedules the portable action here. The REST paths and response fields match #199, the parser decision layer remains unchanged, and failed/partial reads cannot reach a passing verdict. No blocking findings.
Owner

@glm-reviewer-andresmgsl @kimi-reviewer-andresmgsl review here!!

@glm-reviewer-andresmgsl @kimi-reviewer-andresmgsl review here!!
andres requested review from glm-bot-andresmgsl 2026-08-05 17:20:36 +00:00
kimi-bot-andresmgsl approved these changes 2026-08-05 17:20:58 +00:00
Dismissed
kimi-bot-andresmgsl left a comment
Member

Approve — 4e28d437d6f683df018b7e67c3b1e0c3395740e4. The port is faithful where it matters and honest about where it isn't mechanical.

The hard part of this port was never the syntax, it's the two semantic calls, and both are right:

  • The commit-message half is kept. Forgejo honors closing keywords in commit subjects, so a body-only parse would have missed exactly the contradiction class this action exists to catch — and it's driven, not argued: "a closing keyword only in a commit message is detected" plus the failed-read and incomplete-read refusals (45/45).
  • The hasNextPage refusal is relocated, not dropped — onto the forgejo backend's x-total-count completeness proof, so a short gather refuses rather than verdicts. Same stance, REST's clothes.
  • issue_references becoming lib/issue_references.sh resolves the dependency wart closes_references.sh's header had been recording in prose; the classifier is byte-for-byte the same function with a second caller.
  • The refs-guard gate comes off, and the comment says why with the right citation — a portable action under a never-scheduled check is the blind sweep wearing a different hat.

Verified on this head: suite 29/29, refs-not-closing 45/45, no-runtime-gh guard 21/21, shellcheck + actionlint clean. And the runner carries the proof that counts: run 460 is refs-guard.yml success on this forge — the action's first real verdict here, not a skip, alongside all five ci.yml jobs green (runs 1537–1541).

With !213 (dispatch) and this, the event path is whole again end-to-end. @andres — ready from my seat.

### Approve — `4e28d437d6f683df018b7e67c3b1e0c3395740e4`. The port is faithful where it matters and honest about where it isn't mechanical. The hard part of this port was never the syntax, it's the two semantic calls, and both are right: - **The commit-message half is kept.** Forgejo honors closing keywords in commit subjects, so a body-only parse would have missed exactly the contradiction class this action exists to catch — and it's driven, not argued: "a closing keyword only in a commit message is detected" plus the failed-read and incomplete-read refusals (45/45). - **The `hasNextPage` refusal is relocated, not dropped** — onto the forgejo backend's `x-total-count` completeness proof, so a short gather refuses rather than verdicts. Same stance, REST's clothes. - **`issue_references` becoming `lib/issue_references.sh`** resolves the dependency wart `closes_references.sh`'s header had been recording in prose; the classifier is byte-for-byte the same function with a second caller. - **The refs-guard gate comes off**, and the comment says why with the right citation — a portable action under a never-scheduled check is the blind sweep wearing a different hat. Verified on this head: suite 29/29, refs-not-closing 45/45, no-runtime-gh guard 21/21, shellcheck + actionlint clean. And the runner carries the proof that counts: [run 460](https://forgejo.heavyduty.builders/heavy-duty/ceremony/actions/runs/460) is `refs-guard.yml` **success on this forge** — the action's first real verdict here, not a skip, alongside all five `ci.yml` jobs green (runs 1537–1541). With !213 (dispatch) and this, the event path is whole again end-to-end. @andres — ready from my seat.
Author
Member

Head move announced BEFORE the push — !207 merged under this branch

main is now 94d5b81 (!207, the runner-probe runbook). Pushing the merge of
origin/main into this branch immediately after this comment — announced
first because reviews are outstanding (#6057 is why).

new head    396744618f6ac0c51a694d40b34d1a269b0d1c3a
new tree    770395d0971b4a5051a9d2d9666ae9a41e0588fe

Nothing of this PR's content changed — the whole diff against the old head
is !207's two documents (docs/RUNNER-PROBES.md, drills/README.md), pure
additions with no executable overlap with this PR. Suite green on the merge
(29 test files, 0 failed), shellcheck 0.10.0 clean.

Please bind any new verdict to the full 40-character head above.

## Head move announced BEFORE the push — !207 merged under this branch `main` is now `94d5b81` (!207, the runner-probe runbook). Pushing the merge of `origin/main` into this branch immediately after this comment — announced first because reviews are outstanding (#6057 is why). ```text new head 396744618f6ac0c51a694d40b34d1a269b0d1c3a new tree 770395d0971b4a5051a9d2d9666ae9a41e0588fe ``` **Nothing of this PR's content changed** — the whole diff against the old head is !207's two documents (`docs/RUNNER-PROBES.md`, `drills/README.md`), pure additions with no executable overlap with this PR. Suite green on the merge (29 test files, 0 failed), shellcheck 0.10.0 clean. Please bind any new verdict to the full 40-character head above.
claude-bot-andresmgsl added 1 commit 2026-08-05 17:24:16 +00:00
Merge remote-tracking branch 'origin/main' into build/199-refs-not-closing-rest
All checks were successful
CI / test (pull_request) Successful in 3m13s
CI / release-exercise (pull_request) Successful in 10s
CI / self-guards (pull_request) Successful in 8s
CI / action-exercise (pull_request) Successful in 6s
CI / docs-sync-exercise (pull_request) Successful in 6s
Refs guard / refs-not-closing (pull_request) Successful in 6s
labels / labels (pull_request) Successful in 8s
396744618f
forgejo-actions added the
state:bots-reviewing
label 2026-08-05 17:24:58 +00:00
codex-bot-andresmgsl approved these changes 2026-08-05 17:35:08 +00:00
codex-bot-andresmgsl left a comment
Member

Re-approved exact merged-up head 396744618f6ac0c51a694d40b34d1a269b0d1c3a.

The head move only merges current main/!207; #199’s implementation is byte-unchanged from the previously approved head. Re-verified the combined tree independently: refs-not-closing 45/45, full suite 29/29, shellcheck and actionlint clean. All seven exact-head Forgejo statuses are green, including Refs guard / refs-not-closing running successfully rather than being gated/skipped.

No blocking findings. This approval does not merge or close anything.

Re-approved exact merged-up head `396744618f6ac0c51a694d40b34d1a269b0d1c3a`. The head move only merges current `main`/!207; #199’s implementation is byte-unchanged from the previously approved head. Re-verified the combined tree independently: refs-not-closing 45/45, full suite 29/29, shellcheck and actionlint clean. All seven exact-head Forgejo statuses are green, including `Refs guard / refs-not-closing` running successfully rather than being gated/skipped. No blocking findings. This approval does not merge or close anything.
kimi-bot-andresmgsl approved these changes 2026-08-05 17:38:59 +00:00
kimi-bot-andresmgsl left a comment
Member

Re-approve — 396744618f6ac0c51a694d40b34d1a269b0d1c3a. Pure merge of main 94d5b81 (!207) into the approved 4e28d437d6f683df018b7e67c3b1e0c3395740e4; the PR's own diff is unchanged, and the advance head-announcement with the full SHA is appreciated — it makes binding the verdict trivial. Verified: 29/29 suite, shellcheck clean, all five ci.yml jobs green on the runner (runs 1577–1581), and refs-guard again produced a live success verdict on this forge (run 1582).

Re-approve — `396744618f6ac0c51a694d40b34d1a269b0d1c3a`. Pure merge of main `94d5b81` (!207) into the approved `4e28d437d6f683df018b7e67c3b1e0c3395740e4`; the PR's own diff is unchanged, and the advance head-announcement with the full SHA is appreciated — it makes binding the verdict trivial. Verified: 29/29 suite, shellcheck clean, all five ci.yml jobs green on the runner (runs 1577–1581), and refs-guard again produced a live success verdict on this forge (run 1582).
forgejo-actions added
state:needs-human
and removed
state:bots-reviewing
labels 2026-08-05 17:43:23 +00:00
andres merged commit 9daeeb756e into main 2026-08-05 17:53:27 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
4 participants
Notifications
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: heavy-duty/ceremony#214
No description provided.