Skip to content

feat!: SnapshotPolicy deletion cascade + auto-adoption of discovered snapshots - #272

Merged
perfectra1n merged 19 commits into
mainfrom
feat/policy-cascade-adoption
Jul 19, 2026
Merged

perfectra1n merged 19 commits into
mainfrom
feat/policy-cascade-adoption

Conversation

@perfectra1n

Copy link
Copy Markdown
Collaborator

Summary

Two features that close the policy lifecycle gap described in #210: deleting a policy now cleans up its Snapshot CRs (keeping the kopia data), and recreating the policy adopts that history back so retention resumes governing it.

1. SnapshotPolicy deletion cascade

Deleting a SnapshotPolicy now removes its config-labeled Snapshot CRs via a new policy-cleanup finalizer:

  • Default spec.deletion.onPolicyDelete: Retain — the CRs are removed, every kopia snapshot survives (rediscoverable on the next catalog scan, adoptable on recreation). The cascade stamps pruned-by: policy-cascade, so children release quietly with zero repository contact.
  • Opt-in Delete — each CR's own deletionPolicy applies, issued as external deletions so the mass-deletion breaker (feat: mass-deletion protection (cascade guard, per-repo breaker, batched deletes) #265) gates bulk waves behind the allow-mass-deletion ack. Deliberately unstamped: a policy delete with Delete is exactly the kro-incident shape the breaker exists for.
  • Retain-wins-ties for simultaneous schedule+policy deletion (GitOps prune); a terminating policy can no longer be fired against by its schedule (skip+retry); breaker-held children never wedge policy removal.

2. Auto-adoption of discovered snapshots (fixes #210)

Discovered snapshots whose exact resolved identity (structured username/hostname/sourcePath comparison) matches a live SnapshotPolicy are adopted automatically: the row is recreated in the policy's namespace as origin: adopted, phase: Succeeded, config-labeled — indistinguishable from produced snapshots to retention, deletion, batching, catalog dedup, and the breaker. GFS retention then governs and prunes the old history per spec.retention, ending the "pre-DR history occupies storage forever" failure mode.

  • On by default; opt out per policy (spec.adoption: Ignore) or per repository (spec.catalog.adoption: Ignore).
  • Foreign-cluster snapshots in shared repositories are never adopted — three independent guards (structured identity equality with cluster-scoped hostnames, foreign rows are never materialized as discovered under Ignore, and a hard HostClass::ForeignCluster refusal).
  • The delete→recreate flow converges hands-off: the policy reconciler stamps a catalog-scan-requested-at token on the repository; scans retire it by token equality (status.catalog.scanRequestHonored), and launch attempts are rate-limited (scanRequestAttemptAt) so a Ready-but-unreachable repo never churns bootstrap Jobs.
  • Loudly surfaced, not gated: SnapshotsAdopted / AdoptionScanRequested / SnapshotRetainedOnPolicyDelete events, status.adoption summary on the policy, and kopiur_snapshots_adopted + cascade counters.

Hardening that rode along

  • Webhook now resolves Snapshot origin status-first: flipping a discovered row's origin label can no longer unlock deletionPolicy: Delete at admission.
  • New policy-referencing manual Snapshots get the config label at admission, making them visible to retention and the cascade. Deliberately no retro-backfill: labeling pre-existing raw-applied Snapshots would silently make previously-immortal history prunable.
  • The SnapshotRetainedOnScheduleDelete event no longer promises rediscovery "within the catalog refresh interval" (misleading with periodic refresh off).

BREAKING CHANGE

Deleting a SnapshotPolicy now removes its config-labeled Snapshot CRs by default (onPolicyDelete: Retain — kopia snapshots are kept and rediscoverable/adoptable). Previously the CRs were left dangling.

Test coverage

  • Hermetic: new unit/serde coverage across api/controller/webhook — cascade planner matrix (terminating exclusion, origin filter, release condition), the 9-cell PrunedBy × DeletionPolicy prune table, scan-request predicate matrix (equality retirement, launch backoff, no-token regression pins), full adoption matrix (identity near-misses, foreign refusal, 409-heal, knob precedence), webhook origin-flip denials.
  • e2e (6 new scenarios): the Discovered snapshots are permanently exempt from all pruning #210 acceptance test (produce → delete schedule+policy → CRs gone, kopia intact → recreate with keepLatest: 1 → scan request → discovered → adopted → retention prunes kopia-side to exactly 1, with a non-matching-identity negative control), ClusterRepository cross-namespace adoption, adoption opt-out, cascade default-Retain (incl. webhook-labeled manual Snapshot), breaker-gated opt-in Delete (hold → ack → drain), and the schedule+policy simultaneous-delete race.
  • Every milestone landed with cargo test --workspace, clippy -D warnings, gen-check, and the cognitive-complexity ratchet (29/29, unchanged) green. Each milestone was independently review-gated; one Critical (object-store scan-request honoring) was caught in review and fixed with a regression test (734b2f8).

Local fresh-kind validation of the new e2e binaries is running; CI's e2e suite is the authoritative gate for this PR.

Closes #210

Today CONFIG_LABEL is stamped by the schedule controller and by the CLI's
`snapshot now`, but a raw-`kubectl apply`'d manual Snapshot with
spec.policyRef never gets it — making it invisible to GFS retention, the
policy fan-out watch, and the upcoming SnapshotPolicy deletion cascade,
all of which select a policy's children by this label.

Fix the asymmetry in the mutating webhook instead: on Snapshot CREATE
only, stamp kopiur.home-operations.com/config = policyRef.name when the
origin is Manual/Scheduled (never Discovered), the ref has a nonempty
name targeting the Snapshot's own namespace, and the label isn't already
present. The decision is a pure `config_label_stamp` helper so it
unit-tests without a cluster; the JSON-patch op mirrors set_spec_field's
absent-parent handling for /metadata/labels.

Deliberately no controller-side backfill of pre-existing CRs: retro-
labeling would make previously-immortal raw-applied manual Snapshots
GFS-prunable, which is a silent-data-loss footgun.
…cy, Origin::Adopted, SnapshotAdoption)

Lands the full API surface for two upcoming controller/webhook features:

- SnapshotPolicy deletion cascade: PolicyDeletePolicy (Retain/Delete),
  SnapshotPolicySpec.deletion (PolicyDeletionSpec) + effective_on_policy_delete
  resolver, and PrunedBy::PolicyCascade for the finalizer's release-only path.
- Discovered-snapshot auto-adoption (#210): SnapshotAdoption (Adopt/Ignore),
  CatalogBounds.adoption + SnapshotPolicySpec.adoption + effective_adoption
  precedence resolver (policy > repo catalog > Adopt), Origin::Adopted, and
  SnapshotPolicyStatus.adoption (AdoptionSummary) + CatalogStatus.scanRequestHonored.

Validators updated to exhaustive Origin matches (no catch-alls):
validate_backup_deletion_policy allows any deletionPolicy for Adopted;
validate_backup_on_schedule_delete forbids onScheduleDelete for both
Discovered and Adopted with an origin-aware message.

Wires the new Origin::Adopted variant through every exhaustive match outside
crates/api needed to keep the workspace compiling (controller plan.rs/catalog.rs/
build.rs, webhook handlers.rs, CLI snapshots.rs) — adopted rows are treated as
managed/produced wherever discovered-vs-produced is distinguished. Behavioral
reconciler/webhook logic for both features lands in later milestones.

Regenerated deploy/crds, helm chart CRDs, docs/field-reference.md, and the
xtask CRD-schema insta snapshots via `cargo xtask gen-all`.
…rune plan

Lands the pure decision layer for the SnapshotPolicy deletion cascade (M2),
building on M1's PolicyDeletePolicy/PrunedBy::PolicyCascade/Origin::Adopted
types:

- plan_policy_cascade (snapshot_policy.rs): decides how a SnapshotPolicy
  deletion cascades onto its Snapshot children under onPolicyDelete
  Retain/Delete — origin-filtered (Discovered never cascades), terminating
  children excluded from any live-work set, exhaustive over
  (terminating, mode). Nothing calls it yet (wired to the policy finalizer
  in M3).
- plan_prune (snapshot/plan.rs) is now variant-aware: exhaustive 3x3 match
  over (PrunedBy, DeletionPolicy) instead of discarding the prune kind. A
  policy-cascade prune under an effective Delete policy downgrades to the
  new DeletionPlan::RetainSnapshotOnPolicyDelete rather than contacting the
  repository — the loud downgrade that makes the Retain-mode cascade safe.
- retain_on_policy_delete executor (snapshot/mod.rs) mirrors
  retain_on_schedule_delete exactly: releases the finalizer without repo
  contact, bumps the new kopiur_snapshots_policy_cascade_retained counter,
  and emits one SnapshotRetainedOnPolicyDelete Warning event via a pure,
  unit-tested message function that handles both the completed-snapshot and
  cancelled-mid-flight cases.

Extracted execute_deletion_plan out of handle_deletion to keep the latter
under the cognitive-complexity ratchet after adding the new match arm.

No CRD/schema changes; deploy/crds and docs/field-reference.md untouched
(gen-check clean).
…ting-policy fire guard

Wires the pure plan_policy_cascade planner (M2) to a new
policy-cleanup finalizer that drives the SnapshotPolicy deletion
cascade: each pass re-LISTs the policy's Snapshot children, executes
up to 50 batched actions (stamp_and_delete, then stamp_only, then
delete_only), and releases the finalizer only once a pass's plan is
entirely empty. A stamp_only-only pass still requeues rather than
releasing in the same pass, so a breaker-held terminating child never
wedges removal (it drains on the next, now-empty pass instead).

Also closes the SnapshotSchedule refire hole: a policy mid-deletion is
now treated exactly like an absent one (Error::MissingDependency),
via the new policy_usable predicate, so a schedule never fires a
Snapshot into a recipe whose own deletion cascade may already have
listed (and so will never account for) that child.

- crates/controller/src/consts.rs: POLICY_CLEANUP_FINALIZER
- crates/controller/src/io/prune.rs: bare unstamped delete_snapshot
  helper for the delete_only (external-classified) set
- crates/controller/src/metrics.rs: kopiur_policy_cascade_children_deleted
  counter (mode=retain|delete)
- crates/controller/src/snapshot_policy.rs: handle_policy_lifecycle/
  handle_policy_deletion wiring, slice_policy_cascade_batch (unit
  tested) + per-set execute_* helpers
- crates/controller/src/snapshot_schedule.rs: policy_usable (unit
  tested) wired into policy_default_deletion_policy
…nd attempt backoff

Adds repo-side honoring of an on-demand catalog-scan-requested-at token (the
policy-side stamping lands in a later milestone): a pure scan_requested_due
predicate retires the token by EQUALITY against status.catalog.scanRequestHonored
(never a lastRefreshAt comparison, which would let an unrelated periodic/
generation scan silently "honor" a request it predates), and rate-limits
repeat bootstrap attempts via a new status.catalog.scanRequestAttemptAt field
so a pending token against an unreachable backend cannot recreate bootstrap
Jobs forever. Threaded as an OR-arm into scan_due/bootstrap_recycle_due/
bootstrap_create_due in both the Repository and ClusterRepository reconcilers;
regression tests pin that a token=None reconcile is byte-identical to today.

CRDs/RBAC/docs regenerated via cargo xtask gen-all for the new status field.
backup_origin flips from from_label.or(from_status) to from_status.or(from_label)
to match the controller's resolve_origin. With adoption in play, a user could
otherwise flip a discovered row's metadata origin LABEL to "adopted" and unlock
deletionPolicy: Delete at admission while the controller (which only trusts
status.origin, a subresource users can't casually write) still treats the row
as discovered. Safe at CREATE (status.origin is always absent on a new object,
so the label fallback is unchanged) and for existing discovered/produced rows
(their admission defaulting never depended on which arm won).

Adds 4 route-level tests: adopted status admits Delete, discovered status
denies Delete, a flipped label does NOT override a discovered status (the
attack this closes), and CREATE with only the label still resolves discovered.
…it only bootstrap launches

A Ready object-store repo with a pending catalog-scan-request token never
retired it: scan_due's token arm reused the rate-limited scan_requested_due,
so the finalize pass scanning a just-succeeded, token-driven bootstrap Job
saw the very scanRequestAttemptAt stamp that launched it (still fresh) and
refused to scan, wedging the token behind an infinite
recycle-create-succeed-no-scan loop.

Split the predicate: scan_requested_pending (new, pure equality-retirement,
no rate limit) drives scan_due's token arm, so a completed bootstrap with a
fresh listing and a pending token always scans exactly once. The rate-limited
scan_requested_due (now built on top of scan_requested_pending) still gates
bootstrap_recycle_due/bootstrap_create_due, so an unreachable repo still
cannot churn Jobs. Applied identically to Repository and ClusterRepository.
…gate in snapshot reconciler

Converts reconcile_inner's Discovered-only gate into an exhaustive `match
origin` (no `_ =>`) so a new Origin variant cannot compile without a decision.
Adopted rows are retention-visible history, not runs: they now pin
`phase: Succeeded` + `status.origin: Adopted` and self-heal the
snapshot-cleanup finalizer, returning before any backup-run/post-hook/reap/pin
machinery, exactly like the existing Discovered arm. The Discovered arm's body
is a byte-for-byte relocation into pin_discovered_row; pin_adopted_row mirrors
its shape. Both share a new pure predicate (plan::needs_terminal_pin) for the
"only pin when unset/divergent" idempotence.

Adds plan_deletion coverage locking in that Adopted rows are NOT forced-Retain
(unlike Discovered): external deletes flow through the mass-deletion breaker
like produced rows, retention prunes always delete, and policy-cascade prunes
still retain kopia data. Also adds the first-ever tests for resolve_origin's
status-first-over-label precedence.
…ing policies

Adds the adoption engine (fixes #210): a discovered Snapshot whose kopia
identity exactly matches a live SnapshotPolicy's resolved identity is
re-created as an origin: Adopted Snapshot in the policy's namespace and the
discovered row deleted, so GFS retention governs (and eventually prunes) it
instead of it living in the catalog forever.

- New pure module crates/controller/src/adoption.rs: adoption_candidates,
  plan_adoption (exhaustive over SnapshotAdoption), build_adopted_snapshot,
  adoption_event_message — full unit matrix.
- snapshot_policy::run_adoption wires it in AFTER the retention block: a
  cluster-wide scoped_api LIST of this repo's discovered rows, structured
  identity equality, ForeignCluster refusal, create-then-ensure-status-then
  -delete (409-tolerant, no ownerReference), batch cap 50 in snapshot-id order.
- On-demand catalog-scan request stamped on the repository
  (io::request_catalog_scan) + SnapshotsAdopted/AdoptionScanRequested Normal
  events + kopiur_snapshots_adopted metric + status.adoption summary.
- ResolvedRepository gains catalog (for effective_adoption gating).
Documents the M0-M6 policy-deletion-cascade + snapshot-auto-adoption
feature for all three audiences:

- docs/backups.md: new "What happens when the policy is deleted" section
  (onPolicyDelete Retain/Delete, the behavior-change callout, Retain-wins-ties
  against a simultaneous schedule delete, the Running-child-cancelled case,
  discovered-rows-never-touched, both escape hatches, and the manual-Snapshot
  config-label caveat); a retention-section note that adopted rows are pruned
  exactly like produced ones.
- docs/scenarios/adopt-existing-repo.md: extends the scenario past
  "discovered" with "Delete a policy, then recreate it" — the full delete to
  clean-CRs/kept-data to on-demand scan request to rediscovered to adopted to
  retention-resumes flow, plus both adoption opt-outs and the foreign-cluster
  exclusion.
- docs/repositories.md: new `catalog.adoption` section under the catalog —
  the Adopt/Ignore knob, the policy-wins-over-repo resolution order, the
  bounded one-time scan-request burst on a repository shared by many
  policies, and the transiently-stale `discoveredBackupCount` after a wave.
- docs/troubleshooting.md: two entries — "My old snapshots were pruned after
  I recreated a policy" (adoption + GFS is by design, the SnapshotsAdopted
  event is the paper trail, both opt-outs) and "Adoption didn't happen" (a
  checklist: adoption knobs, exact-identity mismatch, foreign-cluster
  exclusion, scan-request pending/backoff, reconcile timing).
- deploy/examples/35-policy-deletion-cascade.yaml and
  36-policy-recreate-adoption.yaml: apply-ready manifests for the cascade and
  the recreate/adopt flow, snippet-included from the pages above (example
  34's inline-in-topic-page format, not the examples.md ladder).

Also fixes a stale event message: retain_on_schedule_delete's Warning Event
promised rediscovery "within the repository's catalog refresh interval",
which is misleading now that periodicRefresh defaults off. Extracted the
message into a pure schedule_cascade_retained_message (mirroring the
existing policy_cascade_retained_message), reworded to name the real
triggers (a catalog scan: bootstrap, spec change, or a recreated policy's
scan request) and the post-rediscovery outcome (default auto-adoption), and
added its first unit tests.

Every field name, event reason, metric, annotation, and status field cited
was checked against crates/api, crates/controller/src/consts.rs, and the
actual reconcile/adoption code before writing prose — not against the
original milestone plan, which evolved during implementation. mise run
test / clippy / gen / gen-check / complexity-check / docs all green; the new
examples pass crates/api's examples_match_crd_shapes test.

BREAKING CHANGE: deleting a SnapshotPolicy now removes its config-labeled
Snapshot CRs by default (onPolicyDelete: Retain — kopia snapshots are kept
and rediscoverable/adoptable). Previously the CRs were left dangling.
Adds crates/e2e/tests/adoption.rs (3 scenarios) and appends 3 policy-cascade
scenarios to crates/e2e/tests/mass_deletion.rs, proving the full policy-cascade +
snapshot-adoption story against a live operator:

adoption.rs
- policy_recreate_adopts_and_prunes_history: THE #210 acceptance test — delete a
  policy+schedule over real backdated history (kopia unchanged, finalizer released),
  recreate with keepLatest:1, and prove hands-off scan-request -> rediscover ->
  adopt (origin=adopted, config-labeled, Succeeded, lastAdoptedCount>=3 +
  SnapshotsAdopted event) -> kopia history pruned to the retention spec (the
  assertion that FAILED before this branch), with a non-matching negative control
  left as a forced-Retain discovered row.
- adoption_opt_out_stays_discovered: catalog.adoption: Ignore leaves matching rows
  discovered (no adopted rows, no scan-request stamping, empty status.adoption).
- cluster_repository_adoption_cross_namespace: discovered row in the workload ns,
  adopting policy in the operator ns (hostname override) -> adopts cross-namespace
  (the cluster-wide-LIST guard), converges again after delete/recreate.

mass_deletion.rs (scenarios 10-12)
- policy_cascade_delete_cleans_crs_keeps_kopia: Retain cascade drains a scheduled +
  a webhook-config-label-stamped MANUAL child, keeps kopia, re-catalogs, fires
  SnapshotRetainedOnPolicyDelete, leaves a pre-existing discovered row untouched.
- policy_cascade_opt_in_delete_is_breaker_gated: onPolicyDelete: Delete children are
  breaker-HELD, yet the policy finalizer still RELEASES; ack drains + deletes kopia.
- schedule_and_policy_simultaneous_delete_drains_to_retain: back-to-back delete
  drains every child with no stuck finalizer, no SnapshotDeleteBatch Job, kopia intact.

New isolated repo subpaths (consts.rs + mise node-seed) and the kopiur-adopt-crepo
bucket back these scenarios.
@greptile-apps

greptile-apps Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR adds SnapshotPolicy lifecycle cleanup and automatic adoption of matching discovered snapshots. The main changes are:

  • A policy cleanup finalizer that removes config-labeled Snapshot CRs on SnapshotPolicy deletion.
  • onPolicyDelete behavior for retaining kopia data by default or opting into Snapshot deletion.
  • Automatic adoption of discovered snapshots whose resolved identity matches a live policy.
  • On-demand catalog scan requests for delete-and-recreate adoption flows.
  • API, webhook, CRD, metrics, events, docs, examples, and tests for the new lifecycle paths.

Confidence Score: 5/5

The changes appear safe to merge.

The cascade, adoption, webhook, API, and scan-request paths did not leave actionable issues.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex attempted to capture the cargo test run using the default shell, but the capture could not proceed because the shell did not support set -o pipefail.
  • T-Rex re-ran the cargo test command under bash -lc to obtain a valid validation artifact.
  • The cargo test run completed with all test groups passing, including final doctest summaries for kopiur_api, kopiur_controller, kopiur_mover, and kopiur_webhook.
  • The validation artifacts together provide the exact command, working directory, timestamps, full cargo output, and EXIT_CODE: 0 for review.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
crates/controller/src/adoption.rs Adds adoption candidate extraction, identity matching, foreign-cluster refusal, bounded batches, and adopted Snapshot construction.
crates/controller/src/snapshot_policy.rs Adds policy deletion cascade handling and adoption orchestration after retention.
crates/controller/src/snapshot/plan.rs Extends Snapshot deletion planning for policy-cascade prunes and namespace teardown precedence.
crates/controller/src/snapshot/mod.rs Adds adopted-row steady-state handling with provenance gating.
crates/controller/src/catalog.rs Adds on-demand catalog scan predicates and scan-request retirement behavior.
crates/controller/src/repository.rs Threads catalog scan request honoring and attempt status through Repository reconciliation.
crates/controller/src/cluster_repository.rs Mirrors scan request honoring and attempt handling for ClusterRepository reconciliation.
crates/controller/src/io/repo.rs Adds a helper for adoption-driven catalog scan requests and carries catalog adoption settings in resolved repositories.
crates/webhook/src/handlers.rs Updates Snapshot admission to resolve origin status-first and stamp policy labels for managed Snapshot origins.
crates/api/src/snapshot_policy.rs Adds policy deletion and adoption API fields plus adoption status summary types.
crates/api/src/common/cache.rs Adds SnapshotAdoption settings and effective adoption precedence.
crates/api/src/validate/snapshot.rs Extends validation so adopted snapshots cannot set schedule-deletion behavior.

Reviews (4): Last reviewed commit: "fix(controller): collision-free adopted ..." | Re-trigger Greptile

…ore rediscovery)

The 'pre-existing untouched discovered row' control in
policy_cascade_delete_cleans_crs_keeps_kopia deleted the seed Snapshot CR while its
identity-matching SEED_POL policy was still LIVE, so the operator CORRECTLY
auto-adopted the re-cataloged seed (flipping it out of 'discovered') and the
repo_discovered_rows wait timed out. Retire SEED_POL first (Retain cascade keeps the
kopia snapshot) and only accept the discovered row once SEED_POL is gone, so no live
policy can adopt the control out from under it. Test-fixture fix only; the operator
behaved correctly (retain held, adoption fired as designed). Confirmed green on a
fresh kind cluster.
…cy-cascade Retain stamp

During namespace teardown the SnapshotPolicy cleanup finalizer stamps its
live children `pruned-by: policy-cascade` (default onPolicyDelete: Retain).
`plan_deletion` routed such a stamped child straight to the quiet-retain
downgrade (RetainSnapshotOnPolicyDelete), silently nullifying an EXPLICIT
`onNamespaceDelete: Delete` opt-in — so `kubectl delete ns` left the kopia
snapshot in the repo instead of reclaiming it (the failing e2e
on_namespace_delete_delete_cascades_snapshot reported 1 snapshot).

Add `plan_ns_delete` (the ns-terminating Delete arm), exhaustive over
PrunedBy: an unstamped OR policy-cascade-stamped child resolves as an
external destructive deletion (breaker-gated) — the explicit ns-delete
opt-in wins over the implicit cascade stamp. Retention/failed-history
operator prunes keep their prune semantics; Orphan/None ns policies stay
non-destructive. The retain-wins-ties rule stays scoped to schedule-vs-
policy cascade races, not explicit namespace-delete opt-ins.

Adds two composite plan_deletion unit tests pinning the contract (the
Delete case fails on HEAD; the Orphan/None case guards the preserved
non-destructive default).
…cade Delete mode

On the policy-cascade branch a config-labeled child is owned by the
SnapshotPolicy deletion cascade, so deleting the policy first now
quiet-releases the child under the default Retain mode — the old
"delete the policy, then delete the Snapshot" flow no longer occurs (the
unwrap panic at credential_projection.rs:652 was the now-404 explicit delete).

Set `spec.deletion.onPolicyDelete: Delete` so policy deletion issues an
UNSTAMPED external delete for the child, whose finalizer must complete the
kopia deletion using the PINNED resolved repository + credentialProjection
with the recipe gone — exactly the #255 property, now also covering the
cascade Delete path end-to-end. The child is removed by the cascade (the
test never touches the Snapshot); its released cleanup finalizer is the
kopia-side proof (a deletionPolicy: Delete external deletion under the
breaker threshold resolves to DeleteSnapshot). Keeps the test name and
updates its doc comment to state the new flow.
Comment thread crates/controller/src/adoption.rs Resolved
…n Delete waves

During a namespace teardown with the explicit `onNamespaceDelete: Delete`
opt-in, the SnapshotPolicy cleanup finalizer stamps its children
`pruned-by: policy-cascade`. Their plan correctly resolves to an external
destructive DeleteSnapshot (plan_ns_delete -> plan_external), but the
mass-deletion breaker never counted or held them, and the batch dispatcher
never even fired them:

- counts_toward_breaker blanket-exempted ANY pruned-by stamp.
- gather_deletion_facts computed breaker_applies in the stable
  ns_terminating:false form, where a policy-cascade child is a quiet retain.
- pending_members (the repo-wide counting/fire set) hardcoded
  ns_terminating:false, so a policy-cascade child was excluded from the
  counting set entirely -> uncounted AND unfireable (fire_batch's self-uid
  bypass only matches members present in the set), so its kopia snapshot was
  never deleted.
- PendingMember.external (== breaker-relevant) was pruned_by().is_none(), so
  even once included a policy-cascade child was treated as an exempt prune by
  unacked_breaker_count / fire_breaker_ok.

Fix: a single exhaustive PrunedBy classification (plan::breaker_relevant) shared
by counts_toward_breaker and the external flag — Retention/FailedHistory stay
exempt everywhere; None/PolicyCascade are breaker-relevant. The counting set now
evaluates each member's plan in its real (ns_terminating, ns_policy) form:
resolve_terminating_namespaces runs over pending_candidate_namespaces (bounded by
CRs actually being deleted) before pending_members, and gather_deletion_facts
computes breaker_applies in the real form. A policy-cascade child under
ns-teardown Delete now counts toward and is held by the per-repo breaker exactly
like an unstamped external child (same threshold, same allow-mass-deletion
ack release, same DeletionHeld condition/events); Retention/FailedHistory keep
their prune semantics; policy-cascade in a live namespace keeps quiet-retain.
The repo-level MassDeletionHeld mirror is likewise made ns-aware for consistency.

No wedge: an already-stamped held terminating child yields zero policy-cascade
work, so handle_policy_deletion still releases the finalizer (guarded by
plan_policy_cascade_only_terminating_children_yields_no_live_work_in_either_mode).
`pin_adopted_row` pinned `phase: Succeeded` for ANY CR resolving to
`Origin::Adopted` — including a user-applied bare `origin: adopted` label
with no controller-written `status.snapshot`. That phantom Succeeded row
entered `retention_view` (via the creationTimestamp fallback), claimed a
GFS bucket, and displaced a REAL snapshot into the breaker-exempt
retention delete set; it also set `has_history`, suppressing a recreated
policy's on-demand scan request.

Gate the pin on controller-written provenance (`status.snapshot` present,
the id `adopt_one`'s create→status-patch flow records) via a new pure
predicate `plan::adopted_row_has_provenance`. The genuine adopt flow
converges within a pass, so an interim row is only transiently phase-less.

Defense in depth:
- `retention_view` now requires `status.snapshot` for a Succeeded row to
  enter GFS — closes the displacement even if a phase ever sneaks in.
- `own_snapshot_ids_and_history`'s `origin == Adopted` arm requires the
  same provenance, so a bare label never counts as history.

Also harden `adopt_one`'s create-409 path: verify the occupant carries
the expected `SNAPSHOT_ID_LABEL` and a catalog origin label
(`adopted`/`discovered`) before overwriting its status — a name-collision
stranger is skipped with a warn instead of clobbered
(`adopted_row_matches_candidate`, pure + unit-tested).

Tests (fail on HEAD's un-gated logic): forged label-only row → not pinned
Succeeded, not in retention_view, doesn't set has_history; genuine adopted
row (status.snapshot present) → unchanged.
…th (C1)

`resolve_terminating_namespaces` mapped a Namespace GET error to
"terminating" (`unwrap_or(true)`). Post the plan-in-real-form change,
that set feeds PLAN EVALUATION in `pending_members`: on a repo with
`onNamespaceDelete: Delete`, a transient read error flipped retain-destined
draining children (policy-cascade-stamped, or schedule-cascade Retain) into
destructive `DeleteSnapshot` members that `fire_namespace_ok` then admitted;
below threshold they enrolled into the next batch Job and deleted real kopia
data that Retain promised to keep — in a namespace that is NOT being deleted.

Split the resolution into two sets by read confidence:
- `confirmed` (`Ok(true)`) — the ONLY set fed to plan evaluation
  (`pending_members`) and breaker counting-as-terminating. An unreadable
  namespace now stays retain-form for plan/count, matching the finalizer's
  self-CR path (`unwrap_or(false)`).
- `unreadable` (`Err`) — folded into the FIRE path's ns-Orphan exclusion
  (`confirmed ∪ unreadable`, so an Orphan peer stays protected) AND excluded
  from the FIRE set OUTRIGHT via a new `fire_namespace_readable_ok`
  predicate: a peer whose namespace we cannot confirm terminating is never
  deleted on the assumption that it is (its own self-fire drains it).
  Counting such a member toward the breaker stays fine — over-count is safe.

Updated the now-wrong Err-direction doc and made the self-CR asymmetry note
coherent (both paths treat an unreadable namespace as NOT terminating for
plan/count). Also documented that a terminating-ns member whose real plan is
`OrphanSnapshot` no longer counts toward the breaker — the alert-mirror
softens correctly during Orphan teardowns.

Pure tests (fail on HEAD's single-set design): member in an unreadable
namespace + `onNamespaceDelete: Delete` → NOT in the fire set and its plan
stays retain; confirmed-terminating + Delete → fires and counts as before;
unreadable + Orphan → still excluded from fire (pre-existing protection).
The first doc paragraph on `scan_requested_pending` ("...should force a
catalog scan/bootstrap NOW...") was left over from the pre-split function
and describes behavior this predicate no longer has. Remove it; keep the
correct "still PENDING: present, non-empty, not yet retired by TOKEN
EQUALITY" header and the scan_due-vs-scan_requested_due rationale.
…sh suffix

Two kopia snapshot ids sharing a >=16-char prefix under the same policy
minted the same adopted-row name (<policy>-adopted-<first16(id)>). On
HEAD (post-91093e7's adopted_row_matches_candidate guard) this doesn't
corrupt status, but permanently wedges the second snapshot: skipped and
re-warned on every reconcile pass, its discovered row never cleaned up.

Append a short_hash(full snapshot_id) suffix (reusing the existing
FNV-1a naming::short_hash idiom, no new hash dep) so the name is
collision-free while staying deterministic per (policy, full id) and
human-correlatable via the retained first-16 prefix. capped_name still
applies for the 63-char cap.

The 409-branch ensure-status logic and adopted_row_matches_candidate
guard are unchanged (defense in depth). The sibling <repo>-disc-<first16>
naming in catalog.rs has the same collision class but predates this
branch; left as a follow-up.
@perfectra1n
perfectra1n merged commit 9da8c26 into main Jul 19, 2026
26 checks passed
@perfectra1n
perfectra1n deleted the feat/policy-cascade-adoption branch July 19, 2026 16:16
perfectra1n added a commit that referenced this pull request Jul 20, 2026
Both binaries had never run in CI, so both had rotted silently. Neither failure
involves the health probe; they are pre-existing and were only made visible by
wiring the orphaned e2e binaries into the shard matrix.

leak_cleanup: the fixture could never age into eligibility. The creds sweep
kernels refuse to reap anything younger than `CREDS_MIN_AGE_FLOOR_SECS` (15m), a
hard floor that deliberately overrides the harness's 30s
`KOPIUR_WORK_SPEC_SWEEP_MIN_AGE_SECS` so an admin who zeroes that shared knob
cannot get a zero-width guard on the project→create-Job window. A Secret created
during the test can therefore never reach 900s inside `default_timeout()` (420s),
so the assertion was arithmetically unsatisfiable. Backdate `projected-at`
instead — `sweep::creds_age_secs` prefers that annotation over the
apiserver-owned `creationTimestamp` — and stamp all three fixtures identically so
the two controls still prove they survive on their marker and in-use guard rather
than on being too young to consider. The production floor is untouched.

cli: `cli_migrate_volsync_kopia` asserted the seeded fork snapshot surfaces with
`origin=discovered`, but #272 (policy auto-adoption) now has the migrated
SnapshotPolicy adopt any discovered row whose identity matches it — creating an
`origin=adopted` row and DELETING the discovered one. The assertion became
unsatisfiable the day #272 landed, and nothing caught it because this binary ran
in no shard. Select on the repository label and accept either origin, since which
one you observe is a race against the adoption pass; the load-bearing assertion
is that the fork identity survives the migration intact, which holds under both.
Origin is read from the label, not `spec.origin`, which is elided for these rows.
doonga pushed a commit to greyrock-labs/home-ops that referenced this pull request Jul 21, 2026
…0.7.5 ➔ 0.8.0) (#107)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/home-operations/charts/kopiur](https://github.com/home-operations/kopiur) | minor | `0.7.5` → `0.8.0` |

---

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

---

### Release Notes

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

### [`v0.8.0`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#080-2026-07-19)

[Compare Source](home-operations/kopiur@0.7.5...0.8.0)

##### ⚠ BREAKING CHANGES

- SnapshotPolicy deletion cascade + auto-adoption of discovered snapshots ([#&#8203;272](home-operations/kopiur#272))

##### Features

- **deps:** update rust crate tokio (1.52.4 → 1.53.0) ([#&#8203;264](home-operations/kopiur#264)) ([6cab0c6](home-operations/kopiur@6cab0c6))
- mass-deletion protection (cascade guard, per-repo breaker, batched deletes) ([#&#8203;265](home-operations/kopiur#265)) ([038ea6d](home-operations/kopiur@038ea6d))
- SnapshotPolicy deletion cascade + auto-adoption of discovered snapshots ([#&#8203;272](home-operations/kopiur#272)) ([9da8c26](home-operations/kopiur@9da8c26))

##### Bug Fixes

- **deps:** update rust crate anyhow (1.0.103 → 1.0.104) ([#&#8203;269](home-operations/kopiur#269)) ([4d81496](home-operations/kopiur@4d81496))
- **deps:** update rust crate clap (4.6.1 → 4.6.2) ([#&#8203;257](home-operations/kopiur#257)) ([c238df1](home-operations/kopiur@c238df1))
- **deps:** update rust crate futures (0.3.32 → 0.3.33) ([#&#8203;268](home-operations/kopiur#268)) ([926b52d](home-operations/kopiur@926b52d))
- **deps:** update rust crate serde (1.0.228 → 1.0.229) ([#&#8203;270](home-operations/kopiur#270)) ([5fbd713](home-operations/kopiur@5fbd713))
- **deps:** update rust crate thiserror (2.0.18 → 2.0.19) ([#&#8203;271](home-operations/kopiur#271)) ([d83730f](home-operations/kopiur@d83730f))
- **deps:** update rust crate tokio (1.52.3 → 1.52.4) ([#&#8203;261](home-operations/kopiur#261)) ([accf6ae](home-operations/kopiur@accf6ae))
- writable backup source ([#&#8203;254](home-operations/kopiur#254)), stuck deletion finalizer ([#&#8203;255](home-operations/kopiur#255)), kopia epoch parameters ([#&#8203;258](home-operations/kopiur#258)) ([#&#8203;263](home-operations/kopiur#263)) ([94f9a15](home-operations/kopiur@94f9a15))

##### Miscellaneous Chores

- **krew:** kopiur 0.7.5 manifest ([16d7390](home-operations/kopiur@16d7390))
- **mise:** Update tool aqua:astral-sh/uv (0.11.28 → 0.11.29) ([#&#8203;256](home-operations/kopiur#256)) ([f822335](home-operations/kopiur@f822335))
- **mise:** Update tool cosign (3.1.1 → 3.1.2) ([#&#8203;267](home-operations/kopiur#267)) ([649ba2e](home-operations/kopiur@649ba2e))

</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](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNjAuMiIsInVwZGF0ZWRJblZlciI6IjQzLjI3Mi4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL21pbm9yIl19-->

Reviewed-on: https://git.greyrock.io/greyrock-labs/home-ops/pulls/107
perfectra1n added a commit that referenced this pull request Jul 22, 2026
The scenario predates policy-cascade auto-adoption (#210/#272): it asserted
the retained kopia snapshots persist as origin=discovered rows — but the
POLICY deliberately survives a schedule cascade, so rediscovered rows are
auto-adopted straight back to it (origin: adopted, spec re-derived from the
policy), often before a 3s poll can observe the interim discovered state.
The old assertion could only pass by racing the adopter.

Now accepts both end states with the correct per-origin invariants: a row
still discovered must be FORCED Retain; an adopted row must reference the
surviving policy and carry its defaultDeletionPolicy. Cleanup drains the
uid-keyed catalog set (adopted rows' Delete at teardown is intended — the
scenario-scoped repo goes next, and the wave is under the breaker threshold).

Validated locally: scenario timed out at 420s pre-fix (both with and without
the recycle fix alone), passes in 141s with recycle fix + this assertion.
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request Jul 23, 2026
…0.7.4 ➔ 0.8.0) (#1627)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/home-operations/charts/kopiur](https://github.com/home-operations/kopiur) | minor | `0.7.4` → `0.8.0` |

---

### Release Notes

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

### [`v0.8.0`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#080-2026-07-19)

[Compare Source](home-operations/kopiur@0.7.5...0.8.0)

##### ⚠ BREAKING CHANGES

- SnapshotPolicy deletion cascade + auto-adoption of discovered snapshots ([#&#8203;272](home-operations/kopiur#272))

##### Features

- **deps:** update rust crate tokio (1.52.4 → 1.53.0) ([#&#8203;264](home-operations/kopiur#264)) ([6cab0c6](home-operations/kopiur@6cab0c6))
- mass-deletion protection (cascade guard, per-repo breaker, batched deletes) ([#&#8203;265](home-operations/kopiur#265)) ([038ea6d](home-operations/kopiur@038ea6d))
- SnapshotPolicy deletion cascade + auto-adoption of discovered snapshots ([#&#8203;272](home-operations/kopiur#272)) ([9da8c26](home-operations/kopiur@9da8c26))

##### Bug Fixes

- **deps:** update rust crate anyhow (1.0.103 → 1.0.104) ([#&#8203;269](home-operations/kopiur#269)) ([4d81496](home-operations/kopiur@4d81496))
- **deps:** update rust crate clap (4.6.1 → 4.6.2) ([#&#8203;257](home-operations/kopiur#257)) ([c238df1](home-operations/kopiur@c238df1))
- **deps:** update rust crate futures (0.3.32 → 0.3.33) ([#&#8203;268](home-operations/kopiur#268)) ([926b52d](home-operations/kopiur@926b52d))
- **deps:** update rust crate serde (1.0.228 → 1.0.229) ([#&#8203;270](home-operations/kopiur#270)) ([5fbd713](home-operations/kopiur@5fbd713))
- **deps:** update rust crate thiserror (2.0.18 → 2.0.19) ([#&#8203;271](home-operations/kopiur#271)) ([d83730f](home-operations/kopiur@d83730f))
- **deps:** update rust crate tokio (1.52.3 → 1.52.4) ([#&#8203;261](home-operations/kopiur#261)) ([accf6ae](home-operations/kopiur@accf6ae))
- writable backup source ([#&#8203;254](home-operations/kopiur#254)), stuck deletion finalizer ([#&#8203;255](home-operations/kopiur#255)), kopia epoch parameters ([#&#8203;258](home-operations/kopiur#258)) ([#&#8203;263](home-operations/kopiur#263)) ([94f9a15](home-operations/kopiur@94f9a15))

##### Miscellaneous Chores

- **krew:** kopiur 0.7.5 manifest ([16d7390](home-operations/kopiur@16d7390))
- **mise:** Update tool aqua:astral-sh/uv (0.11.28 → 0.11.29) ([#&#8203;256](home-operations/kopiur#256)) ([f822335](home-operations/kopiur@f822335))
- **mise:** Update tool cosign (3.1.1 → 3.1.2) ([#&#8203;267](home-operations/kopiur#267)) ([649ba2e](home-operations/kopiur@649ba2e))

### [`v0.7.5`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#075-2026-07-16)

[Compare Source](home-operations/kopiur@0.7.4...0.7.5)

##### Bug Fixes

- **readme:** update readme admonition ([987957c](home-operations/kopiur@987957c))
- **secctx:** SecurityContextCompatible is verified, not asserted; inherit merges with explicit ([#&#8203;259](home-operations/kopiur#259)) ([cbfd97d](home-operations/kopiur@cbfd97d))

</details>

---

### Configuration

📅 **Schedule**: 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 [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL21pbm9yIl19-->

Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/1627
hbjydev pushed a commit to hbjydev/phoebe that referenced this pull request Aug 27, 2026
…0.7.3 → 0.10.4) (#273)

This PR contains the following updates:

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

---

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

---

### Release Notes

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

### [`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))

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

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

##### ⚠ BREAKING CHANGES

- **rust:** Update crate base64 (0.22.1 → 0.23.1) ([#&#8203;366](home-operations/kopiur#366))

##### Features

- **rust:** Update crate base64 (0.22.1 → 0.23.1) ([#&#8203;366](home-operations/kopiur#366)) ([2c6e3e7](home-operations/kopiur@2c6e3e7))
- SnapshotReplication (logical snapshot-level replication) + SnapshotPolicy multi-repository fan-out ([#&#8203;370](home-operations/kopiur#370)) ([4e06e42](home-operations/kopiur@4e06e42))

##### Continuous Integration

- **github-action:** Update action Swatinem/rust-cache (v2.9.1 → v2.9.2) ([#&#8203;372](home-operations/kopiur#372)) ([e4ce173](home-operations/kopiur@e4ce173))

##### Miscellaneous Chores

- **mise:** Update mise tools ([#&#8203;367](home-operations/kopiur#367)) ([d6f82f4](home-operations/kopiur@d6f82f4))
- **mise:** Update tool cosign (3.1.2 → 3.1.3) ([#&#8203;371](home-operations/kopiur#371)) ([558b9f4](home-operations/kopiur@558b9f4))

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

[Compare Source](home-operations/kopiur@0.9.4...0.9.5)

##### Bug Fixes

- deliver s3 tls.caBundleRef to every kopia invocation ([#&#8203;365](home-operations/kopiur#365)) ([cda0237](home-operations/kopiur@cda0237))
- doctor misses structurally-blocked work ([#&#8203;359](home-operations/kopiur#359)) + phase/gate exhaustiveness ratchet ([#&#8203;363](home-operations/kopiur#363)) ([b844974](home-operations/kopiur@b844974))
- **rust:** update crate thiserror (2.0.19 → 2.0.20) ([#&#8203;360](home-operations/kopiur#360)) ([4aa646d](home-operations/kopiur@4aa646d))

##### Continuous Integration

- **github-action:** Update action docker/github-builder (v1.15.0 → v1.16.0) ([#&#8203;362](home-operations/kopiur#362)) ([db32818](home-operations/kopiur@db32818))

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

[Compare Source](home-operations/kopiur@0.9.3...0.9.4)

##### Features

- **docs:** Update README.md ([#&#8203;358](home-operations/kopiur#358)) ([05d2506](home-operations/kopiur@05d2506))

##### Bug Fixes

- implement pvcSelector/groupBy ([#&#8203;346](home-operations/kopiur#346)) and model deduped backups as Unchanged ([#&#8203;351](home-operations/kopiur#351)) ([#&#8203;354](home-operations/kopiur#354)) ([a2b9ae6](home-operations/kopiur@a2b9ae6))
- **rust:** update crate clap (4.6.5 → 4.6.6) ([#&#8203;357](home-operations/kopiur#357)) ([e9ca396](home-operations/kopiur@e9ca396))

##### Miscellaneous Chores

- **mise:** Update tool node (24.18.1 → v24.19.0) ([#&#8203;353](home-operations/kopiur#353)) ([2f28c6d](home-operations/kopiur@2f28c6d))
- **mise:** Update tool oxfmt (0.61.0 → 0.62.0) ([#&#8203;356](home-operations/kopiur#356)) ([e95df6b](home-operations/kopiur@e95df6b))

### [`v0.9.3`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#093-2026-08-05)

[Compare Source](home-operations/kopiur@0.9.2...0.9.3)

##### Features

- repository-level circuit breaker — stop fanning out doomed Jobs when the backend is unreachable ([#&#8203;350](home-operations/kopiur#350)) ([5d787b9](home-operations/kopiur@5d787b9))

##### Bug Fixes

- **rust:** update crate base64 (0.23.0 → 0.23.1) ([#&#8203;349](home-operations/kopiur#349)) ([11e7aa8](home-operations/kopiur@11e7aa8))

##### Continuous Integration

- **github-action:** Update action jdx/mise-action (v4.2.3 → v4.2.4) ([#&#8203;347](home-operations/kopiur#347)) ([1c9cf3b](home-operations/kopiur@1c9cf3b))

##### Miscellaneous Chores

- **renovate:** keep 0.x minors out of automerge ([#&#8203;352](home-operations/kopiur#352)) ([95d672a](home-operations/kopiur@95d672a))

### [`v0.9.2`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#092-2026-08-02)

[Compare Source](home-operations/kopiur@0.9.1...0.9.2)

##### Features

- **repository:** object-lock blob retention ([#&#8203;332](home-operations/kopiur#332)); fix blank volumeSnapshotClassName ([#&#8203;344](home-operations/kopiur#344)) ([dab8fdd](home-operations/kopiur@dab8fdd))

##### Bug Fixes

- **ci:** fail the merge gate on cancelled jobs, and cut e2e artifact time ([#&#8203;330](home-operations/kopiur#330)) ([1847a8a](home-operations/kopiur@1847a8a))
- **rust:** update crate time (0.3.54 → 0.3.55) ([#&#8203;337](home-operations/kopiur#337)) ([ee1b951](home-operations/kopiur@ee1b951))

##### Continuous Integration

- **github-action:** Update action docker/github-builder (v1.14.0 → v1.15.0) ([#&#8203;327](home-operations/kopiur#327)) ([f5514fc](home-operations/kopiur@f5514fc))
- **github-action:** Update action docker/login-action (v4.5.1 → v4.5.2) ([#&#8203;331](home-operations/kopiur#331)) ([d90efdf](home-operations/kopiur@d90efdf))
- **github-action:** Update action docker/login-action (v4.5.2 → v4.6.0) ([#&#8203;339](home-operations/kopiur#339)) ([b783bcc](home-operations/kopiur@b783bcc))
- **github-action:** Update action home-operations/.github/actions/workflow-lint (v1.0.2 → v1.0.3) ([#&#8203;343](home-operations/kopiur#343)) ([4af16d5](home-operations/kopiur@4af16d5))
- update shared actions and use self-repository syntax ([#&#8203;341](home-operations/kopiur#341)) ([559e23f](home-operations/kopiur@559e23f))

##### Miscellaneous Chores

- **deps:** lock file maintenance (pep621) ([#&#8203;334](home-operations/kopiur#334)) ([e19ae0e](home-operations/kopiur@e19ae0e))
- **mise:** Lock file maintenance tool (mise) ([#&#8203;335](home-operations/kopiur#335)) ([666afb5](home-operations/kopiur@666afb5))
- **mise:** prune lockfile to used platforms ([#&#8203;342](home-operations/kopiur#342)) ([741d6a4](home-operations/kopiur@741d6a4))
- **mise:** Update tool aqua:astral-sh/uv (0.12.0 → 0.12.1) ([#&#8203;333](home-operations/kopiur#333)) ([3ed2766](home-operations/kopiur@3ed2766))
- **mise:** Update tool promtool (3.13.1 → 3.13.2) ([#&#8203;329](home-operations/kopiur#329)) ([ac1752e](home-operations/kopiur@ac1752e))
- **mise:** Update tool zizmor (1.28.0 → 1.29.0) ([#&#8203;340](home-operations/kopiur#340)) ([410dc26](home-operations/kopiur@410dc26))
- **release-please:** standardize the release pull request title pattern ([#&#8203;338](home-operations/kopiur#338)) ([21013a3](home-operations/kopiur@21013a3))
- **rust:** lock file maintenance crate (cargo) ([#&#8203;336](home-operations/kopiur#336)) ([8606177](home-operations/kopiur@8606177))

### [`v0.9.1`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#091-2026-07-29)

[Compare Source](home-operations/kopiur@0.9.0...0.9.1)

##### Features

- **rust:** update crate http (1.4.2 → 1.5.0) ([#&#8203;323](home-operations/kopiur#323)) ([cf0b149](home-operations/kopiur@cf0b149))

##### Bug Fixes

- **controller:** survive a slow API server instead of abdicating on the first one ([#&#8203;319](home-operations/kopiur#319)) ([#&#8203;324](home-operations/kopiur#324)) ([df7e90c](home-operations/kopiur@df7e90c))
- **rust:** update crate rustls (0.23.42 → 0.23.43) ([#&#8203;326](home-operations/kopiur#326)) ([2f5123e](home-operations/kopiur@2f5123e))

##### Tests

- **e2e:** retry flaky tests with cargo-nextest ([#&#8203;322](home-operations/kopiur#322)) ([558ac85](home-operations/kopiur@558ac85))

##### Miscellaneous Chores

- **mise:** Update tool aqua:astral-sh/uv (0.11.33 → 0.12.0) ([#&#8203;320](home-operations/kopiur#320)) ([66ec350](home-operations/kopiur@66ec350))
- **mise:** Update tool node (24.18.0 → v24.18.1) ([#&#8203;325](home-operations/kopiur#325)) ([1c833a4](home-operations/kopiur@1c833a4))

### [`v0.9.0`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#090-2026-07-28)

[Compare Source](home-operations/kopiur@0.8.1...0.9.0)

##### ⚠ BREAKING CHANGES

- **rust:** Update crate serial\_test (3.5.0 → 4.0.1) ([#&#8203;307](home-operations/kopiur#307))
- **secctx:** identity-aware mover merge, recorded snapshot identity, restore inherit-from-snapshot ([#&#8203;304](home-operations/kopiur#304))

##### Features

- **rust:** Update crate serial\_test (3.5.0 → 4.0.1) ([#&#8203;307](home-operations/kopiur#307)) ([4392e4a](home-operations/kopiur@4392e4a))
- **secctx:** identity-aware mover merge, recorded snapshot identity, restore inherit-from-snapshot ([#&#8203;304](home-operations/kopiur#304)) ([0632a29](home-operations/kopiur@0632a29))

##### Bug Fixes

- **rust:** update crate cel (0.14.0 → 0.14.1) ([#&#8203;317](home-operations/kopiur#317)) ([00cf539](home-operations/kopiur@00cf539))
- **rust:** update crate schemars (1.2.1 → 1.2.2) ([#&#8203;313](home-operations/kopiur#313)) ([b6e6268](home-operations/kopiur@b6e6268))

##### Build System

- **mise:** add actionlint and refresh the lockfile ([#&#8203;309](home-operations/kopiur#309)) ([e74c9b3](home-operations/kopiur@e74c9b3))

##### Continuous Integration

- adopt the shared workflow-lint and docs-build actions ([#&#8203;310](home-operations/kopiur#310)) ([7d4b329](home-operations/kopiur@7d4b329))
- gate pull requests on Build Success and share the docs build ([#&#8203;306](home-operations/kopiur#306)) ([1b693aa](home-operations/kopiur@1b693aa))
- **github-action:** Update action docker/login-action (v4.5.0 → v4.5.1) ([#&#8203;312](home-operations/kopiur#312)) ([29d00b1](home-operations/kopiur@29d00b1))
- **github-action:** Update action jdx/mise-action (v4.2.1 → v4.2.2) ([#&#8203;311](home-operations/kopiur#311)) ([65bbca4](home-operations/kopiur@65bbca4))
- **github-action:** Update action jdx/mise-action (v4.2.2 → v4.2.3) ([#&#8203;315](home-operations/kopiur#315)) ([5f517d4](home-operations/kopiur@5f517d4))
- skip release-please PRs in checks and drop nightly e2e ([#&#8203;305](home-operations/kopiur#305)) ([ee6d4f5](home-operations/kopiur@ee6d4f5))

##### Miscellaneous Chores

- **krew:** kopiur 0.8.1 manifest ([84df757](home-operations/kopiur@84df757))
- **mise:** Update tool aqua:astral-sh/uv (0.11.32 → 0.11.33) ([#&#8203;318](home-operations/kopiur#318)) ([f63bc3a](home-operations/kopiur@f63bc3a))
- **mise:** Update tool oxfmt (0.60.0 → 0.61.0) ([#&#8203;314](home-operations/kopiur#314)) ([bdbf6c6](home-operations/kopiur@bdbf6c6))
- **renovate:** drop the stale automerge overrides ([#&#8203;308](home-operations/kopiur#308)) ([c13a3d5](home-operations/kopiur@c13a3d5))
- standardize release-please changelog sections ([#&#8203;316](home-operations/kopiur#316)) ([273d0f8](home-operations/kopiur@273d0f8))

### [`v0.8.1`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#081-2026-07-24)

[Compare Source](home-operations/kopiur@0.8.0...0.8.1)

##### Features

- **deps:** update rust crate base64 (0.22.1 → 0.23.0) ([#&#8203;293](home-operations/kopiur#293)) ([5ab14d5](home-operations/kopiur@5ab14d5))
- **deps:** update rust crate kube (4.0.0 → 4.2.0) ([#&#8203;288](home-operations/kopiur#288)) ([b067458](home-operations/kopiur@b067458))
- httpRequest hook headers ([#&#8203;290](home-operations/kopiur#290)) + recovery-aware alerts ([#&#8203;280](home-operations/kopiur#280)) ([#&#8203;296](home-operations/kopiur#296)) ([f388a14](home-operations/kopiur@f388a14))
- **renovate:** automerge if it passes CI ([66f243f](home-operations/kopiur@66f243f))

##### Bug Fixes

- **controller:** survive an API-server outage without exhausting file descriptors ([#&#8203;287](home-operations/kopiur#287)) ([ce184b6](home-operations/kopiur@ce184b6))
- **deps:** update rust crate clap (4.6.2 → 4.6.3) ([#&#8203;277](home-operations/kopiur#277)) ([e3d4113](home-operations/kopiur@e3d4113))
- **deps:** update rust crate libc (0.2.186 → 0.2.189) ([#&#8203;283](home-operations/kopiur#283)) ([2b2e636](home-operations/kopiur@2b2e636))
- **deps:** update rust crate serde\_json (1.0.150 → 1.0.151) ([#&#8203;275](home-operations/kopiur#275)) ([1410f42](home-operations/kopiur@1410f42))
- **deps:** update rust crate time (0.3.53 → 0.3.54) ([#&#8203;276](home-operations/kopiur#276)) ([ff250ff](home-operations/kopiur@ff250ff))
- **deps:** update rust crate tokio (1.53.0 → 1.53.1) ([#&#8203;281](home-operations/kopiur#281)) ([82082e9](home-operations/kopiur@82082e9))
- **helm:** stamp Chart.yaml version on release + document image sub-fields ([#&#8203;289](home-operations/kopiur#289)) ([041549a](home-operations/kopiur@041549a))
- **probe:** stamp the health probe at launch so it stops recycling its Job ([#&#8203;273](home-operations/kopiur#273)) ([#&#8203;278](home-operations/kopiur#278)) ([7fa3657](home-operations/kopiur@7fa3657))
- **release:** use the generic updater for Chart.yaml + stamp README badges ([#&#8203;292](home-operations/kopiur#292)) ([f4a3141](home-operations/kopiur@f4a3141))
- retention-aware adoption stops the adopt/prune/rediscover livelock ([#&#8203;299](home-operations/kopiur#299)) ([4093f12](home-operations/kopiur@4093f12))

##### Styles

- indent markdown at 2 to match embedded yaml ([#&#8203;279](home-operations/kopiur#279)) ([06885e0](home-operations/kopiur@06885e0))

##### Miscellaneous Chores

- **github-release:** Update release helm-unittest/helm-unittest (v1.1.1 → v1.1.2) ([#&#8203;301](home-operations/kopiur#301)) ([e62272d](home-operations/kopiur@e62272d))
- **krew:** kopiur 0.8.0 manifest ([2be416f](home-operations/kopiur@2be416f))
- **mise:** Update tool aqua:astral-sh/uv (0.11.29 → 0.11.30) ([#&#8203;282](home-operations/kopiur#282)) ([e12f96c](home-operations/kopiur@e12f96c))
- **mise:** Update tool aqua:astral-sh/uv (0.11.30 → 0.11.31) ([#&#8203;286](home-operations/kopiur#286)) ([a3f31e6](home-operations/kopiur@a3f31e6))
- **mise:** Update tool aqua:astral-sh/uv (0.11.31 → 0.11.32) ([#&#8203;298](home-operations/kopiur#298)) ([9d8c171](home-operations/kopiur@9d8c171))
- **mise:** Update tool kubectl (1.36.2 → 1.36.3) ([#&#8203;291](home-operations/kopiur#291)) ([ef290f8](home-operations/kopiur@ef290f8))
- **mise:** Update tool oxfmt (0.59.0 → 0.60.0) ([#&#8203;284](home-operations/kopiur#284)) ([ad8a18a](home-operations/kopiur@ad8a18a))
- **mise:** Update tool rust (1.96.1 → 1.97.1) ([#&#8203;222](home-operations/kopiur#222)) ([fbeb2fa](home-operations/kopiur@fbeb2fa))
- **mise:** Update tool zizmor (1.27.0 → 1.28.0) ([#&#8203;285](home-operations/kopiur#285)) ([9ef35ce](home-operations/kopiur@9ef35ce))

### [`v0.8.0`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#080-2026-07-19)

[Compare Source](home-operations/kopiur@0.7.5...0.8.0)

##### ⚠ BREAKING CHANGES

- SnapshotPolicy deletion cascade + auto-adoption of discovered snapshots ([#&#8203;272](home-operations/kopiur#272))

##### Features

- **deps:** update rust crate tokio (1.52.4 → 1.53.0) ([#&#8203;264](home-operations/kopiur#264)) ([6cab0c6](home-operations/kopiur@6cab0c6))
- mass-deletion protection (cascade guard, per-repo breaker, batched deletes) ([#&#8203;265](home-operations/kopiur#265)) ([038ea6d](home-operations/kopiur@038ea6d))
- SnapshotPolicy deletion cascade + auto-adoption of discovered snapshots ([#&#8203;272](home-operations/kopiur#272)) ([9da8c26](home-operations/kopiur@9da8c26))

##### Bug Fixes

- **deps:** update rust crate anyhow (1.0.103 → 1.0.104) ([#&#8203;269](home-operations/kopiur#269)) ([4d81496](home-operations/kopiur@4d81496))
- **deps:** update rust crate clap (4.6.1 → 4.6.2) ([#&#8203;257](home-operations/kopiur#257)) ([c238df1](home-operations/kopiur@c238df1))
- **deps:** update rust crate futures (0.3.32 → 0.3.33) ([#&#8203;268](home-operations/kopiur#268)) ([926b52d](home-operations/kopiur@926b52d))
- **deps:** update rust crate serde (1.0.228 → 1.0.229) ([#&#8203;270](home-operations/kopiur#270)) ([5fbd713](home-operations/kopiur@5fbd713))
- **deps:** update rust crate thiserror (2.0.18 → 2.0.19) ([#&#8203;271](home-operations/kopiur#271)) ([d83730f](home-operations/kopiur@d83730f))
- **deps:** update rust crate tokio (1.52.3 → 1.52.4) ([#&#8203;261](home-operations/kopiur#261)) ([accf6ae](home-operations/kopiur@accf6ae))
- writable backup source ([#&#8203;254](home-operations/kopiur#254)), stuck deletion finalizer ([#&#8203;255](home-operations/kopiur#255)), kopia epoch parameters ([#&#8203;258](home-operations/kopiur#258)) ([#&#8203;263](home-operations/kopiur#263)) ([94f9a15](home-operations/kopiur@94f9a15))

##### Miscellaneous Chores

- **krew:** kopiur 0.7.5 manifest ([16d7390](home-operations/kopiur@16d7390))
- **mise:** Update tool aqua:astral-sh/uv (0.11.28 → 0.11.29) ([#&#8203;256](home-operations/kopiur#256)) ([f822335](home-operations/kopiur@f822335))
- **mise:** Update tool cosign (3.1.1 → 3.1.2) ([#&#8203;267](home-operations/kopiur#267)) ([649ba2e](home-operations/kopiur@649ba2e))

### [`v0.7.5`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#075-2026-07-16)

[Compare Source](home-operations/kopiur@0.7.4...0.7.5)

##### Bug Fixes

- **readme:** update readme admonition ([987957c](home-operations/kopiur@987957c))
- **secctx:** SecurityContextCompatible is verified, not asserted; inherit merges with explicit ([#&#8203;259](home-operations/kopiur#259)) ([cbfd97d](home-operations/kopiur@cbfd97d))

### [`v0.7.4`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#074-2026-07-15)

[Compare Source](home-operations/kopiur@0.7.3...0.7.4)

##### Features

- bound Snapshot CR reconcile cost at scale ([#&#8203;249](home-operations/kopiur#249)) ([#&#8203;253](home-operations/kopiur#253)) ([3697aa9](home-operations/kopiur@3697aa9))
- **docs:** clean up admonitions in docs ([71c5d85](home-operations/kopiur@71c5d85))
- multi-cluster shared repository support — cluster identity, foreign-aware discovery, collision-free maintenance leases ([#&#8203;251](home-operations/kopiur#251)) ([a4593d8](home-operations/kopiur@a4593d8))

##### Bug Fixes

- batch of six open-issue fixes ([#&#8203;248](home-operations/kopiur#248) [#&#8203;238](home-operations/kopiur#238) [#&#8203;245](home-operations/kopiur#245) [#&#8203;196](home-operations/kopiur#196) [#&#8203;237](home-operations/kopiur#237) [#&#8203;250](home-operations/kopiur#250)) ([#&#8203;252](home-operations/kopiur#252)) ([045a5a5](home-operations/kopiur@045a5a5))
- **deps:** update rust crate http-body-util (0.1.3 → 0.1.4) ([#&#8203;244](home-operations/kopiur#244)) ([4878006](home-operations/kopiur@4878006))
- **deps:** update rust crate rustls (0.23.41 → 0.23.42) ([#&#8203;243](home-operations/kopiur#243)) ([13b1dc4](home-operations/kopiur@13b1dc4))

##### Miscellaneous Chores

- **krew:** kopiur 0.7.3 manifest ([d521675](home-operations/kopiur@d521675))
- **mise:** Update tool aqua:EmbarkStudios/cargo-deny (0.19.9 → 0.20.2) ([#&#8203;226](home-operations/kopiur#226)) ([9c109b4](home-operations/kopiur@9c109b4))
- **mise:** Update tool oxfmt (0.58.0 → 0.59.0) ([#&#8203;246](home-operations/kopiur#246)) ([41625e9](home-operations/kopiur@41625e9))
- **mise:** Update tool zizmor (1.26.1 → 1.27.0) ([#&#8203;247](home-operations/kopiur#247)) ([fe10a6a](home-operations/kopiur@fe10a6a))

</details>

---

### Configuration

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

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

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, 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:eyJjcmVhdGVkSW5WZXIiOiI0My4yNjAuMiIsInVwZGF0ZWRJblZlciI6IjQ0LjQ2LjUiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbInJlbm92YXRlL2NvbnRhaW5lciIsInR5cGUvbWlub3IiXX0=-->

Reviewed-on: https://forgejo.hayden.moe/hayden/phoebe/pulls/273
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Discovered snapshots are permanently exempt from all pruning

1 participant