REUSE-WP-0019-T02: hub recompose staleness tracking + Forgejo webhook
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:
2026-07-07 18:24:55 +02:00
parent 443510276b
commit e0a4de3310
7 changed files with 520 additions and 25 deletions

View File

@@ -113,19 +113,48 @@ Implemented:
```task
id: REUSE-WP-0019-T02
status: todo
status: done
priority: high
state_hub_task_id: "691eb32a-6f20-4a2a-b9ff-0ae427b659aa"
```
- Hub service (`reuse_surface/serve`): `POST /v1/recompose` (token-auth) —
marks index stale and triggers recompose from registered raw URLs
- `POST /v1/webhooks/forgejo`: validates Forgejo webhook signature
(`X-Forgejo-Signature`, secret from env), accepts push events, triggers
recompose only when the pushed commits touch `registry/indexes/`
- Debounce/coalesce concurrent triggers; `GET /v1/federated` gains
`composed_at` + `stale` fields
- Extend `specs/FederationHubAPI.md`; pytest with signed fixture payloads
**Note:** `POST /v1/federated/compose` (token-auth, triggers a real
recompose) already existed from earlier hub work — no separate
`/v1/recompose` route was added; the spec now documents this explicitly
rather than duplicating a route that already does the job.
Implemented in `reuse_surface/hub/`:
- `store.py`: `compose_state` table (`composed_at`, `stale`), with
`record_compose()`/`mark_stale()`/`get_compose_state()`. `composed_at`
updates and `stale` clears only on a *forced* recompose (`refresh=true`,
webhook, or future scheduled fallback) — a plain `GET` still serves
current best-effort data but never silently reports itself as freshly
composed
- `webhooks.py`: `verify_signature` (constant-time HMAC-SHA256, fails
closed on empty secret), `push_touches_registry_index` (path-only
inspection of the push payload's `added`/`modified`/`removed` lists —
never parses file content, per design principle 2)
- `app.py`: `POST /v1/webhooks/forgejo` (accepts both `X-Forgejo-Signature`
and `X-Gitea-Signature`, since repos migrate independently);
`GET /v1/federated` and `POST /v1/federated/compose` now share an
`asyncio.Lock` so concurrent recompose triggers (manual, webhook, future
scheduled) coalesce instead of overlapping
- `specs/FederationHubAPI.md` extended (§5.7-5.9, config table, error
codes)
- 28 new pytest cases (16 in `test_hub.py`, 12 in `test_webhooks.py`); 128
total pass
- **Live-verified**: ran the actual hub service locally
(`reuse-surface serve`), sent a real HMAC-signed webhook payload over
HTTP — confirmed `composed_at`/`stale` transitions, signature rejection,
irrelevant-path no-op, and the full webhook-to-recompose path end to end
**Deployment boundary — deliberately not done:** this changes the hub
*service source code* in this repo; it does **not** deploy to the live
`reuse.coulomb.social` hub. That requires a container rebuild, registry
push, and Kubernetes rollout — a production deployment action affecting a
shared external service, out of scope for this task without explicit
sign-off. See `docs/deploy/reuse-kubernetes.md`.
## Forgejo Webhook Rollout And Scheduled Fallback