forked from heavy-duty/ceremony
fix(forge): forge_commit_at — Forgejo serves a single commit at /git/commits/{sha} (#209)
Found by the first post-merge sweep after the 0.6.0 merge — #198's own acceptance probe — not by review. Three PRs in one run: labels: #208: could not read the head commit's date: forge_api: HTTP 404 from 'GET repos/heavy-duty/ceremony/commits/f3a1336…' — blocker:unrequested not judged this pass Measured against this instance: forgejo repos/{o}/{r}/commits/{sha} -> 404 forgejo repos/{o}/{r}/git/commits/{sha} -> 200, date under `.created` github repos/{o}/{r}/commits/{sha} -> 200, date nested A fourth asymmetry, alongside the three lib/forge-forgejo.sh's header already records. #198 ported this call site onto the shim with GitHub's path unchanged — correct against GitHub, and the block it lives in (#236 D2) arrived WITH the merge, so nothing here had ever executed it. So it becomes a verb rather than a path at the call site: the caller wants one timestamp and should not have to know either shape. Cost while it stood was bounded and loud rather than silent — guarded_read refused and the sweep said so — but blocker:unrequested could never be judged on this forge. The tests pin each backend's PATH and FIELD, because a stubbed forge_api cannot catch a wrong path; that is exactly how this shipped and why it took a live sweep to find. Swapping the paths reds the forgejo pair; swapping the fields reds the github one. test/run.sh 28/28 under jq 1.7 and jq 1.6; forge-backends 124/124; shellcheck 0.10.0 and actionlint clean. Refs #209
This commit is contained in:
parent
e236318647
commit
a55fbaef15
5 changed files with 75 additions and 2 deletions
|
|
@ -1094,8 +1094,8 @@ main() {
|
|||
HEAD_COMMIT_AT=""
|
||||
if [ "$DRAFT" != true ]; then
|
||||
HEAD_COMMIT_ERR_FILE="$(mktemp)"
|
||||
HEAD_COMMIT_AT="$(forge_api "repos/$REPO/commits/$HEAD_SHA" \
|
||||
--jq '.commit.committer.date' 2>"$HEAD_COMMIT_ERR_FILE" || echo "")"
|
||||
HEAD_COMMIT_AT="$(forge_commit_at "$HEAD_SHA" \
|
||||
2>"$HEAD_COMMIT_ERR_FILE" || echo "")"
|
||||
HEAD_COMMIT_ERR="$(cat "$HEAD_COMMIT_ERR_FILE")"
|
||||
rm -f "$HEAD_COMMIT_ERR_FILE"
|
||||
case "$HEAD_COMMIT_AT" in
|
||||
|
|
|
|||
16
changelog.d/209.md
Normal file
16
changelog.d/209.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
### Fixed
|
||||
|
||||
- `blocker:unrequested` is judged on this forge again. The head-commit date was
|
||||
read from `repos/{o}/{r}/commits/{sha}`, which Forgejo answers **404** — so
|
||||
every sweep degraded and left the blocker unjudged (#209).
|
||||
|
||||
- `forge_commit_at` is a verb on both backends: GitHub serves a single commit at
|
||||
the bare path with the date nested, Forgejo at `git/commits/{sha}` with it
|
||||
under `.created`. The caller asks for one timestamp and knows neither shape
|
||||
(#209).
|
||||
|
||||
### Added
|
||||
|
||||
- `test/forge-backends.test.sh` pins each backend's path **and** field, because
|
||||
a stubbed `forge_api` cannot catch a wrong path — which is how this shipped
|
||||
and why a live sweep was what found it (#209).
|
||||
|
|
@ -642,6 +642,27 @@ forge_release_exists() {
|
|||
# forge_commit_pulls <sha> — the pull requests whose merge produced <sha>, as
|
||||
# a JSON ARRAY in GitHub's shape. An empty array is a completed read that
|
||||
# found nothing; a non-zero exit is a read that did not complete.
|
||||
# forge_commit_at <sha> — the commit's committer date, ISO-8601, or empty.
|
||||
#
|
||||
# THE FOURTH ASYMMETRY (#209), measured 2026-08-05:
|
||||
#
|
||||
# GET /repos/{o}/{r}/commits/{sha} -> 404 (200 on GitHub)
|
||||
# GET /repos/{o}/{r}/git/commits/{sha} -> 200 date under `.created`
|
||||
#
|
||||
# Found by the first post-merge sweep after the 0.6.0 merge, not by review:
|
||||
# #198 ported this call site onto the shim with GitHub's path unchanged, and
|
||||
# the block it lives in had never executed here before. Every sweep printed
|
||||
# `could not read the head commit's date` and left blocker:unrequested
|
||||
# unjudged.
|
||||
#
|
||||
# `.created` and not `.commit.committer.date`: the /git/commits payload is the
|
||||
# git object, whose top-level `created` is the committer date. The verb hides
|
||||
# both differences so the caller keeps asking for one timestamp.
|
||||
forge_commit_at() {
|
||||
local sha="${1:?forge_commit_at: sha required}"
|
||||
forge_api "repos/$REPO/git/commits/$sha" --jq '.created'
|
||||
}
|
||||
|
||||
forge_commit_pulls() {
|
||||
local sha="${1:?forge_commit_pulls: sha required}" body code out
|
||||
body="$(mktemp)"
|
||||
|
|
|
|||
|
|
@ -186,6 +186,18 @@ forge_release_exists() {
|
|||
# forge_commit_pulls <sha> — the pull requests whose merge produced <sha>, as
|
||||
# a JSON array. GitHub serves the array directly; the forgejo twin builds
|
||||
# one from its single-object endpoint so this call site is identical.
|
||||
# forge_commit_at <sha> — the commit's committer date, ISO-8601, or empty.
|
||||
#
|
||||
# A VERB rather than a path at the call site, because the two forges do not
|
||||
# agree on where a single commit lives: GitHub serves it at /commits/{sha},
|
||||
# Forgejo 404s there and serves it at /git/commits/{sha} with the timestamp
|
||||
# under a different field (#209). The caller wants one timestamp; it should not
|
||||
# have to know either shape.
|
||||
forge_commit_at() {
|
||||
local sha="${1:?forge_commit_at: sha required}"
|
||||
forge_api "repos/$REPO/commits/$sha" --jq '.commit.committer.date'
|
||||
}
|
||||
|
||||
forge_commit_pulls() {
|
||||
local sha="${1:?forge_commit_pulls: sha required}" errf out rc err
|
||||
errf="$(mktemp)"
|
||||
|
|
|
|||
|
|
@ -775,6 +775,21 @@ writes_after() { "$@" >/dev/null 2>&1; cat "$WRITES"; }
|
|||
repo_empty_release() { REPO='' forge_release_exists 1.2.3; }
|
||||
repo_empty_pulls() { REPO='' forge_commit_pulls deadbeef; }
|
||||
|
||||
# forge_commit_at — the FOURTH asymmetry (#209). Forgejo 404s on /commits/{sha}
|
||||
# and serves the object at /git/commits/{sha}, with the committer date under
|
||||
# `.created` rather than `.commit.committer.date`. A stubbed forge_api cannot
|
||||
# catch a wrong PATH, which is how #198 shipped GitHub's path here and every
|
||||
# sweep printed `could not read the head commit's date`.
|
||||
release_stub 200 '{"created":"2026-08-05T13:11:33Z","commit":{"committer":{"date":"WRONG"}}}'
|
||||
check "forgejo: the commit date comes from .created" 0 "2026-08-05T13:11:33Z" \
|
||||
forge_commit_at deadbeef
|
||||
fj_not_nested() { ! forge_commit_at deadbeef | grep -q WRONG; }
|
||||
check "...and never from GitHub's nested field" 0 "" fj_not_nested
|
||||
check "forgejo: it asks /git/commits/{sha}" 0 "git/commits/deadbeef" \
|
||||
writes_after forge_commit_at deadbeef
|
||||
fj_not_bare_path() { ! grep -qE 'repos/o/r/commits/deadbeef( |$)' "$WRITES"; }
|
||||
check "...and never the bare /commits/{sha}, which 404s here" 0 "" fj_not_bare_path
|
||||
|
||||
release_stub 200 '{"number":7,"merged_at":"2026-01-01T00:00:00Z","labels":[{"name":"release"}]}'
|
||||
check "forgejo: one PR object becomes a one-element array" 0 '"number":7' \
|
||||
forge_commit_pulls deadbeef
|
||||
|
|
@ -857,5 +872,14 @@ check "github: the tag goes to /git/refs" 0 "git/refs" \
|
|||
gh_after forge_tag_create 1.2.3 cafebabe
|
||||
check "github: PRs behind a commit use the PLURAL path" 0 "commits/deadbeef/pulls" \
|
||||
gh_after forge_commit_pulls deadbeef
|
||||
# The other half of #209's asymmetry: GitHub serves a single commit at the bare
|
||||
# path, with the date nested. Swapping the two backends' paths must red one of
|
||||
# these two files, which is the whole point of pinning both.
|
||||
check "github: a single commit is the BARE path" 0 "commits/deadbeef" \
|
||||
gh_after forge_commit_at deadbeef
|
||||
gh_not_git_commits() { ! gh_after forge_commit_at deadbeef | grep -q 'git/commits'; }
|
||||
check "...and never Forgejo's git/commits" 0 "" gh_not_git_commits
|
||||
check "...reading the nested committer date" 0 "commit.committer.date" \
|
||||
gh_after forge_commit_at deadbeef
|
||||
|
||||
summary
|
||||
|
|
|
|||
Loading…
Reference in a new issue