Some checks failed
CI / test (pull_request) Failing after 33s
CI / release-exercise (pull_request) Successful in 12s
CI / self-guards (pull_request) Successful in 6s
CI / action-exercise (pull_request) Successful in 6s
CI / docs-sync-exercise (pull_request) Successful in 6s
Refs guard / refs-not-closing (pull_request) Failing after 5s
labels / labels (pull_request) Successful in 43s
`git merge` of upstream `8c3a4d1` onto `dad99dd`, common ancestor `84bb1a4`. 18 hunks in 10 files; `lib/forge.sh`, `lib/forge-github.sh` and `lib/forge-forgejo.sh` conflict in none and come out byte-identical. The resolutions the issue decided: VERSION and both CEREMONY_SELF_REF carriers take upstream's numbers; `.github/labels.conf` and `drills/0.4.1.md` keep this forge's; CHANGELOG keeps both sides and names the upstream commit this tree carries. The part the hunks did not contain. Upstream's 0.5.0/0.6.0 work added whole functions to files this tree already owned, so `git merge` took its side without raising a conflict — and with them, EIGHT runtime `gh` call sites that #188 had removed. Seven are ported onto the shim: two reads and four comment writes in issueflow-reconcile, and labels-reconcile's HEAD_COMMIT_AT read. The eighth is `gh workflow run` in labels.yml, which a workflow cannot declare a client for and whose Forgejo equivalent this instance answers with 500 rather than a 4xx — named with its reason rather than ported on a guess. test/no-runtime-gh.test.sh makes the rule mechanical, because reviewing the diff could not: four reviewers reading it each found a different subset, and the contract suite stubs `gh`, so a reintroduced call site passes it. Three seams the resolution decides are silent when resolved wrongly, and each now has a case that fails on the wrong one: the merged record's `merged_at` third column (without it every sort key ties and the highest PR number comes back), the open gather's one-BODY-row-per-line feed (a whole decoded body as one record loses every declaration including the first), and the whole-board read whose COLLISION_FLAGS/WINDOW_FLAGS consumers auto-merged. The open gather carries CLOSING rows as well as BODY rows. `Refs` alone would drop every `Closes #N` link on the open side and reclaim a claim the PR was holding — the existing base64 round-trip case is red without it. actions/refs-not-closing declares CEREMONY_FORGE_CLIENT=gh: its only gather is GraphQL, which Forgejo does not serve at all. #199 ports it. test/run.sh: 28 test files, 0 failed. shellcheck and actionlint clean. Refs #198
104 lines
4.2 KiB
Bash
104 lines
4.2 KiB
Bash
#!/usr/bin/env bash
|
|
# lib/attention.sh — the `attention` target invariants (#232, epic #229).
|
|
#
|
|
# Both reconcilers source this file. Pure decisions sit above the divider;
|
|
# the impure orchestrator below reads the current label episode and comments
|
|
# through the sourcing script's run()/log(). The machine diagnoses only: it
|
|
# never sets, clears, retargets or assigns anything on the strength of these
|
|
# checks (#229 D2).
|
|
|
|
ATTENTION_MARKER_PREFIX='<!-- ceremony:attention-malformed:'
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pure decisions. Facts in, verdict out. No gh, no clock.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
attention_target_decision() { # $1 pr|issue, $2 assignee count → MALFORMED_* | KEEP
|
|
case "$1" in
|
|
pr) echo MALFORMED_PR ;;
|
|
issue)
|
|
if [ "$2" -eq 0 ]; then echo MALFORMED_UNASSIGNED; else echo KEEP; fi ;;
|
|
*) return 2 ;;
|
|
esac
|
|
}
|
|
|
|
attention_comment_decision() { # $1 target verdict, $2 suppression → POST | SUPPRESS | KEEP
|
|
case "$1" in
|
|
KEEP) echo KEEP ;;
|
|
MALFORMED_UNASSIGNED)
|
|
if [ -n "$2" ]; then echo SUPPRESS; else echo POST; fi ;;
|
|
MALFORMED_PR) echo POST ;;
|
|
*) return 2 ;;
|
|
esac
|
|
}
|
|
|
|
attention_newest_flag() { # labeled-event ISO-8601 timestamps on stdin → newest
|
|
sort | tail -n1
|
|
}
|
|
|
|
attention_episode_marker() { # $1 current episode's labeled timestamp
|
|
printf '%s%s -->\n' "$ATTENTION_MARKER_PREFIX" "$1"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The impure orchestrator. Called only behind a has-attention gate.
|
|
# Needs REPO; uses the caller's run() and log().
|
|
# ---------------------------------------------------------------------------
|
|
|
|
reconcile_attention() { # $1 item, $2 pr|issue, $3 assignees, $4 suppression
|
|
local n="$1" surface="$2" assignees="$3" suppression="${4:-}"
|
|
local target comment labeled_events labeled_at marker comments body timeline
|
|
: "${REPO:?reconcile_attention: REPO is required}"
|
|
|
|
target="$(attention_target_decision "$surface" "$assignees")"
|
|
comment="$(attention_comment_decision "$target" "$suppression")"
|
|
[ "$comment" != KEEP ] || return 0
|
|
|
|
if [ "$comment" = SUPPRESS ]; then
|
|
log "#$n: malformed attention detected; comment suppressed by $suppression precedence"
|
|
return 0
|
|
fi
|
|
|
|
# forge_timeline projects both forges into the GitHub event shape
|
|
# (.event / .label.name) — Forgejo's raw timeline carries neither. Capture
|
|
# its status BEFORE jq: a pipeline's status is the last command's, so
|
|
# `forge_timeline | jq` would collapse an unreadable timeline into an empty
|
|
# one, and those are the two states this function exists to tell apart
|
|
# (#188, and lib/ruling.sh does the identical dance).
|
|
if ! timeline="$(forge_timeline "$n" 2>/dev/null)"; then
|
|
log "#$n: attention timeline unreadable — no verdict invented this pass"
|
|
return 0
|
|
fi
|
|
labeled_events="$(jq -r '.[]
|
|
| select(.event == "labeled" and .label.name == "attention")
|
|
| .created_at' <<<"$timeline")"
|
|
if [ -z "$labeled_events" ]; then
|
|
log "#$n: attention flag has no visible labeled event — no verdict invented this pass"
|
|
return 0
|
|
fi
|
|
labeled_at="$(attention_newest_flag <<<"$labeled_events")"
|
|
marker="$(attention_episode_marker "$labeled_at")"
|
|
|
|
if ! comments="$(forge_api --paginate "repos/$REPO/issues/$n/comments" \
|
|
--jq '.[].body // ""' 2>/dev/null)"; then
|
|
log "#$n: attention comments unreadable — no verdict invented this pass"
|
|
return 0
|
|
fi
|
|
grep -qF "$marker" <<<"$comments" && return 0
|
|
|
|
case "$target" in
|
|
MALFORMED_PR)
|
|
body="$marker
|
|
This pull request carries \`attention\`, but that label is issue-only. Put
|
|
the label on the assigned issue that owns the claim. The sweep cannot infer
|
|
which issue that is, so it reports the malformed target without removing or
|
|
retargeting the label." ;;
|
|
MALFORMED_UNASSIGNED)
|
|
body="$marker
|
|
This issue carries \`attention\` but has no assignee to receive the demand.
|
|
Assign the intended builder or remove the flag. The sweep reports the board
|
|
bug without assigning anyone or changing the label." ;;
|
|
esac
|
|
run forge_issue_comment "$n" "$body" >/dev/null
|
|
log "#$n: malformed attention ($surface) — commented; no label or assignee changed"
|
|
}
|