Skip to content

feat(store): durable webhook deliveries with X-GitHub-Delivery idempotency (#2 phase 1)#44

Merged
BunsDev merged 1 commit into
mainfrom
feat/issue-2-store-phase1
Jul 7, 2026
Merged

feat(store): durable webhook deliveries with X-GitHub-Delivery idempotency (#2 phase 1)#44
BunsDev merged 1 commit into
mainfrom
feat/issue-2-store-phase1

Conversation

@BunsDev

@BunsDev BunsDev commented Jul 7, 2026

Copy link
Copy Markdown
Member

Phase 1 of the durable task store (design, merged in #43) — refs #2.

What lands

New crates/store — embedded SQLite via rusqlite (bundled, WAL, busy_timeout, foreign keys, forward-only migrations tracked by PRAGMA user_version). One writer connection behind a mutex; every async entry point hops through spawn_blocking. Schema: webhook_deliveries, tasks, task_attempts exactly as designed.

Persist-then-ack webhook handling:

  • X-GitHub-Delivery is now required — 400 without it (it's the idempotency key; a caller without one is not GitHub)
  • The delivery record + routing outcome + task row are written in one transaction before the 200 — GitHub only hears success once durable state exists
  • Redelivered ids: 200 {"ok":true,"duplicate":true}, nothing dispatched, original routing stands
  • Routed PR reviews tombstone older still-queued reviews of the same PR (supersede_key) in the same transaction
  • Ping / unroutable deliveries recorded as ignored:<reason>
  • Store unavailable → 500, so GitHub retries instead of treating unheld work as delivered

Operational surface: [storage] path config (+ doctor checks: error if the path is a directory, warning pointing at the volume that must persist), compose coven-data volume, example config section.

Phase discipline

Worker dispatch still rides the in-process channel in this PR, exactly as the design's phase 1 prescribes. Durable claims, restart recovery, and retiring the in-memory TaskStore are phase 2.

Verification

  • 8 new tests: store unit tests (reopen/migration idempotence, duplicate suppression, ignored routing, supersession tombstones, future-schema safety) + route-level proofs (missing delivery id → 400 with nothing persisted; redelivery → one durable row, one dispatch; ping/unroutable recorded) — 143 total, all green
  • cargo check / clippy -D warnings / test --all / python gates green
  • Demo extended with Act 1b: the same X-GitHub-Delivery id redelivered after the full loop → zero additional GitHub API calls, no second Check Run/session/PR — 21/21 assertions green through the real binary
  • scripts/smoke-webhook.sh gains the missing-delivery-id 400 check

…tency (#2 phase 1)

New crates/store crate: embedded SQLite (rusqlite, WAL, user_version
migrations) holding webhook_deliveries, tasks, and task_attempts per
docs/durable-task-store.md. The webhook route now records every delivery —
and its routing outcome — atomically BEFORE GitHub hears success:

- X-GitHub-Delivery is required (400 without it); it is the idempotency key
- redelivered ids answer 200 {duplicate:true} and never dispatch twice
- routed tasks get a durable queued row (with superseded tombstones for
  older queued reviews of the same PR); ping/unroutable deliveries are
  recorded as ignored:<reason>
- a store failure answers 500 so GitHub retries instead of losing work

[storage] config with doctor checks; compose gains a data volume; the demo
gains an Act 1b proving redelivery dedup end to end (21 assertions); the
smoke script covers the missing-delivery-id 400.

Worker dispatch still rides the in-process channel in this phase; durable
claims, restart recovery, and retirement of the in-memory task store land
in phase 2.

Signed-off-by: Val Alexander <bunsthedev@gmail.com>
Copilot AI review requested due to automatic review settings July 7, 2026 03:55
@BunsDev BunsDev merged commit 8d32624 into main Jul 7, 2026
2 checks passed
@BunsDev BunsDev deleted the feat/issue-2-store-phase1 branch July 7, 2026 03:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements phase 1 of issue #2’s durable-task-store design by introducing a new SQLite-backed store and wiring webhook handling to persist + deduplicate deliveries by X-GitHub-Delivery before responding success.

Changes:

  • Add crates/store (rusqlite + WAL + migrations via PRAGMA user_version) to persist webhook deliveries and routed tasks.
  • Update webhook route to require X-GitHub-Delivery, record routing outcomes durably, and short-circuit redeliveries as duplicates.
  • Add [storage] path config surface + doctor diagnostics, and update demo/compose/smoke scripts + docs to reflect the new durable store.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
scripts/smoke-webhook.sh Extends smoke script to assert 400 on missing X-GitHub-Delivery.
README.md Updates feature matrix to mark durable queue/store as Partial with phase-1 semantics.
examples/demo/run-demo.sh Adds durable store config and demonstrates webhook redelivery dedup in the demo loop.
docs/durable-task-store.md Marks the design as accepted and notes phase 1 implemented.
docs/demo.md Documents the new demo “Act 1b” redelivery assertion.
crates/worker/src/lib.rs Updates test config builders to include default storage config.
crates/webhook/src/routes.rs Requires X-GitHub-Delivery; persists deliveries/tasks; adds idempotency tests.
crates/webhook/Cargo.toml Adds dependency on coven-github-store.
crates/store/src/lib.rs New durable SQLite store implementation + migrations + store unit tests.
crates/store/Cargo.toml New crate manifest for the store.
crates/server/src/main.rs Opens the durable store at startup and injects it into webhook state.
crates/server/Cargo.toml Adds dependency on coven-github-store.
crates/config/src/lib.rs Adds [storage] config + default path + doctor diagnostics.
config/example.toml Documents the new [storage] configuration section.
compose.yaml Adds a named volume for durable /data storage.
Cargo.toml Adds the new crate to the workspace and adds workspace rusqlite dependency.
Cargo.lock Locks new transitive dependencies for rusqlite/store crate.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +181 to +185
if state.task_tx.try_send(task).is_err() {
warn!(task_id, "task queue full — dropping task");
} else {
info!(task_id, "task enqueued");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants