feat(forge): GitHub adapter — second implementation keeps the seam a contract (#2693, milestone 288 step 8)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 27s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 1m8s
CI & Build / Build & push image (push) Successful in 45s

ForgeAdapter is now a named base class carrying the shared plumbing
(host join, error taxonomy, contents decoding, archive, default_branch,
latest_commit); GiteaForge keeps its exact behavior and GitHubForge joins
with the real differences: api.github.com / GHE /api/v3 host mapping,
Bearer auth, a commits call for the provenance stamp (GitHub's contents
payload only carries the blob sha), and the codeload tarball redirect.

The contract grew latest_commit, and with it the cached-SHA short-circuit
in pull-time freshness: a stored provenance commit that still heads the
recorded path confirms 'current' without a content transfer — the economy
that fits pulls inside GitHub's rate limits; every surprise falls back to
the full fetch. Webhook deliveries now also accept X-Hub-Signature-256
(sha256=<hex>); the payload shape was already common. Settings card copy
covers both forges' token scopes; the kind selector already flowed from
the server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 16:18:16 -04:00
co-authored by Claude Fable 5
parent cbccb6bd5d
commit 765635bbf2
8 changed files with 482 additions and 63 deletions
+28 -3
View File
@@ -1046,10 +1046,27 @@ async def attach_live_body(note, data: dict) -> None:
data["body_freshness"] = "repo-not-on-this-forge"
return
stored_prov_sha = (fields.get("provenance") or {}).get("commit_sha") or ""
async def _probe():
# Cached-SHA short-circuit (#2693): provenance names the commit the
# cached code was last confirmed at, so one cheap "newest commit
# touching this path" call can prove the file hasn't moved since —
# no content transfer. That economy is what fits pull-time freshness
# inside GitHub's rate limits; it's merely nice on a self-hosted
# Gitea. Any surprise (error, empty, mismatch) falls through to the
# full fetch, which stays the authoritative path.
if stored_prov_sha:
try:
head = await forge.latest_commit(repo, loc["path"])
except ForgeError:
head = ""
if head and head == stored_prov_sha:
return None
return await forge.read_file(repo, loc["path"])
try:
fetched = await asyncio.wait_for(
forge.read_file(repo, loc["path"]), timeout=PULL_FETCH_BUDGET_S
)
fetched = await asyncio.wait_for(_probe(), timeout=PULL_FETCH_BUDGET_S)
except ForgeNotFound:
data["body_source"] = "cache"
data["body_freshness"] = "missing"
@@ -1064,6 +1081,14 @@ async def attach_live_body(note, data: dict) -> None:
data["body_freshness"] = "unreachable"
return
if fetched is None:
# Unchanged since the provenance commit — confirmed against the
# source without moving the file. Same stamp, so nothing to persist
# (the same-sha rule); the body already reflects that commit.
data["body_source"] = "forge"
data["body_freshness"] = "current"
return
cached = _normalized_code(fields.get("code") or "")
if cached and cached in _normalized_code(fetched.content):
data["body_source"] = "forge"