v1.7.0-rc.12
Full Changelog: v1.6.1-rc.10...v1.7.0-rc.12
[1.7.0-rc.12] — 2026-09-06
Fixed
- The demo site did not send
Cross-Origin-Opener-Policy, so the weekly DAST scan failed on ZAP rule 90004 every run.apps/demo/vercel.jsonnow sendssame-originnext to the existingCross-Origin-Embedder-Policyheader. - The arm64 pass of the image arch check failed on every multi-platform cut with
docker: cannot overwrite digest sha256:<index>.scripts/check-image-arch.shran once per platform against the same index-digest reference (ghcr.io/codeswhat/drydock:release-staging-N@sha256:<index>), and docker's classic image store cannot hold two platform variants under one digest, so the amd64 pass succeeded and the arm64 pass that followed it always failed. This killed the v1.6.1-rc.9 cut. The script now resolves each platform's own manifest digest out of the index viadocker buildx imagetools inspect --rawand jq before it runs docker, skipping attestation entries, and falls back to running the reference unchanged when it isn't an index at all. - Two more prunes cleared a container's update policy the same way the startup prune used to. The startup prune stashes a departing record's update policy under its Docker id so the record that replaces it inherits it, but the agent-removal prune and the agent's own stale-container prune still called
deleteContainerplainly. An agent removed from config or renamed, or a container handed from one agent to another (or back to the controller's own watcher), lost its snooze, maturity mode and minimum age, and skipped tags or digests the same way the startup case did before that fix. Both prunes now passidentityChangeExpected: trueas well, so the same Docker-id stash carries the policy across either hand-off. - A manual "check now" (the dashboard, the API, a webhook, or the controller polling an agent) could fire the same notification twice if it landed while a scan was already running.
watch()has no re-entrancy guard of its own, soPOST /api/v1/containers/watch,POST /api/v1/webhook/watch, and the agent's own watcher API each started a second, fully independent scan on top of one already in progress from the cron schedule or another manual request, and underonce=trueboth passes could read "not yet notified" before either had finished writing its own history, so a trigger like Slack or Telegram sent the same update twice. All three now route through the same single-flight scan orchestration the cron schedule uses: a call that lands mid-scan is folded into that scan's one follow-up instead of starting an independent scan, and the response reports it (result.coalescedin the JSON body for the first two, anX-Drydock-Watch-Coalescedheader for the agent endpoint, which keeps its existing bare-array body for compatibility across controller/agent version skew). Manual and API scans still run regardless of a configured maintenance window, exactly as before; only automatic installation is deferred by it. Separately,handleMaturityGateClearedEvent's ownonce=truecheck read notification history with a bare, unreserved lookup, so two overlapping evaluations of the same maturity-cleared event could both pass it before either recorded a result; it now takes the same reservation the generic update-available path does before dispatching. - A once-notification reservation that never settled held its dedup key for the process lifetime.
once=truereserves atriggerId::containerId::eventKind::resultHashkey before sending so an overlapping evaluation of the same result cannot send twice, and releases it once the send settles.runHandlerWithTimeoutdetaches a handler that misses its 30-second deadline instead of waiting on it forever, so a provider that never resolves or rejects left its reservation held forever too, and every later scan for that exact result read it as still in flight and silently skipped the send. The reservation now carries its own expiry, four times the handler timeout, that releases it with a warn log naming the key if nothing released it first. - DR-121: the session store and the main store wrote the same
/store/dd.json, and whichever one saved last erased the other's data.express-session'sconnect-lokistore opened its own independent LokiJS instance on the exact file the main store already used, and LokiJS'ssaveDatabase()always serializes the whole in-memory database, so a session autosave (every 5 seconds, and armed by nothing more than an authenticated request touching its session) reverted every container, setting, and audit row the main store had written since boot, while the main store's own autosave (every 5 minutes) deleted theSessionscollection out from under active logins. In practice this meant containers or settings written after startup could vanish fromdd.jsonafter a crash or a hard stop, and a restart could log every user out. The session store now writes to its own sibling file,dd-sessions.jsonby default (derived fromDD_STORE_FILE, so a custom filename still gets a distinct sibling rather than colliding with anything else on the volume), and the main store drops and logs a staleSessionscollection left behind indd.jsonby an older build instead of continuing to re-save it. Backups remain scoped to the main store file; sessions are not included and are expected to be dropped on restore, which just logs everyone out rather than corrupting anything.
Documentation
- The agents page's paired Gitea registry example had the controller talking HTTPS to an agent serving plain HTTP. The controller block set
DD_AGENT_REMOTE1_CAFILE=/certs/agent-ca.pem, but the agent block above it had noDD_SERVER_TLS_*variables or certificate mounts, so the example copied as written could never connect. The agent block now mountsagent.pem/agent-key.pemand setsDD_SERVER_TLS_ENABLED,DD_SERVER_TLS_CERT, andDD_SERVER_TLS_KEY, with a comment noting the certificate must be signed by theagent-ca.pemthe controller mounts and be valid for the host the controller dials.