generated from coulomb/repo-seed
REUSE-WP-0019-T02: hub recompose staleness tracking + Forgejo webhook
Some checks failed
ci / validate-registry (push) Has been cancelled
Some checks failed
ci / validate-registry (push) Has been cancelled
reuse_surface/hub/store.py: compose_state table tracking composed_at/stale, updated only on a *forced* recompose (refresh=true, webhook, future scheduled fallback) -- a plain GET still serves current best-effort data but never silently reports itself as freshly composed. reuse_surface/hub/webhooks.py: constant-time HMAC-SHA256 signature verification (fails closed on an empty/unconfigured secret) and path-only push-payload inspection (never parses file content, per design principle 2 -- webhook only decides whether to trigger a pull-based recompose). New endpoint POST /v1/webhooks/forgejo, accepting both X-Forgejo-Signature and X-Gitea-Signature headers since sibling repos migrate independently. GET /v1/federated and POST /v1/federated/compose now share an asyncio.Lock with the webhook so concurrent recompose triggers coalesce instead of overlapping. No separate /v1/recompose route was added -- POST /v1/federated/compose already did that job from earlier hub work; specs/FederationHubAPI.md now documents this explicitly instead of duplicating it. 28 new pytest cases (128 total pass). Live-verified: ran the actual hub service locally and sent a real HMAC-signed webhook over HTTP, confirming the full webhook-to-recompose path, signature rejection, and the irrelevant-path no-op. Does NOT deploy to the live reuse.coulomb.social hub -- that needs a container rebuild/push/k8s rollout, a separate production-deployment action out of scope here without explicit sign-off. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,23 @@ class HubStore:
|
||||
)
|
||||
"""
|
||||
)
|
||||
# Single-row table tracking the composed federated index's
|
||||
# freshness (REUSE-WP-0019-T02). composed_at is set whenever a
|
||||
# real compose (refresh=True) completes; stale is set by the
|
||||
# Forgejo webhook receiver when a registry/indexes/ change is
|
||||
# pushed, and cleared on the next successful compose.
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS compose_state (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
composed_at TEXT,
|
||||
stale INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO compose_state (id, composed_at, stale) VALUES (1, NULL, 0)"
|
||||
)
|
||||
|
||||
def list_repos(self) -> list[dict[str, Any]]:
|
||||
with self._connect() as conn:
|
||||
@@ -160,4 +177,28 @@ class HubStore:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM registrations ORDER BY repo"
|
||||
).fetchall()
|
||||
return [_row_to_registration(row) for row in rows]
|
||||
return [_row_to_registration(row) for row in rows]
|
||||
|
||||
def record_compose(self) -> str:
|
||||
"""Marks the federated index freshly composed (stale cleared).
|
||||
Returns the recorded timestamp."""
|
||||
now = _utc_now()
|
||||
with self._connect() as conn:
|
||||
conn.execute(
|
||||
"UPDATE compose_state SET composed_at = ?, stale = 0 WHERE id = 1",
|
||||
(now,),
|
||||
)
|
||||
return now
|
||||
|
||||
def mark_stale(self) -> None:
|
||||
with self._connect() as conn:
|
||||
conn.execute("UPDATE compose_state SET stale = 1 WHERE id = 1")
|
||||
|
||||
def get_compose_state(self) -> dict[str, Any]:
|
||||
with self._connect() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT composed_at, stale FROM compose_state WHERE id = 1"
|
||||
).fetchone()
|
||||
if row is None:
|
||||
return {"composed_at": None, "stale": False}
|
||||
return {"composed_at": row["composed_at"], "stale": bool(row["stale"])}
|
||||
Reference in New Issue
Block a user