Commit 173dceb
Phase 18: Webhook Payload + State-Filter + Coalescing (#51)
* docs(18): capture phase context .planning/phases/18-webhook-payload-state-filter-coalescing/18-CONTEXT.md .planning/phases/18-webhook-payload-state-filter-coalescing/18-DISCUSSION-LOG.md
* docs(state): record phase 18 context session .planning/STATE.md
* docs(18): add validation strategy and research
* docs(18): map patterns from existing codebase analogs
* docs(18): create phase plan for webhook payload + state-filter + coalescing
6 plans across 4 waves implementing WH-01 (config), WH-03 (Standard
Webhooks v1 headers + HMAC-SHA256), WH-06 (edge-triggered streak
coalescing), WH-09 (locked 15-field v1 payload schema). Wave 0 lands
deps + telemetry; waves 1-2 build the schema/encoder/coalesce/dispatcher;
wave 3 wires the bin layer + integration tests + maintainer UAT.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(18): address checker feedback (name-keyed map, D-15 sentinel, UAT recipe-wraps-curl, 16-field, todo!, Connection: close, validation map)
* docs(18): mark process Ds informational; cite D-12/13/14/19 in coalescing/dispatcher plans
* docs(state): record Phase 18 planning completion (6 plans, ready to execute)
* chore(18-01): add Phase 18 webhook deps (reqwest 0.13 rustls + hmac + base64 + ulid + wiremock-dev)
- reqwest = { version = "0.13", default-features = false, features = ["rustls", "json"] }
(Pitfall A: 0.13 renamed `rustls-tls` to `rustls`; CONTEXT D-20 annotation embedded)
- hmac = "0.13", base64 = "0.22", ulid = "1.2" as runtime deps
- wiremock = "0.6" as dev-dependency for HttpDispatcher integration tests
- cargo build clean; `cargo tree -i openssl-sys` returns empty (rustls-only invariant holds)
* feat(18-03): add WebhookPayload struct with deterministic JSON encoding
- src/webhooks/payload.rs: 16-field WebhookPayload<'a> struct (D-06) +
build() constructor; field declaration order == JSON serialization
order; chrono::SecondsFormat::Secs + use_z=true for RFC3339 Z suffix
(Pitfall F); tags: Vec<String> = vec![] until Phase 22 (D-07);
cronduit_version passed in by caller (D-07)
- src/webhooks/coalesce.rs: stub module (Task 2 fills in)
- src/webhooks/mod.rs: pub mod coalesce + pub mod payload + pub use
WebhookPayload re-export
- 9 unit tests cover: payload_version="v1" (D-08), event_type=
"run_finalized" (D-06), all 16 field keys present, deterministic
byte output on repeat (Pitfall B), no newlines in compact JSON
(Pitfall C), declaration order, image_digest/config_hash null when
None, tags=[] empty array, RFC3339 Z suffix, cronduit_version from
env! macro
Mitigates T-18-10 (byte determinism), T-18-11 (timestamp drift),
T-18-12 (compact JSON whitespace).
* feat(18-01): describe + zero-baseline sent/failed webhook counters; add just test-unit recipe
- src/telemetry.rs: describe_counter! + zero-baseline for cronduit_webhook_delivery_sent_total
and cronduit_webhook_delivery_failed_total inside setup_metrics OnceLock init
(Pitfall 3 — families render HELP/TYPE from boot, not first observation)
- tests/metrics_endpoint.rs: extend metrics_families_described_from_boot to assert
HELP + TYPE for both new counters; existing dropped_total assertions preserved
- justfile: add `just test-unit` recipe (cargo test --lib --all-features) under
[group('test')] for the per-task fast feedback loop referenced by 18-VALIDATION.md
* docs(18-01): complete webhook foundation deps + telemetry scaffolding plan
- 5 new crates landed (reqwest 0.13 rustls, hmac 0.13, base64 0.22, ulid 1.2, wiremock 0.6 dev)
- 2 new counter families described + zero-baselined; regression-locked in metrics_endpoint test
- just test-unit recipe for fast per-task feedback
- cargo tree -i openssl-sys empty (rustls-only invariant holds)
Requirements completed: WH-01, WH-03, WH-06, WH-09
* feat(18-03): add filter_position helper with D-15 success sentinel
- src/webhooks/coalesce.rs: filter_position(pool, job_id, current_start,
states) returns the count of consecutive matching runs counting back
from the current run, stopping at the first non-match OR the first
success (D-12, D-15). Mirrors get_failure_context's dual-SQL CTE shape
(PoolRef dispatch, '1970-01-01T00:00:00Z' epoch sentinel).
- D-15 success sentinel hard-coded in CASE expression on BOTH backends:
WHEN status='success' THEN 0 BEFORE the IN-list check, so success is
ALWAYS a streak break even if the operator misconfigures states.
- pad_states_to_6 helper (Pitfall I): repeats the last entry to fill 6
bind placeholders; duplicates collapse harmlessly inside SQL IN(...).
- 4 unit tests (in-process SQLite memory pool):
* filter_position_basic_streak: 3 failures -> 3
* filter_position_stops_at_success: success+2failures -> 2
* filter_position_d13_scenario: failed->timeout, states=[timeout] -> 1
* filter_position_treats_success_as_break_even_when_in_states (D-15
regression): without the SQL sentinel this returns 3; with it, 2
- tests/v12_webhook_filter_position_explain.rs: EXPLAIN regression test
asserting idx_job_runs_job_id_start hit on SQLite (>120 rows + ANALYZE)
and Postgres (#[ignore]-gated testcontainer, 10000 rows + ANALYZE),
mirroring tests/v12_fctx_explain.rs precedent.
Also: corrected the first_break.break_time aggregate from MIN -> MAX
(Rule 1 bug fix on plan-template SQL); MAX correctly identifies the
most-recent non-match before the current run, ensuring count of
matches strictly newer than the most recent break.
Mitigates T-18-13 (SQL injection — bind parameters), T-18-14 (DoS via
slow query — index-backed), T-18-15 (placeholder mismatch — pre-pad),
T-18-37 (operator success-in-states misconfig — sentinel hard-coded).
* docs(18-03): complete webhook payload + filter-position plan
Summary of WH-06 + WH-09 deliverables: 16-field WebhookPayload struct
with byte-stable serde_json output (Pitfalls B/C/F mitigations);
coalesce::filter_position helper with D-15 success sentinel hard-coded
in CASE expression on both SQLite and Postgres backends; EXPLAIN
regression test asserting idx_job_runs_job_id_start hit. 13 unit tests
+ 1 SQLite EXPLAIN test pass; 1 Postgres EXPLAIN test #[ignore]-gated.
One Rule 1 deviation: corrected first_break aggregate from MIN to MAX
on the plan-template SQL.
* feat(18-02): add WebhookConfig struct + JobConfig.webhook + DefaultsConfig.webhook
WH-01 / D-02: introduce per-job and [defaults] webhook configuration.
- WebhookConfig with 5 fields: url, states (default ["failed","timeout"]),
secret (Option<SecretString>, scrubbed), unsigned (bool), fire_every (i64,
default 1)
- JobConfig.webhook + DefaultsConfig.webhook fields (Option<WebhookConfig>)
- default_webhook_states() and default_fire_every() helper fns
- 4 unit tests cover per-job parse, [defaults] parse, default-state vec,
default-fire_every
webhook is intentionally NOT added to DockerJobConfig / serialize_config_json /
compute_config_hash — the 5-layer parity invariant from Phase 17 LBL applies to
docker-execution surface only. The dispatcher (Plan 04) reads webhook config
from a bin-layer Arc<HashMap<i64, WebhookConfig>> built at startup (Plan 05).
Existing JobConfig/DefaultsConfig literal fixtures across the codebase get
mechanical webhook: None additions to keep them compiling.
* feat(18-02): extend apply_defaults with webhook merge (replace-on-collision)
WH-01 / D-01: per-job webhook always wins; defaults.webhook fills only when
job.webhook is None and use_defaults != Some(false).
Mirrors the image/network/volumes/timeout/delete per-field replace pattern,
NOT the labels HashMap union — webhook is a single inline block, so collision
semantics are replace, not field-merge.
NO type-gate (unlike LBL-04): webhooks fire on RunFinalized for every job
type (command/script/docker). The dispatcher reads webhook config from a
bin-layer HashMap; it never enters DockerJobConfig.
4 new tests cover: fill from defaults, use_defaults=false discards, per-job
overrides entirely, command-jobs receive webhook (no type-gate).
* feat(18-02): add check_webhook_url + check_webhook_block_completeness validators
WH-01 / D-04: LOAD-time rejection of every malformed webhook config combination.
- check_webhook_url: rejects unparsable URLs and non-http/https schemes
- check_webhook_block_completeness: 5 independent assertions in one fn
1. states non-empty (rejects `states = []`)
2. every state ∈ VALID_WEBHOOK_STATES (sorted offending list — Pitfall G)
3. secret xor unsigned (both: ambiguous; neither: signing intent unclear)
4. fire_every >= 0 (rejects negative coalescing knob)
5. Pitfall H: secret resolved to empty string (silent HMAC-with-empty-key)
Both wired into run_all_checks per-job loop. VALID_WEBHOOK_STATES const
enumerates the canonical RunFinalized status values from src/scheduler/run.rs.
Error messages name the offending field, the offending value, and the
remediation — Phase 17 LBL precedent. T-18-07 mitigated: no expose_secret()
output in any error message; only the `is_empty()` boolean is referenced.
11 unit tests cover every D-04 branch + Pitfall G ordering + Pitfall H empty
secret + signed/unsigned acceptance paths.
* docs(18): log pre-existing v12_labels_interpolation flaky test as deferred item
* docs(18-02): complete WebhookConfig + apply_defaults merge + validators plan
* fix(18): post-merge integration — relocate config tests to file end
Wave 1 cross-plan defect: 18-02 placed `mod tests {}` at line 213 with
`pub fn parse_and_validate` at line 284. Once worktrees merged, clippy's
`items_after_test_module` lint caught it. Move the tests to the end of
the file (canonical Rust convention) so the lint and `cargo fmt --check`
both pass on the merged tree.
Also runs `cargo fmt` to settle a multi-line formatting diff in
`validate.rs` that surfaced after the merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(18-04): add HttpDispatcher with Standard Webhooks v1 wire format
WH-03 surface: HttpDispatcher implementing WebhookDispatcher trait.
- HttpDispatcher struct owns reqwest::Client (rustls; 10s per-request
timeout per D-18; pool_idle_timeout=90s for keep-alive), DbPool, and
Arc<HashMap<i64, WebhookConfig>> handed in at construction (Plan 05
wire-up consumes).
- sign_v1: HMAC-SHA256 over `${webhook-id}.${webhook-timestamp}.${body}`
with literal `.` separators (D-09); base64 STANDARD alphabet WITH `=`
padding (D-10, Pitfall E).
- should_fire: pure D-16 coalesce decision matrix (0=always, 1=first-of-
stream, N>1=every Nth match where filter_position % N == 1).
- 11-step deliver() flow: webhook lookup → state filter → filter_position
→ coalesce decision → get_failure_context → get_run_by_id → payload
build → serialize ONCE (Pitfall B) → headers → conditional sign →
send same body bytes → metrics + WARN-on-failure (D-17).
- D-05 unsigned mode: omits `webhook-signature` header but still emits
`webhook-id` and `webhook-timestamp`.
- WebhookError extended with HttpStatus / Network / Timeout / InvalidUrl
/ SerializationFailed variants for Phase 20 RetryingDispatcher to
surface distinctly; Phase 18 funnels through DispatchFailed log path.
- 6 unit tests cover sign_v1 fixture, base64 alphabet (200 random
bodies — Pitfall E regression), v1,b64 header value, ULID 26-char
width, 10-digit Unix seconds (Pitfall D regression), and the full
coalesce decision matrix.
- mod.rs re-exports HttpDispatcher.
Rule 3 fixes applied:
- hmac 0.13: `new_from_slice` lives on `KeyInit` trait, NOT on `Mac`.
Plan's `<Hmac<Sha256> as Mac>::new_from_slice` form fails to compile.
Use prelude-resolved `Hmac::<Sha256>::new_from_slice` after importing
`KeyInit` alongside `Mac`.
- queries helper is `get_run_by_id` returning `Option<DbRunDetail>`,
not `get_run_detail`. Call existing helper and surface a clear error
if the row is missing (should be impossible — scheduler just inserted
the finalize row before emitting RunFinalized).
Verification: cargo build clean, 19 webhook tests pass, just openssl-check
confirms rustls invariant holds across native + arm64-musl + amd64-musl
targets, clippy --all-targets --all-features -D warnings is clean.
* docs(18-04): complete HttpDispatcher plan
Summary of Plan 04: HttpDispatcher landed alongside NoopDispatcher with
Standard Webhooks v1 wire format (HMAC-SHA256 sign-once / send-once),
D-16 coalesce decision, D-05 unsigned mode, and a connection-pooled
rustls reqwest::Client.
- 1 task / 1 commit (`47f9cd4`).
- 6 unit tests added (sign_v1 fixture, base64 alphabet, v1,b64 header,
ULID width, 10-digit timestamp, coalesce matrix); 19 webhook tests
green; 256 lib tests green; clippy clean; rustls invariant holds.
- 2 Rule 3 deviations auto-fixed (hmac KeyInit trait routing, missing
get_run_detail → use existing get_run_by_id).
- WH-03 requirement complete.
* fix(18-04): apply cargo fmt to merged dispatcher.rs
Trivial whitespace-only formatting fixes applied by `cargo fmt` after
worktree merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(18-05): wire HttpDispatcher into bin layer with name-keyed lookup
- Build per-job webhook map in src/cli/run.rs by NAME-keyed lookup
(cfg.jobs by name -> sync_result.jobs by name -> DbJob.id), NOT
blind index-aligned zip. Mitigates T-18-36: a future reorder/filter
inside sync_config_to_db cannot silently mis-wire one job's webhook
to another job's id.
- Conditional dispatcher swap: HttpDispatcher when at least one job has
a webhook configured; NoopDispatcher fallback when none — avoids
spinning up a reqwest::Client unnecessarily.
- HttpDispatcher::new errors are surfaced via anyhow::anyhow! so the
surrounding fn's anyhow::Result error-propagation idiom is preserved.
Verified: cargo build clean, cargo clippy --all-targets --all-features
-D warnings clean, all 256 lib unit tests pass.
* feat(18-06): add Phase 18 UAT scaffolding (recipes + mock receiver + config)
- Add 4 new just recipes:
- api-run-now JOB_NAME (cross-cutting curl wrapper for /api/jobs/.../run-now)
- uat-webhook-mock (start mock receiver via cargo example)
- uat-webhook-fire JOB_NAME (delegates to api-run-now; zero raw curl)
- uat-webhook-verify (tail /tmp/cronduit-webhook-mock.log)
- Add examples/webhook_mock_server.rs (~110-line cargo example):
- Listens on 127.0.0.1:9999, logs requests to stdout + /tmp log
- Connection: close response framing for reqwest keep-alive safety
- Header-end + Content-Length aware reader; 1 MiB safety cap
- Extend examples/cronduit.toml with 3 webhook variants:
- wh-example-signed (default state filter, default coalescing)
- wh-example-unsigned (states=[failed,stopped], unsigned=true)
- wh-example-fire-every-zero (states=[timeout], fire_every=0)
- check-config validates the extended toml when WEBHOOK_SECRET is set
* test(18-05): add wiremock e2e + unsigned + state-filter integration tests
Adds 4 wiremock-based integration tests covering the WH-XX wire contract:
- tests/v12_webhook_delivery_e2e.rs::webhook_delivery_e2e_signed
Asserts the 3 Standard Webhooks v1 headers (webhook-id 26-char ULID,
webhook-timestamp 10-digit Unix seconds, webhook-signature v1,<b64>),
the 16-field payload body, and recomputes the HMAC-SHA256 signature
client-side to confirm it matches what the dispatcher sent.
- tests/v12_webhook_delivery_e2e.rs::v12_webhook_two_jobs_distinct_urls
T-18-36 regression: two jobs with different webhook URLs must each
route to their own URL. Locks the name-keyed lookup in src/cli/run.rs
against future drift if sync_config_to_db ever reorders jobs[].
- tests/v12_webhook_unsigned_omits_signature.rs
D-05: unsigned=true with secret=None must omit webhook-signature
header but still emit webhook-id and webhook-timestamp.
- tests/v12_webhook_state_filter_excludes_success.rs
states=["failed"] with a status="success" event MUST NOT fire
(received.is_empty() against a permissive mock).
All four tests pass; clippy on the test crate is clean.
* docs(18-06): author 18-HUMAN-UAT.md (7 maintainer-validated scenarios, all unchecked)
- 7 scenarios cover WH-01/03/06/09 + ${WEBHOOK_SECRET} flow + metrics families:
- S1: signed delivery + 3 headers + 16-field payload
- S2: unsigned delivery omits webhook-signature header
- S3: default coalescing (fire_every = 1) — first-of-streak only
- S4: state filter excludes success
- S5: fire_every = 0 legacy always-fire mode
- S6: ${WEBHOOK_SECRET} env-var interpolation flow (unset/empty/set)
- S7: cronduit_webhook_delivery_{dropped,sent,failed}_total via metrics-check
- All scenarios reference only `just` recipes (per feedback_uat_use_just_commands.md)
- All 7 boxes UNCHECKED — Claude does NOT mark UAT passed (per feedback_uat_user_validates.md)
- Document text awaits maintainer review and approval before scenario runs
* docs(18-06): add 18-06 SUMMARY.md
UAT scaffolding for Phase 18 webhook delivery is committed; UAT itself
awaits maintainer validation per project memory feedback_uat_user_validates.md.
Documents:
- 4 new just recipes (3 uat-webhook-* + 1 api-run-now helper)
- examples/webhook_mock_server.rs (loopback HTTP/1.1 receiver)
- 3 webhook variants in examples/cronduit.toml
- 18-HUMAN-UAT.md with 7 unchecked maintainer-validated scenarios
- 2 deferred issues (Scenario 7 metrics-check gap; pre-existing curl wrappers)
- 2 deviations (checkbox order; use_defaults=false on command jobs)
* test(18-05): add D-17 webhook metric integration tests (3 files)
Adds delta-asserted metric counter tests using the canonical pattern
from tests/v12_webhook_queue_drop.rs (baseline before action, then
assert final - baseline == expected_delta):
- tests/v12_webhook_success_metric.rs
2xx response increments cronduit_webhook_delivery_sent_total by 1;
cronduit_webhook_delivery_failed_total unchanged.
- tests/v12_webhook_failed_metric.rs
5xx response increments cronduit_webhook_delivery_failed_total by 1;
cronduit_webhook_delivery_sent_total unchanged.
- tests/v12_webhook_network_error_metric.rs
Connection refused (URL pointed at 127.0.0.1:1, an unbindable
privileged port) increments cronduit_webhook_delivery_failed_total
by 1. Note: drop(MockServer) was tried first but proved unreliable
on macOS where the wiremock listener can linger past the drop point;
127.0.0.1:1 connection refused is deterministic across macOS / Linux
/ CI runners.
All 3 tests pass; clippy clean on the test binaries.
* docs(18-05): complete bin-layer wire-up + wiremock integration tests plan
Adds 18-05-SUMMARY.md documenting the bin-layer HttpDispatcher wire-up
(name-keyed lookup, NOT blind zip — T-18-36 mitigation), the 4 wiremock
wire-format tests (single-signed e2e + multi-job alignment regression +
unsigned mode + state-filter exclusion), the 3 metric counter tests
(D-17 sent/failed/network-error branches), and the rustls invariant
re-verification. All 7 plan-05 integration tests green; cargo build +
clippy --workspace clean.
* fix(18): post-merge — fmt + clippy collapsible_if on webhook scaffolding
cargo fmt reflowed the `dispatcher` Arc construction in run.rs to a
single-line variant; clippy::collapsible_if rewrote two nested-if
branches in examples/webhook_mock_server.rs to use let-chains.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(18): record VERIFICATION.md — 8/9 PASS, UAT awaiting maintainer
Verifier verdict: PASS for all automated checks (WH-01/03/06/09 wire
contract, bin-layer wiring, 256 unit + 7 wiremock integration tests
green, fmt + clippy + rustls invariant clean). UAT artifacts present
but the 7 scenarios in 18-HUMAN-UAT.md remain unchecked per
feedback_uat_user_validates.md — maintainer flips the boxes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(state): record Phase 18 complete — verifier 8/9 PASS, UAT awaiting
- ROADMAP: Phase 18 [x] (completed 2026-04-29); all 6 plans [x]
- STATE: status=awaiting-uat; 4/10 phases complete; current focus = maintainer UAT
- Resume: maintainer runs 18-HUMAN-UAT.md (7 scenarios), then /gsd-discuss-phase 19
Per project memory feedback_uat_user_validates.md, Phase 18 is NOT
declared shipped — the 7 maintainer-validated checkboxes in
18-HUMAN-UAT.md remain `[ ]` until the maintainer confirms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(18-06): make uat-webhook-fire actually work — CSRF + name→id lookup
The original 18-06 UAT recipe (`api-run-now JOB_NAME`) hit
`/api/jobs/{NAME}/run-now` with a plain `curl -X POST`. Three real bugs
that the maintainer-validation step caught on first run:
1. Wrong path: actual route is `/api/jobs/{id}/run`, not `/run-now`.
2. Wrong key: handler is `Path<i64>` (numeric job id), not name.
3. No CSRF: the handler validates `cronduit_csrf` cookie + `csrf_token`
form body — a plain POST gets 403.
Fix:
- `api-run-now` now takes a numeric `JOB_ID` matching the handler
signature. It primes the CSRF cookie via GET `/api/jobs`, extracts
the `cronduit_csrf` value from curl's Netscape-format cookie jar,
and POSTs with cookie + form-body token.
- New `api-job-id JOB_NAME` recipe wraps `GET /api/jobs | jq` to
resolve the operator's NAME (from cronduit.toml) to the numeric id.
- `uat-webhook-fire JOB_NAME` (the public UAT-callable surface) is
unchanged in shape: it still takes a name, but now composes the two
helpers via a single bash recipe body.
Operator workflow stays identical:
just dev # terminal A
just uat-webhook-mock # terminal B
just uat-webhook-fire wh-example-unsigned # terminal C
Verified statically: route/form-field/cookie names all match the
handler in src/web/handlers/api.rs:run_now and src/web/csrf.rs.
Live verification awaits the next maintainer UAT run.
Requires: `jq` and `curl` (already implicit in the existing UAT recipes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(18): UAT 7/7 validated — flip all maintainer-validated boxes
Maintainer (robert@simplicityguy.com) confirmed all 7 scenarios in
18-HUMAN-UAT.md pass against the running cronduit instance via the
schedule-driven path (waited for the natural cron fires; the
`uat-webhook-fire` recipe was broken at validation time and has since
been fixed in e01a186).
STATE moves from awaiting-uat → ready-for-pr. Next step: open PR for
phase-18-webhook-payload against main, then start Phase 19.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(18): ship phase 18 — PR #51
PR: #51
Branch: phase-18-webhook-payload → main
Commits: 41 ahead, 47 files changed (+11876/-34)
Verification: 9/9 PASS (8/9 automated + maintainer UAT 7/7)
STATE: ready-for-pr → shipped-pending-merge
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(18-06): unbreak compose-smoke — comment out signed webhook examples
The Phase 18 plan-06 added 3 webhook examples to examples/cronduit.toml.
Two of them (wh-example-signed and wh-example-fire-every-zero) reference
`${WEBHOOK_SECRET}`, which the LOAD validator rejects when unset/empty
(Pitfall H). The compose-smoke CI workflow boots the example config
inside docker without setting the env var, so the container exits before
/health responds — both `quickstart compose smoke (docker-compose.yml)`
and `(docker-compose.secure.yml)` jobs failed on PR #51.
Fix: leave wh-example-unsigned active (no env var needed; exercises the
full Phase 18 dispatcher minus the signature header) and convert the
two signed examples into commented-out templates with a clear "set
WEBHOOK_SECRET first, then uncomment" instruction.
Coverage trade-off: CI smoke still exercises the dispatcher, payload,
state-filter, and coalescing through the unsigned path. The signed
templates remain in-file as documentation; UAT scenarios 1/3/5/6 add a
prerequisite to uncomment them before `just dev`.
Verified locally: `just check-config examples/cronduit.toml` exits 0
with WEBHOOK_SECRET unset (the failure mode the CI was hitting).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 9fd85f0 commit 173dceb
47 files changed
Lines changed: 11893 additions & 34 deletions
File tree
- .planning
- phases/18-webhook-payload-state-filter-coalescing
- examples
- src
- cli
- config
- scheduler
- webhooks
- tests
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
47 | | - | |
| 47 | + | |
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
| |||
145 | 145 | | |
146 | 146 | | |
147 | 147 | | |
148 | | - | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
149 | 157 | | |
150 | 158 | | |
151 | 159 | | |
| |||
263 | 271 | | |
264 | 272 | | |
265 | 273 | | |
266 | | - | |
| 274 | + | |
267 | 275 | | |
268 | 276 | | |
269 | 277 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
6 | | - | |
7 | | - | |
8 | | - | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
12 | | - | |
13 | | - | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
31 | | - | |
32 | | - | |
33 | | - | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
34 | 34 | | |
35 | | - | |
| 35 | + | |
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| |||
173 | 173 | | |
174 | 174 | | |
175 | 175 | | |
176 | | - | |
177 | | - | |
178 | | - | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
179 | 179 | | |
180 | | - | |
| 180 | + | |
0 commit comments