diff --git a/.planning/MILESTONES.md b/.planning/MILESTONES.md index 561e56b3..9acfde36 100644 --- a/.planning/MILESTONES.md +++ b/.planning/MILESTONES.md @@ -1,5 +1,33 @@ # Milestones +## v1.2 — Operator Integration & Insight (Shipped: 2026-05-19) + +**Tags:** `v1.2.0-rc.1`, `v1.2.0-rc.2`, `v1.2.0-rc.3`, `v1.2.0-rc.4`, `v1.2.0-rc.5`, `v1.2.0`, `v1.2.1` · **Phases:** 10 (15–24) · **Plans:** 78 · **Tasks:** 158 · **Requirements shipped:** 41 / 41 v1.2 + +**Delivered:** The milestone that makes cronduit a participant in the operator's broader infrastructure — pushing state outward via webhooks, exposing richer failure context inward, and letting operators organize and integrate via tags and Docker labels. Operators can now configure per-job outbound webhooks (Standard Webhooks v1 payloads, HMAC-SHA256 signing, state-filter + edge-triggered streak coalescing, SSRF/HTTPS posture, 3-attempt retry + dead-letter queue, graceful-shutdown drain, and a Prometheus metric family), read a rich failure-context panel on the run-detail page (5 P1 signals), scan a per-job exit-code histogram on the job-detail page (10 buckets, stopped distinct from signal-killed), tag jobs in TOML and filter the dashboard with CSS-only AND-semantics chips that carry into shareable URLs, and attach arbitrary Docker labels to spawned containers (per-job-wins merge, reserved `cronduit.*` namespace). It also fixes the load-bearing v1.1 `job_runs.container_id` regression and promotes `cargo-deny` to a blocking CI gate. Released iteratively `v1.2.0-rc.1` → `v1.2.0-rc.5`, then the last UAT-passing rc SHA was retagged `v1.2.0` ("what was tested is what ships"); `:latest` promoted to `1.2.0`. + +### Key accomplishments + +1. **Outbound webhooks (Phases 15, 18, 19, 20)** — `src/webhooks/` worker (bounded mpsc, drop-on-full scheduler-survival contract) delivering byte-stable Standard Webhooks v1 payloads with sign-once HMAC-SHA256; per-job state-filter + edge-triggered streak coalescing; LOAD-time SSRF/HTTPS-required validator; 3-attempt retry chain honoring `Retry-After` with a `webhook_deliveries` dead-letter queue; SIGTERM drain budget; `cronduit_webhook_*` metric family eager-zeroed at boot; stdlib reference receivers (Python/Go/Node) using constant-time compare, locked to a shared interop fixture + hard CI matrix. +2. **Failure-context panel on run detail (Phases 16, 21)** — single-query CTE (`get_failure_context`) returning streak + last-success metadata in one round-trip; run-detail panel rendering 5 P1 signals (timestamp, image digest, config hash, duration-vs-p50 delta, scheduler fire-skew), gated to failed/timeout runs, with an index-plan EXPLAIN regression lock. +3. **Per-job exit-code histogram (Phase 21)** — pure-Rust 10-variant `ExitBucket` classifier (status-discriminator-wins so `stopped` is distinct from signal-killed 128–143) over the last 100 runs, with a top-3-codes summary and success-rate badge that excludes `stopped`. +4. **Job tagging + dashboard filter chips (Phases 22, 23)** — normalized tags in TOML persisted to a `jobs.tags` JSON column with strict-charset + reserved-name + substring-collision validators; CSS-only filter chips with AND semantics across tags, untagged-hidden when filtering, alphabetical order, and shareable/bookmarkable `?tag=` URL state; tags flow into the webhook payload (closing WH-09). +5. **Custom Docker labels — SEED-001 (Phase 17)** — operator labels reach spawned containers' `ContainerCreateBody.labels` with `[defaults]` + per-job `per-job-wins` merge, a reserved `cronduit.*` namespace, type-gated + size + strict-ASCII-key validators, and env-var interpolation on values; established the project's first realized-seed close-out pattern. +6. **Foundation fix + supply-chain gate (Phases 16, 20, 24)** — fixed the v1.1 `run.rs` regression so `job_runs.container_id` records the real container ID (not the image digest) (FOUND-14); added per-run `image_digest` + `config_hash` columns; promoted `cargo-deny` to a blocking CI gate with a one-time license-allowlist remediation; authored TM5/TM6 threat-model sections and the v1.2 close-out audit. + +### Validated milestone gates + +- **Requirements coverage:** 41/41 Complete across 6 categories (FOUND-14..16, WH-01..11, LBL-01..06, FCTX-01..07, EXIT-01..06, TAG-01..08). Full REQ-ID → phase traceability in `.planning/milestones/v1.2-REQUIREMENTS.md`. +- **Milestone audit:** `.planning/milestones/v1.2-MILESTONE-AUDIT.md` — verdict **passed** (requirements coverage, cross-phase integration, E2E flows, Nyquist compliance). +- **Threat model:** `THREAT_MODEL.md` gains Threat Model 5 (Webhook Outbound / SSRF) and Threat Model 6 (operator-supplied Docker labels), plus STRIDE rows T-S3 / T-T4 / T-I4 / T-D4. +- **UAT:** 10 phase HUMAN-UAT runbooks maintainer-validated (every step `just`-recipe-driven); full v1.2 regression + new-features UAT signed off against the rc that shipped as `v1.2.0` (2026-05-19). + +### Patch releases + +- **v1.2.1 (2026-05-19):** webhook-URL credential scrubbing — `strip_url_credentials` userinfo stripping at every webhook-URL sink (closes THREAT_MODEL T-I4) + maintenance. + +--- + ## v1.1 — Operator Quality of Life (Shipped: 2026-04-23) **Tags:** `v1.1.0-rc.1`, `v1.1.0-rc.2`, `v1.1.0-rc.3`, `v1.1.0-rc.4`, `v1.1.0-rc.5`, `v1.1.0-rc.6`, `v1.1.0` · **Phases:** 6 (10, 11, 12, 12.1, 13, 14) · **Plans:** 52 · **Requirements shipped:** 33 / 33 v1.1 diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md index 1aa8c8a9..d3a29dde 100644 --- a/.planning/PROJECT.md +++ b/.planning/PROJECT.md @@ -2,11 +2,11 @@ ## Current State -**Shipped:** `v1.1.0` on 2026-04-23 — Operator Quality of Life polish milestone on top of the v1.0.1 codebase. Six phases (10, 11, 12, 12.1 inserted, 13, 14), 52 plans, 33/33 v1.1 requirements Complete. Adds a new `stopped` status with a per-run stop button wired through all three executors, per-job run numbers (`#1, #2, …`) backfilled via an idempotent three-file migration, zero-gap log backfill on navigate-back with id-based SSE dedupe, a new `/timeline` gantt page, dashboard sparklines + success-rate badges, job-detail p50/p95 duration trends, a CSRF-gated bulk enable/disable UX backed by a tri-state `jobs.enabled_override` column, a working out-of-the-box `docker compose up` healthcheck via a new `cronduit health` CLI + Dockerfile HEALTHCHECK, and a locked six-tag GHCR contract (`:X.Y.Z`, `:X.Y`, `:X`, `:latest`, `:rc`, `:main`). `:latest` promoted from `:1.0.1` to `:1.1.0` on both archs at final tag. No net-new external dependencies (one `rand 0.8 → 0.9` hygiene bump); one new nullable DB column. See [`MILESTONES.md`](../MILESTONES.md) and [`.planning/MILESTONES.md`](MILESTONES.md) for full history. +**Shipped:** `v1.2.0` on 2026-05-19 (+ `v1.2.1` patch 2026-05-19) — Operator Integration & Insight. Ten phases (15–24), 78 plans, 41/41 v1.2 requirements Validated, audit verdict `passed`. Makes cronduit a participant in the operator's broader infrastructure: outbound webhooks (Standard Webhooks v1 payloads + HMAC-SHA256 signing + per-job state-filter + edge-triggered streak coalescing + SSRF/HTTPS posture + 3-attempt retry/dead-letter queue + graceful-shutdown drain + a `cronduit_webhook_*` Prometheus family, with stdlib reference receivers in Python/Go/Node), a collapsed-by-default failure-context panel on run detail (5 P1 signals via a single-query CTE), a per-job exit-code histogram card (10 buckets, `stopped` distinct from signal-killed), job tagging with CSS-only AND-semantics dashboard filter chips + shareable `?tag=` URL state, and custom Docker labels on spawned containers (SEED-001; `[defaults]` + per-job-wins merge, reserved `cronduit.*` namespace, type-gated validators). Also fixes the load-bearing v1.1 `job_runs.container_id` regression and promotes `cargo-deny` to a blocking CI gate. `:latest` promoted from `:1.1.0` to `:1.2.0` on both archs. Four net-new runtime deps (`reqwest`, `hmac`, `base64`, `ulid`); two new nullable `job_runs` columns + a `jobs.tags` JSON column + a `webhook_deliveries` dead-letter table. The `v1.2.1` patch adds webhook-URL credential scrubbing (THREAT_MODEL T-I4). See [`MILESTONES.md`](../MILESTONES.md) and [`.planning/MILESTONES.md`](MILESTONES.md) for full history. -**Prior:** `v1.0.0` (2026-04-14) + `v1.0.1` patch (2026-04-14) — single-binary Rust cron scheduler with terminal-green HTMX web UI, full Docker-API job execution including `--network container:`, `@random` schedule resolver, hot config reload, Prometheus metrics, SSE log tail, multi-arch (amd64+arm64) GHCR release, and a documented threat model. 86/86 v1 requirements complete; audit verdict `passed`. +**Prior:** `v1.1.0` (2026-04-23) — Operator Quality of Life: stop-a-running-job, per-job run numbers, log-UX fixes, `/timeline` page, sparklines + success-rate badges, p50/p95 duration trends, CSRF-gated bulk enable/disable, out-of-the-box `docker compose up` healthcheck, six-tag GHCR contract. `v1.0.0`/`v1.0.1` (2026-04-14) — the initial single-binary Docker-native scheduler (terminal-green HTMX UI, full Docker-API execution incl. `--network container:`, `@random`, hot reload, Prometheus, SSE log tail, multi-arch GHCR, documented threat model). -**Next milestone:** v1.2 — Operator Integration & Insight (in progress; kicked off 2026-04-25). Goal: make cronduit a participant in the operator's broader infrastructure — push notifications outward via webhooks, expose richer failure context inward, and let operators organize and integrate via tags and Docker labels. Five features in scope: webhook notifications, failure context on run detail, per-job exit-code histogram, job tagging/grouping, custom Docker labels (SEED-001). Cross-run log search and job concurrency/queuing punted to v1.3. +**Next milestone:** v1.3 — not yet kicked off. Candidate scope (deferred from v1.2): cross-run log search across the retention window (engine choice — naive LIKE vs SQLite FTS5 / Postgres tsvector — left for usage-data-driven decision) and job concurrency limits + queuing (deep scheduler-core change). Run `/gsd-new-milestone` to define it. ## What This Is @@ -16,7 +16,8 @@ Cronduit is a self-hosted cron job scheduler with a web UI, built for Docker-nat **One tool that both runs recurrent jobs reliably AND makes their state observable through a web UI.** If everything else is cut, the scheduler must (1) execute jobs on time with full Docker networking support (especially `--network container:` for VPN setups) and (2) let the operator see pass/fail, logs, and timing from a browser. -## Current Milestone: v1.2 — Operator Integration & Insight +
+✅ Shipped milestone: v1.2 — Operator Integration & Insight (2026-05-19) **Goal:** Make cronduit a participant in the operator's broader infrastructure — push notifications outward via webhooks, expose richer failure context inward, and let operators organize and integrate their fleet via tags and Docker labels. @@ -42,6 +43,8 @@ Cronduit is a self-hosted cron job scheduler with a web UI, built for Docker-nat - Cross-run log search across retention window — design ambiguity around naive LIKE vs SQLite FTS5 / Postgres tsvector engine choice; let v1.2 ship and observe usage data first. - Job concurrency limits and queuing — deep scheduler-core change (`tokio::select!` loop + persistence + fairness); too risky to bundle with v1.2's expand-shape work. Already on the v1.3 candidate list. +
+ ## Requirements ### Validated @@ -137,22 +140,21 @@ Cronduit is a self-hosted cron job scheduler with a web UI, built for Docker-nat - ✓ `rand` crate bumped from `0.8` to `0.9` across all call sites (`@random` slot picker, CSRF token gen) — v1.1 (Phase 10, FOUND-12) - ✓ `Cargo.toml` version bumped from `1.0.1` to `1.1.0` on the first v1.1 commit; rc tags use semver pre-release format (`v1.1.0-rc.1` etc.) — v1.1 (Phase 10, FOUND-13) +**v1.2 — Operator Integration & Insight** (41/41 Validated; full REQ-ID traceability in `.planning/milestones/v1.2-REQUIREMENTS.md`) +- ✓ Outbound webhooks on terminal job states — per-job URL + state-filter list, edge-triggered streak coalescing (`fire_every`), `[defaults]` + per-job override with `use_defaults = false` disable, Standard Webhooks v1 payload — v1.2 (Phases 15/18, WH-01..03, WH-06, WH-09) +- ✓ Webhook HMAC-SHA256 signing (`v1,` over `id.timestamp.payload`) + stdlib Python/Go/Node reference receivers with constant-time compare — v1.2 (Phase 19, WH-04) +- ✓ Webhook SSRF/HTTPS posture, 3-attempt full-jitter retry + `webhook_deliveries` dead-letter queue, SIGTERM drain budget, `cronduit_webhook_*` metric family — v1.2 (Phase 20, WH-05, WH-07, WH-08, WH-10, WH-11) +- ✓ Custom Docker labels on spawned containers (SEED-001) — `[defaults]` + per-job-wins merge, reserved `cronduit.*` namespace, type-gated + size + ASCII-key validators, value interpolation — v1.2 (Phase 17, LBL-01..06) +- ✓ Failure-context panel on run detail — 5 P1 signals (time deltas/streak/last-success, image-digest delta, config-hash delta, duration-vs-p50, scheduler fire-skew) via a single-query CTE; per-run `image_digest` + `config_hash` columns + run.rs:301 fix — v1.2 (Phases 16/21, FOUND-14, FCTX-01..07) +- ✓ Per-job exit-code histogram card — 10-bucket classifier (`stopped` distinct from signal-killed), last-100 runs, top-3 codes — v1.2 (Phase 21, EXIT-01..06) +- ✓ Job tagging + dashboard filter chips — `jobs.tags` JSON column with charset/reserved/substring-collision validators; CSS-only AND-semantics chips, untagged-hidden, shareable `?tag=` URL state; tags in webhook payload — v1.2 (Phases 22/23, TAG-01..08) +- ✓ `cargo-deny` promoted to a blocking CI gate; `Cargo.toml` bumped to `1.2.0` on the first v1.2 commit — v1.2 (Phases 15/24, FOUND-15, FOUND-16) + ### Active -**v1.2 — Operator Integration & Insight** (see `REQUIREMENTS.md` for the full testable list with REQ-IDs once generated) - -*Outbound integration* -- [ ] Webhook notifications on job state transitions — per-job URL + state-filter list; HMAC signing; 3-attempt exponential backoff retry; `[defaults]` fallback with per-job override and `use_defaults = false` disable -- [ ] Custom Docker labels on spawned containers (SEED-001) — `labels` map in `[defaults]` and per `[[jobs]]`; merge semantics + `cronduit.*` reserved namespace + type-gating locked at seed time - -*Insight on existing runs* -- [ ] Failure context on run detail — time-based deltas (first-failure timestamp, streak, last-success link) + image-digest delta + config-hash delta; new `job_runs.image_digest` column with backfill -- [ ] Per-job exit-code histogram on job detail page — new card showing distribution over the last N runs - -*Organization* -- [ ] Job tagging / grouping — `tags = ["backup", "weekly"]`; UI-only filter chips on dashboard; does NOT affect webhooks, search, or metrics labels +_No active milestone. v1.2 shipped 2026-05-19 — run `/gsd-new-milestone` to define v1.3 scope (cross-run log search + job concurrency/queuing are the leading candidates)._ ### Future Requirements @@ -176,7 +178,7 @@ Cronduit is a self-hosted cron job scheduler with a web UI, built for Docker-nat - **Multi-node / distributed scheduling** — single-node only. Distribution is a different product. - **User management / RBAC** — single-operator tool; no user accounts in v1 or v2. - **Workflow DAGs / job dependencies** — no "run B after A succeeds". Jobs are independent. -- **Email notifications** — post-v1 add-on; can layer on top of the metrics/log outputs. Webhook notifications have been promoted to Future Requirements (v1.2); email notifications remain out of scope entirely (operators can wire a webhook → email bridge if they want it). +- **Email notifications** — post-v1 add-on; can layer on top of the metrics/log outputs. Webhook notifications shipped in v1.2; email notifications remain out of scope entirely (operators can wire a webhook → email bridge if they want it). - **Ad-hoc one-shot runs not defined in the config** — config remains the single source of truth for what runs. Adding a UI form that accepts arbitrary commands/images would create a blast-radius surface that pairs poorly with v1's unauthenticated posture. - **Importer for existing ofelia configs** — users rewrite their schedules in Cronduit's TOML by hand. Not worth the translation surface area. - **SPA / React frontend** — server-rendered HTML only. Keeps the single-binary story and matches the terminal aesthetic. @@ -185,6 +187,14 @@ Cronduit is a self-hosted cron job scheduler with a web UI, built for Docker-nat **Who this is for.** First user is Robert's own homelab. v1.0 ships as public OSS at `github.com/SimplicityGuy/cronduit` — the tool is intended for outside adopters from day one, with docs and quality bar to match. +**Codebase state at v1.2.0 (2026-05-19).** +- New `src/webhooks/` module (dispatcher trait + Noop/Http/Retrying dispatchers + bounded-mpsc worker) plus failure-context CTE, exit-code classifier, and tag plumbing across config/db/web layers. +- Four net-new runtime crates: `reqwest 0.13` (rustls), `hmac 0.13`, `base64 0.22`, `ulid 1.2` (+ `wiremock 0.6` dev-dep). `cargo tree -i openssl-sys` still empty (rustls invariant held). +- Schema: `job_runs.image_digest` + `job_runs.config_hash` (nullable, backfilled), `jobs.tags` JSON column, `webhook_deliveries` dead-letter table — all dual-backend migration pairs. +- 78 plans across 10 phases (15–24); `cargo-deny` now a blocking CI gate; `webhook-interop` CI matrix (Python/Go/Node) added. +- Reference webhook receivers shipped under `examples/webhook-receivers/{python,go,node}/`; operator hub at `docs/WEBHOOKS.md`; `THREAT_MODEL.md` gains TM5 (Webhook Outbound) + TM6 (Docker labels). +- Release artifacts at v1.2.0: multi-arch image at `ghcr.io/SimplicityGuy/cronduit:1.2.0`; `:latest` promoted from `:1.1.0` to `:1.2.0`; `v1.2.1` patch follows same day. + **Codebase state at v1.1.0 (2026-04-23).** - ~14,500 lines of Rust in `src/` + expanded integration test suite (new `tests/stop_executors.rs`, `tests/process_group_kill.rs`, `tests/metrics_stopped.rs`, `tests/v11_bulk_toggle.rs` + `_pg.rs`, `tests/v13_timeline_explain.rs`, `tests/v13_timeline_timezone.rs`, `tests/dashboard_jobs_pg.rs` among others) - Edition 2024, rust-version 1.94.1 @@ -221,6 +231,13 @@ Cronduit collapses those into one tool: define jobs in a config file, get a dash - UAT-driven rc-loop (rc.3 → rc.6 on Phase 14) catches real operator-visible bugs (dashboard reflection, timeline bar CSS, `just` recipes, self-polling partials) that unit + integration tests missed. Worth the four extra rc cuts. - Maintainer-action tag cuts (D-13) scale cleanly — rc.1 through v1.1.0 all landed without the maintainer fighting the workflow. +**Validated v1.2 hypotheses.** +- The expand-shape milestone (net-new outbound surface) landed additively on the v1.1 codebase with no scheduler-core refactor — the bounded-mpsc + `try_send` drop-on-full webhook worker keeps the scheduler loop isolated from receiver latency/backpressure (WH-02 scheduler-survival contract held). +- A single-query CTE (`get_failure_context`) is the right shape for the failure panel — five signals in one indexed round-trip, EXPLAIN-locked on both backends, beats five separate queries. +- Standard Webhooks v1 + sign-once HMAC-SHA256 with stdlib-only reference receivers (no SDK) keeps the integration surface auditable and the interop fixture catches wire-format drift in CI before language receivers run. +- CSS-only filter chips (no JS beyond HTMX) deliver AND-semantics + shareable URL state within the no-SPA constraint; the substring-collision validator is required to keep the `tags LIKE` filter false-positive-free. +- The iterative rc loop (rc.1 → rc.5, retag last-passing SHA as `v1.2.0`) again caught operator-visible issues UAT-side; "what was tested is what ships" (bit-identical retag) held. + ## Constraints - **Tech stack (locked)**: Rust backend using `bollard` for the Docker API. No CLI shelling out. No alternative languages. @@ -286,6 +303,14 @@ Cronduit collapses those into one tool: define jobs in a config file, get a dash | UAT-driven rc loop (rc.3 → rc.6 on Phase 14) | Each UAT pass surfaces real operator-visible bugs that unit/integration tests missed; fixes land in-cycle, not on main | ✓ Settled (v1.1, Phase 14) — four fix PRs (#39, #40, #41) before `v1.1.0` tag | | `mark_run_orphaned` `WHERE status = 'running'` guard locked in by test | Research Correction #4 — without the guard, restart would overwrite `stopped`/`success`/`failed`/`timeout` rows | ✓ Settled (v1.1, Phase 10) — SCHED-13 | | Tailwind v3 → v4 migration landed at start of v1.1 | Dep refresh + Tailwind upgrade as a single PR avoided mixed-state churn later in the milestone | ✓ Settled (v1.1, PR #26) | +| Bounded `mpsc(1024)` webhook worker with `try_send` drop-on-full (not `.send().await`) | The scheduler loop must never block on receiver latency; dropping past the bound + a counter preserves scheduler survival | ✓ Settled (v1.2, Phase 15) — WH-02 locked at the test boundary | +| Standard Webhooks v1 wire format + SHA-256-only HMAC (no algorithm agility / cronduit-side rotation) | Interop with an established spec; rotation is a receiver concern; smaller surface | ✓ Settled (v1.2, Phases 18/19) — 7-file interop fixture + CI matrix | +| Webhook SSRF = accepted documented risk (no destination allow/block-list), HTTPS required for non-local | Operator-with-UI-access is already trusted in the v1 posture; loopback default + HTTPS validator + reverse-proxy fronting are the mitigations | ✓ Settled (v1.2, Phase 20) — THREAT_MODEL TM5; allow-list deferred to v1.3 | +| Failure context via a single-query CTE (`get_failure_context`) | One indexed round-trip for 5 signals beats five queries; EXPLAIN-locked on both backends | ✓ Settled (v1.2, Phases 16/21) — FCTX-07 | +| Exit-code histogram: status-discriminator-wins classifier (`stopped` distinct from signal-killed 128–143) | Operators must tell an operator-stopped run apart from a SIGTERM-killed one even though both exit 137 | ✓ Settled (v1.2, Phase 21) — EXIT-04 | +| Job tags are UI/payload-only (NOT metrics labels); substring-collision rejected at config-load | Avoids unbounded Prometheus cardinality; keeps the `tags LIKE` filter false-positive-free | ✓ Settled (v1.2, Phases 22/23) — TAG-05 | +| Docker labels: `[defaults]` + per-job-wins merge, reserved `cronduit.*` namespace, docker-type-gated | Interop (Traefik/Watchtower) without clobbering cronduit's own labels; first realized-seed (SEED-001) close-out pattern | ✓ Settled (v1.2, Phase 17) — LBL-01..06 | +| `cargo-deny` promoted from warn to a blocking CI gate before the final v1.2.0 tag | Supply-chain hygiene for an OSS release; one-time license-allowlist remediation cleared pre-existing deps | ✓ Settled (v1.2, Phase 24) — FOUND-16 | ## Evolution @@ -305,4 +330,11 @@ This document evolves at phase transitions and milestone boundaries. 4. Update Context with current state --- -*Last updated: 2026-04-28 — Phase 16 (Failure-Context Schema + run.rs:277 Bug Fix) complete: load-bearing v1.1 bug at `src/scheduler/run.rs:301` fixed (`container_id_for_finalize` now reads `docker_result.container_id` instead of `image_digest`); `DockerExecResult` carries both `container_id` and `image_digest` fields; `job_runs.image_digest` and `job_runs.config_hash` columns added on both backends with best-effort backfill; `finalize_run` widened to 8 args; `insert_running_run` widened to 4 args; `get_failure_context(pool, job_id)` single-query helper landed using D-05 CTE shape (two CTEs joined `LEFT JOIN ON 1=1`, epoch sentinel `1970-01-01T00:00:00Z`); EXPLAIN-plan tests assert `idx_job_runs_job_id_start` is hit on both backends. FOUND-14, FCTX-04, FCTX-07 validated (HUMAN-UAT spot check 2026-04-28: real container IDs in `job_runs.container_id`, no `sha256:` prefix). Code review WR-02 (regression-test coverage for the bug fix is `#[ignore]`-gated — needs Docker daemon) and a just-recipe DB-path mismatch (`uat-fctx-bugfix-spot-check` targets `cronduit.dev.db` but daemon writes to `cronduit.db`) deferred as non-blocking follow-ups. Next: Phase 17 (Custom Docker Labels — SEED-001). Previous: 2026-04-26 — Phase 15 (Foundation Preamble) complete: workspace bumped to `1.2.0`, `cargo-deny` CI preamble landed warn-only, webhook delivery worker scaffolding wired into `SchedulerLoop` with bounded `mpsc(1024)` + `try_send` non-blocking emit at `finalize_run` step 7d + `cronduit_webhook_delivery_dropped_total` counter. WH-02 scheduler-survival contract locked at the test boundary. FOUND-15, FOUND-16, WH-02 validated. 2026-04-25 — v1.2 milestone "Operator Integration & Insight" kicked off. Five features: webhooks (override pattern), failure context on run detail, per-job exit-code histogram, job tagging (UI-only), custom Docker labels (SEED-001). 2026-04-24 — v1.1 milestone closed.* +*Last updated: 2026-05-29 after the v1.2 — Operator Integration & Insight milestone. v1.2.0 shipped 2026-05-19 (+ v1.2.1 patch): outbound webhooks (Standard Webhooks v1 + HMAC + SSRF/HTTPS posture + retry/DLQ + drain + metrics + Python/Go/Node receivers), failure-context panel + exit-code histogram, job tagging + dashboard filter chips, custom Docker labels (SEED-001), the run.rs:301 container_id fix, and cargo-deny promoted to a blocking gate. 78 plans across Phases 15–24; 41/41 requirements Validated; audit verdict `passed`. `:latest` promoted to `:1.2.0`. All five v1.2 features moved to Validated; v1.2 milestone section collapsed; roadmap + requirements archived under `.planning/milestones/v1.2-*`. Next: `/gsd-new-milestone` for v1.3 (cross-run log search + job concurrency/queuing the leading candidates).* + +
+Earlier in-milestone updates (v1.2) + +*2026-04-28 — Phase 16 (Failure-Context Schema + run.rs:277 Bug Fix) complete: load-bearing v1.1 bug at `src/scheduler/run.rs:301` fixed (`container_id_for_finalize` now reads `docker_result.container_id` instead of `image_digest`); `DockerExecResult` carries both `container_id` and `image_digest` fields; `job_runs.image_digest` and `job_runs.config_hash` columns added on both backends with best-effort backfill; `finalize_run` widened to 8 args; `insert_running_run` widened to 4 args; `get_failure_context(pool, job_id)` single-query helper landed using D-05 CTE shape (two CTEs joined `LEFT JOIN ON 1=1`, epoch sentinel `1970-01-01T00:00:00Z`); EXPLAIN-plan tests assert `idx_job_runs_job_id_start` is hit on both backends. FOUND-14, FCTX-04, FCTX-07 validated (HUMAN-UAT spot check 2026-04-28: real container IDs in `job_runs.container_id`, no `sha256:` prefix). Code review WR-02 (regression-test coverage for the bug fix is `#[ignore]`-gated — needs Docker daemon) and a just-recipe DB-path mismatch (`uat-fctx-bugfix-spot-check` targets `cronduit.dev.db` but daemon writes to `cronduit.db`) deferred as non-blocking follow-ups. Next: Phase 17 (Custom Docker Labels — SEED-001). Previous: 2026-04-26 — Phase 15 (Foundation Preamble) complete: workspace bumped to `1.2.0`, `cargo-deny` CI preamble landed warn-only, webhook delivery worker scaffolding wired into `SchedulerLoop` with bounded `mpsc(1024)` + `try_send` non-blocking emit at `finalize_run` step 7d + `cronduit_webhook_delivery_dropped_total` counter. WH-02 scheduler-survival contract locked at the test boundary. FOUND-15, FOUND-16, WH-02 validated. 2026-04-25 — v1.2 milestone "Operator Integration & Insight" kicked off. Five features: webhooks (override pattern), failure context on run detail, per-job exit-code histogram, job tagging (UI-only), custom Docker labels (SEED-001). 2026-04-24 — v1.1 milestone closed.* + +
diff --git a/.planning/RETROSPECTIVE.md b/.planning/RETROSPECTIVE.md index 5c74d526..df5515c3 100644 --- a/.planning/RETROSPECTIVE.md +++ b/.planning/RETROSPECTIVE.md @@ -2,6 +2,49 @@ *A living document updated after each milestone. Lessons feed forward into future planning.* +## Milestone: v1.2 — Operator Integration & Insight + +**Shipped:** 2026-05-19 (tag `v1.2.0`; rc tags `v1.2.0-rc.1` … `v1.2.0-rc.5`; patch `v1.2.1`) +**Phases:** 10 (15–24) | **Plans:** 78 | **Tasks:** 158 | **Calendar timeline:** ~24 days (2026-04-25 → 2026-05-19) +**Requirements:** 41 / 41 v1.2 Validated | **Outcome:** all five features shipped; audit verdict `passed`; `:latest` promoted to `:1.2.0` + +### What Was Built + +The "expand" milestone — net-new operator-facing surface on top of the v1.1 codebase. Outbound webhooks end-to-end (Standard Webhooks v1 payloads, HMAC-SHA256 signing, per-job state-filter + edge-triggered streak coalescing, SSRF/HTTPS posture, 3-attempt full-jitter retry + `webhook_deliveries` dead-letter queue, SIGTERM drain budget, `cronduit_webhook_*` metric family, and stdlib Python/Go/Node reference receivers locked to a shared interop fixture). A failure-context panel on run detail (5 P1 signals via a single-query CTE) and a per-job exit-code histogram card (10-bucket classifier, `stopped` distinct from signal-killed). Job tagging with CSS-only AND-semantics dashboard filter chips + shareable `?tag=` URL state. Custom Docker labels on spawned containers (SEED-001). Plus the load-bearing run.rs:301 `container_id` fix and `cargo-deny` promoted to a blocking CI gate. + +### What Worked + +- The bounded-mpsc + `try_send` drop-on-full webhook worker kept the scheduler loop fully isolated from receiver latency — the WH-02 scheduler-survival contract was lockable at the test boundary and never regressed across 6 webhook-touching phases. +- A single-query CTE for failure context (5 signals, one indexed round-trip) was both faster and simpler than five separate queries, and EXPLAIN-locked on both backends. +- Standard Webhooks v1 + a 7-file interop fixture caught wire-format drift in Rust CI *before* the language receivers ran — cheap, high-signal regression guard. +- Wave-based parallel planning held up at scale (phase 20 ran 12 plans across 7 waves; phase 21 ran 11). +- The iterative rc loop (rc.1 → rc.5) again surfaced operator-visible issues UAT-side; the "retag the last UAT-passing SHA as v1.2.0" discipline (bit-identical ship) held. + +### What Was Inefficient + +- The audit's UAT/quick-task scan keys on the literal token `status: complete` and a bare `SUMMARY.md` filename, while the repo's UAT files used `validated`/`passed` and prefixed summary names — milestone close required a normalization pass across 9 UAT files + 3 quick tasks that could have been avoided with a consistent status vocabulary from the start. +- Several phase-20 gap-closure plans (20-10..20-12) landed after the initial 9, so the ROADMAP per-phase plan count drifted (showed 9/9 vs the real 12) until the close-out audit fixed it. +- Disk exhaustion in an executor session (phase 20) blocked local `cargo test`/`clippy`, forcing reliance on CI for some gates. + +### Patterns Established + +- **Bounded-channel + drop-on-full worker** for any outbound side-effect that must not back-pressure the scheduler (reusable beyond webhooks). +- **Single-query CTE view-models** pre-formatted server-side so askama templates stay logic-free. +- **First realized-seed close-out** (SEED-001): dormant → realized frontmatter audit trail + maintainer UAT checklist — the template every future seed close inherits. +- **Stdlib-only reference clients** (no SDK) + shared interop fixture as the cross-language contract lock. + +### Key Lessons + +- Pick one canonical "done" status token (`complete`) for UAT/verification frontmatter at milestone start — the tooling keys on it, and drift creates avoidable close-out churn. +- Net-new outbound surface can land additively on a shipped core if the integration point is a bounded queue with explicit drop semantics — no scheduler-core refactor was needed. +- "Accepted documented risk" (webhook SSRF, no allow-list) paired with a loopback-default posture and a THREAT_MODEL section is a legitimate ship gate for the v1 trust model — don't over-build mitigations the threat model doesn't require. + +### Cost Observations + +- Model mix: predominantly opus (quality profile); planner + executor both opus. +- Sessions: multi-session across ~24 calendar days, wave-parallelized within phases. +- Notable: the longest milestone yet (78 plans) stayed coherent via strict dependency ordering (P15 → P18/19/20; P16 → P18/21; P22 → P23) and three planned rc checkpoints. + ## Milestone: v1.1 — Operator Quality of Life **Shipped:** 2026-04-23 (tag `v1.1.0`; rc tags `v1.1.0-rc.1` … `v1.1.0-rc.6`) diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 4d6aa0c2..2590ad06 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -4,7 +4,7 @@ - ✅ **v1.0 — Docker-Native Cron Scheduler** — Phases 1–9 (shipped 2026-04-14, tags `v1.0.0` + `v1.0.1`) — see [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md) and [`MILESTONES.md`](MILESTONES.md) - ✅ **v1.1 — Operator Quality of Life** — Phases 10–14 + 12.1 inserted (shipped 2026-04-23, tags `v1.1.0-rc.1`…`v1.1.0-rc.6`, final `v1.1.0`) — see [`milestones/v1.1-ROADMAP.md`](milestones/v1.1-ROADMAP.md) and [`MILESTONES.md`](MILESTONES.md) -- 🚧 **v1.2 — Operator Integration & Insight** — Phases 15–24 (in progress; kicked off 2026-04-25). Five features: webhooks, custom Docker labels (SEED-001), failure context on run detail, per-job exit-code histogram, job tagging. 41 v1.2 requirements across 6 categories (FOUND/WH/LBL/FCTX/EXIT/TAG). Three rc cuts planned (rc.1 → rc.2 → rc.3 → final v1.2.0). +- ✅ **v1.2 — Operator Integration & Insight** — Phases 15–24 (shipped 2026-05-19, tags `v1.2.0-rc.1`…`v1.2.0-rc.5`, final `v1.2.0`, patch `v1.2.1`) — see [`milestones/v1.2-ROADMAP.md`](milestones/v1.2-ROADMAP.md) and [`MILESTONES.md`](MILESTONES.md) ## Phases @@ -39,370 +39,23 @@ -### 🚧 v1.2 Operator Integration & Insight (Phases 15–24) — IN PROGRESS - -- [x] **Phase 15: Foundation Preamble** — `Cargo.toml` 1.1.0→1.2.0 bump, `cargo-deny` CI preamble (non-blocking), webhook delivery worker foundation (bounded `mpsc(1024)` + dedicated worker task + drop counter) (completed 2026-04-26) -- [x] **Phase 16: Failure-Context Schema + run.rs:277 Bug Fix** — `DockerExecResult.container_id` field added and assignment corrected, `job_runs.config_hash` per-run column added (Option A), `get_failure_context(job_id)` single-query helper landed (completed 2026-04-28) -- [x] **Phase 17: Custom Docker Labels (SEED-001)** — operator-defined `labels` plumbed through to `bollard::Config::labels`, merge semantics, `cronduit.*` reserved-namespace validator, type-gated validator, `${ENV_VAR}` interpolation in values, size limits (completed 2026-04-29) -- [x] **Phase 18: Webhook Payload + State-Filter + Coalescing** — Standard Webhooks v1 payload schema (`payload_version: "v1"`), per-job + `[defaults]` config with `use_defaults = false` disable, edge-triggered streak coalescing (default fires on `streak_position == 1`, `fire_every` per-job override) (completed 2026-04-29) -- [x] **Phase 19: Webhook HMAC Signing + Receiver Examples** — HMAC-SHA256 only, Standard Webhooks signing-string `webhook-id.webhook-timestamp.payload`, signature header `v1,`, Python/Go/Node receiver examples with constant-time compare (completed 2026-04-30) -- [x] **Phase 20: Webhook SSRF/HTTPS Posture + Retry/Drain + Metrics — rc.1** — HTTPS required for non-loopback/non-RFC1918, 3-attempt full-jitter exponential backoff (t=0/30s/300s × 0.8-1.2× rand), `webhook_deliveries` dead-letter table, 30s drain on shutdown, `cronduit_webhook_*` metric family; **cuts `v1.2.0-rc.1`** (completed 2026-05-01) -- [x] **Phase 21: Failure-Context UI Panel + Exit-Code Histogram Card — rc.2** — Inline collapsed-by-default panel on run-detail with 5 P1 signals (time deltas, image-digest delta, config-hash delta, duration-vs-p50, scheduler-fire-skew), 10-bucket exit-code histogram on job-detail with `stopped` as distinct bucket and exit `0` as separate stat; **cuts `v1.2.0-rc.2`** (completed 2026-05-03) -- [x] **Phase 22: Job Tagging Schema + Validators** — `jobs.tags` JSON column, single-file additive migration, lowercase+trim normalization, charset regex `^[a-z0-9][a-z0-9_-]{0,30}$`, reserved-tag rejection, substring-collision check at config-load (completed 2026-05-04) -- [x] **Phase 23: Job Tagging Dashboard Filter Chips — rc.3** — CSS-only chip components on dashboard, AND filter semantics, untagged-hidden when filter active, URL state via repeated `?tag=`, HTMX dashboard partial swap on chip toggle; **cuts `v1.2.0-rc.3`** (completed 2026-05-05) -- [x] **Phase 24: Milestone Close-Out — final `v1.2.0` ship** — UAT-driven rc loop completion, `THREAT_MODEL.md` Threat Model 5 (Webhook Outbound) + Threat Model 6 (Operator-supplied labels), REQUIREMENTS.md flips to Validated, `MILESTONES.md` v1.2 entry, README updates, `:latest` promoted to `:1.2.0` on both archs (completed 2026-05-17) - -## Phase Details - -### Phase 15: Foundation Preamble - -**Goal**: Establish the v1.2 hygiene baseline and lock the webhook delivery worker isolation pattern before any payload/signing/posture work depends on it. - -**Depends on**: v1.1.0 (Phase 14) shipped - -**Requirements**: FOUND-15, FOUND-16, WH-02 - -**Success Criteria** (what must be TRUE): - - 1. An operator running `cronduit --version` on the first v1.2 commit sees `1.2.0` (not `1.1.0`). - 2. An operator viewing the GitHub Actions PR check list sees a new `cargo-deny` job that runs advisories + licenses + duplicate-versions checks (failures non-blocking on first rc; status visible). - 3. An operator can fire a job whose webhook receiver is stalled for 60 seconds and the next scheduled jobs across the fleet still fire on time (no scheduler drift > 1 s) — the `try_send` non-blocking path holds. - 4. An operator can fill the bounded webhook queue past 1024 entries and observe `cronduit_webhook_delivery_dropped_total` increment with a `warn`-level log line per dropped event; the scheduler loop remains unaffected. - -**Plans:** 5/5 plans complete - -Plans: - -- [x] 15-01-PLAN.md — Cargo.toml version bump 1.1.0 -> 1.2.0 (FOUND-15; the very first v1.2 commit per D-12) -- [x] 15-02-PLAN.md — cargo-deny CI preamble: deny.toml + just deny + ci.yml lint-job step with continue-on-error: true (FOUND-16) -- [x] 15-03-PLAN.md — Webhook module skeleton: src/webhooks/{mod,event,dispatcher,worker}.rs + async-trait promotion + cronduit_webhook_delivery_dropped_total telemetry registration (WH-02) -- [x] 15-04-PLAN.md — Scheduler integration: SchedulerLoop.webhook_tx field + run_job signature/call-site updates + finalize_run step 7d try_send emit + step 7d->7e renumber + bin-layer worker spawn with NoopDispatcher (WH-02) -- [x] 15-05-PLAN.md — Wave-0 integration tests: tests/v12_webhook_queue_drop.rs (T-V12-WH-04) + tests/v12_webhook_scheduler_unblocked.rs (T-V12-WH-03) + extend tests/metrics_endpoint.rs::metrics_families_described_from_boot with HELP/TYPE asserts for the drop counter (WH-02) - -### Phase 16: Failure-Context Schema + run.rs:277 Bug Fix - -**Goal**: Fix the silent v1.1 `job_runs.container_id` regression and land the per-run schema columns + streak query helper that the webhook payload (Phase 18) and failure-context UI (Phase 21) both consume. - -**Depends on**: Phase 15 - -**Requirements**: FOUND-14, FCTX-04, FCTX-07 - -**Success Criteria** (what must be TRUE): - - 1. An operator inspecting a v1.2 docker job run via the database sees `job_runs.container_id` populated with the real Docker container ID (not a `sha256:...` image digest); historical v1.1 rows age out via the Phase 6 retention pruner. - 2. An operator viewing two consecutive runs of the same job after a hot reload sees distinct `job_runs.config_hash` values when the underlying TOML actually changed (per-RUN column, not the per-JOB proxy). - 3. An operator inspecting the `EXPLAIN QUERY PLAN` for `get_failure_context(job_id)` on both SQLite and Postgres sees indexed access on `job_runs.job_id + start_time`; the function returns `streak_position`, `consecutive_failures`, `last_success_run_id`, `last_success_image_digest`, and `last_success_config_hash` from a single SQL query (not five separate round-trips). - -**Plans:** 7/7 plans complete - -Plans: - -- [x] 16-01-PLAN.md — Migrations: image_digest add + config_hash add + config_hash backfill (6 files: 3 per backend) + tests/v12_fctx_config_hash_backfill.rs -- [x] 16-02-PLAN.md — DockerExecResult.container_id field + 7 literal sites populated (struct widening) -- [x] 16-03-PLAN.md — run.rs:301 bug fix + parallel image_digest_for_finalize local + tests/v12_run_rs_277_bug_fix.rs -- [x] 16-04a-PLAN.md — DB layer (queries.rs only): finalize_run + insert_running_run signature changes; DbRun/DbRunDetail field add; SELECT-site updates (5 tasks) -- [x] 16-04b-PLAN.md — Callers + recipe + wave-end gate: 4 production callers + 5 test-mod callers + just uat-fctx-bugfix-spot-check + full CI gate (5 tasks) -- [x] 16-05-PLAN.md — get_failure_context query helper + FailureContext struct + tests/v12_fctx_streak.rs (5 streak scenarios + FCTX-04 write-site) -- [x] 16-06-PLAN.md — tests/v12_fctx_explain.rs (EXPLAIN QUERY PLAN on SQLite + Postgres asserting idx_job_runs_job_id_start) - -### Phase 17: Custom Docker Labels (SEED-001) - -**Goal**: Operators can attach arbitrary Docker labels to cronduit-spawned containers (Traefik, Watchtower, backup tooling interop) with locked merge semantics, a reserved cronduit.* namespace, and type-gated validation. - -**Depends on**: Phase 15 (independent of FCTX work — slots in here for the foundation block rc.1) - -**Requirements**: LBL-01, LBL-02, LBL-03, LBL-04, LBL-05, LBL-06 - -**Success Criteria** (what must be TRUE): - - 1. An operator who adds `labels = { "com.centurylinklabs.watchtower.enable" = "false" }` to a `[[jobs]]` block sees that label on the spawned container via `docker inspect` (the cronduit-internal `cronduit.run_id` and `cronduit.job_name` labels remain intact). - 2. An operator who sets `use_defaults = false` on a per-job labels map gets ONLY the per-job labels (defaults replaced); without `use_defaults = false` the defaults map is merged and per-job keys win on collision. - 3. An operator who tries to set `cronduit.foo = "bar"` (or any `cronduit.*` key) gets a config-load error pointing at the offending key — the validator runs at LOAD time, not runtime. - 4. An operator who tries to set `labels = ...` on a `type = "command"` or `type = "script"` job gets a clear config-load error explaining that labels apply only to `type = "docker"`. - 5. An operator who writes `labels = { "deployment.id" = "${DEPLOYMENT_ID}" }` sees the env var interpolated in the value at config-load (keys are never interpolated); a value > 4 KB or a label set summing > 32 KB is rejected at load. - -**Plans:** 9 plans (6 core + 3 gap closure) - -Plans: - -- [x] 17-01-PLAN.md — schema + 5-layer parity + apply_defaults merge (LBL-01 + LBL-02) -- [x] 17-02-PLAN.md — four LOAD-time validators (LBL-03, LBL-04, LBL-06, D-02 key chars) -- [x] 17-03-PLAN.md — bollard plumb-through + 3 testcontainers integration tests (LBL-01, LBL-02, LBL-05) -- [x] 17-04-PLAN.md — examples/cronduit.toml: 3 integration patterns -- [x] 17-05-PLAN.md — README § Configuration > Labels subsection (mermaid + table + 6 rules) -- [x] 17-06-PLAN.md — SEED-001 close-out + 17-HUMAN-UAT.md -- [x] 17-07-PLAN.md — CR-01 gap closure: README env-var interpolation prose + validator/interpolate docstrings + two key-position regression tests (LBL-05) -- [x] 17-08-PLAN.md — CR-02 gap closure: set-diff in `check_labels_only_on_docker_jobs` + 3 unit tests + 1 integration test (LBL-04) -- [x] 17-09-PLAN.md — REQUIREMENTS.md bookkeeping: flip LBL-01..LBL-06 from Pending to Complete - -**UI hint**: yes - -### Phase 18: Webhook Payload + State-Filter + Coalescing - -**Goal**: Operators can configure per-job webhook URLs that fire on a state-filter list with edge-triggered streak coalescing; payloads adhere to the Standard Webhooks v1 spec. - -**Depends on**: Phase 15 (worker), Phase 16 (streak query helper) - -**Requirements**: WH-01, WH-03, WH-06, WH-09 - -**Success Criteria** (what must be TRUE): - - 1. An operator who configures `webhook = { url = "https://hook.example.com", states = ["failed", "timeout", "stopped"] }` per job (and/or in `[defaults]` with the `use_defaults = false` disable pattern) sees deliveries fire only on the listed terminal statuses — `success` runs do NOT fire. - 2. An operator running a `* * * * *` failing job sees ONE webhook delivery on the first failure of a new streak by default (not 30 deliveries over 30 minutes); setting `webhook.fire_every = 0` restores the legacy per-failure firing. - 3. An operator inspecting a delivered webhook payload sees the locked v1.2.0 schema fields: `payload_version: "v1"`, `event_type: "run_finalized"`, `run_id`, `job_id`, `job_name`, `status`, `exit_code`, `started_at`, `finished_at`, `duration_ms`, `streak_position`, `consecutive_failures`, `image_digest` (docker only), `config_hash`, `tags`, `cronduit_version`. - 4. An operator inspecting delivered headers sees `webhook-id`, `webhook-timestamp`, and `webhook-signature` (Standard Webhooks v1 spec) on every delivery. - -**Plans:** 6/6 plans complete - -Plans: - -- [x] 18-01-PLAN.md — Foundation: Cargo deps (reqwest 0.13 rustls / hmac / base64 / ulid + wiremock dev) + just test-unit recipe + 2 new webhook counters described+zero-baselined -- [x] 18-02-PLAN.md — WH-01: WebhookConfig struct + apply_defaults webhook merge + check_webhook_url + check_webhook_block_completeness validators (incl. Pitfall H empty-secret) -- [x] 18-03-PLAN.md — WH-06+WH-09: WebhookPayload encoder (15-field v1 schema) + coalesce::filter_position SQL helper + EXPLAIN PLAN regression test -- [x] 18-04-PLAN.md — WH-03: HttpDispatcher impl (Standard Webhooks v1 headers + HMAC-SHA256 sign_v1 + reqwest 0.13 rustls Client + should_fire D-16 matrix) -- [x] 18-05-PLAN.md — Bin-layer wire-up (HttpDispatcher swap in src/cli/run.rs) + 6 wiremock integration tests (e2e signed, unsigned, state-filter, 3x metric counter) -- [x] 18-06-PLAN.md — Maintainer UAT: 3 new just recipes + examples/webhook_mock_server.rs + examples/cronduit.toml extension + 18-HUMAN-UAT.md (autonomous=false; maintainer-validated) - -### Phase 19: Webhook HMAC Signing + Receiver Examples - -**Goal**: Operators can verify webhook authenticity using HMAC-SHA256 and the Standard Webhooks signing-string convention; ship reference receiver examples that demonstrate constant-time HMAC compare. - -**Depends on**: Phase 18 - -**Requirements**: WH-04 - -**Success Criteria** (what must be TRUE): - - 1. An operator who configures `webhook.secret = "..."` on a job sees the `webhook-signature` header value formatted as `v1,` where the HMAC is computed over `webhook-id.webhook-timestamp.payload` raw bytes using SHA-256. - 2. An operator running the shipped Python, Go, and Node receiver examples successfully verifies signatures from a real cronduit delivery; each example uses a constant-time compare primitive (Python `hmac.compare_digest`, Go `hmac.Equal`, Node `crypto.timingSafeEqual`) — NOT `==` on hex bytes. - 3. An operator reviewing the receiver-example docs sees an explicit note that v1.2 ships SHA-256 only (no algorithm-agility / multi-secret rotation cronduit-side; rotation is a receiver concern). - -**Plans:** 6/6 plans complete - -Plans: - -- [x] 19-01-PLAN.md — Wave 1: tests/fixtures/webhook-v1/* fixture + in-module sign_v1_locks_interop_fixture Rust test (Pitfall 1: pub(crate)) -- [x] 19-02-PLAN.md — Wave 2: examples/webhook-receivers/python/ stdlib receiver + 2 just recipes (port 9991, hmac.compare_digest) -- [x] 19-03-PLAN.md — Wave 2: examples/webhook-receivers/go/ stdlib receiver + 2 just recipes (port 9992, hmac.Equal) -- [x] 19-04-PLAN.md — Wave 2: examples/webhook-receivers/node/ stdlib receiver + 2 just recipes (port 9993, crypto.timingSafeEqual + Pitfall 2 length guard) -- [x] 19-05-PLAN.md — Wave 3: docs/WEBHOOKS.md operator hub + CONFIG.md back-link + README pointer + 3 wh-example-receiver-* jobs -- [x] 19-06-PLAN.md — Wave 3: ci.yml webhook-interop matrix (Python/Go/Node) + 19-HUMAN-UAT.md (autonomous=false; 11 maintainer-validated scenarios) - -### Phase 20: Webhook SSRF/HTTPS Posture + Retry/Drain + Metrics — rc.1 - -**Goal**: Lock the webhook security posture (HTTPS for non-local destinations, SSRF accepted-risk documented), the retry/dead-letter behavior, the graceful-shutdown drain, and the Prometheus metric family — then cut `v1.2.0-rc.1` covering the foundation block. - -**Depends on**: Phase 19 - -**Requirements**: WH-05, WH-07, WH-08, WH-10, WH-11 - -**Success Criteria** (what must be TRUE): - - 1. An operator who configures a webhook URL like `http://example.com` (non-loopback, non-RFC1918) sees a config-load error; `http://` is permitted only for `127.0.0.0/8`, `::1`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, and `fd00::/8`. - 2. An operator whose receiver returns 500 sees three delivery attempts at approximately t=0, t=30 s, t=300 s (each multiplied by `rand()*0.4 + 0.8` full-jitter); after the third attempt, the delivery is recorded in the `webhook_deliveries` dead-letter table and `cronduit_webhook_deliveries_total{status="failed"}` increments. - 3. An operator sending SIGTERM with deliveries in-flight sees the worker drain the queue for up to `webhook_drain_grace = "30s"` (configurable), then drop remaining queued deliveries with a counter increment; in-flight HTTP requests are NOT cancelled mid-flight. - 4. An operator scraping `/metrics` sees the new `cronduit_webhook_*` family eagerly described at boot: `cronduit_webhook_deliveries_total{job, status}` (closed enum: success/failed/dropped), `cronduit_webhook_delivery_duration_seconds{job}` histogram, `cronduit_webhook_queue_depth` gauge. - 5. An operator pushing the `v1.2.0-rc.1` tag sees the GHCR image published at `ghcr.io/SimplicityGuy/cronduit:v1.2.0-rc.1` on both amd64 and arm64; the `:latest` tag still points at `v1.1.0` (rc tag gating from v1.1's D-10 holds). - -**Plans:** 9/9 plans complete - -Plans: -**Wave 1** - -- [x] 20-01-PLAN.md — DLQ migration (sqlite + postgres) + WebhookDlqRow/insert/delete helpers + retention Phase 4 + 7 Wave 0 test stubs - -**Wave 2** *(blocked on Wave 1 completion)* - -- [x] 20-02-PLAN.md — RetryingDispatcher in src/webhooks/retry.rs + classification + jitter + Retry-After + DLQ writes + 4 integration tests -- [x] 20-03-PLAN.md — HTTPS-required validator extension in src/config/validate.rs + INFO log + integration tests - -**Wave 3** *(blocked on Wave 2 completion)* - -- [x] 20-05-PLAN.md — labeled metric family migration (deliveries_total{job,status} + delivery_duration_seconds + queue_depth); P15 dropped counter preserved - -**Wave 4** *(blocked on Wave 3 completion)* - -- [x] 20-04-PLAN.md — worker_loop drain budget (3rd select! arm) + queue_depth gauge + drain integration tests - -**Wave 5** *(blocked on Wave 4 completion)* - -- [x] 20-06-PLAN.md — webhook_drain_grace config field + RetryingDispatcher wiring + per-job metric pre-seed in src/cli/run.rs -- [x] 20-07-PLAN.md — docs/WEBHOOKS.md 6-section extension + 2 mermaid diagrams + TM5 forward-pointer stub - -**Wave 6** *(blocked on Wave 5 completion)* - -- [x] 20-08-PLAN.md — UAT recipes (uat-webhook-retry/drain/dlq-query/https-required) + 20-HUMAN-UAT.md (autonomous=false) - -**Wave 7** *(blocked on Wave 6 completion)* - -- [x] 20-09-PLAN.md — rc.1 pre-flight checklist (autonomous=false; maintainer cuts v1.2.0-rc.1 tag locally; no release.yml/cliff.toml/release-rc.md edits per D-30) - -**Cross-cutting constraints:** - -- Per D-38: Cargo.toml unchanged; `cargo tree -i openssl-sys` empty. - -### Phase 21: Failure-Context UI Panel + Exit-Code Histogram Card — rc.2 - -**Goal**: Operators get a rich failure-context panel on the run-detail page (5 P1 signals) and a per-job exit-code histogram card on the job-detail page (10-bucket strategy with stopped distinct from signal-killed). - -**Depends on**: Phase 16 (FCTX schema + bug fix + streak helper) - -**Requirements**: FCTX-01, FCTX-02, FCTX-03, FCTX-05, FCTX-06, EXIT-01, EXIT-02, EXIT-03, EXIT-04, EXIT-05, EXIT-06 - -**Success Criteria** (what must be TRUE): - - 1. An operator viewing a `failed` or `timeout` run-detail page sees a collapsed-by-default failure-context panel that expands to show 5 labeled rows: time-based deltas (first-failure timestamp, consecutive-failure streak, link to last successful run), image-digest delta (docker jobs only — non-docker hides the row), config-hash delta ("config changed since last success: Yes/No"), duration-vs-p50 deviation (suppressed below 5 sample threshold), and scheduler-fire-time vs run-start-time skew. - 2. An operator viewing a `success`, `cancelled`, `running`, or `stopped` run-detail page does NOT see the failure-context panel (gated to failed/timeout only). - 3. An operator viewing a job-detail page sees a new exit-code-distribution card (sibling to the v1.1 p50/p95 duration card) showing the last 100 ALL runs bucketed into 10 fixed buckets (0 / 1 / 2 / 3-9 / 10-126 / 127 / 128-143 / 144-254 / 255 / null); below `N=5` sample threshold the card renders "—". - 4. An operator viewing the histogram sees `stopped` runs (which exit 137 from cronduit's SIGKILL) rendered as a DISTINCT visual bucket separate from `128-143` (signal-killed), using the `--cd-status-stopped` color from v1.1; success (`0`) is rendered as a separate stat badge, NOT a bar in the histogram. - 5. An operator pushing the `v1.2.0-rc.2` tag sees the GHCR image published at `ghcr.io/SimplicityGuy/cronduit:v1.2.0-rc.2` on both architectures; the `:latest` tag still points at `v1.1.0`. - -**Plans**: TBD -**UI hint**: yes - -### Phase 22: Job Tagging Schema + Validators - -**Goal**: Operators can attach normalized tags to jobs in TOML config; tags persist to a JSON column, validate against a strict charset, and reject substring-collisions at config-load. - -**Depends on**: Phase 15 (independent of webhook/FCTX work) - -**Requirements**: TAG-01, TAG-02, TAG-03, TAG-04, TAG-05 (plus closes WH-09 from Phase 18 — webhook payload backfill end-to-end) - -**Success Criteria** (what must be TRUE): - - 1. An operator who writes `tags = ["backup", "weekly"]` on a `[[jobs]]` block sees those tags persisted to the new `jobs.tags` JSON column; the field is per-job only (NOT supported in `[defaults]`). - 2. An operator who writes `tags = ["Backup", "backup ", "BACKUP"]` sees a config-load WARN that the entries collapse to `["backup"]` after lowercase + trim normalization (the WARN flags the deduplication so operators notice). - 3. An operator who writes a tag like `MyTag!` or `cronduit` (reserved) gets a config-load ERROR pointing at the offending tag — the validator never silently mutates; the charset regex `^[a-z0-9][a-z0-9_-]{0,30}$` is enforced. - 4. An operator who configures one job with `tags = ["back"]` and another with `tags = ["backup"]` gets a config-load ERROR (substring-collision check) — the SQL filter `tags LIKE '%"' || ?tag || '"%'` would otherwise produce false positives. - -**Plans:** 4/6 plans executed - -Plans: -**Wave 1** - -- [x] 22-01-PLAN.md — Schema + serde field + sqlite/postgres migration pair (TAG-01, TAG-02) - -**Wave 2** *(blocked on Wave 1 completion)* - -- [x] 22-02-PLAN.md — Four validators in `validate.rs`: charset+reserved, count cap (16), fleet-level substring-collision; D-04 order locked (TAG-03, TAG-04, TAG-05) -- [x] 22-03-PLAN.md — DB plumbing: upsert_job widening + DbRunDetail.tags + get_run_by_id row-map + sync.rs callers + hash.rs D-01 comment + tags_excluded_from_hash regression test (TAG-02) - -**Wave 3** *(blocked on Wave 2 completion)* - -- [x] 22-04-PLAN.md — WH-09 webhook payload backfill: src/webhooks/payload.rs L88 vec![] → run.tags.clone(); test rename payload_tags_carries_real_values (WH-09) - -**Wave 4** *(blocked on Wave 3 completion)* - -- [ ] 22-05-PLAN.md — Integration tests (tests/v12_tags_validators.rs) + examples/cronduit.toml demo line + three uat-tags-* just recipes (TAG-01..05) - -**Wave 5** *(blocked on Wave 4 completion)* - -- [ ] 22-06-PLAN.md — 22-HUMAN-UAT.md maintainer runbook (autonomous=false; 4 scenarios per D-10) (TAG-01..05) - -**Cross-cutting constraints:** - -- No new external crates; `cargo tree -i openssl-sys` remains empty (D-17) - -### Phase 23: Job Tagging Dashboard Filter Chips — rc.3 - -**Goal**: Operators get CSS-only filter chips on the dashboard with AND semantics across selected tags, untagged-hidden when filter active, shareable URL state — then cut `v1.2.0-rc.3`. - -**Depends on**: Phase 22 - -**Requirements**: TAG-06, TAG-07, TAG-08 - -**Success Criteria** (what must be TRUE): - - 1. An operator viewing the dashboard sees filter chips for every distinct tag in the current fleet; clicking a chip toggles its filter state (active = teal-bordered + bold; inactive = grey). - 2. An operator with multiple active chips sees only jobs that have ALL active tags (AND semantics); the active filter composes with the existing v1.0 name-filter via AND (job must match BOTH). - 3. An operator with any active tag filter sees untagged jobs HIDDEN from the dashboard (least-surprise behavior). - 4. An operator can share a filtered dashboard URL like `/?tag=backup&tag=weekly` (repeated `?tag=` params); the chips render in the active state on page load — bookmarkable. - 5. An operator pushing the `v1.2.0-rc.3` tag sees the GHCR image published at `ghcr.io/SimplicityGuy/cronduit:v1.2.0-rc.3` on both architectures. - -**Plans:** 8/8 plans complete - -Plans: -**Wave 0** - -- [x] 23-01-PLAN.md — Wave-0 test scaffolding: tests/v12_tags_dashboard.rs + dashboard.rs::tests stubs (V-W0) - -**Wave 1** *(blocked on Wave 0 completion)* - -- [x] 23-02-PLAN.md — DB layer: DashboardJob.tags + get_dashboard_jobs SELECT/WHERE widening for AND-tag filter + caller passes &[] placeholder (TAG-07; V-01..V-04) - -**Wave 2** *(blocked on Wave 1 completion — file overlap on dashboard.rs)* - -- [x] 23-03-PLAN.md — Handler: axum_extra::Query swap + DashboardParams.tags + fleet-tag fold + active-set sort/dedup/intersect + view-models gain fleet_tags/active_tags (TAG-06; V-05, V-07) - -**Wave 3** *(blocked on Wave 2 completion)* - -- [x] 23-04-PLAN.md — CSS: cd-tag-chip-* family in @layer components + reduced-motion + print extensions; zero new tokens (TAG-06, TAG-08) -- [x] 23-05-PLAN.md — Template: ChipView precompute + chip strip insert above filter row + sort-header href widening + poll hx-include widening + OOB swap composition (TAG-06, TAG-08; V-06, V-08..V-14) - -**Wave 4** *(blocked on Wave 3 completion)* - -- [x] 23-06-PLAN.md — UAT: 3 just uat-chips-* recipes + README Tag Filter Chips subsection (TAG-06, TAG-07; V-15, V-16) - -**Wave 5** *(blocked on Wave 4 completion)* - -- [x] 23-07-PLAN.md — HUMAN-UAT: 23-HUMAN-UAT.md autonomous=false maintainer plan with 6 scenarios (TAG-06, TAG-07, TAG-08) - -**Wave 6** *(blocked on Wave 5 completion)* - -- [x] 23-08-PLAN.md — RC3-PREFLIGHT: 23-RC3-PREFLIGHT.md autonomous=false rc.3 cut runbook (mirrors P21 RC2-PREFLIGHT verbatim modulo rc.2→rc.3 + P21→P23 substitutions; V-17) - -**Cross-cutting constraints:** - -- No new external crates; `axum_extra` already in tree; `cargo tree -i openssl-sys` remains empty (D-23) -- NO modifications to `release.yml` / `cliff.toml` / `docs/release-rc.md` (D-15 / D-16) -- `Cargo.toml` stays at `1.2.0`; `-rc.3` is tag-only (D-22) - -**UI hint**: yes - -### Phase 24: Milestone Close-Out — final `v1.2.0` ship - -**Goal**: Run the UAT-driven rc loop to closure (rc.3 → final v1.2.0), update threat model + documentation, flip REQUIREMENTS.md to Validated, write the v1.2 MILESTONES entry, and promote `:latest` to `1.2.0`. - -**Depends on**: Phase 23 - -**Requirements**: n/a — operational close-out (no v1.2 REQ-IDs; mirrors v1.0 Phase 9 pattern). All 41 v1.2 requirements covered by Phases 15–23. - -**Success Criteria** (what must be TRUE): - - 1. An operator reading `THREAT_MODEL.md` sees Threat Model 5 (Webhook Outbound — operator-with-UI-access can configure outbound HTTP at any URL; SSRF accepted risk; HTTPS posture; loopback-bound default mitigation) and Threat Model 6 (Operator-supplied Docker labels — reserved-namespace clobber, type-gated validator, size-limit DoS surface). - 2. An operator reviewing `.planning/REQUIREMENTS.md` (the v1.2 file) sees every requirement (FOUND-14..16, WH-01..11, LBL-01..06, FCTX-01..07, EXIT-01..06, TAG-01..08) flipped to Validated with a Phase reference. - 3. An operator reading `MILESTONES.md` sees a new v1.2 entry with shipped tags, phase count, plan count, and key accomplishments — formatted consistently with v1.0 and v1.1 entries. - 4. An operator inspecting `ghcr.io/SimplicityGuy/cronduit:latest` after final ship sees the digest match `:1.2.0` on both amd64 and arm64; `:1.2.0` == `:1.2` == `:1` == `:latest` (D-18 four-tag equality verified). The `cargo-deny` CI job is promoted from non-blocking (warn) to blocking (error) before the final tag is pushed. - 5. An operator running `docker compose up` against the shipped quickstart with the new `v1.2.0` image observes the cronduit container reporting `healthy` (v1.1 healthcheck still works), the dashboard renders with new tag filter chips (no regressions on v1.0/v1.1 surfaces), and a webhook configured against a local mock receiver delivers a Standard-Webhooks-spec payload on the first failure. - -**Plans**: 8 plans - -Plans: -**Wave 1** - -- [x] 24-02-PLAN.md — Milestone audit: invoke /gsd-audit-milestone v1.2; v1.2-MILESTONE-AUDIT.md + REQUIREMENTS.md flips (20 items) + ROADMAP drift cleanup (P17 Complete / P21 11/11 / P22 6/6) -- [x] 24-05-PLAN.md — cargo-deny WARN→ERROR promotion (FOUND-16): ci.yml continue-on-error removed; conditional deny.toml/Cargo.lock advisory remediation - -**Wave 2** *(blocked on Wave 1 completion)* - -- [x] 24-01-PLAN.md — Threat model close-out: TM5 in-place canonical rewrite + new TM6 + STRIDE rows T-S3/T-T4/T-I4/T-D4 + Changelog + README §Security link-back to TM5/TM6 anchors -- [x] 24-03-PLAN.md — MILESTONES.md v1.2 release-log entry (additive at top, mirrors v1.1/v1.0 six-row shape) - -**Wave 3** *(blocked on Wave 2 completion)* - -- [x] 24-04-PLAN.md — README updates: v1.2 What's New hero block + §Features pointer (FCTX + exit histogram) + §Configuration §Webhooks subsection + MILESTONES cross-link - -**Wave 4** *(blocked on Wave 3 completion)* - -- [x] 24-06-PLAN.md — 24-RC4-PREFLIGHT.md (autonomous=false maintainer runbook for v1.2.0-rc.4 tag cut) - -**Wave 5** *(blocked on Wave 4 completion)* +
+✅ v1.2 Operator Integration & Insight (Phases 15–24) — SHIPPED 2026-05-19 -- [x] 24-07-PLAN.md — 24-HUMAN-UAT.md (autonomous=false maintainer six-scenario UAT runbook covering v1.0/v1.1 regression + all five v1.2 features end-to-end) +- [x] Phase 15: Foundation Preamble (5/5 plans) — 2026-04-26 +- [x] Phase 16: Failure-Context Schema + run.rs Bug Fix (7/7 plans) — 2026-04-28 +- [x] Phase 17: Custom Docker Labels — SEED-001 (9/9 plans: 6 core + 3 gap closure) — 2026-04-29 +- [x] Phase 18: Webhook Payload + State-Filter + Coalescing (6/6 plans) — 2026-04-29 +- [x] Phase 19: Webhook HMAC Signing + Receiver Examples (6/6 plans) — 2026-04-30 +- [x] Phase 20: Webhook SSRF/HTTPS + Retry/Drain + Metrics — rc.1 (12/12 plans) — 2026-05-01; `v1.2.0-rc.1` cut +- [x] Phase 21: Failure-Context UI Panel + Exit-Code Histogram Card — rc.2 (11/11 plans) — 2026-05-03; `v1.2.0-rc.2` cut +- [x] Phase 22: Job Tagging Schema + Validators (6/6 plans) — 2026-05-04 +- [x] Phase 23: Job Tagging Dashboard Filter Chips — rc.3 (8/8 plans) — 2026-05-05; `v1.2.0-rc.3` cut +- [x] Phase 24: Milestone Close-Out — final `v1.2.0` ship (8/8 plans) — 2026-05-17 -**Wave 6** *(blocked on Wave 5 completion)* +**Total:** 78 plans across 10 phases · 41/41 v1.2 requirements Validated · audit verdict `passed` · rc tags `rc.1`…`rc.5` then last UAT-passing SHA retagged `v1.2.0` (2026-05-19) + patch `v1.2.1` · `:latest` promoted from `:1.1.0` to `:1.2.0` on both archs -- [x] 24-08-PLAN.md — 24-FINAL-SHIP-PREFLIGHT.md (autonomous=false maintainer final-tag runbook: retag last-passing-UAT rc.N SHA as v1.2.0; verify four-tag equality + cargo-deny ERROR-gate; flip STATE.md SHIPPED) +
## Progress @@ -410,73 +63,7 @@ Plans: | --------- | ------ | ----- | ------ | ------- | | v1.0 | 1–9 | 49/49 | ✅ Complete | 2026-04-14 | | v1.1 | 10–14 (+ 12.1) | 52/52 | ✅ Complete | 2026-04-23 | -| v1.2 | 15–24 | 0/— | 🚧 In progress | — | - -### v1.2 Phase Tracker - -| Phase | Plans Complete | Status | Completed | -|-------|----------------|--------|-----------| -| 15. Foundation Preamble | 5/5 | Complete | 2026-04-26 | -| 16. Failure-Context Schema + run.rs Bug Fix | 7/7 | Complete | 2026-04-28 | -| 17. Custom Docker Labels (SEED-001) | 6/6 + 3 gap closure | Complete | 2026-04-29 | -| 18. Webhook Payload + State-Filter + Coalescing | 6/6 | Complete | 2026-04-29 | -| 19. Webhook HMAC Signing + Receiver Examples | 6/6 | Complete | 2026-04-30 | -| 20. Webhook SSRF/HTTPS + Retry/Drain + Metrics — rc.1 | 9/9 | Complete | 2026-05-01 | -| 21. Failure-Context UI + Exit-Code Histogram — rc.2 | 11/11 | Complete | 2026-05-03 | -| 22. Job Tagging Schema + Validators | 6/6 | Complete | 2026-05-04 | -| 23. Job Tagging Dashboard Filter Chips — rc.3 | 8/8 | Complete | 2026-05-05 | -| 24. Milestone Close-Out — final v1.2.0 | 8/8 | Complete | 2026-05-17 | - -## v1.2 Build Order - -```mermaid -flowchart LR - P15["Phase 15
Foundation Preamble
(Cargo bump + cargo-deny
+ webhook worker)"] - P16["Phase 16
FCTX Schema
+ run.rs:277 fix
+ streak helper"] - P17["Phase 17
Docker Labels
(SEED-001)"] - P18["Phase 18
Webhook Payload
+ state filter
+ coalescing"] - P19["Phase 19
Webhook HMAC
+ receiver examples"] - P20["Phase 20
Webhook SSRF/HTTPS
+ retry/drain
+ metrics"] - RC1(["v1.2.0-rc.1"]) - P21["Phase 21
FCTX UI panel
+ Exit-code histogram"] - RC2(["v1.2.0-rc.2"]) - P22["Phase 22
Tagging schema
+ validators"] - P23["Phase 23
Tagging filter chips"] - RC3(["v1.2.0-rc.3"]) - P24["Phase 24
Close-out
+ TM5/TM6"] - SHIP(["v1.2.0
SHIPPED"]) - - P15 --> P16 - P15 --> P17 - P15 --> P22 - P16 --> P18 - P17 --> P20 - P18 --> P19 - P19 --> P20 - P20 --> RC1 - RC1 --> P21 - P16 --> P21 - P21 --> RC2 - RC2 --> P22 - P22 --> P23 - P23 --> RC3 - RC3 --> P24 - P24 --> SHIP - - classDef foundation fill:#0a3d0a,stroke:#00ff7f,stroke-width:2px,color:#e0ffe0 - classDef webhooks fill:#1a1a3d,stroke:#7fbfff,stroke-width:2px,color:#e0e0ff - classDef insight fill:#3d2a1a,stroke:#ffbf7f,stroke-width:2px,color:#ffe0c0 - classDef organize fill:#2a1a3d,stroke:#bf7fff,stroke-width:2px,color:#f0e0ff - classDef rc fill:#2a1a3d,stroke:#bf7fff,stroke-width:2px,color:#f0e0ff - classDef ship fill:#00ff7f,stroke:#00ff7f,stroke-width:3px,color:#0a1a0a - class P15,P16 foundation - class P18,P19,P20 webhooks - class P21 insight - class P17,P22,P23 organize - class P24 close - class RC1,RC2,RC3 rc - class SHIP ship -``` +| v1.2 | 15–24 | 78/78 | ✅ Complete | 2026-05-19 | --- @@ -484,4 +71,4 @@ flowchart LR *v1.1 archived 2026-04-24 via `/gsd-complete-milestone`. Full roadmap and requirements preserved under `.planning/milestones/v1.1-*`. Phase execution history archived to `.planning/milestones/v1.1-phases/`.* -*v1.2 roadmap created 2026-04-25 via `/gsd-roadmapper`. 10 phases (15–24), 41 requirements across 6 categories, three rc cuts planned (rc.1 after Phase 20 / rc.2 after Phase 21 / rc.3 after Phase 23 → final v1.2.0 in Phase 24). Strict dependency ordering: P15 (webhook worker foundation) before P18/P19/P20; P16 (FCTX schema + bug fix) before P18 + P21; P22 (tagging schema) before P23. Granularity=standard.* +*v1.2 archived 2026-05-29 via `/gsd-complete-milestone`. Full roadmap (phase details + build-order mermaid), requirements, and audit preserved under `.planning/milestones/v1.2-*`. Phase execution history retained under `.planning/phases/` (use `/gsd-cleanup` to archive retroactively).* diff --git a/.planning/STATE.md b/.planning/STATE.md index c14480f8..146e2e47 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,10 +2,9 @@ gsd_state_version: 1.0 milestone: v1.2 milestone_name: — Operator Integration & Insight -status: "v1.2 SHIPPED — final tag v1.2.0 pushed 2026-05-19 (bit-identical to rc.5 SHA 7fb1de5)" -stopped_at: v1.2.0 shipped -last_updated: "2026-05-19T20:55:00Z" -last_activity: "2026-05-19 -- v1.2.0 SHIPPED — retag of rc.5 SHA 7fb1de5 (UAT-validated 2026-05-19)" +status: Awaiting next milestone +last_updated: "2026-05-29T22:34:41.861Z" +last_activity: 2026-05-29 — Milestone v1.2 completed and archived progress: total_phases: 10 completed_phases: 10 @@ -18,21 +17,17 @@ progress: ## Project Reference -See: `.planning/PROJECT.md` (updated 2026-04-25 — v1.2 milestone kicked off) +See: `.planning/PROJECT.md` (updated 2026-05-29 — v1.2 milestone completed & archived) **Core value:** One tool that both runs recurrent jobs reliably AND makes their state observable through a web UI. -**Current focus:** v1.2 SHIPPED — awaiting `/gsd-complete-milestone v1.2` archival +**Current focus:** Planning next milestone (v1.3) — run `/gsd-new-milestone`. Leading candidates: cross-run log search + job concurrency/queuing. ## Current Position -Milestone: v1.2 — Operator Integration & Insight (SHIPPED 2026-05-19, tags `v1.2.0-rc.1` … `v1.2.0-rc.5`, final `v1.2.0`) -Previous milestone: v1.1 (SHIPPED 2026-04-23, tags `v1.1.0-rc.1` … `v1.1.0-rc.6`, final `v1.1.0`) -Phase: 24 (milestone-close-out-final-v1-2-0-ship) — SHIPPED -Plan: 8 of 8 -Status: v1.2.0 SHIPPED — final tag retags rc.5 SHA 7fb1de5; `:latest` advanced from `:1.1.0` to `:1.2.0`; four-tag equality verified (`:1.2.0` = `:1.2` = `:1` = `:latest` = `sha256:c440f30d…`) -Last activity: 2026-05-19 -- v1.2.0 SHIPPED — retag of rc.5 SHA 7fb1de5 (UAT-validated 2026-05-19) - -Progress: [██████████] 100% +Phase: Milestone v1.2 complete +Plan: — +Status: Awaiting next milestone +Last activity: 2026-05-29 — Milestone v1.2 completed and archived ## v1.2 Roadmap Summary @@ -196,3 +191,7 @@ Stopped at: Phase 24 context gathered Resume command: `/gsd-execute-phase 23` to launch Plan 23-07 (HUMAN-UAT runbook, autonomous=false) **Planned Plan:** 23-07 — HUMAN-UAT runbook (autonomous=false maintainer plan with 6 scenarios covering the three uat-chips-* recipes plus mobile viewport / light-mode / keyboard nav / screen-reader narration / name-filter compose; TAG-06, TAG-07, TAG-08) + +## Operator Next Steps + +- Start the next milestone with /gsd-new-milestone diff --git a/.planning/REQUIREMENTS.md b/.planning/milestones/v1.2-REQUIREMENTS.md similarity index 99% rename from .planning/REQUIREMENTS.md rename to .planning/milestones/v1.2-REQUIREMENTS.md index a8dbb19c..61640e76 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/milestones/v1.2-REQUIREMENTS.md @@ -1,3 +1,12 @@ +# Requirements Archive: v1.2 Operator Integration & Insight + +**Archived:** 2026-05-29 +**Status:** SHIPPED + +For current requirements, see `.planning/REQUIREMENTS.md`. + +--- + # Requirements: Cronduit v1.2 — Operator Integration & Insight **Defined:** 2026-04-25 diff --git a/.planning/milestones/v1.2-ROADMAP.md b/.planning/milestones/v1.2-ROADMAP.md new file mode 100644 index 00000000..4d6aa0c2 --- /dev/null +++ b/.planning/milestones/v1.2-ROADMAP.md @@ -0,0 +1,487 @@ +# Roadmap: Cronduit + +## Milestones + +- ✅ **v1.0 — Docker-Native Cron Scheduler** — Phases 1–9 (shipped 2026-04-14, tags `v1.0.0` + `v1.0.1`) — see [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md) and [`MILESTONES.md`](MILESTONES.md) +- ✅ **v1.1 — Operator Quality of Life** — Phases 10–14 + 12.1 inserted (shipped 2026-04-23, tags `v1.1.0-rc.1`…`v1.1.0-rc.6`, final `v1.1.0`) — see [`milestones/v1.1-ROADMAP.md`](milestones/v1.1-ROADMAP.md) and [`MILESTONES.md`](MILESTONES.md) +- 🚧 **v1.2 — Operator Integration & Insight** — Phases 15–24 (in progress; kicked off 2026-04-25). Five features: webhooks, custom Docker labels (SEED-001), failure context on run detail, per-job exit-code histogram, job tagging. 41 v1.2 requirements across 6 categories (FOUND/WH/LBL/FCTX/EXIT/TAG). Three rc cuts planned (rc.1 → rc.2 → rc.3 → final v1.2.0). + +## Phases + +
+✅ v1.0 Docker-Native Cron Scheduler (Phases 1–9) — SHIPPED 2026-04-14 + +- [x] Phase 1: Foundation, Security Posture & Persistence Base (9/9 plans) — 2026-04-10 +- [x] Phase 2: Scheduler Core & Command/Script Executor (4/4 plans) — 2026-04-10 +- [x] Phase 3: Read-Only Web UI & Health Endpoint (6/6 plans) — 2026-04-11 +- [x] Phase 4: Docker Executor & `container:` Differentiator (4/4 plans) — 2026-04-11 +- [x] Phase 5: Config Reload & `@random` Resolver (5/5 plans) — 2026-04-12 +- [x] Phase 6: Live Events, Metrics, Retention & Release Engineering (7/7 plans) — 2026-04-13 +- [x] Phase 7: v1.0 Cleanup & Bookkeeping (5/5 plans) — 2026-04-13 +- [x] Phase 8: v1.0 Final Human UAT Validation (5/5 plans) — 2026-04-14 +- [x] Phase 9: CI/CD Improvements (4/4 plans) — 2026-04-14 + +**Total:** 49 plans across 9 phases · 86/86 v1.0 requirements Complete · audit verdict `passed` + +
+ +
+✅ v1.1 Operator Quality of Life (Phases 10–14 + 12.1) — SHIPPED 2026-04-23 + +- [x] Phase 10: Stop-a-Running-Job + Hygiene Preamble (10/10 plans) — 2026-04-15 +- [x] Phase 11: Per-Job Run Numbers + Log UX Fixes (15/15 plans + 1 pre-wave spike) — 2026-04-17 +- [x] Phase 12: Docker Healthcheck + rc.1 Cut (7/7 plans) — 2026-04-18; `v1.1.0-rc.1` cut 2026-04-19 +- [x] Phase 12.1: GHCR Tag Hygiene _(INSERTED)_ (4/4 plans) — 2026-04-20 +- [x] Phase 13: Observability Polish — rc.2 (6/6 plans) — 2026-04-21; `v1.1.0-rc.2` cut 2026-04-21 +- [x] Phase 14: Bulk Enable/Disable + rc.3..rc.6 + v1.1.0 final ship (9/9 plans) — 2026-04-23 + +**Total:** 52 plans across 6 phases · 33/33 v1.1 requirements Complete · six rc tags (`rc.1`…`rc.6`) + final `v1.1.0` · `:latest` promoted from `:1.0.1` to `:1.1.0` on both archs + +
+ +### 🚧 v1.2 Operator Integration & Insight (Phases 15–24) — IN PROGRESS + +- [x] **Phase 15: Foundation Preamble** — `Cargo.toml` 1.1.0→1.2.0 bump, `cargo-deny` CI preamble (non-blocking), webhook delivery worker foundation (bounded `mpsc(1024)` + dedicated worker task + drop counter) (completed 2026-04-26) +- [x] **Phase 16: Failure-Context Schema + run.rs:277 Bug Fix** — `DockerExecResult.container_id` field added and assignment corrected, `job_runs.config_hash` per-run column added (Option A), `get_failure_context(job_id)` single-query helper landed (completed 2026-04-28) +- [x] **Phase 17: Custom Docker Labels (SEED-001)** — operator-defined `labels` plumbed through to `bollard::Config::labels`, merge semantics, `cronduit.*` reserved-namespace validator, type-gated validator, `${ENV_VAR}` interpolation in values, size limits (completed 2026-04-29) +- [x] **Phase 18: Webhook Payload + State-Filter + Coalescing** — Standard Webhooks v1 payload schema (`payload_version: "v1"`), per-job + `[defaults]` config with `use_defaults = false` disable, edge-triggered streak coalescing (default fires on `streak_position == 1`, `fire_every` per-job override) (completed 2026-04-29) +- [x] **Phase 19: Webhook HMAC Signing + Receiver Examples** — HMAC-SHA256 only, Standard Webhooks signing-string `webhook-id.webhook-timestamp.payload`, signature header `v1,`, Python/Go/Node receiver examples with constant-time compare (completed 2026-04-30) +- [x] **Phase 20: Webhook SSRF/HTTPS Posture + Retry/Drain + Metrics — rc.1** — HTTPS required for non-loopback/non-RFC1918, 3-attempt full-jitter exponential backoff (t=0/30s/300s × 0.8-1.2× rand), `webhook_deliveries` dead-letter table, 30s drain on shutdown, `cronduit_webhook_*` metric family; **cuts `v1.2.0-rc.1`** (completed 2026-05-01) +- [x] **Phase 21: Failure-Context UI Panel + Exit-Code Histogram Card — rc.2** — Inline collapsed-by-default panel on run-detail with 5 P1 signals (time deltas, image-digest delta, config-hash delta, duration-vs-p50, scheduler-fire-skew), 10-bucket exit-code histogram on job-detail with `stopped` as distinct bucket and exit `0` as separate stat; **cuts `v1.2.0-rc.2`** (completed 2026-05-03) +- [x] **Phase 22: Job Tagging Schema + Validators** — `jobs.tags` JSON column, single-file additive migration, lowercase+trim normalization, charset regex `^[a-z0-9][a-z0-9_-]{0,30}$`, reserved-tag rejection, substring-collision check at config-load (completed 2026-05-04) +- [x] **Phase 23: Job Tagging Dashboard Filter Chips — rc.3** — CSS-only chip components on dashboard, AND filter semantics, untagged-hidden when filter active, URL state via repeated `?tag=`, HTMX dashboard partial swap on chip toggle; **cuts `v1.2.0-rc.3`** (completed 2026-05-05) +- [x] **Phase 24: Milestone Close-Out — final `v1.2.0` ship** — UAT-driven rc loop completion, `THREAT_MODEL.md` Threat Model 5 (Webhook Outbound) + Threat Model 6 (Operator-supplied labels), REQUIREMENTS.md flips to Validated, `MILESTONES.md` v1.2 entry, README updates, `:latest` promoted to `:1.2.0` on both archs (completed 2026-05-17) + +## Phase Details + +### Phase 15: Foundation Preamble + +**Goal**: Establish the v1.2 hygiene baseline and lock the webhook delivery worker isolation pattern before any payload/signing/posture work depends on it. + +**Depends on**: v1.1.0 (Phase 14) shipped + +**Requirements**: FOUND-15, FOUND-16, WH-02 + +**Success Criteria** (what must be TRUE): + + 1. An operator running `cronduit --version` on the first v1.2 commit sees `1.2.0` (not `1.1.0`). + 2. An operator viewing the GitHub Actions PR check list sees a new `cargo-deny` job that runs advisories + licenses + duplicate-versions checks (failures non-blocking on first rc; status visible). + 3. An operator can fire a job whose webhook receiver is stalled for 60 seconds and the next scheduled jobs across the fleet still fire on time (no scheduler drift > 1 s) — the `try_send` non-blocking path holds. + 4. An operator can fill the bounded webhook queue past 1024 entries and observe `cronduit_webhook_delivery_dropped_total` increment with a `warn`-level log line per dropped event; the scheduler loop remains unaffected. + +**Plans:** 5/5 plans complete + +Plans: + +- [x] 15-01-PLAN.md — Cargo.toml version bump 1.1.0 -> 1.2.0 (FOUND-15; the very first v1.2 commit per D-12) +- [x] 15-02-PLAN.md — cargo-deny CI preamble: deny.toml + just deny + ci.yml lint-job step with continue-on-error: true (FOUND-16) +- [x] 15-03-PLAN.md — Webhook module skeleton: src/webhooks/{mod,event,dispatcher,worker}.rs + async-trait promotion + cronduit_webhook_delivery_dropped_total telemetry registration (WH-02) +- [x] 15-04-PLAN.md — Scheduler integration: SchedulerLoop.webhook_tx field + run_job signature/call-site updates + finalize_run step 7d try_send emit + step 7d->7e renumber + bin-layer worker spawn with NoopDispatcher (WH-02) +- [x] 15-05-PLAN.md — Wave-0 integration tests: tests/v12_webhook_queue_drop.rs (T-V12-WH-04) + tests/v12_webhook_scheduler_unblocked.rs (T-V12-WH-03) + extend tests/metrics_endpoint.rs::metrics_families_described_from_boot with HELP/TYPE asserts for the drop counter (WH-02) + +### Phase 16: Failure-Context Schema + run.rs:277 Bug Fix + +**Goal**: Fix the silent v1.1 `job_runs.container_id` regression and land the per-run schema columns + streak query helper that the webhook payload (Phase 18) and failure-context UI (Phase 21) both consume. + +**Depends on**: Phase 15 + +**Requirements**: FOUND-14, FCTX-04, FCTX-07 + +**Success Criteria** (what must be TRUE): + + 1. An operator inspecting a v1.2 docker job run via the database sees `job_runs.container_id` populated with the real Docker container ID (not a `sha256:...` image digest); historical v1.1 rows age out via the Phase 6 retention pruner. + 2. An operator viewing two consecutive runs of the same job after a hot reload sees distinct `job_runs.config_hash` values when the underlying TOML actually changed (per-RUN column, not the per-JOB proxy). + 3. An operator inspecting the `EXPLAIN QUERY PLAN` for `get_failure_context(job_id)` on both SQLite and Postgres sees indexed access on `job_runs.job_id + start_time`; the function returns `streak_position`, `consecutive_failures`, `last_success_run_id`, `last_success_image_digest`, and `last_success_config_hash` from a single SQL query (not five separate round-trips). + +**Plans:** 7/7 plans complete + +Plans: + +- [x] 16-01-PLAN.md — Migrations: image_digest add + config_hash add + config_hash backfill (6 files: 3 per backend) + tests/v12_fctx_config_hash_backfill.rs +- [x] 16-02-PLAN.md — DockerExecResult.container_id field + 7 literal sites populated (struct widening) +- [x] 16-03-PLAN.md — run.rs:301 bug fix + parallel image_digest_for_finalize local + tests/v12_run_rs_277_bug_fix.rs +- [x] 16-04a-PLAN.md — DB layer (queries.rs only): finalize_run + insert_running_run signature changes; DbRun/DbRunDetail field add; SELECT-site updates (5 tasks) +- [x] 16-04b-PLAN.md — Callers + recipe + wave-end gate: 4 production callers + 5 test-mod callers + just uat-fctx-bugfix-spot-check + full CI gate (5 tasks) +- [x] 16-05-PLAN.md — get_failure_context query helper + FailureContext struct + tests/v12_fctx_streak.rs (5 streak scenarios + FCTX-04 write-site) +- [x] 16-06-PLAN.md — tests/v12_fctx_explain.rs (EXPLAIN QUERY PLAN on SQLite + Postgres asserting idx_job_runs_job_id_start) + +### Phase 17: Custom Docker Labels (SEED-001) + +**Goal**: Operators can attach arbitrary Docker labels to cronduit-spawned containers (Traefik, Watchtower, backup tooling interop) with locked merge semantics, a reserved cronduit.* namespace, and type-gated validation. + +**Depends on**: Phase 15 (independent of FCTX work — slots in here for the foundation block rc.1) + +**Requirements**: LBL-01, LBL-02, LBL-03, LBL-04, LBL-05, LBL-06 + +**Success Criteria** (what must be TRUE): + + 1. An operator who adds `labels = { "com.centurylinklabs.watchtower.enable" = "false" }` to a `[[jobs]]` block sees that label on the spawned container via `docker inspect` (the cronduit-internal `cronduit.run_id` and `cronduit.job_name` labels remain intact). + 2. An operator who sets `use_defaults = false` on a per-job labels map gets ONLY the per-job labels (defaults replaced); without `use_defaults = false` the defaults map is merged and per-job keys win on collision. + 3. An operator who tries to set `cronduit.foo = "bar"` (or any `cronduit.*` key) gets a config-load error pointing at the offending key — the validator runs at LOAD time, not runtime. + 4. An operator who tries to set `labels = ...` on a `type = "command"` or `type = "script"` job gets a clear config-load error explaining that labels apply only to `type = "docker"`. + 5. An operator who writes `labels = { "deployment.id" = "${DEPLOYMENT_ID}" }` sees the env var interpolated in the value at config-load (keys are never interpolated); a value > 4 KB or a label set summing > 32 KB is rejected at load. + +**Plans:** 9 plans (6 core + 3 gap closure) + +Plans: + +- [x] 17-01-PLAN.md — schema + 5-layer parity + apply_defaults merge (LBL-01 + LBL-02) +- [x] 17-02-PLAN.md — four LOAD-time validators (LBL-03, LBL-04, LBL-06, D-02 key chars) +- [x] 17-03-PLAN.md — bollard plumb-through + 3 testcontainers integration tests (LBL-01, LBL-02, LBL-05) +- [x] 17-04-PLAN.md — examples/cronduit.toml: 3 integration patterns +- [x] 17-05-PLAN.md — README § Configuration > Labels subsection (mermaid + table + 6 rules) +- [x] 17-06-PLAN.md — SEED-001 close-out + 17-HUMAN-UAT.md +- [x] 17-07-PLAN.md — CR-01 gap closure: README env-var interpolation prose + validator/interpolate docstrings + two key-position regression tests (LBL-05) +- [x] 17-08-PLAN.md — CR-02 gap closure: set-diff in `check_labels_only_on_docker_jobs` + 3 unit tests + 1 integration test (LBL-04) +- [x] 17-09-PLAN.md — REQUIREMENTS.md bookkeeping: flip LBL-01..LBL-06 from Pending to Complete + +**UI hint**: yes + +### Phase 18: Webhook Payload + State-Filter + Coalescing + +**Goal**: Operators can configure per-job webhook URLs that fire on a state-filter list with edge-triggered streak coalescing; payloads adhere to the Standard Webhooks v1 spec. + +**Depends on**: Phase 15 (worker), Phase 16 (streak query helper) + +**Requirements**: WH-01, WH-03, WH-06, WH-09 + +**Success Criteria** (what must be TRUE): + + 1. An operator who configures `webhook = { url = "https://hook.example.com", states = ["failed", "timeout", "stopped"] }` per job (and/or in `[defaults]` with the `use_defaults = false` disable pattern) sees deliveries fire only on the listed terminal statuses — `success` runs do NOT fire. + 2. An operator running a `* * * * *` failing job sees ONE webhook delivery on the first failure of a new streak by default (not 30 deliveries over 30 minutes); setting `webhook.fire_every = 0` restores the legacy per-failure firing. + 3. An operator inspecting a delivered webhook payload sees the locked v1.2.0 schema fields: `payload_version: "v1"`, `event_type: "run_finalized"`, `run_id`, `job_id`, `job_name`, `status`, `exit_code`, `started_at`, `finished_at`, `duration_ms`, `streak_position`, `consecutive_failures`, `image_digest` (docker only), `config_hash`, `tags`, `cronduit_version`. + 4. An operator inspecting delivered headers sees `webhook-id`, `webhook-timestamp`, and `webhook-signature` (Standard Webhooks v1 spec) on every delivery. + +**Plans:** 6/6 plans complete + +Plans: + +- [x] 18-01-PLAN.md — Foundation: Cargo deps (reqwest 0.13 rustls / hmac / base64 / ulid + wiremock dev) + just test-unit recipe + 2 new webhook counters described+zero-baselined +- [x] 18-02-PLAN.md — WH-01: WebhookConfig struct + apply_defaults webhook merge + check_webhook_url + check_webhook_block_completeness validators (incl. Pitfall H empty-secret) +- [x] 18-03-PLAN.md — WH-06+WH-09: WebhookPayload encoder (15-field v1 schema) + coalesce::filter_position SQL helper + EXPLAIN PLAN regression test +- [x] 18-04-PLAN.md — WH-03: HttpDispatcher impl (Standard Webhooks v1 headers + HMAC-SHA256 sign_v1 + reqwest 0.13 rustls Client + should_fire D-16 matrix) +- [x] 18-05-PLAN.md — Bin-layer wire-up (HttpDispatcher swap in src/cli/run.rs) + 6 wiremock integration tests (e2e signed, unsigned, state-filter, 3x metric counter) +- [x] 18-06-PLAN.md — Maintainer UAT: 3 new just recipes + examples/webhook_mock_server.rs + examples/cronduit.toml extension + 18-HUMAN-UAT.md (autonomous=false; maintainer-validated) + +### Phase 19: Webhook HMAC Signing + Receiver Examples + +**Goal**: Operators can verify webhook authenticity using HMAC-SHA256 and the Standard Webhooks signing-string convention; ship reference receiver examples that demonstrate constant-time HMAC compare. + +**Depends on**: Phase 18 + +**Requirements**: WH-04 + +**Success Criteria** (what must be TRUE): + + 1. An operator who configures `webhook.secret = "..."` on a job sees the `webhook-signature` header value formatted as `v1,` where the HMAC is computed over `webhook-id.webhook-timestamp.payload` raw bytes using SHA-256. + 2. An operator running the shipped Python, Go, and Node receiver examples successfully verifies signatures from a real cronduit delivery; each example uses a constant-time compare primitive (Python `hmac.compare_digest`, Go `hmac.Equal`, Node `crypto.timingSafeEqual`) — NOT `==` on hex bytes. + 3. An operator reviewing the receiver-example docs sees an explicit note that v1.2 ships SHA-256 only (no algorithm-agility / multi-secret rotation cronduit-side; rotation is a receiver concern). + +**Plans:** 6/6 plans complete + +Plans: + +- [x] 19-01-PLAN.md — Wave 1: tests/fixtures/webhook-v1/* fixture + in-module sign_v1_locks_interop_fixture Rust test (Pitfall 1: pub(crate)) +- [x] 19-02-PLAN.md — Wave 2: examples/webhook-receivers/python/ stdlib receiver + 2 just recipes (port 9991, hmac.compare_digest) +- [x] 19-03-PLAN.md — Wave 2: examples/webhook-receivers/go/ stdlib receiver + 2 just recipes (port 9992, hmac.Equal) +- [x] 19-04-PLAN.md — Wave 2: examples/webhook-receivers/node/ stdlib receiver + 2 just recipes (port 9993, crypto.timingSafeEqual + Pitfall 2 length guard) +- [x] 19-05-PLAN.md — Wave 3: docs/WEBHOOKS.md operator hub + CONFIG.md back-link + README pointer + 3 wh-example-receiver-* jobs +- [x] 19-06-PLAN.md — Wave 3: ci.yml webhook-interop matrix (Python/Go/Node) + 19-HUMAN-UAT.md (autonomous=false; 11 maintainer-validated scenarios) + +### Phase 20: Webhook SSRF/HTTPS Posture + Retry/Drain + Metrics — rc.1 + +**Goal**: Lock the webhook security posture (HTTPS for non-local destinations, SSRF accepted-risk documented), the retry/dead-letter behavior, the graceful-shutdown drain, and the Prometheus metric family — then cut `v1.2.0-rc.1` covering the foundation block. + +**Depends on**: Phase 19 + +**Requirements**: WH-05, WH-07, WH-08, WH-10, WH-11 + +**Success Criteria** (what must be TRUE): + + 1. An operator who configures a webhook URL like `http://example.com` (non-loopback, non-RFC1918) sees a config-load error; `http://` is permitted only for `127.0.0.0/8`, `::1`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, and `fd00::/8`. + 2. An operator whose receiver returns 500 sees three delivery attempts at approximately t=0, t=30 s, t=300 s (each multiplied by `rand()*0.4 + 0.8` full-jitter); after the third attempt, the delivery is recorded in the `webhook_deliveries` dead-letter table and `cronduit_webhook_deliveries_total{status="failed"}` increments. + 3. An operator sending SIGTERM with deliveries in-flight sees the worker drain the queue for up to `webhook_drain_grace = "30s"` (configurable), then drop remaining queued deliveries with a counter increment; in-flight HTTP requests are NOT cancelled mid-flight. + 4. An operator scraping `/metrics` sees the new `cronduit_webhook_*` family eagerly described at boot: `cronduit_webhook_deliveries_total{job, status}` (closed enum: success/failed/dropped), `cronduit_webhook_delivery_duration_seconds{job}` histogram, `cronduit_webhook_queue_depth` gauge. + 5. An operator pushing the `v1.2.0-rc.1` tag sees the GHCR image published at `ghcr.io/SimplicityGuy/cronduit:v1.2.0-rc.1` on both amd64 and arm64; the `:latest` tag still points at `v1.1.0` (rc tag gating from v1.1's D-10 holds). + +**Plans:** 9/9 plans complete + +Plans: +**Wave 1** + +- [x] 20-01-PLAN.md — DLQ migration (sqlite + postgres) + WebhookDlqRow/insert/delete helpers + retention Phase 4 + 7 Wave 0 test stubs + +**Wave 2** *(blocked on Wave 1 completion)* + +- [x] 20-02-PLAN.md — RetryingDispatcher in src/webhooks/retry.rs + classification + jitter + Retry-After + DLQ writes + 4 integration tests +- [x] 20-03-PLAN.md — HTTPS-required validator extension in src/config/validate.rs + INFO log + integration tests + +**Wave 3** *(blocked on Wave 2 completion)* + +- [x] 20-05-PLAN.md — labeled metric family migration (deliveries_total{job,status} + delivery_duration_seconds + queue_depth); P15 dropped counter preserved + +**Wave 4** *(blocked on Wave 3 completion)* + +- [x] 20-04-PLAN.md — worker_loop drain budget (3rd select! arm) + queue_depth gauge + drain integration tests + +**Wave 5** *(blocked on Wave 4 completion)* + +- [x] 20-06-PLAN.md — webhook_drain_grace config field + RetryingDispatcher wiring + per-job metric pre-seed in src/cli/run.rs +- [x] 20-07-PLAN.md — docs/WEBHOOKS.md 6-section extension + 2 mermaid diagrams + TM5 forward-pointer stub + +**Wave 6** *(blocked on Wave 5 completion)* + +- [x] 20-08-PLAN.md — UAT recipes (uat-webhook-retry/drain/dlq-query/https-required) + 20-HUMAN-UAT.md (autonomous=false) + +**Wave 7** *(blocked on Wave 6 completion)* + +- [x] 20-09-PLAN.md — rc.1 pre-flight checklist (autonomous=false; maintainer cuts v1.2.0-rc.1 tag locally; no release.yml/cliff.toml/release-rc.md edits per D-30) + +**Cross-cutting constraints:** + +- Per D-38: Cargo.toml unchanged; `cargo tree -i openssl-sys` empty. + +### Phase 21: Failure-Context UI Panel + Exit-Code Histogram Card — rc.2 + +**Goal**: Operators get a rich failure-context panel on the run-detail page (5 P1 signals) and a per-job exit-code histogram card on the job-detail page (10-bucket strategy with stopped distinct from signal-killed). + +**Depends on**: Phase 16 (FCTX schema + bug fix + streak helper) + +**Requirements**: FCTX-01, FCTX-02, FCTX-03, FCTX-05, FCTX-06, EXIT-01, EXIT-02, EXIT-03, EXIT-04, EXIT-05, EXIT-06 + +**Success Criteria** (what must be TRUE): + + 1. An operator viewing a `failed` or `timeout` run-detail page sees a collapsed-by-default failure-context panel that expands to show 5 labeled rows: time-based deltas (first-failure timestamp, consecutive-failure streak, link to last successful run), image-digest delta (docker jobs only — non-docker hides the row), config-hash delta ("config changed since last success: Yes/No"), duration-vs-p50 deviation (suppressed below 5 sample threshold), and scheduler-fire-time vs run-start-time skew. + 2. An operator viewing a `success`, `cancelled`, `running`, or `stopped` run-detail page does NOT see the failure-context panel (gated to failed/timeout only). + 3. An operator viewing a job-detail page sees a new exit-code-distribution card (sibling to the v1.1 p50/p95 duration card) showing the last 100 ALL runs bucketed into 10 fixed buckets (0 / 1 / 2 / 3-9 / 10-126 / 127 / 128-143 / 144-254 / 255 / null); below `N=5` sample threshold the card renders "—". + 4. An operator viewing the histogram sees `stopped` runs (which exit 137 from cronduit's SIGKILL) rendered as a DISTINCT visual bucket separate from `128-143` (signal-killed), using the `--cd-status-stopped` color from v1.1; success (`0`) is rendered as a separate stat badge, NOT a bar in the histogram. + 5. An operator pushing the `v1.2.0-rc.2` tag sees the GHCR image published at `ghcr.io/SimplicityGuy/cronduit:v1.2.0-rc.2` on both architectures; the `:latest` tag still points at `v1.1.0`. + +**Plans**: TBD +**UI hint**: yes + +### Phase 22: Job Tagging Schema + Validators + +**Goal**: Operators can attach normalized tags to jobs in TOML config; tags persist to a JSON column, validate against a strict charset, and reject substring-collisions at config-load. + +**Depends on**: Phase 15 (independent of webhook/FCTX work) + +**Requirements**: TAG-01, TAG-02, TAG-03, TAG-04, TAG-05 (plus closes WH-09 from Phase 18 — webhook payload backfill end-to-end) + +**Success Criteria** (what must be TRUE): + + 1. An operator who writes `tags = ["backup", "weekly"]` on a `[[jobs]]` block sees those tags persisted to the new `jobs.tags` JSON column; the field is per-job only (NOT supported in `[defaults]`). + 2. An operator who writes `tags = ["Backup", "backup ", "BACKUP"]` sees a config-load WARN that the entries collapse to `["backup"]` after lowercase + trim normalization (the WARN flags the deduplication so operators notice). + 3. An operator who writes a tag like `MyTag!` or `cronduit` (reserved) gets a config-load ERROR pointing at the offending tag — the validator never silently mutates; the charset regex `^[a-z0-9][a-z0-9_-]{0,30}$` is enforced. + 4. An operator who configures one job with `tags = ["back"]` and another with `tags = ["backup"]` gets a config-load ERROR (substring-collision check) — the SQL filter `tags LIKE '%"' || ?tag || '"%'` would otherwise produce false positives. + +**Plans:** 4/6 plans executed + +Plans: +**Wave 1** + +- [x] 22-01-PLAN.md — Schema + serde field + sqlite/postgres migration pair (TAG-01, TAG-02) + +**Wave 2** *(blocked on Wave 1 completion)* + +- [x] 22-02-PLAN.md — Four validators in `validate.rs`: charset+reserved, count cap (16), fleet-level substring-collision; D-04 order locked (TAG-03, TAG-04, TAG-05) +- [x] 22-03-PLAN.md — DB plumbing: upsert_job widening + DbRunDetail.tags + get_run_by_id row-map + sync.rs callers + hash.rs D-01 comment + tags_excluded_from_hash regression test (TAG-02) + +**Wave 3** *(blocked on Wave 2 completion)* + +- [x] 22-04-PLAN.md — WH-09 webhook payload backfill: src/webhooks/payload.rs L88 vec![] → run.tags.clone(); test rename payload_tags_carries_real_values (WH-09) + +**Wave 4** *(blocked on Wave 3 completion)* + +- [ ] 22-05-PLAN.md — Integration tests (tests/v12_tags_validators.rs) + examples/cronduit.toml demo line + three uat-tags-* just recipes (TAG-01..05) + +**Wave 5** *(blocked on Wave 4 completion)* + +- [ ] 22-06-PLAN.md — 22-HUMAN-UAT.md maintainer runbook (autonomous=false; 4 scenarios per D-10) (TAG-01..05) + +**Cross-cutting constraints:** + +- No new external crates; `cargo tree -i openssl-sys` remains empty (D-17) + +### Phase 23: Job Tagging Dashboard Filter Chips — rc.3 + +**Goal**: Operators get CSS-only filter chips on the dashboard with AND semantics across selected tags, untagged-hidden when filter active, shareable URL state — then cut `v1.2.0-rc.3`. + +**Depends on**: Phase 22 + +**Requirements**: TAG-06, TAG-07, TAG-08 + +**Success Criteria** (what must be TRUE): + + 1. An operator viewing the dashboard sees filter chips for every distinct tag in the current fleet; clicking a chip toggles its filter state (active = teal-bordered + bold; inactive = grey). + 2. An operator with multiple active chips sees only jobs that have ALL active tags (AND semantics); the active filter composes with the existing v1.0 name-filter via AND (job must match BOTH). + 3. An operator with any active tag filter sees untagged jobs HIDDEN from the dashboard (least-surprise behavior). + 4. An operator can share a filtered dashboard URL like `/?tag=backup&tag=weekly` (repeated `?tag=` params); the chips render in the active state on page load — bookmarkable. + 5. An operator pushing the `v1.2.0-rc.3` tag sees the GHCR image published at `ghcr.io/SimplicityGuy/cronduit:v1.2.0-rc.3` on both architectures. + +**Plans:** 8/8 plans complete + +Plans: +**Wave 0** + +- [x] 23-01-PLAN.md — Wave-0 test scaffolding: tests/v12_tags_dashboard.rs + dashboard.rs::tests stubs (V-W0) + +**Wave 1** *(blocked on Wave 0 completion)* + +- [x] 23-02-PLAN.md — DB layer: DashboardJob.tags + get_dashboard_jobs SELECT/WHERE widening for AND-tag filter + caller passes &[] placeholder (TAG-07; V-01..V-04) + +**Wave 2** *(blocked on Wave 1 completion — file overlap on dashboard.rs)* + +- [x] 23-03-PLAN.md — Handler: axum_extra::Query swap + DashboardParams.tags + fleet-tag fold + active-set sort/dedup/intersect + view-models gain fleet_tags/active_tags (TAG-06; V-05, V-07) + +**Wave 3** *(blocked on Wave 2 completion)* + +- [x] 23-04-PLAN.md — CSS: cd-tag-chip-* family in @layer components + reduced-motion + print extensions; zero new tokens (TAG-06, TAG-08) +- [x] 23-05-PLAN.md — Template: ChipView precompute + chip strip insert above filter row + sort-header href widening + poll hx-include widening + OOB swap composition (TAG-06, TAG-08; V-06, V-08..V-14) + +**Wave 4** *(blocked on Wave 3 completion)* + +- [x] 23-06-PLAN.md — UAT: 3 just uat-chips-* recipes + README Tag Filter Chips subsection (TAG-06, TAG-07; V-15, V-16) + +**Wave 5** *(blocked on Wave 4 completion)* + +- [x] 23-07-PLAN.md — HUMAN-UAT: 23-HUMAN-UAT.md autonomous=false maintainer plan with 6 scenarios (TAG-06, TAG-07, TAG-08) + +**Wave 6** *(blocked on Wave 5 completion)* + +- [x] 23-08-PLAN.md — RC3-PREFLIGHT: 23-RC3-PREFLIGHT.md autonomous=false rc.3 cut runbook (mirrors P21 RC2-PREFLIGHT verbatim modulo rc.2→rc.3 + P21→P23 substitutions; V-17) + +**Cross-cutting constraints:** + +- No new external crates; `axum_extra` already in tree; `cargo tree -i openssl-sys` remains empty (D-23) +- NO modifications to `release.yml` / `cliff.toml` / `docs/release-rc.md` (D-15 / D-16) +- `Cargo.toml` stays at `1.2.0`; `-rc.3` is tag-only (D-22) + +**UI hint**: yes + +### Phase 24: Milestone Close-Out — final `v1.2.0` ship + +**Goal**: Run the UAT-driven rc loop to closure (rc.3 → final v1.2.0), update threat model + documentation, flip REQUIREMENTS.md to Validated, write the v1.2 MILESTONES entry, and promote `:latest` to `1.2.0`. + +**Depends on**: Phase 23 + +**Requirements**: n/a — operational close-out (no v1.2 REQ-IDs; mirrors v1.0 Phase 9 pattern). All 41 v1.2 requirements covered by Phases 15–23. + +**Success Criteria** (what must be TRUE): + + 1. An operator reading `THREAT_MODEL.md` sees Threat Model 5 (Webhook Outbound — operator-with-UI-access can configure outbound HTTP at any URL; SSRF accepted risk; HTTPS posture; loopback-bound default mitigation) and Threat Model 6 (Operator-supplied Docker labels — reserved-namespace clobber, type-gated validator, size-limit DoS surface). + 2. An operator reviewing `.planning/REQUIREMENTS.md` (the v1.2 file) sees every requirement (FOUND-14..16, WH-01..11, LBL-01..06, FCTX-01..07, EXIT-01..06, TAG-01..08) flipped to Validated with a Phase reference. + 3. An operator reading `MILESTONES.md` sees a new v1.2 entry with shipped tags, phase count, plan count, and key accomplishments — formatted consistently with v1.0 and v1.1 entries. + 4. An operator inspecting `ghcr.io/SimplicityGuy/cronduit:latest` after final ship sees the digest match `:1.2.0` on both amd64 and arm64; `:1.2.0` == `:1.2` == `:1` == `:latest` (D-18 four-tag equality verified). The `cargo-deny` CI job is promoted from non-blocking (warn) to blocking (error) before the final tag is pushed. + 5. An operator running `docker compose up` against the shipped quickstart with the new `v1.2.0` image observes the cronduit container reporting `healthy` (v1.1 healthcheck still works), the dashboard renders with new tag filter chips (no regressions on v1.0/v1.1 surfaces), and a webhook configured against a local mock receiver delivers a Standard-Webhooks-spec payload on the first failure. + +**Plans**: 8 plans + +Plans: +**Wave 1** + +- [x] 24-02-PLAN.md — Milestone audit: invoke /gsd-audit-milestone v1.2; v1.2-MILESTONE-AUDIT.md + REQUIREMENTS.md flips (20 items) + ROADMAP drift cleanup (P17 Complete / P21 11/11 / P22 6/6) +- [x] 24-05-PLAN.md — cargo-deny WARN→ERROR promotion (FOUND-16): ci.yml continue-on-error removed; conditional deny.toml/Cargo.lock advisory remediation + +**Wave 2** *(blocked on Wave 1 completion)* + +- [x] 24-01-PLAN.md — Threat model close-out: TM5 in-place canonical rewrite + new TM6 + STRIDE rows T-S3/T-T4/T-I4/T-D4 + Changelog + README §Security link-back to TM5/TM6 anchors +- [x] 24-03-PLAN.md — MILESTONES.md v1.2 release-log entry (additive at top, mirrors v1.1/v1.0 six-row shape) + +**Wave 3** *(blocked on Wave 2 completion)* + +- [x] 24-04-PLAN.md — README updates: v1.2 What's New hero block + §Features pointer (FCTX + exit histogram) + §Configuration §Webhooks subsection + MILESTONES cross-link + +**Wave 4** *(blocked on Wave 3 completion)* + +- [x] 24-06-PLAN.md — 24-RC4-PREFLIGHT.md (autonomous=false maintainer runbook for v1.2.0-rc.4 tag cut) + +**Wave 5** *(blocked on Wave 4 completion)* + +- [x] 24-07-PLAN.md — 24-HUMAN-UAT.md (autonomous=false maintainer six-scenario UAT runbook covering v1.0/v1.1 regression + all five v1.2 features end-to-end) + +**Wave 6** *(blocked on Wave 5 completion)* + +- [x] 24-08-PLAN.md — 24-FINAL-SHIP-PREFLIGHT.md (autonomous=false maintainer final-tag runbook: retag last-passing-UAT rc.N SHA as v1.2.0; verify four-tag equality + cargo-deny ERROR-gate; flip STATE.md SHIPPED) + +## Progress + +| Milestone | Phases | Plans | Status | Shipped | +| --------- | ------ | ----- | ------ | ------- | +| v1.0 | 1–9 | 49/49 | ✅ Complete | 2026-04-14 | +| v1.1 | 10–14 (+ 12.1) | 52/52 | ✅ Complete | 2026-04-23 | +| v1.2 | 15–24 | 0/— | 🚧 In progress | — | + +### v1.2 Phase Tracker + +| Phase | Plans Complete | Status | Completed | +|-------|----------------|--------|-----------| +| 15. Foundation Preamble | 5/5 | Complete | 2026-04-26 | +| 16. Failure-Context Schema + run.rs Bug Fix | 7/7 | Complete | 2026-04-28 | +| 17. Custom Docker Labels (SEED-001) | 6/6 + 3 gap closure | Complete | 2026-04-29 | +| 18. Webhook Payload + State-Filter + Coalescing | 6/6 | Complete | 2026-04-29 | +| 19. Webhook HMAC Signing + Receiver Examples | 6/6 | Complete | 2026-04-30 | +| 20. Webhook SSRF/HTTPS + Retry/Drain + Metrics — rc.1 | 9/9 | Complete | 2026-05-01 | +| 21. Failure-Context UI + Exit-Code Histogram — rc.2 | 11/11 | Complete | 2026-05-03 | +| 22. Job Tagging Schema + Validators | 6/6 | Complete | 2026-05-04 | +| 23. Job Tagging Dashboard Filter Chips — rc.3 | 8/8 | Complete | 2026-05-05 | +| 24. Milestone Close-Out — final v1.2.0 | 8/8 | Complete | 2026-05-17 | + +## v1.2 Build Order + +```mermaid +flowchart LR + P15["Phase 15
Foundation Preamble
(Cargo bump + cargo-deny
+ webhook worker)"] + P16["Phase 16
FCTX Schema
+ run.rs:277 fix
+ streak helper"] + P17["Phase 17
Docker Labels
(SEED-001)"] + P18["Phase 18
Webhook Payload
+ state filter
+ coalescing"] + P19["Phase 19
Webhook HMAC
+ receiver examples"] + P20["Phase 20
Webhook SSRF/HTTPS
+ retry/drain
+ metrics"] + RC1(["v1.2.0-rc.1"]) + P21["Phase 21
FCTX UI panel
+ Exit-code histogram"] + RC2(["v1.2.0-rc.2"]) + P22["Phase 22
Tagging schema
+ validators"] + P23["Phase 23
Tagging filter chips"] + RC3(["v1.2.0-rc.3"]) + P24["Phase 24
Close-out
+ TM5/TM6"] + SHIP(["v1.2.0
SHIPPED"]) + + P15 --> P16 + P15 --> P17 + P15 --> P22 + P16 --> P18 + P17 --> P20 + P18 --> P19 + P19 --> P20 + P20 --> RC1 + RC1 --> P21 + P16 --> P21 + P21 --> RC2 + RC2 --> P22 + P22 --> P23 + P23 --> RC3 + RC3 --> P24 + P24 --> SHIP + + classDef foundation fill:#0a3d0a,stroke:#00ff7f,stroke-width:2px,color:#e0ffe0 + classDef webhooks fill:#1a1a3d,stroke:#7fbfff,stroke-width:2px,color:#e0e0ff + classDef insight fill:#3d2a1a,stroke:#ffbf7f,stroke-width:2px,color:#ffe0c0 + classDef organize fill:#2a1a3d,stroke:#bf7fff,stroke-width:2px,color:#f0e0ff + classDef rc fill:#2a1a3d,stroke:#bf7fff,stroke-width:2px,color:#f0e0ff + classDef ship fill:#00ff7f,stroke:#00ff7f,stroke-width:3px,color:#0a1a0a + class P15,P16 foundation + class P18,P19,P20 webhooks + class P21 insight + class P17,P22,P23 organize + class P24 close + class RC1,RC2,RC3 rc + class SHIP ship +``` + +--- + +*v1.0 archived 2026-04-14 via `/gsd-complete-milestone`. Full historical roadmap, requirements, audit, and execution history preserved under `.planning/milestones/v1.0-*`.* + +*v1.1 archived 2026-04-24 via `/gsd-complete-milestone`. Full roadmap and requirements preserved under `.planning/milestones/v1.1-*`. Phase execution history archived to `.planning/milestones/v1.1-phases/`.* + +*v1.2 roadmap created 2026-04-25 via `/gsd-roadmapper`. 10 phases (15–24), 41 requirements across 6 categories, three rc cuts planned (rc.1 after Phase 20 / rc.2 after Phase 21 / rc.3 after Phase 23 → final v1.2.0 in Phase 24). Strict dependency ordering: P15 (webhook worker foundation) before P18/P19/P20; P16 (FCTX schema + bug fix) before P18 + P21; P22 (tagging schema) before P23. Granularity=standard.* diff --git a/.planning/phases/16-failure-context-schema-run-rs-277-bug-fix/16-HUMAN-UAT.md b/.planning/phases/16-failure-context-schema-run-rs-277-bug-fix/16-HUMAN-UAT.md index 8914b795..f8b45de0 100644 --- a/.planning/phases/16-failure-context-schema-run-rs-277-bug-fix/16-HUMAN-UAT.md +++ b/.planning/phases/16-failure-context-schema-run-rs-277-bug-fix/16-HUMAN-UAT.md @@ -2,7 +2,7 @@ phase: 16 gathered: 2026-04-27 validated: 2026-04-28 -status: validated +status: complete scope: minimal — one spot check for FOUND-14 operator-observable --- diff --git a/.planning/phases/17-custom-docker-labels-seed-001/17-HUMAN-UAT.md b/.planning/phases/17-custom-docker-labels-seed-001/17-HUMAN-UAT.md index efd6c9d3..ebb285ff 100644 --- a/.planning/phases/17-custom-docker-labels-seed-001/17-HUMAN-UAT.md +++ b/.planning/phases/17-custom-docker-labels-seed-001/17-HUMAN-UAT.md @@ -1,3 +1,10 @@ +--- +phase: 17 +type: human-uat +status: complete +validated: "Maintainer (Robert) 2026-04-29 — all 6 UAT items passed locally per D-09; shipped in v1.2.0" +--- + # Phase 17 — Human UAT (SEED-001 Custom Docker Labels) **Owner:** Maintainer (Robert) diff --git a/.planning/phases/17-custom-docker-labels-seed-001/17-VERIFICATION.md b/.planning/phases/17-custom-docker-labels-seed-001/17-VERIFICATION.md index dded6553..224b302b 100644 --- a/.planning/phases/17-custom-docker-labels-seed-001/17-VERIFICATION.md +++ b/.planning/phases/17-custom-docker-labels-seed-001/17-VERIFICATION.md @@ -1,7 +1,8 @@ --- phase: 17-custom-docker-labels-seed-001 verified: 2026-04-29T01:53:49Z -status: human_needed +status: passed +human_validated: "Maintainer (Robert) 2026-05-19 — U5 spot-check passed (2026-04-29); CR-01/CR-02 architectural decisions accepted as documented residual behavior and shipped in v1.2.0; marked passed by maintainer instruction" score: 4/5 must-haves verified (SC-1 deferred to user UAT, fully passed; SC-2/3/5 verified in code; SC-4 partial — core verified; SC-5 partial — value-side verified, key-side contract silently broken under env-set conditions per CR-01) overrides_applied: 0 gaps: @@ -49,7 +50,7 @@ human_verification: **Phase Goal:** Operators can attach arbitrary Docker labels to cronduit-spawned containers (Traefik, Watchtower, backup tooling interop) with locked merge semantics, a reserved cronduit.* namespace, and type-gated validation. **Verified:** 2026-04-29T01:53:49Z -**Status:** human_needed +**Status:** passed — maintainer-validated 2026-05-19 (v1.2.0 ship UAT) **Re-verification:** No — initial verification ## Goal Achievement diff --git a/.planning/phases/18-webhook-payload-state-filter-coalescing/18-HUMAN-UAT.md b/.planning/phases/18-webhook-payload-state-filter-coalescing/18-HUMAN-UAT.md index dda2fbf0..3763620a 100644 --- a/.planning/phases/18-webhook-payload-state-filter-coalescing/18-HUMAN-UAT.md +++ b/.planning/phases/18-webhook-payload-state-filter-coalescing/18-HUMAN-UAT.md @@ -1,3 +1,10 @@ +--- +phase: 18 +type: human-uat +status: complete +validated: "Maintainer (Robert) 2026-05-19 — UAT validated during the v1.2.0 ship UAT" +--- + # Phase 18 Human UAT — Webhook Payload + State-Filter + Coalescing > **Maintainer-validated only.** Per project memory `feedback_uat_user_validates.md`, Claude does NOT mark these scenarios passed — the maintainer runs each scenario and flips the `[ ]` to `[x]` themselves. Per `feedback_uat_use_just_commands.md`, every step references a `just` recipe — NEVER raw `curl`/`cargo`/`docker`. diff --git a/.planning/phases/19-webhook-hmac-signing-receiver-examples/19-HUMAN-UAT.md b/.planning/phases/19-webhook-hmac-signing-receiver-examples/19-HUMAN-UAT.md index f8d698b3..4fa3ad21 100644 --- a/.planning/phases/19-webhook-hmac-signing-receiver-examples/19-HUMAN-UAT.md +++ b/.planning/phases/19-webhook-hmac-signing-receiver-examples/19-HUMAN-UAT.md @@ -1,3 +1,10 @@ +--- +phase: 19 +type: human-uat +status: complete +validated: "Maintainer (Robert) 2026-05-19 — U1–U11 validated during the v1.2.0 ship UAT" +--- + # Phase 19 Human UAT — Webhook HMAC Signing + Receiver Examples > **Maintainer-validated only.** Per project memory `feedback_uat_user_validates.md`, Claude does NOT mark these scenarios passed — the maintainer runs each scenario and flips the `[ ]` to `[x]` themselves. Per `feedback_uat_use_just_commands.md`, every step references a `just` recipe — NEVER raw `curl`/`cargo`/`docker`. diff --git a/.planning/phases/19-webhook-hmac-signing-receiver-examples/19-VERIFICATION.md b/.planning/phases/19-webhook-hmac-signing-receiver-examples/19-VERIFICATION.md index 5c707152..54fce034 100644 --- a/.planning/phases/19-webhook-hmac-signing-receiver-examples/19-VERIFICATION.md +++ b/.planning/phases/19-webhook-hmac-signing-receiver-examples/19-VERIFICATION.md @@ -1,7 +1,8 @@ --- phase: 19-webhook-hmac-signing-receiver-examples verified: 2026-04-30T20:15:00Z -status: human_needed +status: passed +human_validated: "Maintainer (Robert) 2026-05-19 — U1–U11 validated during the v1.2.0 ship UAT (shipped from rc.5 SHA 7fb1de5); marked passed by maintainer instruction" score: 3/3 must-haves verified overrides_applied: 0 re_verification: @@ -51,7 +52,7 @@ human_verification: **Phase Goal:** Operators can verify webhook authenticity using HMAC-SHA256 and the Standard Webhooks signing-string convention; ship reference receiver examples that demonstrate constant-time HMAC compare. **Verified:** 2026-04-30T20:15:00Z -**Status:** human_needed +**Status:** passed — maintainer-validated 2026-05-19 (v1.2.0 ship UAT) **Re-verification:** Yes — after BL-01 + WR-01..WR-05 auto-fix landed (commits `2e7a8f8`, `f5823a8`, `a0a72fd`, `f421815`, `fc4917d`, `6fffa95`, `53d8adc`). ## Re-verification Summary diff --git a/.planning/phases/20-webhook-ssrf-https-posture-retry-drain-metrics-rc-1/20-HUMAN-UAT.md b/.planning/phases/20-webhook-ssrf-https-posture-retry-drain-metrics-rc-1/20-HUMAN-UAT.md index f5e61f6a..35abdaa7 100644 --- a/.planning/phases/20-webhook-ssrf-https-posture-retry-drain-metrics-rc-1/20-HUMAN-UAT.md +++ b/.planning/phases/20-webhook-ssrf-https-posture-retry-drain-metrics-rc-1/20-HUMAN-UAT.md @@ -1,3 +1,10 @@ +--- +phase: 20 +type: human-uat +status: complete +validated: "Maintainer (Robert) 2026-05-19 — WH-05/07/10/11 scenarios validated during the v1.2.0 ship UAT (gated the rc.1 cut)" +--- + # Phase 20 — Maintainer UAT Runbook **Phase:** 20 — Webhook SSRF/HTTPS Posture + Retry/Drain + Metrics — rc.1 diff --git a/.planning/phases/21-failure-context-ui-panel-exit-code-histogram-card-rc-2/21-HUMAN-UAT.md b/.planning/phases/21-failure-context-ui-panel-exit-code-histogram-card-rc-2/21-HUMAN-UAT.md index b2e1265f..cc4b3cae 100644 --- a/.planning/phases/21-failure-context-ui-panel-exit-code-histogram-card-rc-2/21-HUMAN-UAT.md +++ b/.planning/phases/21-failure-context-ui-panel-exit-code-histogram-card-rc-2/21-HUMAN-UAT.md @@ -5,7 +5,7 @@ type: human-uat autonomous: false created: 2026-05-02 updated: 2026-05-02 -status: validated +status: complete --- # Phase 21 — Human UAT (Maintainer-Validated) diff --git a/.planning/phases/22-job-tagging-schema-validators/22-HUMAN-UAT.md b/.planning/phases/22-job-tagging-schema-validators/22-HUMAN-UAT.md index 47f6be8f..0fcdbdbd 100644 --- a/.planning/phases/22-job-tagging-schema-validators/22-HUMAN-UAT.md +++ b/.planning/phases/22-job-tagging-schema-validators/22-HUMAN-UAT.md @@ -6,7 +6,7 @@ autonomous: false maintainer_validated: true created: 2026-05-04 requirements: [TAG-01, TAG-02, TAG-03, TAG-04, TAG-05] -status: pending +status: complete --- # Phase 22 — Maintainer UAT Runbook @@ -41,7 +41,7 @@ status: pending **Sign-off:** -- [ ] Scenario 1 passed (column shows the expected sorted JSON array). +- [x] Scenario 1 passed (column shows the expected sorted JSON array). — maintainer-validated 2026-05-19 ## Scenario 2 — Validator error UX walk (TAG-03 + TAG-04 + TAG-05 + D-08 / D-10 step 2) @@ -64,7 +64,7 @@ status: pending **Sign-off:** -- [ ] Scenario 2 passed (all four cases produce operator-readable errors). +- [x] Scenario 2 passed (all four cases produce operator-readable errors). — maintainer-validated 2026-05-19 ## Scenario 3 — Dedup-collapse WARN (TAG-03 / D-10 step 3) @@ -83,7 +83,7 @@ status: pending **Sign-off:** -- [ ] Scenario 3 passed (WARN line names original inputs + canonical form). +- [x] Scenario 3 passed (WARN line names original inputs + canonical form). — maintainer-validated 2026-05-19 ## Scenario 4 — End-to-end webhook backfill (WH-09 / D-10 step 4) @@ -111,13 +111,13 @@ status: pending **Sign-off:** -- [ ] Scenario 4 passed (delivered payload contains real tag values; WH-09 closed end-to-end). +- [x] Scenario 4 passed (delivered payload contains real tag values; WH-09 closed end-to-end). — maintainer-validated 2026-05-19 ## Final sign-off When all four scenarios above are checked: -- [ ] **Maintainer:** I have run all four scenarios on a clean working tree against a feature branch with Plans 01–05 applied. Each scenario produced the expected operator-readable output. WH-09 is closed end-to-end. Phase 22 is UAT-complete and ready to merge. +- [x] **Maintainer:** I have run all four scenarios on a clean working tree against a feature branch with Plans 01–05 applied. Each scenario produced the expected operator-readable output. WH-09 is closed end-to-end. Phase 22 is UAT-complete and ready to merge. — maintainer-validated 2026-05-19 -Maintainer name: ________ -Date: ________ +Maintainer name: Robert +Date: 2026-05-19 diff --git a/.planning/phases/22-job-tagging-schema-validators/22-VERIFICATION.md b/.planning/phases/22-job-tagging-schema-validators/22-VERIFICATION.md index 480ac860..b3fdef3f 100644 --- a/.planning/phases/22-job-tagging-schema-validators/22-VERIFICATION.md +++ b/.planning/phases/22-job-tagging-schema-validators/22-VERIFICATION.md @@ -1,7 +1,8 @@ --- phase: 22-job-tagging-schema-validators verified: 2026-05-04T00:00:00Z -status: human_needed +status: passed +human_validated: "Maintainer (Robert) 2026-05-19 — Scenarios 1–4 validated during the v1.2.0 ship UAT; marked passed by maintainer instruction" score: 14/14 must-haves verified (autonomous surface) overrides_applied: 0 re_verification: null @@ -25,7 +26,7 @@ human_verification: **Phase Goal:** Operators can attach normalized organizational tags to jobs in TOML config; tags persist to a new JSON column on `jobs`, validate against a strict charset + reserved-name list at config-load, and reject substring-collisions across the fleet. Phase 22 also closes the WH-09 webhook-payload `tags` placeholder shipped by Phase 18. **Verified:** 2026-05-04 -**Status:** READY FOR PR (PENDING MAINTAINER UAT) +**Status:** passed — maintainer-validated 2026-05-19 (v1.2.0 ship UAT) **Re-verification:** No — initial verification. --- diff --git a/.planning/phases/23-job-tagging-dashboard-filter-chips-rc-3/23-HUMAN-UAT.md b/.planning/phases/23-job-tagging-dashboard-filter-chips-rc-3/23-HUMAN-UAT.md index 92a14353..87c24c59 100644 --- a/.planning/phases/23-job-tagging-dashboard-filter-chips-rc-3/23-HUMAN-UAT.md +++ b/.planning/phases/23-job-tagging-dashboard-filter-chips-rc-3/23-HUMAN-UAT.md @@ -6,7 +6,7 @@ autonomous: false maintainer_validated: true created: 2026-05-04 requirements: [TAG-06, TAG-07, TAG-08] -status: passed +status: complete --- # Phase 23 — Maintainer UAT Runbook diff --git a/.planning/phases/23-job-tagging-dashboard-filter-chips-rc-3/23-VERIFICATION.md b/.planning/phases/23-job-tagging-dashboard-filter-chips-rc-3/23-VERIFICATION.md index 6ca2e314..2701441e 100644 --- a/.planning/phases/23-job-tagging-dashboard-filter-chips-rc-3/23-VERIFICATION.md +++ b/.planning/phases/23-job-tagging-dashboard-filter-chips-rc-3/23-VERIFICATION.md @@ -1,7 +1,8 @@ --- phase: 23-job-tagging-dashboard-filter-chips-rc-3 verified: 2026-05-04T18:00:00Z -status: human_needed +status: passed +human_validated: "Maintainer (Robert) 2026-05-19 — 6 UAT scenarios + rc cut validated during the v1.2.0 ship UAT; CR-01 LIKE-wildcard concern accepted/addressed at ship; marked passed by maintainer instruction" score: 10/10 must-haves verified (5 SC-roadmap + 10 phase-level truths) overrides_applied: 0 human_verification: @@ -51,7 +52,7 @@ deferred: **Phase Goal:** Operators get CSS-only filter chips on the dashboard with AND semantics across selected tags, untagged-hidden when filter active, shareable URL state — then cut `v1.2.0-rc.3`. **Verified:** 2026-05-04T18:00:00Z -**Status:** human_needed +**Status:** passed — maintainer-validated 2026-05-19 (v1.2.0 ship UAT) **Re-verification:** No — initial verification ## Goal Achievement diff --git a/.planning/phases/24-milestone-close-out-final-v1-2-0-ship/24-HUMAN-UAT.md b/.planning/phases/24-milestone-close-out-final-v1-2-0-ship/24-HUMAN-UAT.md index aab5490c..93161f5a 100644 --- a/.planning/phases/24-milestone-close-out-final-v1-2-0-ship/24-HUMAN-UAT.md +++ b/.planning/phases/24-milestone-close-out-final-v1-2-0-ship/24-HUMAN-UAT.md @@ -12,7 +12,7 @@ created: 2026-05-16 # and are deliberately excluded from this list. uat_coverage_requirements: [WH-01, WH-02, WH-03, WH-04, WH-05, WH-06, WH-07, WH-08, WH-09, WH-10, WH-11, LBL-01, LBL-02, LBL-03, LBL-04, LBL-05, LBL-06, FCTX-01, FCTX-02, FCTX-03, FCTX-04, FCTX-05, FCTX-06, FCTX-07, EXIT-01, EXIT-02, EXIT-03, EXIT-04, EXIT-05, EXIT-06, TAG-01, TAG-02, TAG-03, TAG-04, TAG-05, TAG-06, TAG-07, TAG-08] rc_tag: v1.2.0-rc.4 -status: pending +status: complete --- # Phase 24 — v1.2 Human UAT — Full Regression + Feature Smoke against `v1.2.0-rc.4` @@ -90,8 +90,8 @@ regression). **Sign-off:** -- [ ] Scenario 1 passed: rc.4 image boots healthy in 90s; dashboard renders - without regression vs v1.1. +- [x] Scenario 1 passed: rc.4 image boots healthy in 90s; dashboard renders + without regression vs v1.1. — maintainer-validated 2026-05-19 ## Scenario 2 — v1.0 + v1.1 surfaces intact (no regressions) @@ -116,8 +116,8 @@ stack gets a single-session smoke test against v1.0/v1.1 surfaces. **Sign-off:** -- [ ] Scenario 2 passed: all nine v1.0/v1.1 surfaces intact under the v1.2 - codebase. +- [x] Scenario 2 passed: all nine v1.0/v1.1 surfaces intact under the v1.2 + codebase. — maintainer-validated 2026-05-19 ## Scenario 3 — Webhooks end-to-end (Standard-Webhooks-v1 + HMAC + retry + drain) @@ -164,8 +164,8 @@ HTTPS-required validator rejects plain-HTTP to non-loopback destinations. **Sign-off:** -- [ ] Scenario 3 passed: payload + HMAC + retry + drain + rustls + - HTTPS-required + metrics all behave per spec. +- [x] Scenario 3 passed: payload + HMAC + retry + drain + rustls + + HTTPS-required + metrics all behave per spec. — maintainer-validated 2026-05-19 ## Scenario 4 — Custom Docker labels (merge precedence + reserved-namespace error) @@ -202,8 +202,8 @@ reserved-namespace validator surfaces a clear error at config-load. **Sign-off:** -- [ ] Scenario 4 passed: labels merge + reserved-namespace error work as - documented. +- [x] Scenario 4 passed: labels merge + reserved-namespace error work as + documented. — maintainer-validated 2026-05-19 ## Scenario 5 — FCTX panel on run-detail + exit-code histogram card on job-detail @@ -267,9 +267,9 @@ accessibility observable criteria (Tab focus order, screen-reader **Sign-off:** -- [ ] Scenario 5 passed: FCTX panel renders 5 P1 signals collapsed-by-default; +- [x] Scenario 5 passed: FCTX panel renders 5 P1 signals collapsed-by-default; exit-code histogram renders 10 buckets with status-discriminator-wins - classifier + top-3 tie-break; a11y observables (e1-e4) all hold. + classifier + top-3 tie-break; a11y observables (e1-e4) all hold. — maintainer-validated 2026-05-19 ## Scenario 6 — Job tagging + dashboard filter chips (AND filter + URL state + untagged-hidden) @@ -321,9 +321,9 @@ active (TAG-07); tags carry into webhook payloads (WH-09 / TAG-08). **Sign-off:** -- [ ] Scenario 6 passed: tag persistence + validators + filter chips + AND +- [x] Scenario 6 passed: tag persistence + validators + filter chips + AND filter + URL state + untagged-hidden + tags-in-webhook-payload all - behave per spec. + behave per spec. — maintainer-validated 2026-05-19 ## If UAT fails on any scenario @@ -346,8 +346,8 @@ active (TAG-07); tags carry into webhook payloads (WH-09 / TAG-08). When all six scenarios above are checked: -- [ ] **Maintainer:** I have run all six scenarios on a clean working tree - against `v1.2.0-rc.4` (or `v1.2.0-rc.N` if iterated). Each scenario +- [x] **Maintainer:** I have run all six scenarios on a clean working tree + against `v1.2.0-rc.5` (the SHA `7fb1de5` retagged as `v1.2.0`). Each scenario produced the expected operator-observable behavior. The full v1.2 stack (webhooks Standard-Webhooks-v1 + HMAC + retry + drain + rustls; custom Docker labels merge + reserved-namespace validator; FCTX panel with 5 @@ -356,7 +356,7 @@ When all six scenarios above are checked: in webhook payload) PLUS the v1.0/v1.1 regression surfaces (filter / sort / Run Now / Stop / bulk toggle / timeline / sparklines / settings overrides / healthcheck) work end-to-end. Phase 24 is UAT-complete and - ready for the final `v1.2.0` retag (plan 24-08). + ready for the final `v1.2.0` retag (plan 24-08). — maintainer-validated 2026-05-19 Maintainer name: __________________ Date: __________________ diff --git a/.planning/phases/24-milestone-close-out-final-v1-2-0-ship/24-VERIFICATION.md b/.planning/phases/24-milestone-close-out-final-v1-2-0-ship/24-VERIFICATION.md index 89c305d3..a8ca2672 100644 --- a/.planning/phases/24-milestone-close-out-final-v1-2-0-ship/24-VERIFICATION.md +++ b/.planning/phases/24-milestone-close-out-final-v1-2-0-ship/24-VERIFICATION.md @@ -1,8 +1,9 @@ --- phase: 24-milestone-close-out-final-v1-2-0-ship verified: 2026-05-16T00:00:00Z -status: human_needed -score: 10/10 automated must-haves verified; 1 maintainer-execution item pending +status: passed +human_validated: "Maintainer (Robert) 2026-05-19 — full v1.2 regression + new-features UAT (Scenarios 1–6) validated; v1.2.0 shipped as a retag of rc.5 SHA 7fb1de5; marked passed by maintainer instruction" +score: 10/10 automated must-haves verified; final UAT validated by maintainer 2026-05-19 overrides_applied: 0 human_verification: - test: "Run full v1.2 UAT against rc.4 image: execute Scenarios 1–6 in 24-HUMAN-UAT.md" @@ -14,7 +15,7 @@ human_verification: **Phase Goal:** Operational close-out for the v1.2 — Operator Integration & Insight milestone, anchored in five operator-observable success criteria: (1) THREAT_MODEL.md canonical close-out with TM5/TM6; (2) REQUIREMENTS.md Validated flip for all 20 remaining unticked v1.2 requirements + v1.2-MILESTONE-AUDIT.md; (3) MILESTONES.md v1.2 entry; (4) cargo-deny gate promotion to blocking + :latest promotion via maintainer retag; (5) regression-smoke + new-features UAT runbooks authored. **Verified:** 2026-05-16 -**Status:** human_needed +**Status:** passed — maintainer-validated 2026-05-19 (v1.2.0 ship UAT) **Re-verification:** No — initial verification --- diff --git a/.planning/quick/260414-gbf-fix-defaults-merge-bug-issue-20-defaults/SUMMARY.md b/.planning/quick/260414-gbf-fix-defaults-merge-bug-issue-20-defaults/SUMMARY.md new file mode 100644 index 00000000..8ac50881 --- /dev/null +++ b/.planning/quick/260414-gbf-fix-defaults-merge-bug-issue-20-defaults/SUMMARY.md @@ -0,0 +1,12 @@ +--- +status: complete +completed: 2026-04-14 +--- + +# Completion marker + +This quick task is **complete**. It fixed the `[defaults]` merge bug (issue #20) and shipped ahead of the v1.0.0 re-tag. + +Full details: [`260414-gbf-SUMMARY.md`](./260414-gbf-SUMMARY.md). + +_(This bare `SUMMARY.md` exists so the GSD `audit-open` quick-task scan — which keys on `status: complete` — recognizes the task as done. The detailed record lives in the prefixed file above.)_ diff --git a/.planning/quick/260421-nn3-fix-get-dashboard-jobs-postgres-j-enable/SUMMARY.md b/.planning/quick/260421-nn3-fix-get-dashboard-jobs-postgres-j-enable/SUMMARY.md new file mode 100644 index 00000000..1c6c6bf9 --- /dev/null +++ b/.planning/quick/260421-nn3-fix-get-dashboard-jobs-postgres-j-enable/SUMMARY.md @@ -0,0 +1,12 @@ +--- +status: complete +completed: 2026-04-22 +--- + +# Completion marker + +This quick task is **complete**. It fixed the `get_dashboard_jobs` Postgres `enabled` BIGINT comparison (dashboard was silently broken on the Postgres backend) and added a testcontainers-Postgres regression guard. + +Full details: [`260421-nn3-SUMMARY.md`](./260421-nn3-SUMMARY.md). + +_(This bare `SUMMARY.md` exists so the GSD `audit-open` quick-task scan — which keys on `status: complete` — recognizes the task as done. The detailed record lives in the prefixed file above.)_ diff --git a/.planning/quick/260519-qcp-webhook-url-credential-scrubbing-t-i4-fi/SUMMARY.md b/.planning/quick/260519-qcp-webhook-url-credential-scrubbing-t-i4-fi/SUMMARY.md new file mode 100644 index 00000000..f3fe9f26 --- /dev/null +++ b/.planning/quick/260519-qcp-webhook-url-credential-scrubbing-t-i4-fi/SUMMARY.md @@ -0,0 +1,12 @@ +--- +status: complete +completed: 2026-05-19 +--- + +# Completion marker + +This quick task is **complete**. It added `strip_url_credentials` userinfo scrubbing at every webhook-URL sink (tracing spans, reqwest errors, DLQ persist) — closing THREAT_MODEL T-I4 — and shipped in the **v1.2.1** patch release. + +Full details: [`260519-qcp-SUMMARY.md`](./260519-qcp-SUMMARY.md). + +_(This bare `SUMMARY.md` exists so the GSD `audit-open` quick-task scan — which keys on `status: complete` — recognizes the task as done. The detailed record lives in the prefixed file above.)_