v1.6.1-rc.8
Full Changelog: v1.7.0-rc.9...v1.6.1-rc.8
[1.6.1-rc.8] — 2026-09-04
Fixed
watchFromCron()had no re-entrancy guard, so overlapping scans on a large fleet fired the same trigger multiple times for one update. A full scan can take minutes, and the cron schedule, the docker-events debounce, and the startup timer could all start one while the previous scan was still running. Each overlapping call ran its ownwatch(), and a tag first seen mid-burst passed theonce=truehistory check in every one of them before any of them recorded it, so a trigger like Telegram fired once per overlapping scan for the same update.watchFromCron()is now single-flight: a request while a scan is running does not start a second one, it records that a rescan was requested, and exactly one follow-up scan runs once the current scan finishes, so a docker event that arrives mid-scan is not lost. A scan that never settles cannot wedge later ticks either: the in-flight scan is raced against a deadline, and when it fires every caller waiting on that scan, the one that started it and any that coalesced into it, resolves to an empty result and the next tick starts a fresh scan. The "Cron started" log line now also names what triggered the scan (schedule, docker-event, startup, or maintenance-window). Reported by @tarzan77cz in #972.- A
once=truetrigger re-fired hours later for a tag update it had already announced, whenever a registry rate-limited the accompanying digest lookup. The notification-history hash for a tag-kind update included the digest and, when the digest was absent, the image'screatedtimestamp. A transientDigest watch failed (429)dropped the digest for that scan only, socreatedswapped into the hash on the failed scan and back out once the digest lookup next succeeded, and the once history stopped matching an update it had already sent. The hash for a tag-kind update on a container with digest watching configured is now keyed on the tag alone, so a digest lookup failing or succeeding does not change it; a container without digest watching configured keeps the originalcreatedfallback, and digest-kind updates stay keyed on the digest. Concurrent evaluations of the same history key are also now atomic within the process: the check and the reservation happen in the same synchronous step, so two overlapping scans evaluating the same result cannot both pass the check and both send, which is why one trigger could fire four times in the same millisecond while another skipped. After upgrading, a container with digest watching enabled, a tag update already pending, andonce=truewhose stored hash came from a scan where the digest lookup had succeeded re-notifies exactly once, because the stored hash format changed; every scan after that stays stable. Reported by @tarzan77cz in #972. - An update could be announced twice, and a torn-down watcher could still warn about a scan deadline it no longer owned. The
once=truereservation added for #972 covered the simple notification path only, so batch and digest eligibility still did a plain read of the notification history, and that history is written after the send resolves. A manual single-container scan overlapping a cron scan therefore passed the check in both evaluations and sent the same candidate twice in batch mode, and in digest mode a report landing while a flush was still sending re-buffered the same result behind the send, so the post-send delete skipped it and the next flush sent it again. Batch and digest eligibility now take the same synchronous reservation the simple path does, the batch releases every reservation it took in a finally, and the digest flush holds one for exactly the results it is sending. Two paths that bypassed the gate entirely are closed with it. The digest flush swaps the current store container in for the buffered one at send time, and when that substitute was a result an earlier flush had already sent it went out again while the post-send delete quietly dropped the newer candidate the buffer was holding; it is now skipped and evicted instead, and a genuinely pending update re-enters the buffer on the next scan. Entries in the batch retry buffer, which every batch re-sends until one succeeds, took no reservation at all, so two overlapping retries both carried the same entry to the trigger. Separately, the cron scan's deadline timer lived only in the closure that raced it, so deregistering a watcher while itswatch()was stalled left the timer running to the full deadline and then logged "exceeded its deadline" against a watcher that no longer existed, while the caller that started the scan and every caller coalesced into it stayed pending. The timer and a cancel handle now live on the watcher state, deregistration clears the timer and settles the race with its own sentinel so every caller resolves to an empty result with no deadline warning, and the per-scan cleanup is identity-guarded so a late settlement cannot clear a newer scan's handle. A scan requested after teardown is now refused outright, which also covers the docker-events debounce:just-debounceexposes no cancel, so its pending timeout still fires up to five seconds after the watcher is gone and used to start a full scan against it.
Documentation
- Deprecation docs realigned with the shipped code.
DEPRECATIONS.mdand the generated deprecations page now double the$in the curl-healthcheck compose migration snippet so it actually expands inside the container instead of the host, namePUT /api/v1/settingsinstead of the tombstoned unversioned path, note that theGET /api/auth/methodsSunsetheader's 2027-07-01 date is a conservative earliest instant rather than the real removal (the v1.7.0 release), add the previously undocumentedWUD_AGENT_SECRET/WUD_AGENT_SECRET_FILEfallback removal, add the WebSocket origin-check, anonymous-auth grandfather, and session cookie rename entries that were missing from the generated page, and use**Removal**instead of**Removed in**for still-pending removals so the table doesn't assert a version has already shipped.
Note: this is a maintenance cut, built from dev/v1.6 at 54f92b69da7c2a664c2dad0e3b21d183a82f9633, not from main.
- The container image and release artifact are cosign-signed (identity
release-cut.yml@refs/heads/main, since the workflow run itself always executes at that ref), but carry no SLSA build-provenance attestation — this workflow's own OIDC token always claimsmainHEAD as the build source, which would be false for this artifact, so attestation is skipped rather than publish a false claim.