Chanes for v1.1.0 #2

Merged
w33ble merged 28 commits from dev into main 2026-08-26 19:20:40 -06:00
Owner

Changelog

[v1.1.0] — 2026-08-22 (unreleased)

All notable changes since v1.0.0. Supply-chain-safety posture is unchanged:
lagom only ever pulls digests it has previously observed, regardless of feature.

Added

  • lagom inspect [image] — lists every digest lagom has recorded for a
    tracked image from local BoltDB state, marking the digest currently running.
    No registry or pull queries; filter by substring argument.
  • --skip-first-run for lagom daemon (also skip_first_run config /
    LAGOM_SKIP_FIRST_RUN env) — skips the startup track+update cycle and waits
    for the first daemon_interval_hours instead of running immediately.

Changed

  • Age policy is now first-seen based. Eligibility is judged from when lagom
    first observed a digest (FirstSeen), not the registry-reported build
    date. Registry creation times are capped at the observation time when
    recorded, so a backdated or spoofed registry timestamp cannot make an
    unpulled digest eligible early.
  • update pulls the newest eligible observed digest. Previously the pull
    decision used the registry's live tag digest; now it advances to the newest
    digest lagom has tracked that satisfies min_age_days. Pulls remain
    digest-addressed with up to max_pull_candidates fallbacks.
  • update plans running containers only. Update decisions are made for
    images currently running (or from images config); images whose containers
    are stopped are left alone. status still shows them.
  • --force bypasses the age gate but stays observed-only. It pulls the
    newest digest already tracked regardless of age, rather than reaching for the
    live registry digest. Run lagom track first if nothing is recorded.
  • status no longer queries the registry. The LOCAL DIGEST / NEXT DIGEST /
    AGE / STATUS columns are derived from local state plus one Docker listing, so
    status works without network access to registries.
  • Daemon does one container fetch per cycle, shared between the track and
    update phases (docker ps once, not twice).

Fixed

  • Deterministic update decisions on first-seen ties. A running digest and a
    newer remote digest recorded in the same track pass share a first-seen time;
    once both were age-eligible the pull decision was left to map-iteration
    order, so lagom could skip ("already on newest eligible image") or pull at
    random across cycles and disagree with status. Update now prefers the
    non-live digest on ties, matching status.
  • update reports "already at latest" when running the newest observed
    digest instead of emitting a misleading age-gate countdown.
  • update keeps newer images when a pulled replacement is older. The
    prune path checks the replacement's effective creation time before pruning
    the orphaned image, preventing a newer build from being destroyed to reclaim
    space.
  • update fails loudly on container-list errors instead of silently
    planning from an empty set.
  • status verifies the local digest against the Docker daemon, so the
    LOCAL DIGEST column reflects what is actually running, not only the last
    recorded state.
  • Health-server and store-close errors are logged instead of swallowed.
  • Warn when the local digest lookup fails rather than proceeding silently.

Removed

  • inspect no longer shows a size column.
  • Dead errcheck exclusions dropped from .golangci.yml; remaining suppressions
    are inlined as documented //nolint comments.

Upgrade notes

  • The age-policy change (first-seen) and the observed-only --force semantics
    are intentional behavior changes. Existing state carries over unchanged;
    FirstSeen timestamps already in the DB drive the new policy.
  • If you relied on --force pulling a never-before-tracked digest, run
    lagom track (or let the daemon cycle) once first.
# Changelog ## [v1.1.0] — 2026-08-22 (unreleased) All notable changes since v1.0.0. Supply-chain-safety posture is unchanged: lagom only ever pulls digests it has previously observed, regardless of feature. ## Added - **`lagom inspect [image]`** — lists every digest lagom has recorded for a tracked image from local BoltDB state, marking the digest currently running. No registry or pull queries; filter by substring argument. - **`--skip-first-run`** for `lagom daemon` (also `skip_first_run` config / `LAGOM_SKIP_FIRST_RUN` env) — skips the startup track+update cycle and waits for the first `daemon_interval_hours` instead of running immediately. ## Changed - **Age policy is now first-seen based.** Eligibility is judged from when lagom first *observed* a digest (`FirstSeen`), not the registry-reported build date. Registry creation times are capped at the observation time when recorded, so a backdated or spoofed registry timestamp cannot make an unpulled digest eligible early. - **`update` pulls the newest eligible observed digest.** Previously the pull decision used the registry's live tag digest; now it advances to the newest digest lagom has tracked that satisfies `min_age_days`. Pulls remain digest-addressed with up to `max_pull_candidates` fallbacks. - **`update` plans running containers only.** Update decisions are made for images currently running (or from `images` config); images whose containers are stopped are left alone. `status` still shows them. - **`--force` bypasses the age gate but stays observed-only.** It pulls the newest digest already tracked regardless of age, rather than reaching for the live registry digest. Run `lagom track` first if nothing is recorded. - **`status` no longer queries the registry.** The LOCAL DIGEST / NEXT DIGEST / AGE / STATUS columns are derived from local state plus one Docker listing, so `status` works without network access to registries. - **Daemon does one container fetch per cycle**, shared between the track and update phases (`docker ps` once, not twice). ## Fixed - **Deterministic update decisions on first-seen ties.** A running digest and a newer remote digest recorded in the same track pass share a first-seen time; once both were age-eligible the pull decision was left to map-iteration order, so lagom could skip ("already on newest eligible image") or pull at random across cycles and disagree with `status`. Update now prefers the non-live digest on ties, matching `status`. - **`update` reports "already at latest"** when running the newest observed digest instead of emitting a misleading age-gate countdown. - **`update` keeps newer images when a pulled replacement is older.** The prune path checks the replacement's effective creation time before pruning the orphaned image, preventing a newer build from being destroyed to reclaim space. - **`update` fails loudly on container-list errors** instead of silently planning from an empty set. - **`status` verifies the local digest against the Docker daemon**, so the LOCAL DIGEST column reflects what is actually running, not only the last recorded state. - **Health-server and store-close errors are logged** instead of swallowed. - Warn when the local digest lookup fails rather than proceeding silently. ## Removed - `inspect` no longer shows a size column. - Dead errcheck exclusions dropped from `.golangci.yml`; remaining suppressions are inlined as documented `//nolint` comments. ## Upgrade notes - The age-policy change (first-seen) and the observed-only `--force` semantics are intentional behavior changes. Existing state carries over unchanged; `FirstSeen` timestamps already in the DB drive the new policy. - If you relied on `--force` pulling a never-before-tracked digest, run `lagom track` (or let the daemon cycle) once first.
When the remote tag moves to a digest too new for the age policy, the fallback path previously pulled a different historic digest and re-tagged it over the running image — even when the running image was already eligible. This downgraded stable images and churned through archive digests every daemon cycle.

Now update skips when the local digest's recorded metadata already satisfies the age policy, so a healthy image stays put until the remote ages in.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <[email protected]>
Archive digests recorded during track carried the remote registry's self-reported creation time unchanged. A backdated timestamp could make a never-validated digest look old enough to pass the fallback age filter immediately.

Creation time is now capped at the observation time, so a digest cannot be eligible before lagom itself has seen it for min_age_days.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <[email protected]>
New 'lagom inspect [image]' command dumps the full digest archive from local BoltDB state: every digest lagom has recorded per ref, with first/last-seen times, recorded creation time, size, and a marker for the current digest. An optional positional argument filters refs by substring.

This makes it possible to diagnose fallback behavior — e.g. why a particular historic digest was chosen for a pull — without digging into the state file by hand.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <[email protected]>
README: add inspect command to command list and its own section with example output, note the hold-steady fallback behavior in update, document the observation-time cap on recorded creation timestamps, update architecture listing and v1 scope.

update: refresh Long description to describe the new hold-steady check before fallback pulls.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <[email protected]>
The cached history is about digest identity and observation windows; size is uninformative for archive entries (never recorded for unpulled digests) and misleading (renders 0B for unknown). Drop the column from the table and the README example.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <[email protected]>
Rewrite effectiveAge around a new effectiveCreated helper that caps a digest's creation time at the first time lagom observed it, instead of the previous lastTrack (max LastSeen across all digests) with an uncapped seenDigest shortcut.

Wire it into both age gates that previously trusted the raw registry timestamp:
- primary pull decision: remote digest must be old enough as measured from first observation
- fallback candidate filter: same cap applied to archived digest metadata

This closes the backdated-timestamp bypass (RC2): a registry that reports an old creation time for a digest lagom just saw can no longer satisfy min_age_days immediately. For existing archives recorded before the observation-time cap (fix C2), this also corrects already-stored backdated metadata at decision time.

Add unit tests for effectiveCreated/effectiveAge against a real BoltDB store.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <[email protected]>
Prune previously deleted the old image on every replacement, even when the pulled digest was older than the one it replaced — a downgrade irreversibly destroyed the better image.

Now prune compares effective creation times (capped at first observation, so backdated registry timestamps cannot spoof the comparison) of the old and new digests and keeps the old image unless the replacement is genuinely newer or equal. Unknown creation times are treated as unprovable and also keep the image.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <[email protected]>
Replace the hold-steady + build-date fallback logic with a single rule: among digests observed for a ref that are at least min_age_days old by first-observation time (FirstSeen), pull the most recently observed one. Age is judged purely from when lagom first recorded a digest, so a spoofed or backdated registry build date cannot make a fresh SHA eligible early. Always advancing to the newest eligible observed image means the running image is never downgraded and the latest build we have vetted for the cooldown is always used.

This supersedes the fix-A hold-steady behavior (27fd511) which stalled on the running image when the tag churned within min_age_days, and the findHistoricCandidates build-date sort + local/remote exclusion.

- actions.go: add eligibleObserved (FirstSeen-based, newest-first) and waitUntilEligible; rewrite the UpdatePlan per-ref decision; drop effectiveAge and findHistoricCandidates; keep effectiveCreated (fix D uses it)
- update.go/daemon.go: generalize age-skip logging to report every skipped decision (the 'too new' prefix no longer exists); drop unused policy import
- actions_test.go: add TestEligibleObserved and TestWaitUntilEligible; remove TestEffectiveAge

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <[email protected]>
Track records the running and remote digests with the same first-seen
time, so once min_age_days elapses they are both eligible and the sort
leaves the tie to map-iteration order. updatePlan then coin-flipped
between pulling and skipping 'already on newest eligible image', and
disagreed with status, cycle to cycle.

Pick the non-live digest when the newest candidates share a first-seen,
mirroring nextTarget, so pull decisions are deterministic.
Author
Owner

Follow-up safety fix in commit 249b6e7 (fix(update): keep live digest on first-seen ties):

  • An age-eligible live digest now wins when another observed digest has the same FirstSeen.
  • If the remote tag is too new, an age-eligible live digest is retained instead of replacing it with a historic candidate whose observation time is newer.
  • status uses the same tie behavior.

Verified with mise run build, go test -shuffle=on -count=1 ./..., go vet ./..., and golangci-lint run ./....

Follow-up safety fix in commit `249b6e7` (`fix(update): keep live digest on first-seen ties`): - An age-eligible live digest now wins when another observed digest has the same `FirstSeen`. - If the remote tag is too new, an age-eligible live digest is retained instead of replacing it with a historic candidate whose observation time is newer. - `status` uses the same tie behavior. Verified with `mise run build`, `go test -shuffle=on -count=1 ./...`, `go vet ./...`, and `golangci-lint run ./...`.
w33ble changed title from Release v1.1.0 to Chanes for v1.1.0 2026-08-26 19:20:31 -06:00
w33ble merged commit 249b6e7a53 into main 2026-08-26 19:20:40 -06:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
w33ble/lagom!2
No description provided.