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
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:
@@ -19,7 +19,7 @@ import hmac
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from scribe.routes.webhooks import push_facts, signature_ok
|
||||
from scribe.routes.webhooks import delivered_signature, push_facts, signature_ok
|
||||
from scribe.services.snippets import _path_touches
|
||||
|
||||
SECRET = "wh-secret"
|
||||
@@ -41,6 +41,46 @@ def test_signature_gate():
|
||||
assert signature_ok("", body, _sign(body)) is False
|
||||
|
||||
|
||||
def test_delivered_signature_reads_both_forges_headers():
|
||||
"""#2693: GitHub signs the same HMAC but ships it as
|
||||
X-Hub-Signature-256: sha256=<hex> — the whole webhook payload mapping is
|
||||
this header, so pin it."""
|
||||
hexsig = _sign(b"{}")
|
||||
assert delivered_signature({"X-Gitea-Signature": hexsig}) == hexsig
|
||||
assert delivered_signature({"X-Hub-Signature-256": f"sha256={hexsig}"}) == hexsig
|
||||
# Gitea's header wins when both appear; absence reads as empty (→ 401).
|
||||
assert delivered_signature({}) == ""
|
||||
# The stripped GitHub form still passes the gate end to end.
|
||||
assert signature_ok(
|
||||
SECRET, b'{"x": 1}',
|
||||
delivered_signature({"X-Hub-Signature-256": "sha256=" + _sign(b'{"x": 1}')}),
|
||||
)
|
||||
|
||||
|
||||
def test_push_facts_reads_a_github_shaped_payload():
|
||||
"""GitHub's push payload carries the same fields push_facts consumes —
|
||||
asserted against a real-shaped sample so a rename on either side of the
|
||||
mapping breaks a test instead of silently flagging nothing."""
|
||||
payload = {
|
||||
"ref": "refs/heads/main",
|
||||
"after": HEAD,
|
||||
"repository": {
|
||||
"full_name": "alice/widget",
|
||||
"clone_url": "https://github.com/alice/widget.git",
|
||||
"html_url": "https://github.com/alice/widget",
|
||||
},
|
||||
"commits": [
|
||||
{"id": "a" * 40, "added": [], "modified": ["src/x.py"], "removed": []},
|
||||
],
|
||||
"head_commit": {"id": HEAD},
|
||||
}
|
||||
raw, changed, removed, head = push_facts(payload)
|
||||
assert raw == "https://github.com/alice/widget.git"
|
||||
assert changed == ["src/x.py"]
|
||||
assert removed == []
|
||||
assert head == HEAD
|
||||
|
||||
|
||||
# --- unit: payload parsing ---------------------------------------------------
|
||||
|
||||
def test_push_facts_collects_and_dedups_paths():
|
||||
|
||||
Reference in New Issue
Block a user