Compare commits

...

6 commits

Author SHA1 Message Date
codex-bot-andresmgsl
afcc95fbbd docs: record upstream 0.6.3 sync baseline 2026-08-24 00:35:16 +00:00
68b304d713 Merge pull request 'fix(labels): grade Forgejo review states' (#244) from codex-bot-andresmgsl/ceremony:build/235-forgejo-review-vocabulary into main
Reviewed-on: heavy-duty/ceremony#244
Reviewed-by: glm-bot-andresmgsl <andres+5@heavyduty.builders>
Reviewed-by: claude-bot-andresmgsl <andres+1@heavyduty.builders>
Reviewed-by: kimi-bot-andresmgsl <andres+4@heavyduty.builders>
2026-08-24 00:16:46 +00:00
codex-bot-andresmgsl
1cd46028ed test(labels): guard Forgejo comment ingestion 2026-08-23 23:29:09 +00:00
codex-bot-andresmgsl
c2cca7c1be test(labels): mark indirect filter probe call 2026-08-23 23:17:14 +00:00
codex-bot-andresmgsl
58e58f2ada fix(labels): grade Forgejo review states 2026-08-23 23:13:05 +00:00
codex-bot-andresmgsl
e5ebbf57fb test(labels): reproduce Forgejo review vocabulary gaps 2026-08-23 23:11:11 +00:00
5 changed files with 157 additions and 10 deletions

View file

@ -272,7 +272,7 @@ set_required_bots() { # the PR author is recused by construction
# HEAD_SHA the PR's current head commit # HEAD_SHA the PR's current head commit
# BASE_SHA the PR's base branch head (the release-shape guard's ref) # BASE_SHA the PR's base branch head (the release-shape guard's ref)
# REQUESTED newline-separated logins with a review currently requested # REQUESTED newline-separated logins with a review currently requested
# REVIEWS_JSON JSON array of submitted (non-PENDING) reviews # REVIEWS_JSON JSON array of submitted, gradeable reviews
# MERGEABLE MERGEABLE | CONFLICTING | UNKNOWN (GitHub's own verdict) # MERGEABLE MERGEABLE | CONFLICTING | UNKNOWN (GitHub's own verdict)
# CHECKS SUCCESS | FAILURE | PENDING | NONE (the check rollup) # CHECKS SUCCESS | FAILURE | PENDING | NONE (the check rollup)
# LABELS newline-separated labels currently on the PR # LABELS newline-separated labels currently on the PR
@ -444,18 +444,25 @@ bot_verdict() { # $1 = login → MISSING | BLOCK | APPROVE | STALE | FEEDBACK
if [ -z "$review" ]; then echo MISSING; return; fi if [ -z "$review" ]; then echo MISSING; return; fi
state="$(jq -r '.state' <<<"$review")" state="$(jq -r '.state' <<<"$review")"
commit="$(jq -r '.commit_id' <<<"$review")" commit="$(jq -r '.commit_id' <<<"$review")"
# This case grades a submitted verdict. The ingestion allow-list answers the
# separate question of whether a row is a submitted review at all (#235).
case "$state" in case "$state" in
CHANGES_REQUESTED) CHANGES_REQUESTED | REQUEST_CHANGES)
# blocks at ANY head — GitHub's own semantic: only a newer review # blocks at ANY head — both forges' semantic: only a newer review from
# from the same reviewer clears it # the same reviewer clears it
echo BLOCK ;; echo BLOCK ;;
APPROVED) APPROVED)
if [ "$commit" = "$HEAD_SHA" ]; then echo APPROVE; else echo STALE; fi ;; if [ "$commit" = "$HEAD_SHA" ]; then echo APPROVE; else echo STALE; fi ;;
*) COMMENTED | COMMENT)
# COMMENTED and anything else: a non-verdict. The machine does not # A comment is a non-verdict. The machine does not read bodies — if the
# read bodies — if the comment is really an agreement, the AUTHOR # comment is really an agreement, the AUTHOR says so by requesting the
# says so by requesting the human's review. # human's review.
echo FEEDBACK ;; echo FEEDBACK ;;
*)
# An unknown state is not evidence that a reviewer answered. Keep the
# round open and make the next forge vocabulary surprise visible (#235).
log "$1: unrecognised review state $state" >&2
echo MISSING ;;
esac esac
} }
@ -1054,9 +1061,15 @@ main() {
HEAD_SHA="$(jq -r '.head.sha' <<<"$PR_JSON")" HEAD_SHA="$(jq -r '.head.sha' <<<"$PR_JSON")"
BASE_SHA="$(jq -r '.base.sha' <<<"$PR_JSON")" BASE_SHA="$(jq -r '.base.sha' <<<"$PR_JSON")"
LABELS="$(jq -r '.labels[].name' <<<"$PR_JSON")" LABELS="$(jq -r '.labels[].name' <<<"$PR_JSON")"
# PENDING reviews are unsubmitted drafts in someone's browser — not a verdict # This allow-list answers whether a row is a submitted, gradeable review;
# bot_verdict separately answers what that submitted verdict says (#235).
# PENDING drafts and Forgejo REQUEST_REVIEW request rows are not reviews.
REVIEWS_JSON="$(forge_api --paginate "repos/$REPO/pulls/$n/reviews" --jq '.[]' \ REVIEWS_JSON="$(forge_api --paginate "repos/$REPO/pulls/$n/reviews" --jq '.[]' \
| jq -s '[.[] | select(.state != "PENDING")]')" | jq -s '[.[] | select(.state == "APPROVED"
or .state == "CHANGES_REQUESTED"
or .state == "REQUEST_CHANGES"
or .state == "COMMENTED"
or .state == "COMMENT")]')"
# Read AFTER the reviews, because the raw field is not portable: Forgejo # Read AFTER the reviews, because the raw field is not portable: Forgejo
# never clears it, so it is intersected with who still owes a verdict on # never clears it, so it is intersected with who still owes a verdict on
# this head (#188 term 4). A no-op on GitHub, which clears it itself. # this head (#188 term 4). A no-op on GitHub, which clears it itself.

7
changelog.d/231.md Normal file
View file

@ -0,0 +1,7 @@
### Changed
- Forge release 0.6.2 consolidates upstream 0.6.1 doctrine from upstream #316, #311, #330, and #329, plus upstream 0.6.2's #336 clarification (#231).
- Upstream 0.6.3's #343 membership record and #327 gate fixes land through Forgejo-adapted reconciler changes rather than upstream bytes (#231).
- The upstream drill-record fixes at `86dc2eb`, `a72085b`, and `13ffb0d`, plus upstream 0.7.00.7.4, remain explicitly deferred (#231).

3
changelog.d/235.md Normal file
View file

@ -0,0 +1,3 @@
### Fixed
- Forgejo review requests no longer count as verdicts, while its blocking and comment states now grade like their GitHub equivalents (#235).

View file

@ -17,6 +17,27 @@ resolution is standing and does not get re-decided each sync.
Worked example throughout: the `0.6.0` sync (#197, #198), which merged upstream Worked example throughout: the `0.6.0` sync (#197, #198), which merged upstream
`8c3a4d1` onto `dad99dd` and took four heads to get green. `8c3a4d1` onto `dad99dd` and took four heads to get green.
## Sync ledger
### 2026-08-24 — upstream 0.6.10.6.3 into forge 0.6.2
- **Consumed through:** upstream tag `upstream-0.6.3`
(`cf2892148305ab18ff234fa0a2ef28ab6fef6c0f`). The `upstream-` prefix is
mandatory in this record because bare `0.6.x` names collide with this
forge's own releases and can identify different trees.
- **Previous baseline:** upstream `0.6.0`, merge-base
`8c3a4d1dee2bdb5ac06a632a285bb65ab2615214`. This adoption ported the
upstream changes in #229 and #230 instead of merging upstream ancestry, so
`.upstream-ref` and the Git merge-base intentionally remain at that commit;
`upstream-0.6.3` is the new content baseline for the next campaign.
- **Adapted here:** the membership and gate changes were applied to the
Forgejo reconciler and its tests, and the upstream CONTRIBUTING changes
were reconciled with this forge's roster and conventions rather than copied
byte-for-byte.
- **Deferred:** upstream's drill-record bookkeeping commits `86dc2eb`,
`a72085b`, and `13ffb0d` correct drill files this forge does not mirror.
Upstream 0.7.00.7.4 is the next sync campaign's standing queue.
## The standing resolutions ## The standing resolutions
These recur every sync. They are decided; re-deciding them is the cost this These recur every sync. They are decided; re-deciding them is the cost this

View file

@ -172,6 +172,20 @@ $BOT2
$BOT3" REVIEWS_JSON='[]' $BOT3" REVIEWS_JSON='[]'
expect "requested bots mean bots-reviewing" state:bots-reviewing "$(decide_state)" expect "requested bots mean bots-reviewing" state:bots-reviewing "$(decide_state)"
# Forgejo materializes each live request as a REQUEST_REVIEW row. Those rows
# are not submitted verdicts (#235): they must leave all three logins
# outstanding, so an opening round stays with the panel rather than falling
# through to the builder as three comment-only answers.
REVIEWS_JSON="$(reviews \
"$(rev "$BOT1" REQUEST_REVIEW "" "" t1)" \
"$(rev "$BOT2" REQUEST_REVIEW "" "" t2)" \
"$(rev "$BOT3" REQUEST_REVIEW "" "" t3)")"
REQUESTED="$(outstanding_requests "$BOT1
$BOT2
$BOT3" 2>"$RTMP/request-round-log")"
expect "three Forgejo request rows keep the opening round with the panel" \
state:bots-reviewing "$(round_state)"
# -- a bot that never reviewed keeps the round open --------------------------- # -- a bot that never reviewed keeps the round open ---------------------------
# With a live request that is the bots' ball; with NO request outstanding it # With a live request that is the bots' ball; with NO request outstanding it
# is the agent's, because nothing is coming until somebody asks. # is the agent's, because nothing is coming until somebody asks.
@ -215,6 +229,25 @@ REVIEWS_JSON="$(reviews \
"$(rev "$BOT3" APPROVED head1 "" t3)")" "$(rev "$BOT3" APPROVED head1 "" t3)")"
expect "changes-requested blocks even from an old head" state:addressing "$(decide_state)" expect "changes-requested blocks even from an old head" state:addressing "$(decide_state)"
# bot_verdict grades submitted states from both forges (#235). Each direct
# assertion names one arm so a later vocabulary regression cannot hide behind
# round_state's shared BLOCK/FEEDBACK handling.
expect "GitHub CHANGES_REQUESTED grades as a block" BLOCK \
"$(bot_verdict "$BOT1")"
REVIEWS_JSON="$(reviews "$(rev "$BOT1" REQUEST_CHANGES old1 "blockers below" t1)")"
expect "Forgejo REQUEST_CHANGES grades as a block" BLOCK \
"$(bot_verdict "$BOT1")"
REVIEWS_JSON="$(reviews "$(rev "$BOT1" COMMENT head1 "non-blocking note" t1)")"
expect "Forgejo COMMENT grades as feedback" FEEDBACK \
"$(bot_verdict "$BOT1")"
REVIEWS_JSON="$(reviews "$(rev "$BOT1" FUTURE_FORGE_STATE head1 "" t1)")"
bot_verdict "$BOT1" >"$RTMP/unknown-verdict" 2>"$RTMP/unknown-verdict-log"
expect "an unrecognised review state is conservatively missing" MISSING \
"$(cat "$RTMP/unknown-verdict")"
expect "an unrecognised review state logs the login and spelling" yes \
"$(grep -qF "$BOT1: unrecognised review state FUTURE_FORGE_STATE" \
"$RTMP/unknown-verdict-log" && echo yes || echo no)"
# -- a stale approval must not promote unreviewed code ------------------------ # -- a stale approval must not promote unreviewed code ------------------------
REVIEWS_JSON="$(reviews \ REVIEWS_JSON="$(reviews \
"$(rev "$BOT1" APPROVED old1 "" t1)" \ "$(rev "$BOT1" APPROVED old1 "" t1)" \
@ -253,6 +286,24 @@ REQUESTED="$HUMAN"
expect "re-requested human is needs-human again" state:needs-human "$(decide_state)" expect "re-requested human is needs-human again" state:needs-human "$(decide_state)"
REQUESTED="" REQUESTED=""
# Forgejo's human-block spelling carries the same meaning (#235). This is
# independently observable because only BLOCK prevents state:needs-human once
# every bot approves.
REVIEWS_JSON="$(reviews \
"$(rev "$BOT1" APPROVED head1 "" t1)" \
"$(rev "$BOT2" APPROVED head1 "" t2)" \
"$(rev "$BOT3" APPROVED head1 "" t3)" \
"$(rev "$HUMAN" REQUEST_CHANGES head1 "not yet" t4)")"
expect "Forgejo human request-changes with bots approving is addressing" \
state:addressing "$(decide_state)"
REVIEWS_JSON="$(reviews \
"$(rev "$BOT1" APPROVED head1 "" t1)" \
"$(rev "$BOT2" APPROVED head1 "" t2)" \
"$(rev "$BOT3" APPROVED head1 "" t3)" \
"$(rev "$HUMAN" APPROVED head1 "" t4)")"
expect "the same Forgejo-shaped fixture with human approval reaches needs-human" \
state:needs-human "$(decide_state)"
# -- an old human comment must not wedge the handoff (codex, #85 round 3) ----- # -- an old human comment must not wedge the handoff (codex, #85 round 3) -----
REVIEWS_JSON="$(reviews \ REVIEWS_JSON="$(reviews \
"$(rev "$HUMAN" COMMENTED old1 "early thoughts" t0)" \ "$(rev "$HUMAN" COMMENTED old1 "early thoughts" t0)" \
@ -1588,6 +1639,49 @@ expect "...with no 'reconciled.' token in the output" \
expect "...naming the attempt that did not happen" \ expect "...naming the attempt that did not happen" \
yes "$(grep -q 'label edit FAILED' <<<"$sf_out" && echo yes || echo no)" yes "$(grep -q 'label edit FAILED' <<<"$sf_out" && echo yes || echo no)"
# Drive the ingestion expression through main(), independently of
# bot_verdict (#235). Capturing REVIEWS_JSON at the outstanding_requests
# boundary proves REQUEST_REVIEW never reaches the grader; the COMMENT and
# APPROVED controls prove both gradeable Forgejo states and rows generally
# survive the filter.
review_filter_probe() {
(
REPO=owner/repo
LABELS_CONF="$FIXTURE_CONF"
CEREMONY_FORGE=github
# shellcheck disable=SC2317 # reached through the forge backend, not called directly (#188)
gh() {
if [ "$1" = label ] && [ "$2" = list ]; then core_label_rows | cut -d'|' -f1; return 0; fi
if [ "$1" = pr ] && [ "$2" = list ]; then printf '%s\n' 601; return 0; fi
if [ "$1" = pr ] && [ "$2" = view ]; then
jq -n '{mergeable:"MERGEABLE",statusCheckRollup:[]}'
return 0
fi
if [ "$1" = issue ] && [ "$2" = edit ]; then return 0; fi
case "$(forge_stub_path "$*")" in
*repos/owner/repo/pulls/601/reviews*)
jq -nc \
'{user:{login:"fixture-bot-one"},state:"REQUEST_REVIEW",commit_id:"",submitted_at:"2026-08-22T00:46:05Z"},
{user:{login:"fixture-bot-three"},state:"COMMENT",commit_id:"head1",submitted_at:"2026-08-22T00:46:35Z"},
{user:{login:"fixture-bot-two"},state:"APPROVED",commit_id:"head1",submitted_at:"2026-08-22T00:47:05Z"}' ;;
*/pulls/601)
jq -n '{draft:true,user:{login:"fixture-builder"},head:{sha:"head1"},base:{sha:"base1"},
labels:[{name:"state:building"}],requested_reviewers:[],
created_at:"2026-08-22T00:45:00Z"}' ;;
*) printf '[]\n' ;;
esac
}
# shellcheck disable=SC2317 # main invokes the probe override indirectly
outstanding_requests() {
printf '%s\n' "$REVIEWS_JSON" >"$RTMP/gradeable-reviews.json"
}
main >/dev/null
)
}
review_filter_probe
expect "REQUEST_REVIEW is removed before REVIEWS_JSON reaches the grader" \
COMMENT,APPROVED "$(jq -r 'map(.state) | join(",")' "$RTMP/gradeable-reviews.json")"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# outstanding_requests — the portable "who still owes a verdict" (#188 term 4) # outstanding_requests — the portable "who still owes a verdict" (#188 term 4)
# #
@ -1615,6 +1709,15 @@ expect "a stale approval still owes a verdict" "$BOT3" \
expect "a reviewer who never reviewed still owes one" "nobody" \ expect "a reviewer who never reviewed still owes one" "nobody" \
"$(outstanding_requests "nobody")" "$(outstanding_requests "nobody")"
REVIEWS_JSON="$(reviews \
"$(rev "$BOT1" REQUEST_REVIEW "" "" 2026-08-22T00:46:05Z)")"
expect "a Forgejo request row is not an answer and leaves the login outstanding" \
"$BOT1" "$(outstanding_requests "$BOT1" 2>"$RTMP/request-outstanding-log")"
REVIEWS_JSON="$(reviews \
"$(rev "$BOT1" APPROVED head1 "" 2026-08-01T00:00:00Z)" \
"$(rev "$BOT2" CHANGES_REQUESTED head1 "" 2026-08-01T00:00:00Z)" \
"$(rev "$BOT3" APPROVED head0 "" 2026-07-01T00:00:00Z)")"
# The Forgejo shape, end to end: the field lists all three long after every # The Forgejo shape, end to end: the field lists all three long after every
# verdict landed. Only the stale one may survive the filter. # verdict landed. Only the stale one may survive the filter.
expect "the never-cleared forgejo field collapses to who actually owes" \ expect "the never-cleared forgejo field collapses to who actually owes" \