Skip to content

fix: manualRun explicit-null completedAt, tri-state Restore gate, throttle honored in every mover flow (#394, #393, #374) - #411

Merged
perfectra1n merged 16 commits into
mainfrom
fix/manualrun-restore-gate-throttle
Aug 28, 2026
Merged

perfectra1n merged 16 commits into
mainfrom
fix/manualrun-restore-gate-throttle

Conversation

@perfectra1n

Copy link
Copy Markdown
Collaborator

Fixes #394, Fixes #393, Fixes #374

Three follow-ups from the recent replication/restore work, landed as one PR with per-issue milestone commits.

#394 — manualRun: explicit-null completedAt in non-terminal patches

patch_manual_run merge-patched {requestedAt, phase} and skipped completedAt when None; RFC-7386 keeps omitted keys, so a follow-up run's Pending/Running displayed the previous run's stamp, and (replications only) the noop guard never converged — a server-no-op'd PATCH re-fired every queued pass. Fix is serde-level in the two shared status types: completed_at drops skip_serializing_if, so both the desired patch and the guard's re-serialized current emit "completedAt": null — the merge-patch clears the stale stamp (the apiserver deletes the key or stores the null verbatim — both observed, both converge) and the guard settles. Maintenance's unguarded copy just stops displaying the stale stamp. Upgrade behavior: a stored stale completedAt gets exactly one clearing PATCH, then converges (pinned by an RFC-7386 replay test using json-patch). E2e: after a completed manual run, a suspend+re-request (one patch — ordering matters) parks in Pending and the raw status carries no completedAt key.

#393 — tri-state Restore readiness gate

The gate conflated "verified Ready" with three cannot-determine shapes, so waitStartedAt could stamp ≈ creation while the Restore was effectively parked behind a missing referent. Now:

  • Missing Repository/ClusterRepository object or missing fromPolicy SnapshotPolicy → the Restore parks in Pending (new registry-backed reason RestoreReferentMissing, new ReferentAvailable condition so doctor explains the park, Warning Event on the transition only) and waitStartedAt is not stamped; the window opens on the first pass after the referent exists and verifies Ready.
  • A snapshotRef whose Snapshot row is absent, and underivable/spec-bug shapes, still fall through — the window must run there so onMissingSnapshot: Fail fires for a typo'd ref (existing e2es pin this).
  • restore_repository_ref returns a 4-variant RepoRefLookup and the gate a 3-variant outcome — both compiler-guarded enums, exhaustively matched.
  • Fixed en route: the gate-clear write initially alternated with the four unconditional gate-park writers in run_restore_mover (a permanent 2-writes-per-pass hot loop); the clear is now carried through the pass (CarriedRestore), with a mutation-verified guard test that also proves the old clobber still reproduces from the stale copy.
  • Docs hedge narrowed (not dropped): the window doesn't open while the referent is missing, and still opens for the snapshotRef-missing/already-launched shapes.
  • Wake-up contract: explicit spec.repository un-parks via the Repository watch; fromPolicy has no SnapshotPolicy→Restore watch and un-parks via the 15s requeue (e2e exercises exactly that path).

#374 — throttle honored everywhere it was promised (wider than the issue)

The issue asked for a destination throttle on SnapshotReplication. Investigation found repository throttle set ran only in the mover's generic connect arm — bootstrap, maintenance, verify, replicate, snapshot-replicate, and seed-migrate all silently ignored the spec.throttle their controllers populate with "§13(e)" intent comments. This PR:

  • Mover: apply_repository_throttle + a connect_and_throttle funnel; the dispatch _ => catch-all is replaced by an exhaustive Operation match; a source-level ratchet test pins the sanctioned direct-connect list. Every batch mover now applies moverDefaults.throttle right after its connect (bootstrap probes included). Each application logs a literal applied repository throttle line — asserted per-pod in e2e (no silent green) and doubling as the version-skew signal.
  • Empirical ground truth (integration-tested against the mise-pinned kopia 0.23.1): repository throttle set succeeds on a readonly connection (the planned read-write flip was unnecessary — repository set-client ships unwired as a tested contingency); limits persist in the local config and snapshot migrate honors them; the readonly bit survives the no-flip sequence; throttling bites only cold-cache backend traffic and low byte caps are far more punitive than nominal on small-object workloads (documented).
  • Per-CR overrides (sub-object, reusing Throttle): SnapshotReplication.spec.migrate.throttle: {source, destination} and Repository/ClusterRepository spec.seed.migrate.throttle: {source, destination} — each side overrides that repo's moverDefaults.throttle field-by-field for the run (kopia snapshot migrate has no speed flags; per-connection throttle set is the only lever). Validated per side at admission; wire fields (destinationThrottle, replicaThrottle) are serde-defaulted with old-wire decode tests.
  • Deliberately exempt (documented in shared-types): serve + browse (interactive plane — see follow-up list below), RepositoryReplication's destination side (sync-to is blob-level; spec.sync speed caps remain the knob), batched deletions + pins (work specs carry no throttle today).
  • Known-and-deliberate: moverDefaults.throttle itself stays unvalidated (adding min-1 now would reject specs that admit today — see follow-up list below); CRDs in Helm's crds/ dir are never upgraded by helm upgrade, so the new fields need kubectl apply of the CRDs (documented in three places, and the e2es read the CR back to catch silent pruning).

Verification

  • Hermetic loop green per milestone: mise run test, clippy (-D warnings), fmt-check, complexity-check, gen+gen-check, wiring-check, cargo xtask check-phases.
  • crates/kopia integration test (real kopia binary) pins the whole throttle premise chain, including a self-calibrating ≥10× timing bound proving migrate honors config-persisted limits.
  • Full e2e suite on a fresh kind cluster (consolidated, all shards in one cluster): 222/227, with every failure dispositioned — the two known local-only NFS tests; two findings fixed on this branch as a result of the run (see below); and leader_election_holds_and_fails_over (a 420s leader-repo phase=Ready timeout, shown NOT to be caused by this branch's changes — it ran 118 tests before the hot loop existed on a single-threaded runner; still being triaged, CI shards will arbitrate). A post-fix re-run is in progress locally; CI runs the sharded matrix on this PR.

What the consolidated run caught (both fixed here)

  1. The apiserver stored an explicit "completedAt": null for the nullable CRD field instead of deleting the key on merge-patch. manualRun/maintenance status: emit explicit-null completedAt in non-terminal patches #394's semantics are correct either way (no stale timestamp survives; the noop guard converges in both worlds) — the e2e assertion and code comments now cover both apiserver behaviors.
  2. A pre-existing hot loop (from feat(repository): seed a fresh repository from its replica — DR for replicated repositories (#380) #396's seeding, not this branch — proven by running the repro at the merge-base): seed_success_fold re-stamped seededAt = now() on every finalize pass of a Ready seeded repo while its bootstrap Job lingered, and wrote a partial seed sub-object the shallow noop guard could never match — ~4 reconciles/sec, forever. Only visible when the seed test and the steady-state guard share a cluster, which the consolidated run does and CI's sharding does not. Fixed by stamping seededAt once and carrying startedAt forward so the write is byte-stable, with a regression test that fails if either half is reverted.

Every milestone passed an independent spec+quality review; two Criticals found and fixed en route (the #393 hot loop; a silent-green e2e design) — both now carry regression guards.

Follow-up work (documented here rather than filed as issues, per review)

  1. serve/browse plane cannot be throttled. ServerWorkSpec carries no throttle field (the serve connects apply nothing) and the CLI-built browse work spec hardcodes an empty throttle — a long-lived web-UI/browse session against a capped repository can saturate the link. Documented as an exemption in docs/reference/crds/shared-types.md; the fix is a throttle field on ServerWorkSpec + controller population + the browse spec builder, applied post-connect like apply_repository_throttle.
  2. moverDefaults.throttle is unvalidated (a 0 still admits) while the new per-CR migrate.throttle surfaces reject < 1. Wiring the shared validate_throttle in would newly reject specs that admit today — deliberately left out as a compat break needing its own decision; the validator drops straight in once decided.
  3. status_patch_is_noop is a shallow whole-value compare — any writer patching a partial sub-object never converges to wrote=false. The seed fold was fixed point-wise here; a recursive-subset compare would close the class. Known residual instance (no hot loop — the PATCH is value-identical so the apiserver no-ops it — but the transition-only gate keeps re-firing the RepositorySeeded Event, the seed metric, and reap_seed_projection each finalize pass while a bootstrap Job lingers): finalize_bootstrap writes storageStats = {indexBlobCount} while the catalog scan stores three keys. Guard-semantics change needs its own review.
  4. Batched snapshot deletions and pins carry no throttle (work specs leave the block empty — documented in shared-types); populating them is a small follow-up if capping bulk deletes matters.
  5. leader_election_holds_and_fails_over local failure (420s Ready timeout, independent of this branch) — needs its own triage if CI reproduces it.

…null (#394)

`completedAt` carried `skip_serializing_if = "Option::is_none"`, so a
non-terminal manualRun patch simply omitted the key. An RFC-7386 merge
patch never removes what it omits, so the PREVIOUS run's completion stamp
stayed visible under a fresh `Pending`/`Running`.

In the two replication controllers this is worse than a stale display: they
build the noop guard's `current` by re-serializing the same typed status, so
`desired` (key absent) never matched `current` (stale value) and every queued
pass re-fired a PATCH the apiserver no-op'd — a guard that cannot converge.
Maintenance's `patch_manual_run` is unguarded, so there it was display only.

Fix is serde-level in the two shared status types: drop the
`skip_serializing_if`, keep `#[serde(default)]`. A non-terminal phase now
emits `"completedAt": null` and the merge patch DELETES the stale key. No
controller changes — every non-terminal site already sets `None`.

Zero CRD drift: the rationale is a plain `//` comment, not rustdoc, because
doc comments become the CRD `description` and serde mechanics are not the
user's business.
…393)

The readiness gate's clear wrote `ReferentAvailable=True` and then let the
rest of the pass keep using the reconcile-start copy. The four gate parks in
`run_restore_mover` (MissingCaBundle, MissingServiceAccount, PrivilegedMover,
MissingCredentials) rebuild `conditions` from that copy and patch it
UNCONDITIONALLY, so they wrote the stale `False` straight back: two
resourceVersion-bumping writes per iteration, each waking the watch and
re-enqueueing, collapsing error-policy backoff into a tight loop for as long as
the missing Secret/SA/ConfigMap went unfixed — exactly the GitOps bring-up this
feature serves. Those parks are byte-identical no-ops today only because
nothing wrote conditions ahead of them.

`RepositoryGate::Proceed` now carries the `Cow<Restore>` the rest of the pass
must build on (borrowed when nothing was cleared, so the common path pays no
clone), and `reconcile_inner` rebinds `restore` to it — the caller cannot get a
`&Restore` to continue with without taking the carried one. The cleared-but-
stale combination has one construction site, the pure and total
`carried_after_clear`, so it is unwritable elsewhere.
Adds `KopiaClient::repository_set_client_read_only(bool)` →
`kopia repository set-client --read-only|--read-write`, the primitive a
read-only-connected source config needs to run a write-requiring verb:
flip read-write, run the verb, flip back.

Covered by a shim-binary unit test (exact argv, both polarities) and a new
`integration_set_client_throttle` test against the mise-pinned kopia 0.23.1,
which is the empirical gate for the dependent throttle milestones. It pins:

* set-client works on a `--readonly` connection and only rewrites the local
  config's `readonly` key;
* the flip is real, not cosmetic — `repository set-parameters` fails while
  read-only, succeeds inside the window, and fails again after the flip back;
* `throttle set` does NOT actually need the window. It succeeds directly on a
  read-only connection (it never puts a blob), contradicting the design premise
  this milestone was written to confirm. The flip is defensive there, not
  required; if a future kopia changes that, the test fails loudly;
* limits persist as `throttlingLimits` and survive the flip back;
* the load-bearing claim: `snapshot migrate --source-config` — which has no
  speed flags of its own — genuinely honors those config-persisted limits.

Two measured caveats the dependent milestones must design around, both
documented at the test's calibration table: throttling bites only on traffic
that reaches the backend (a warm source cache makes even a 1 KB/s cap
invisible), and kopia's throttler costs far more than its nominal rate implies
(28 KiB takes 14 s at a 2 MB/s cap; caps above ~10 MB/s do not bind at all).
)

dd23782 gave `RepositoryGate::Proceed` a `Cow<'a, Restore>`. A Cow stores its
owned variant INLINE, so the whole enum grew to a `Restore` while its sibling
variants are one `Action` each — `clippy::large_enum_variant`, and
`mise run clippy` (--workspace --all-targets -D warnings) exited 101.

Clippy's suggested `Box<Cow<..>>` would allocate on the BORROWED path too, and
that path is the common one this design deliberately keeps free. Instead
`CarriedRestore` boxes only its owned arm:

    Unchanged(&'a Restore)   // no clone, no allocation
    Cleared(Box<Restore>)    // one Box atop a clone already being made

so the gate enum is pointer-sized in both arms. Semantics are unchanged: the
no-clone fast path, the 'caller cannot get a &Restore without taking the carried
one' property, and the single total construction site all survive, and `get()`
is an exhaustive match so a third carrier shape must decide which object the
pass continues with.

The guard test now pins the carrier variant in both arms; its mutation-detection
property was re-verified after the refactor.
…ad-only (#374)

Review finding: the test proved every piece of the source-side sequence but
never asserted the invariant the pieces exist to protect. Since `throttle set`
turns out NOT to need the read-write window, what M3c/M3d will ship is
`connect --readonly` -> `throttle set` -> migrate with no flip — and
`throttle set` rewrites the config file. If that rewrite silently dropped the
`readonly` key, a replication SOURCE would be left writable and nothing
downstream would notice: the next step flipped read-write on purpose, and the
step after re-established the bit explicitly.

Restructures so the measured path IS the production path:

* the cap now lands at step (1b) on the read-only connection, and step (1c)
  immediately asserts the bit survived — in the config JSON AND behaviorally,
  via a write-requiring `set-parameters` probe;
* steps (2)-(4) keep the flip proof for the verbs that genuinely need the
  window, and (5) asserts the cap set on the read-only connection came through
  the flip sequence untouched, so (6) times the config the no-flip sequence
  wrote;
* the timing bound is now self-calibrating: an identical UNTHROTTLED baseline
  migrate is timed first and the capped run must be >= 10x it, so the verdict
  no longer depends on machine speed. The 3s absolute floor stays as a backstop
  against a pathologically slow baseline making the ratio trivial.

Both new assertions were mutation-checked against the real binary: forcing the
read-only bit off before (1c) fails it with "the config must record
`readonly: true`", and a non-binding 20 MB/s cap fails the ratio bound at
190.5ms vs a 189.9ms baseline.

Also from the review ledger: pin "set-client writes nothing to the backend" by
fingerprinting every blob across the flip instead of asserting it in prose, and
truncate the shim's argv capture directory before use so an appending shim
cannot inherit stale lines.
)

`repository throttle set` ran only in `run()`'s generic connect arm, so every
flow that opens its OWN connection silently dropped the `spec.throttle` its
controller had faithfully put on the wire: bootstrap (all three connects,
including the `mode: ReadOnly` probe), seed-migrate's local repository,
maintenance, verify and replicate. kopia's limits are per-CONNECTION — they
persist into that connection's client config — so a second connect under a
second config needs its own call; there is nothing repository-wide to inherit.

The invariant this ships: EVERY mover flow that connects a repository applies
`MoverWorkSpec.throttle` immediately after its connect, no exceptions. One
shared `apply_repository_throttle` helper does it, skipping when no limit is
set (no kopia process spawned for the common case), treating a refusal as
terminal (running uncapped over the link the user capped is worse than not
running), and emitting the one `applied repository throttle` line that is both
the e2e-assertable proof and the old-mover/new-controller skew signal.

Read-only connections are throttled too. M3a measured `repository throttle set`
succeeding on a `--readonly` connect against kopia 0.23.1 (with `set-parameters`
as the control probe that does hard-error there), so the read-write flip is not
needed and `repository_set_client_read_only` stays an unwired contingency.

The dispatch `_ =>` catch-all in `run()` is replaced by an exhaustive
`Operation` match — that catch-all is how this bug shipped, and a new operation
must now decide its throttle story to compile. Bootstrap's three connects go
through one funnel that applies the throttle by construction; a throttle failure
there is kept structurally distinct from a CONNECT failure (`BootstrapConnectFailure`),
since the probe reads a connect failure to decide whether to CREATE or SEED a
repository over the backend and a throttle failure says nothing about it.

Deliberately out of scope, both stated rather than forgotten:
- snapshot delete-batch and pin work specs carry `Default` (empty) throttle
  from their controllers, so they ride the generic arm as a no-op — a
  controller-side gap, not a mover one;
- browse builds its work spec in the CLI with `throttle: Default::default()`,
  so its `spec.throttle` is structurally always empty (comment only);
- SnapshotReplicate's dual connect and the seed SOURCE (replica) connect land
  with their own per-CR override surfaces in the following commits.

E2e, no silent green: three fixture repositories gain a non-binding 100 MiB/s
`moverDefaults.throttle` and the scenarios assert the mover's own log line —
timing proves nothing, since kopia's cap only bites cold-cache backend traffic.
Covers bootstrap + maintenance (S3 maintenance scenario), verify, and replicate;
each pre-created repository is read back so a dropped overlay cannot make the
assertion vacuous.
…#374)

Review follow-up to 869cf8f. Two findings, both about the fix being remembered
rather than enforced.

**The six own-connect flows were still remembered call sites.** Deleting
`apply_repository_throttle` from `run_verify_flow` compiled and passed every
hermetic gate — the run just went uncapped — which is the structure that caused
#374 in the first place, one layer up. Replaced with a `connect_and_throttle`
funnel (mirroring the `bootstrap_connect` one this PR already added): the
generic `run()` arm, maintenance, verify, replicate and a seed's local
repository now obtain a connection and its cap as ONE operation, so a future
flow cannot get the first without the second. Its `ConnectFailure` keeps the
connect and throttle halves distinct and labels the connect half with the
calling flow's own `KopiaOp`, so a status block still names the invocation that
actually failed.

Nothing in the type system stops a new flow from calling `repository_connect`
directly, so `connect_funnel_is_the_only_way_flows_connect` ratchets the count
of direct connects left in the file against a documented allowlist (the two
funnels plus the four connects that genuinely carry no `MoverWorkSpec.throttle`:
serve, browse, snapshot-replication's dual connect, a seed's source). Verified
to fire: adding one bypassing connect fails it 10 vs 9.

**Seed-migrate's local connect had no coverage of any kind** — no hermetic test
reaches `seed_create_and_connect_local`, and the e2e table covered
bootstrap/maintenance/verify/replicate only, so the highest-value call site of
the change ("the heaviest transfer kopiur performs") could have been deleted
and stayed green forever. The migrate-seed scenario now sets
`moverDefaults.throttle` (read back, so a dropped overlay cannot make it
vacuous) and asserts the mover's log line appears at least TWICE in a SINGLE
pod: a seeding bootstrap caps two connections (the seed's own local kopia config
— the only cap `snapshot migrate` can honor — and the post-seed reconnect).
Per-pod counting is load-bearing: the same selector also matches later
catalog-rescan pods that log it once each, so a bare `contains`, or a count
across pods, would pass with the seed-local call removed. `wait_for_pod_log` is
now the `times = 1` case of `wait_for_pod_log_times`.

Also, from the review's minor ledger: the helper's doc no longer claims a
no-exceptions invariant it cannot hold — it carries the exception list with each
reason and owning milestone; the generic arm's throttle failure now flows into
`result` instead of returning early past `metrics.record`/`shutdown`; and the
pod-log helper retains the last per-pod log error so a poll that times out
reports why rather than just "not found".
)

`kopia snapshot migrate` has no speed flags, and kopia's limits are
per-CONNECTION — so a snapshot replication, which opens two repositories
under two kopia configs, needs two independently resolved caps. Before
this, only the source connection was even reachable by a throttle (via
the work spec's `throttle`, itself only just wired), and the destination
— the off-site link that is usually the scarce one — had no cap surface
at all.

- API: `MigrateOptions.throttle: Option<MigrateThrottle>` with
  `source`/`destination` sub-blocks reusing `Throttle`. `MigrateOptions`
  loses `Copy`. Admission rejects a non-positive rate on either side,
  naming the side and the knob (a shared `validate_throttle` helper).
- Controller: `io::merged_throttle` resolves ONE side — that repository's
  `moverDefaults.throttle` overlaid by the CR's override FIELD BY FIELD,
  so pinning one knob never drops the repository's others. Called twice;
  the destination block rides the op as `destinationThrottle`.
- Mover: both srepl connects apply their own block immediately after
  connecting, each terminal via `srepl_terminal` — proceeding uncapped
  would saturate exactly the link the user asked to protect. The source
  application lands on the read-only connect (no read-write flip: kopia
  accepts `throttle set` there, pinned by the kopia integration test).
- Tests: the controller-glue guard now carries per-side overrides over
  two repositories that BOTH set defaults, asserting the merge and that
  neither side's knobs cross into the other; merge unit tests; wire-shape
  + old-wire-decode for `destinationThrottle`; camelCase roundtrip.
- E2e: the full-history scenario throttles both fixture repos and both
  sides of the CR, reads the CR back to prove the new field survived
  admission, asserts both merged blocks on the wire, and requires the
  srepl pod to log `applied repository throttle` twice — one occurrence
  means one side's cap is gone.
- Docs: per-side semantics and precedence, why migrate has no speed
  flags, the measured caveats (byte caps are far more punitive than
  nominal on small-object cold-cache work; warm reads bypass throttling
  entirely), and the helm-`crds/`-is-install-only trap that silently
  prunes new fields.
#374)

The reworked `moverDefaults.throttle` bullet enumerated the movers that
honor it and listed `deletion` — but the SnapshotDeleteBatch and
SnapshotPin work specs are built with `throttle: Default::default()`
unconditionally, so the generic mover arm applies nothing for them. A
precise-looking enumeration that is wrong is worse than the vague claim
it replaced: someone capping a repository to protect an object store
would not get that cap on bulk deletes, and nothing would say so.

Deletion moves to the exceptions, which are now their own paragraph
(serve/browse, a RepositoryReplication's destination side, batched
deletions and pins), each with its reason, and deletion's actual bound
named — per-repository batching plus `maxConcurrentDeleteJobs` — so the
gap reads as scoped rather than unqualified. The builders are left
alone; populating them stays out of scope.

Two adjacent cleanups:

- `srepl_terminal` logs `class` for a `MoverError::Kopia` the way
  `srepl_terminal_kopia` already does, so whether an operator can filter
  on it no longer depends on which helper a call site picked.
- The throttle integration test's prose said M3c/M3d "will ship" the
  read-write flip sequence. They didn't: the flip was empirically shown
  unnecessary and `repository set-client` stays unwired. The test now
  says so and states its own role as a contingency-only guard.
A seed is the single heaviest transfer kopiur performs — a whole
repository pulled across a link on the worst day you have had all year —
and migrate mode had no cap surface at all. Blob mode at least offered
`sync-to`'s speed flags; a migrate seed offered nothing, because `kopia
snapshot migrate` carries no speed flags either. The only lever is
`repository throttle set` on each CONNECTION, and a migrate seed opens
two under two kopia configs: the replica it reads and the repository it
writes.

- API: `SeedMigrateOptions.throttle: Option<MigrateThrottle>`, reusing
  the same per-side {source, destination} block SnapshotReplication's
  `spec.migrate.throttle` uses — here `source` is the REPLICA named by
  `seed.from.repository` and `destination` is this repository.
  `SeedMigrateOptions` loses `Copy`. Admission rejects a non-positive
  rate on either side, naming the side and the knob (the shared
  `validate_throttle`); a throttle on a BLOB seed stays refused by the
  existing mode-pairing rule, as an inert field.
- Controller: the replica's block is resolved WITH the source it belongs
  to, inside `seed_op_for`, so the two can never describe different
  repositories — that repository's own `moverDefaults.throttle` overlaid
  field-wise by the CR's override, and empty in blob mode. It rides the
  op as `replicaThrottle`. This repository's side rides the bootstrap
  work spec's `throttle`, now `seed_bootstrap_throttle`: the same merge,
  gated on the seed being ARMED so a number chosen for a one-time copy
  does not go on capping every later connect. Both repository kinds take
  the resolved block instead of `moverDefaults`, so they cannot drift.
- Mover: `seed_connect_source` applies the block in place immediately
  after the read-only replica connect (no read-write flip — kopia accepts
  `throttle set` there), BEFORE the persisted-password probe, so the
  probe sees the config `snapshot migrate` will reopen in its final
  state. Terminal, like every other application.
- Tests: a seed glue guard over two repositories that BOTH set defaults,
  asserting each side's partial override merges and that neither side's
  knobs cross into the other, plus the unarmed and blob arms; wire shape
  + old-wire decode for `replicaThrottle`; per-side validation; a
  camelCase round-trip through the apiserver path.
- E2e: the migrate scenario now throttles the replica repo, the seeded
  repo and both sides of the CR, reads the CR back to prove the new
  fields survived admission, and requires the seeding pod to log
  `applied repository throttle` THREE times — replica, seed-local,
  post-seed reconnect (the probe connect fails on an uninitialized
  backend, which is what arms the seed, so it never reaches its own).
- Docs: per-side semantics and precedence, why migrate has no speed
  flags, why the override is armed-only, the measured caveats (byte caps
  bite far harder than nominal on small-object cold-cache work; warm
  reads bypass throttling), and the helm-`crds/`-is-install-only trap
  that silently prunes new fields.
…374)

Both described the world before the replica connect was capped, in the two
places a reader is most likely to trust them.

- `seed_create_and_connect_local` still said the seed SOURCE's cap was
  "wired in a later change" — this WAS that change, so the comment told a
  reader of the local connect that the replica runs uncapped. It now names
  `replicaThrottle` and where it is applied.
- `apply_repository_throttle`'s doc used a seed as its worked example of
  "one line per APPLICATION" and put the count at two connections (the
  bootstrap's and the local repository's). A migrate seed now opens three,
  and the e2e asserts three. Restated as the general rule plus the exact
  seed arithmetic: three for a FIRST seed — the replica, the seed-local
  client and the post-seed reconnect, its probe connect having failed on
  the uninitialized backend, which is what armed the seed — and four when
  resuming, where that probe connect succeeds and is capped too.

Same off-by-one in docs/repositories.md's grep hint, which claimed three
unconditionally; it now distinguishes the first seed from a resume.

Comments and prose only; no behavior change.
…rose (#374, #393, #394)

Four small, behavior-neutral findings from final review:

- crates/mover/src/main.rs: remove a doc-comment paragraph pair duplicated
  back-to-back on apply_repository_throttle.
- crates/mover/src/main.rs: reword the maintenance/verify/replicate-source
  error! log literals, which also fire on a refused throttle set
  (ConnectFailure::Throttle) and previously read as pure connectivity
  failures. No change to error classes, propagation, or status bodies.
- docs/dev/api-conventions.md: document the manualRun.completedAt exception
  to the blanket skip_serializing_if convention (#394).
- crates/controller/src/restore/tests.rs: fix a stale doc comment referring
  to Cow<Restore>; the type is CarriedRestore since 7c89887 (#393).
- docs/reference/crds/shared-types.md: add "seed" to the throttle
  honored-flows list, consistent with the seed-shape sentence that follows.
#396 follow-up)

A repository that finished a seed span its own reconciler at ~4 passes a
second until its bootstrap Job's TTL expired — a self-sustaining hot loop
that starved every other Repository in the cluster.

`finalize_bootstrap` re-enters on EVERY reconcile for as long as the
finished bootstrap Job exists: the Job is deleted only on the health-probe
arm, and a seeding bootstrap is never a probe run. Each re-entry re-reads
the same result ConfigMap, so `result.seed` is still the same
`SeedOutcome`, forever — and `seed_success_fold` stamped
`status.seed.seededAt = Utc::now()` unconditionally. A fresh timestamp on
every pass meant `status_patch_is_noop` never held, the status PATCH bumped
`resourceVersion`, the controller's own primary watch re-delivered the
object as `object.reason="object updated"`, and round it went. The
`RepositorySeeded` Event is gated on that same write, so the loop also
minted one Event aggregation per pass.

A second, independent defect kept the loop alive even with a stable clock:
the no-op guard compares the whole `seed` value, while the fold wrote a
PARTIAL sub-object that omitted the `startedAt` marker `seed_marker_patch`
had stamped. A partial object can never equal the stored one, so the write
repeated regardless of the timestamp.

- `seed_success_fold` now takes the live `status.seed`. `seededAt` is
  stamped ONCE, at the first fold, and reused verbatim afterwards — a seed
  happens exactly once, so its completion time is a fact, not a heartbeat
  (the rule `seed_marker_patch` already applies to `startedAt`). The marker
  itself is carried forward into the patch so the fold's `seed` object
  equals the stored one byte for byte. Both repository kinds pass their own
  `status.seed`, so neither can drift.
- Regression test: re-folding a finished MIGRATE seed minutes later must be
  a `status_patch_is_noop`, shaped like the e2e's `e2e-seed-mig-dst`. It
  covers both arms — the advancing wall clock and the missing marker — and
  pins that the FIRST fold still stamps `now`. Reverting either half of the
  fix fails it.

This predates the branch: the unguarded `seededAt = now` arrived with
74665dc (#380 / #396) and reproduces identically at the merge-base of main.
It only went red now because the consolidated e2e run puts the migrate-seed
scenario (which deliberately leaves its seeded Repository standing) in the
same cluster as `ready_repository_steady_state_is_quiet`, which counts
fleet-wide Repository reconciles.

The shared `status_patch_is_noop` shallow-compare stays as it is; making it
a recursive-subset compare is a global guard-semantics change and is
deferred to its own review.
…394)

The #394 e2e demanded the stale `completedAt` key be ABSENT after the
non-terminal patch nulled it. Against a real apiserver it is not: on
k8s 1.33 a nullable CRD field answers a merge-patch null by STORING the
null rather than deleting the key. The scenario failed on a property it
never meant to assert.

#394's semantics are correct either way — an absent key and an explicit
null both decode to `None`, both re-serialize to `null`, and neither
leaves the previous run's timestamp standing, which is the whole point.
Only the assertion and the prose were over-specified.

- E2e: assert there is no completedAt VALUE (absent OR explicit null is
  fine; a non-null string is the bug), with the message rewritten to name
  that property instead of the deletion mechanic.
- Comments at both serde sites (`common::ReplicationManualRunStatus`,
  `maintenance::ManualRunStatus`) and in the api/controller tests now say
  the apiserver either deletes the key or stores the null, and why both
  converge. The `Patch::Merge`-not-`Patch::Apply` dependency is unchanged
  and still called out.
- `manual_run_patch_converges_after_clearing_a_stale_completed_at` now
  replays BOTH apiserver outcomes against the noop guard, so the stored-null
  shape is pinned rather than assumed.
@greptile-apps

greptile-apps Bot commented Aug 26, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR addresses three replication and restore follow-ups while extending repository throttling across mover workflows.

  • Serializes null manual-run completion timestamps so non-terminal status patches clear stale values and converge.
  • Adds tri-state Restore referent handling, an explanatory structural gate, and carried status updates that preserve gate clearing.
  • Applies repository throttles through shared mover connection paths and adds per-side migration overrides.
  • Stabilizes seed finalization status writes and updates validation, CRDs, documentation, and regression coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
crates/api/src/common/mod.rs Changes manual-run status serialization so an absent completion timestamp is emitted as null and clears stale merge-patched state.
crates/controller/src/restore/mod.rs Implements tri-state repository readiness handling and preserves cleared referent conditions through downstream restore reconciliation.
crates/controller/src/restore/plan.rs Adds typed repository-reference lookup outcomes and carried Restore state used by the readiness gate.
crates/controller/src/io/repo.rs Adds field-wise resolution of repository defaults and explicit per-run throttle overrides.
crates/mover/src/main.rs Funnels repository connections through throttle application across supported mover operations.
crates/controller/src/repo_seed.rs Makes seed-success status folding byte-stable across repeated finalization passes.
crates/api/src/validate/repository.rs Validates newly introduced per-side migration throttle values.
deploy/crds/snapshotreplications.yaml Publishes the SnapshotReplication migration throttle schema in the generated CRD.

Reviews (2): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

@perfectra1n
perfectra1n added this pull request to the merge queue Aug 28, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Aug 28, 2026
…-gate-throttle

# Conflicts:
#	crates/controller/src/repository.rs
@perfectra1n
perfectra1n enabled auto-merge August 28, 2026 17:30
@perfectra1n
perfectra1n added this pull request to the merge queue Aug 28, 2026
Merged via the queue into main with commit 8f0399f Aug 28, 2026
43 checks passed
@perfectra1n
perfectra1n deleted the fix/manualrun-restore-gate-throttle branch August 28, 2026 18:30
@sticky-gecko sticky-gecko Bot mentioned this pull request Aug 28, 2026
gabrielcosi pushed a commit to gabrielcosi/home-ops that referenced this pull request Aug 28, 2026
….10.4 ➔ 0.10.5) (#566)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/home-operations/charts/kopiur](https://github.com/home-operations/kopiur) | patch | `0.10.4` → `0.10.5` |

---

### Release Notes

<details>
<summary>home-operations/kopiur (ghcr.io/home-operations/charts/kopiur)</summary>

### [`v0.10.5`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0105-2026-08-28)

[Compare Source](home-operations/kopiur@0.10.4...0.10.5)

##### Bug Fixes

- deadline-killed bootstrap classification, maintenance gate exemption, relaunch backoff + self-healing escalation ([#&#8203;417](home-operations/kopiur#417)) ([ff8b396](home-operations/kopiur@ff8b396))
- manualRun explicit-null completedAt, tri-state Restore gate, throttle honored in every mover flow ([#&#8203;394](home-operations/kopiur#394), [#&#8203;393](home-operations/kopiur#393), [#&#8203;374](home-operations/kopiur#374)) ([#&#8203;411](home-operations/kopiur#411)) ([8f0399f](home-operations/kopiur@8f0399f))

##### Miscellaneous Chores

- **github-action:** update action jdx/mise-action (v4.2.5 → v4.3.0) ([#&#8203;419](home-operations/kopiur#419)) ([6234791](home-operations/kopiur@6234791))

</details>

---

### Configuration

📅 **Schedule**: (in timezone Europe/Berlin)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zOS4xIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsicmVub3ZhdGUvY29udGFpbmVyIiwidHlwZS9wYXRjaCJdfQ==-->

Reviewed-on: https://git.xcd.dev/gabrielcosi/home-ops/pulls/566
doonga pushed a commit to greyrock-labs/home-ops that referenced this pull request Sep 5, 2026
….10.4 ➔ 0.10.7) (#87)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/home-operations/charts/kopiur](https://github.com/home-operations/kopiur) | patch | `0.10.4` → `0.10.7` |

---

### Release Notes

<details>
<summary>home-operations/kopiur (ghcr.io/home-operations/charts/kopiur)</summary>

### [`v0.10.7`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0107-2026-09-02)

[Compare Source](home-operations/kopiur@0.10.6...0.10.7)

##### Features

- per-repository mover-Job concurrency limits + jitter hardening ([#&#8203;437](home-operations/kopiur#437)) ([1529aa9](home-operations/kopiur@1529aa9))

### [`v0.10.6`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0106-2026-09-01)

[Compare Source](home-operations/kopiur@0.10.5...0.10.6)

##### Features

- **errors:** clarify user-facing Kubernetes error/warning messages ([#&#8203;431](home-operations/kopiur#431)) ([099d0a1](home-operations/kopiur@099d0a1))

##### Bug Fixes

- **controller:** remove the NoAdoptableHistory warning event ([#&#8203;429](home-operations/kopiur#429)) ([a3dccb4](home-operations/kopiur@a3dccb4))
- kopia UI server never received the backend auth Secret — split-secret creds, per-secret mirrors, server identity ([#&#8203;416](home-operations/kopiur#416)) ([#&#8203;424](home-operations/kopiur#424)) ([ab1327a](home-operations/kopiur@ab1327a))
- **rust:** update crate rcgen (0.14.9 → 0.14.10) ([#&#8203;423](home-operations/kopiur#423)) ([48c10a3](home-operations/kopiur@48c10a3))

##### Miscellaneous Chores

- **github-action:** update action anchore/sbom-action (v0.24.0 → v0.24.1) ([#&#8203;427](home-operations/kopiur#427)) ([4aee76d](home-operations/kopiur@4aee76d))
- **github-action:** update action anchore/sbom-action (v0.24.1 → v0.24.2) ([#&#8203;432](home-operations/kopiur#432)) ([0d029d4](home-operations/kopiur@0d029d4))
- **mise:** update tool aqua:astral-sh/uv (0.12.5 → 0.12.6) ([#&#8203;422](home-operations/kopiur#422)) ([d06d57c](home-operations/kopiur@d06d57c))
- **mise:** update tool aqua:astral-sh/uv (0.12.6 → 0.12.7) ([#&#8203;428](home-operations/kopiur#428)) ([36f8f56](home-operations/kopiur@36f8f56))
- **mise:** update tool aqua:dadav/helm-schema (0.23.4 → 0.23.5) ([#&#8203;412](home-operations/kopiur#412)) ([c1ad2a1](home-operations/kopiur@c1ad2a1))
- **mise:** update tool kubectl (1.36.4 → 1.37.0) ([#&#8203;426](home-operations/kopiur#426)) ([08eb492](home-operations/kopiur@08eb492))
- **mise:** update tool lefthook (2.1.11 → 2.1.12) ([#&#8203;430](home-operations/kopiur#430)) ([21312e5](home-operations/kopiur@21312e5))
- **mise:** update tool node (24.19.0 → v24.20.0) ([#&#8203;425](home-operations/kopiur#425)) ([9e4ffb8](home-operations/kopiur@9e4ffb8))

### [`v0.10.5`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0105-2026-08-28)

[Compare Source](home-operations/kopiur@0.10.4...0.10.5)

##### Bug Fixes

- deadline-killed bootstrap classification, maintenance gate exemption, relaunch backoff + self-healing escalation ([#&#8203;417](home-operations/kopiur#417)) ([ff8b396](home-operations/kopiur@ff8b396))
- manualRun explicit-null completedAt, tri-state Restore gate, throttle honored in every mover flow ([#&#8203;394](home-operations/kopiur#394), [#&#8203;393](home-operations/kopiur#393), [#&#8203;374](home-operations/kopiur#374)) ([#&#8203;411](home-operations/kopiur#411)) ([8f0399f](home-operations/kopiur@8f0399f))

##### Miscellaneous Chores

- **github-action:** update action jdx/mise-action (v4.2.5 → v4.3.0) ([#&#8203;419](home-operations/kopiur#419)) ([6234791](home-operations/kopiur@6234791))

</details>

---

### Configuration

📅 **Schedule**: (in timezone America/New_York)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC41Mi4xIiwidXBkYXRlZEluVmVyIjoiNDQuNTIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsicmVub3ZhdGUvY29udGFpbmVyIiwidHlwZS9wYXRjaCJdfQ==-->

Reviewed-on: https://git.greyrock.io/todd/home-ops/pulls/87
GiorgioAresu pushed a commit to GiorgioAresu/home-ops that referenced this pull request Sep 13, 2026
….10.0 ➔ 0.10.8) (#2042)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/home-operations/charts/kopiur](https://github.com/home-operations/kopiur) | patch | `0.10.0` → `0.10.8` |

---

> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/6) for more information.

---

### Release Notes

<details>
<summary>home-operations/kopiur (ghcr.io/home-operations/charts/kopiur)</summary>

### [`v0.10.8`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0108-2026-09-08)

[Compare Source](home-operations/kopiur@0.10.7...0.10.8)

##### Bug Fixes

- **container:** update image busybox ([`9532d8c`](home-operations/kopiur@9532d8c) → [`dc2d74b`](home-operations/kopiur@dc2d74b)) ([#&#8203;439](home-operations/kopiur#439)) ([0bad734](home-operations/kopiur@0bad734))
- re-initialize ack for wiped repositories ([#&#8203;435](home-operations/kopiur#435)), populator fan-out with per-PVC source paths ([#&#8203;443](home-operations/kopiur#443)), helm README ([#&#8203;436](home-operations/kopiur#436)) ([#&#8203;449](home-operations/kopiur#449)) ([fdfa9a6](home-operations/kopiur@fdfa9a6))
- **rust:** update crate rustls (0.23.43 → 0.23.44) ([#&#8203;448](home-operations/kopiur#448)) ([b9e10a6](home-operations/kopiur@b9e10a6))

##### Miscellaneous Chores

- **github-action:** update action actions/deploy-pages (v5.0.0 → v5.0.1) ([#&#8203;444](home-operations/kopiur#444)) ([f601ebd](home-operations/kopiur@f601ebd))
- **github-action:** update action helm/kind-action (v1.14.0 → v1.15.0) ([#&#8203;446](home-operations/kopiur#446)) ([94ab9de](home-operations/kopiur@94ab9de))
- **mise:** update mise tools ([#&#8203;399](home-operations/kopiur#399)) ([ffae98a](home-operations/kopiur@ffae98a))
- **mise:** update tool aqua:astral-sh/uv (0.12.7 → 0.12.8) ([#&#8203;442](home-operations/kopiur#442)) ([8954063](home-operations/kopiur@8954063))
- **mise:** update tool aqua:astral-sh/uv (0.12.8 → 0.12.9) ([#&#8203;445](home-operations/kopiur#445)) ([5a88ac6](home-operations/kopiur@5a88ac6))
- **mise:** update tool aqua:astral-sh/uv (0.12.9 → 0.12.10) ([#&#8203;450](home-operations/kopiur#450)) ([c32246a](home-operations/kopiur@c32246a))
- **mise:** update tool zizmor (1.29.0 → 1.30.0) ([#&#8203;441](home-operations/kopiur#441)) ([908a427](home-operations/kopiur@908a427))

### [`v0.10.7`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0107-2026-09-02)

[Compare Source](home-operations/kopiur@0.10.6...0.10.7)

##### Features

- per-repository mover-Job concurrency limits + jitter hardening ([#&#8203;437](home-operations/kopiur#437)) ([1529aa9](home-operations/kopiur@1529aa9))

### [`v0.10.6`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0106-2026-09-01)

[Compare Source](home-operations/kopiur@0.10.5...0.10.6)

##### Features

- **errors:** clarify user-facing Kubernetes error/warning messages ([#&#8203;431](home-operations/kopiur#431)) ([099d0a1](home-operations/kopiur@099d0a1))

##### Bug Fixes

- **controller:** remove the NoAdoptableHistory warning event ([#&#8203;429](home-operations/kopiur#429)) ([a3dccb4](home-operations/kopiur@a3dccb4))
- kopia UI server never received the backend auth Secret — split-secret creds, per-secret mirrors, server identity ([#&#8203;416](home-operations/kopiur#416)) ([#&#8203;424](home-operations/kopiur#424)) ([ab1327a](home-operations/kopiur@ab1327a))
- **rust:** update crate rcgen (0.14.9 → 0.14.10) ([#&#8203;423](home-operations/kopiur#423)) ([48c10a3](home-operations/kopiur@48c10a3))

##### Miscellaneous Chores

- **github-action:** update action anchore/sbom-action (v0.24.0 → v0.24.1) ([#&#8203;427](home-operations/kopiur#427)) ([4aee76d](home-operations/kopiur@4aee76d))
- **github-action:** update action anchore/sbom-action (v0.24.1 → v0.24.2) ([#&#8203;432](home-operations/kopiur#432)) ([0d029d4](home-operations/kopiur@0d029d4))
- **mise:** update tool aqua:astral-sh/uv (0.12.5 → 0.12.6) ([#&#8203;422](home-operations/kopiur#422)) ([d06d57c](home-operations/kopiur@d06d57c))
- **mise:** update tool aqua:astral-sh/uv (0.12.6 → 0.12.7) ([#&#8203;428](home-operations/kopiur#428)) ([36f8f56](home-operations/kopiur@36f8f56))
- **mise:** update tool aqua:dadav/helm-schema (0.23.4 → 0.23.5) ([#&#8203;412](home-operations/kopiur#412)) ([c1ad2a1](home-operations/kopiur@c1ad2a1))
- **mise:** update tool kubectl (1.36.4 → 1.37.0) ([#&#8203;426](home-operations/kopiur#426)) ([08eb492](home-operations/kopiur@08eb492))
- **mise:** update tool lefthook (2.1.11 → 2.1.12) ([#&#8203;430](home-operations/kopiur#430)) ([21312e5](home-operations/kopiur@21312e5))
- **mise:** update tool node (24.19.0 → v24.20.0) ([#&#8203;425](home-operations/kopiur#425)) ([9e4ffb8](home-operations/kopiur@9e4ffb8))

### [`v0.10.5`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0105-2026-08-28)

[Compare Source](home-operations/kopiur@0.10.4...0.10.5)

##### Bug Fixes

- deadline-killed bootstrap classification, maintenance gate exemption, relaunch backoff + self-healing escalation ([#&#8203;417](home-operations/kopiur#417)) ([ff8b396](home-operations/kopiur@ff8b396))
- manualRun explicit-null completedAt, tri-state Restore gate, throttle honored in every mover flow ([#&#8203;394](home-operations/kopiur#394), [#&#8203;393](home-operations/kopiur#393), [#&#8203;374](home-operations/kopiur#374)) ([#&#8203;411](home-operations/kopiur#411)) ([8f0399f](home-operations/kopiur@8f0399f))

##### Miscellaneous Chores

- **github-action:** update action jdx/mise-action (v4.2.5 → v4.3.0) ([#&#8203;419](home-operations/kopiur#419)) ([6234791](home-operations/kopiur@6234791))

### [`v0.10.4`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0104-2026-08-24)

[Compare Source](home-operations/kopiur@0.10.3...0.10.4)

##### Bug Fixes

- **chart:** give flowSchema its doc comment and timings a clean schema entry ([#&#8203;397](home-operations/kopiur#397)) ([7c8dd3f](home-operations/kopiur@7c8dd3f))
- **mover:** read status.resolved via the /status subresource ([#&#8203;408](home-operations/kopiur#408)) ([06fc2f9](home-operations/kopiur@06fc2f9))

##### Miscellaneous Chores

- **github-action:** update action docker/github-builder (v1.16.0 → v1.17.0) ([#&#8203;407](home-operations/kopiur#407)) ([b6d7d04](home-operations/kopiur@b6d7d04))
- **mise:** update mise tools ([#&#8203;400](home-operations/kopiur#400)) ([b1a83f8](home-operations/kopiur@b1a83f8))
- **mise:** update tool kubectl (1.36.3 → 1.36.4) ([#&#8203;405](home-operations/kopiur#405)) ([274e3d6](home-operations/kopiur@274e3d6))
- **mise:** update tool lefthook (2.1.10 → 2.1.11) ([#&#8203;406](home-operations/kopiur#406)) ([2ba700d](home-operations/kopiur@2ba700d))
- **mise:** update tool rust (1.97.1 → 1.98.0) ([#&#8203;403](home-operations/kopiur#403)) ([c655005](home-operations/kopiur@c655005))
- **mise:** update tool yq (4.53.3 → 4.53.4) ([#&#8203;402](home-operations/kopiur#402)) ([d1ed8a7](home-operations/kopiur@d1ed8a7))
- **mise:** update tool yq (4.53.4 → 4.53.6) ([#&#8203;404](home-operations/kopiur#404)) ([dd5878c](home-operations/kopiur@dd5878c))

### [`v0.10.3`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0103-2026-08-18)

[Compare Source](home-operations/kopiur@0.10.2...0.10.3)

##### Features

- **repository:** seed a fresh repository from its replica — DR for replicated repositories ([#&#8203;380](home-operations/kopiur#380)) ([#&#8203;396](home-operations/kopiur#396)) ([74665dc](home-operations/kopiur@74665dc))

##### Bug Fixes

- **rust:** update crate cel (0.14.2 → 0.14.3) ([#&#8203;387](home-operations/kopiur#387)) ([bf9aa0c](home-operations/kopiur@bf9aa0c))
- **rust:** update crate http-body-util (0.1.4 → 0.1.5) ([#&#8203;381](home-operations/kopiur#381)) ([6354e17](home-operations/kopiur@6354e17))

##### Miscellaneous Chores

- **github-action:** update action jdx/mise-action (v4.2.4 → v4.2.5) ([#&#8203;388](home-operations/kopiur#388)) ([1040c9e](home-operations/kopiur@1040c9e))
- **mise:** update tool aqua:astral-sh/uv (0.12.3 → 0.12.4) ([#&#8203;390](home-operations/kopiur#390)) ([bf3916c](home-operations/kopiur@bf3916c))
- **mise:** update tool aqua:astral-sh/uv (0.12.4 → 0.12.5) ([#&#8203;395](home-operations/kopiur#395)) ([b919878](home-operations/kopiur@b919878))
- **mise:** update tool helm (4.2.3 → 4.2.4) ([#&#8203;389](home-operations/kopiur#389)) ([5b134fa](home-operations/kopiur@5b134fa))
- **mise:** Update tool oxfmt (0.62.0 → 0.63.0) ([#&#8203;386](home-operations/kopiur#386)) ([65db237](home-operations/kopiur@65db237))

### [`v0.10.2`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0102-2026-08-13)

[Compare Source](home-operations/kopiur@0.10.1...0.10.2)

##### Performance Improvements

- **controller:** store-backed reads, LIST dedup, bounded PVC retries, client-side request metrics ([#&#8203;382](home-operations/kopiur#382)) ([#&#8203;383](home-operations/kopiur#383)) ([2b07022](home-operations/kopiur@2b07022))

### [`v0.10.1`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0101-2026-08-11)

[Compare Source](home-operations/kopiur@0.10.0...0.10.1)

##### Bug Fixes

- **mise:** something about mise warnings ([#&#8203;373](home-operations/kopiur#373)) ([fde8073](home-operations/kopiur@fde8073))
- **rust:** update crate cel (0.14.1 → 0.14.2) ([#&#8203;379](home-operations/kopiur#379)) ([4a60221](home-operations/kopiur@4a60221))
- **rust:** update crate futures (0.3.33 → 0.3.34) ([#&#8203;378](home-operations/kopiur#378)) ([ff657b8](home-operations/kopiur@ff657b8))
- **rust:** update crate rcgen (0.14.8 → 0.14.9) ([#&#8203;377](home-operations/kopiur#377)) ([e3b3b3f](home-operations/kopiur@e3b3b3f))

##### Miscellaneous Chores

- **mise:** Update tool aqua:astral-sh/uv (0.12.2 → 0.12.3) ([#&#8203;375](home-operations/kopiur#375)) ([952af6d](home-operations/kopiur@952af6d))

</details>

---

### Configuration

📅 **Schedule**: (in timezone Europe/Rome)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xNy4xIiwidXBkYXRlZEluVmVyIjoiNDQuMTcuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsicmVub3ZhdGUvY29udGFpbmVyIiwidHlwZS9wYXRjaCJdfQ==-->

Reviewed-on: https://git.aresu.eu/GiorgioAresu/home-ops/pulls/2042
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant