From 717a12bc18e69aa35e5e3618d7c49b76ee5b9927 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Thu, 30 Jul 2026 19:26:44 +0200 Subject: [PATCH 1/4] fix(rotation): repin every rotator to a dated tag, not a digest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the breakage in #458: 11 of the 14 rotators were pinned to image digests that no longer exist in the registry. Verified by pulling each rather than by inspecting metadata (the registry API returns 401 for a PAT, so metadata checks mislead here): gone 159e5888 authentik, crowdsec-bouncer-key, paperless, shelly-manager, woodpecker, home-assistant (x3) gone 978fe53c cloudflare, external-dns gone 49bfddd8 netbird-ingress gone 97c22bcf pve-exporter present b41cc364 flux-git present 828bf486 claude-oauth, review-bot The two survivors are exactly the rotators that run often; every gone digest belongs to a quarterly one. Untagged manifests are collected once :latest and the dated tags move past them. Impact had this not been caught: every quarterly CronJob shows lastScheduleTime — they were created 2026-07-18..23, after the last quarterly fire — so none has ever run. Their first fire is 2026-10-01, where ~10 would have ImagePullBackOff'd at once. A rotation that never starts looks identical to nothing happening, so credentials would have silently stopped rotating while the manifests kept asserting they did. All 16 references (13 files; home-assistant has 3 CronJobs, review-bot 2) now use secret-rotator:2026-07-30 — dated tag, no digest, no :latest, per owner directive and matching ci-ansible:2026-04-29 and the alpine-jq / cni-plugins / alpine-openssl pins. Also repinned flux-git, claude-oauth and review-bot, whose digests still resolve today. They are pinned the same way and would decay identically; converting them now avoids a second pass when they break. Verified before repinning that one image serves all of them: the modes in use across these files are exactly cloudflare-roll gitea-token ha-token netbird-key proxmox-token random woodpecker-secret and the 2026-07-30 image's usage string lists exactly those seven. So this is a single binary and splitting the pin would serve no purpose. Rationale recorded in kustomization.yaml rather than repeated 16 times, including WHY digests decayed: the build triggers on .woodpecker/build-ci-images.yml, so adding an unrelated image republishes secret-rotator and orphans the manifest every rotator was pinned to. That is how these went stale without anyone touching the rotator source. Co-Authored-By: Claude Opus 5 (1M context) --- .../openbao/rotation/authentik.yaml | 2 +- .../openbao/rotation/claude-oauth.yaml | 2 +- .../openbao/rotation/cloudflare.yaml | 2 +- .../rotation/crowdsec-bouncer-key.yaml | 2 +- .../openbao/rotation/external-dns.yaml | 2 +- .../openbao/rotation/flux-git.yaml | 2 +- .../openbao/rotation/home-assistant.yaml | 6 +++--- .../openbao/rotation/kustomization.yaml | 20 ++++++++++++++++++ .../openbao/rotation/mosquitto-health.yaml | 21 +++++-------------- .../openbao/rotation/netbird-ingress.yaml | 2 +- .../openbao/rotation/paperless.yaml | 2 +- .../openbao/rotation/pve-exporter.yaml | 2 +- .../openbao/rotation/review-bot.yaml | 4 ++-- .../openbao/rotation/shelly-manager.yaml | 2 +- .../openbao/rotation/woodpecker.yaml | 2 +- 15 files changed, 41 insertions(+), 32 deletions(-) diff --git a/kubernetes/infrastructure/openbao/rotation/authentik.yaml b/kubernetes/infrastructure/openbao/rotation/authentik.yaml index dd906b68..6717282d 100644 --- a/kubernetes/infrastructure/openbao/rotation/authentik.yaml +++ b/kubernetes/infrastructure/openbao/rotation/authentik.yaml @@ -63,7 +63,7 @@ spec: type: RuntimeDefault containers: - name: rotate - image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:159e5888ff4e23f5fe7fb1284711d81591635f840888e61402af311b0a76ba92 + image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["random"] securityContext: allowPrivilegeEscalation: false diff --git a/kubernetes/infrastructure/openbao/rotation/claude-oauth.yaml b/kubernetes/infrastructure/openbao/rotation/claude-oauth.yaml index 47d889b8..ff561fab 100644 --- a/kubernetes/infrastructure/openbao/rotation/claude-oauth.yaml +++ b/kubernetes/infrastructure/openbao/rotation/claude-oauth.yaml @@ -68,7 +68,7 @@ spec: type: RuntimeDefault containers: - name: sync-woodpecker - image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:828bf4863286411bfd7ef9fdff617586588050c814743bec601a9222692162e4 + image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["woodpecker-secret"] securityContext: allowPrivilegeEscalation: false diff --git a/kubernetes/infrastructure/openbao/rotation/cloudflare.yaml b/kubernetes/infrastructure/openbao/rotation/cloudflare.yaml index 6b24d1a2..df71717e 100644 --- a/kubernetes/infrastructure/openbao/rotation/cloudflare.yaml +++ b/kubernetes/infrastructure/openbao/rotation/cloudflare.yaml @@ -61,7 +61,7 @@ spec: type: RuntimeDefault containers: - name: rotate - image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:978fe53cad83a8d55319f6201ecd171775a101b21aaa21b7ec3c7b14304c6a42 + image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["cloudflare-roll"] securityContext: allowPrivilegeEscalation: false diff --git a/kubernetes/infrastructure/openbao/rotation/crowdsec-bouncer-key.yaml b/kubernetes/infrastructure/openbao/rotation/crowdsec-bouncer-key.yaml index d623ee35..61c6c5a0 100644 --- a/kubernetes/infrastructure/openbao/rotation/crowdsec-bouncer-key.yaml +++ b/kubernetes/infrastructure/openbao/rotation/crowdsec-bouncer-key.yaml @@ -63,7 +63,7 @@ spec: type: RuntimeDefault containers: - name: rotate - image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:159e5888ff4e23f5fe7fb1284711d81591635f840888e61402af311b0a76ba92 + image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["random"] securityContext: allowPrivilegeEscalation: false diff --git a/kubernetes/infrastructure/openbao/rotation/external-dns.yaml b/kubernetes/infrastructure/openbao/rotation/external-dns.yaml index cecc14fd..09e56795 100644 --- a/kubernetes/infrastructure/openbao/rotation/external-dns.yaml +++ b/kubernetes/infrastructure/openbao/rotation/external-dns.yaml @@ -56,7 +56,7 @@ spec: type: RuntimeDefault containers: - name: rotate - image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:978fe53cad83a8d55319f6201ecd171775a101b21aaa21b7ec3c7b14304c6a42 + image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["cloudflare-roll"] securityContext: allowPrivilegeEscalation: false diff --git a/kubernetes/infrastructure/openbao/rotation/flux-git.yaml b/kubernetes/infrastructure/openbao/rotation/flux-git.yaml index 09537af1..0f2576ba 100644 --- a/kubernetes/infrastructure/openbao/rotation/flux-git.yaml +++ b/kubernetes/infrastructure/openbao/rotation/flux-git.yaml @@ -84,7 +84,7 @@ spec: type: RuntimeDefault containers: - name: rotate - image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:b41cc3645f144179dddaa5ab1d32282c57c257432a613c15a6e15634fd0c4b81 + image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["gitea-token"] securityContext: allowPrivilegeEscalation: false diff --git a/kubernetes/infrastructure/openbao/rotation/home-assistant.yaml b/kubernetes/infrastructure/openbao/rotation/home-assistant.yaml index b2b81bb8..b986897e 100644 --- a/kubernetes/infrastructure/openbao/rotation/home-assistant.yaml +++ b/kubernetes/infrastructure/openbao/rotation/home-assistant.yaml @@ -68,7 +68,7 @@ spec: type: RuntimeDefault containers: - name: rotate - image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:159e5888ff4e23f5fe7fb1284711d81591635f840888e61402af311b0a76ba92 + image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["ha-token"] securityContext: allowPrivilegeEscalation: false @@ -130,7 +130,7 @@ spec: type: RuntimeDefault containers: - name: rotate - image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:159e5888ff4e23f5fe7fb1284711d81591635f840888e61402af311b0a76ba92 + image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["ha-token"] securityContext: allowPrivilegeEscalation: false @@ -192,7 +192,7 @@ spec: type: RuntimeDefault containers: - name: rotate - image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:159e5888ff4e23f5fe7fb1284711d81591635f840888e61402af311b0a76ba92 + image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["ha-token"] securityContext: allowPrivilegeEscalation: false diff --git a/kubernetes/infrastructure/openbao/rotation/kustomization.yaml b/kubernetes/infrastructure/openbao/rotation/kustomization.yaml index a81b73b7..47201e1e 100644 --- a/kubernetes/infrastructure/openbao/rotation/kustomization.yaml +++ b/kubernetes/infrastructure/openbao/rotation/kustomization.yaml @@ -4,6 +4,26 @@ kind: Kustomization # (intra-namespace OpenBao access → no NetworkPolicy needed). One file per rotated # secret. The matching OpenBao policy/role for each lives in ../policies/ and is # bootstrapped manually (not GitOps-managed). +# +# IMAGE PINNING: every rotator here uses a DATED TAG of secret-rotator +# (`:YYYY-MM-DD`), never a digest and never `:latest`. This is a registry-local +# image, so it does not need digest pinning, and a dated tag makes "what actually +# runs" a reviewable diff. +# +# Digests were tried and actively broke: untagged manifests are garbage-collected +# once :latest and the dated tags move past them, so a digest pinned today stops +# resolving after a few rebuilds. That decay is driven by edits to +# .woodpecker/build-ci-images.yml — a shared file — so adding an UNRELATED image +# republishes secret-rotator and orphans the manifest every rotator here was +# pinned to. 11 of these were pinned to digests that no longer existed and would +# have ImagePullBackOff'd on their first scheduled run (issue #458); because they +# are quarterly, that would have looked like nothing happening rather than like a +# failure. +# +# Keep them all on one tag: it is a single binary containing every mode +# (random | ha-token | cloudflare-roll | netbird-key | proxmox-token | +# gitea-token | woodpecker-secret), so a split serves no purpose and just makes +# the next bump partial. resources: - paperless.yaml - woodpecker.yaml diff --git a/kubernetes/infrastructure/openbao/rotation/mosquitto-health.yaml b/kubernetes/infrastructure/openbao/rotation/mosquitto-health.yaml index d61f1c10..bc7ec805 100644 --- a/kubernetes/infrastructure/openbao/rotation/mosquitto-health.yaml +++ b/kubernetes/infrastructure/openbao/rotation/mosquitto-health.yaml @@ -88,22 +88,11 @@ spec: type: RuntimeDefault containers: - name: rotate - # DATED TAG, not a digest and never :latest — this is our own - # image, so it does not need digest pinning, and a floating tag - # would make what runs here unreviewable. - # - # Deliberately NOT copying the sibling rotators' digest pins: those - # digests NO LONGER EXIST in the registry (verified by pulling them - # — NotFound), because untagged manifests are garbage-collected - # once :latest and the dated tags move past them. 11 of the 14 - # existing rotators are pinned that way and will ImagePullBackOff - # on their first run — see issue #458. Copying the convention would - # have shipped a CronJob broken from the day it was written. - # - # Verified for THIS tag: it pulls, `random` is among its modes, and - # it carries the RANDOM_BYTES>=16 floor documented below - # (RANDOM_BYTES=8 exits "refusing to generate a weak secret"). - # Bumping the date is a deliberate, reviewable edit. + # Dated tag, per the convention documented in kustomization.yaml — + # every rotator here now uses the same one. Verified for this tag: + # it pulls, `random` is among its modes, and it carries the + # RANDOM_BYTES>=16 floor documented below (RANDOM_BYTES=8 exits + # "refusing to generate a weak secret"). image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["random"] securityContext: diff --git a/kubernetes/infrastructure/openbao/rotation/netbird-ingress.yaml b/kubernetes/infrastructure/openbao/rotation/netbird-ingress.yaml index 08d4e0ea..3735d203 100644 --- a/kubernetes/infrastructure/openbao/rotation/netbird-ingress.yaml +++ b/kubernetes/infrastructure/openbao/rotation/netbird-ingress.yaml @@ -59,7 +59,7 @@ spec: type: RuntimeDefault containers: - name: rotate - image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:49bfddd8eeb5fd871cbeb05bc3b4e2fcaaab7d9e24cc3007caae6084b4634c18 + image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["netbird-key"] securityContext: allowPrivilegeEscalation: false diff --git a/kubernetes/infrastructure/openbao/rotation/paperless.yaml b/kubernetes/infrastructure/openbao/rotation/paperless.yaml index 258d504e..111cad63 100644 --- a/kubernetes/infrastructure/openbao/rotation/paperless.yaml +++ b/kubernetes/infrastructure/openbao/rotation/paperless.yaml @@ -63,7 +63,7 @@ spec: type: RuntimeDefault containers: - name: rotate - image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:159e5888ff4e23f5fe7fb1284711d81591635f840888e61402af311b0a76ba92 + image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["random"] securityContext: allowPrivilegeEscalation: false diff --git a/kubernetes/infrastructure/openbao/rotation/pve-exporter.yaml b/kubernetes/infrastructure/openbao/rotation/pve-exporter.yaml index fe204763..dd5ca494 100644 --- a/kubernetes/infrastructure/openbao/rotation/pve-exporter.yaml +++ b/kubernetes/infrastructure/openbao/rotation/pve-exporter.yaml @@ -63,7 +63,7 @@ spec: type: RuntimeDefault containers: - name: rotate - image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:97c22bcff324a93d2bb7bf3adb2894b405ed1a8035d02ad1ff6baced3813cefe + image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["proxmox-token"] securityContext: allowPrivilegeEscalation: false diff --git a/kubernetes/infrastructure/openbao/rotation/review-bot.yaml b/kubernetes/infrastructure/openbao/rotation/review-bot.yaml index cc863669..91662e2b 100644 --- a/kubernetes/infrastructure/openbao/rotation/review-bot.yaml +++ b/kubernetes/infrastructure/openbao/rotation/review-bot.yaml @@ -81,7 +81,7 @@ spec: type: RuntimeDefault initContainers: - name: rotate-mint - image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:828bf4863286411bfd7ef9fdff617586588050c814743bec601a9222692162e4 + image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["gitea-token"] securityContext: allowPrivilegeEscalation: false @@ -128,7 +128,7 @@ spec: memory: 48Mi containers: - name: sync-woodpecker - image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:828bf4863286411bfd7ef9fdff617586588050c814743bec601a9222692162e4 + image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["woodpecker-secret"] securityContext: allowPrivilegeEscalation: false diff --git a/kubernetes/infrastructure/openbao/rotation/shelly-manager.yaml b/kubernetes/infrastructure/openbao/rotation/shelly-manager.yaml index 2fe7c5ad..6eb593ca 100644 --- a/kubernetes/infrastructure/openbao/rotation/shelly-manager.yaml +++ b/kubernetes/infrastructure/openbao/rotation/shelly-manager.yaml @@ -63,7 +63,7 @@ spec: type: RuntimeDefault containers: - name: rotate - image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:159e5888ff4e23f5fe7fb1284711d81591635f840888e61402af311b0a76ba92 + image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["random"] securityContext: allowPrivilegeEscalation: false diff --git a/kubernetes/infrastructure/openbao/rotation/woodpecker.yaml b/kubernetes/infrastructure/openbao/rotation/woodpecker.yaml index 0fa7936b..fc32b36c 100644 --- a/kubernetes/infrastructure/openbao/rotation/woodpecker.yaml +++ b/kubernetes/infrastructure/openbao/rotation/woodpecker.yaml @@ -63,7 +63,7 @@ spec: type: RuntimeDefault containers: - name: rotate - image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:159e5888ff4e23f5fe7fb1284711d81591635f840888e61402af311b0a76ba92 + image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 args: ["random"] securityContext: allowPrivilegeEscalation: false -- 2.54.0 From 1cad57984cb08e9a6d0db173112089296f97d71f Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Thu, 30 Jul 2026 19:31:30 +0200 Subject: [PATCH 2/4] docs(rotation): tell future authors how to avoid reintroducing digest pins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review point on #459. Nothing stops the next rotator from copy-pasting a @sha256 pin from an older example, and the failure only surfaces months later on the first scheduled run — the same silent, delayed failure this PR just cleaned up 11 instances of. Added two copy-pasteable checks to the kustomization header: a grep that must return nothing before adding a rotator, and a pull test to run BEFORE committing a tag bump. The second matters because a tag that was never built fails exactly like the dead digests did, and just as quietly. Not a CI gate, deliberately. Nothing in CI covers kubernetes/** at all (deploy-infrastructure.yml only covers opentofu/**), so a real guard means standing up a lint/validate pipeline for that tree — worth doing, but as its own change rather than smuggled into a repin PR. Documented here so the knowledge is not lost while that waits. Co-Authored-By: Claude Opus 5 (1M context) --- .../openbao/rotation/kustomization.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/kubernetes/infrastructure/openbao/rotation/kustomization.yaml b/kubernetes/infrastructure/openbao/rotation/kustomization.yaml index 47201e1e..495330ab 100644 --- a/kubernetes/infrastructure/openbao/rotation/kustomization.yaml +++ b/kubernetes/infrastructure/openbao/rotation/kustomization.yaml @@ -24,6 +24,20 @@ kind: Kustomization # (random | ha-token | cloudflare-roll | netbird-key | proxmox-token | # gitea-token | woodpecker-secret), so a split serves no purpose and just makes # the next bump partial. +# +# ADDING A ROTATOR: copy an existing file, and check you did not inherit a +# digest pin from an older example — nothing enforces this automatically, so it +# is easy to reintroduce silently and the failure only shows up months later on +# the first scheduled run: +# +# grep -l '@sha256' kubernetes/infrastructure/openbao/rotation/*.yaml # want: no output +# +# BUMPING the tag: verify the new one exists BEFORE committing — a tag that was +# never built fails exactly like the digests did, and just as quietly: +# +# kubectl run tagcheck --rm -i --restart=Never \ +# --image=gitea.renner.uno/gitea_admin/secret-rotator: \ +# --command -- /secret-rotator # prints the usage/mode list if the tag resolves resources: - paperless.yaml - woodpecker.yaml -- 2.54.0 From 5035085426e10c063fee02deecc335a4c1f92358 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Thu, 30 Jul 2026 19:32:08 +0200 Subject: [PATCH 3/4] fix(rotation): make the documented digest-guard not flag its own docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ran the check I had just documented and it returned kustomization.yaml — because the comment explaining the rule contains the literal string it greps for. A guard whose only hit is its own documentation trains people to ignore it, which is worse than not having one. Anchored on `image:` instead. Verified both directions: returns nothing across this directory as it stands, and still flags a file containing a real `image: ...@sha256:` pin. Co-Authored-By: Claude Opus 5 (1M context) --- .../infrastructure/openbao/rotation/kustomization.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/kubernetes/infrastructure/openbao/rotation/kustomization.yaml b/kubernetes/infrastructure/openbao/rotation/kustomization.yaml index 495330ab..15a055c2 100644 --- a/kubernetes/infrastructure/openbao/rotation/kustomization.yaml +++ b/kubernetes/infrastructure/openbao/rotation/kustomization.yaml @@ -30,7 +30,12 @@ kind: Kustomization # is easy to reintroduce silently and the failure only shows up months later on # the first scheduled run: # -# grep -l '@sha256' kubernetes/infrastructure/openbao/rotation/*.yaml # want: no output +# grep -l 'image:.*@sha256' kubernetes/infrastructure/openbao/rotation/*.yaml +# +# (want: no output. Match on `image:` rather than the bare digest string, or the +# check flags this file for merely documenting the pattern — verified: the bare +# form returns kustomization.yaml, the anchored form returns nothing here and +# still catches a real pin.) # # BUMPING the tag: verify the new one exists BEFORE committing — a tag that was # never built fails exactly like the digests did, and just as quietly: -- 2.54.0 From 5d32065b8ffbabbddca167220f5529a8443643dd Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Thu, 30 Jul 2026 19:33:04 +0200 Subject: [PATCH 4/4] fix(rotation): make the documented guard actually pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second attempt, and the first one was wrong for a subtler reason than I fixed. Anchoring on `image:` did not help: this file documents the pattern, so the comment text `image:.*@sha256` matches the regex `image:.*@sha256`. The guard kept reporting its own documentation. Added the `| grep -v kustomization.yaml` filter and this time ran the command exactly as written rather than a paraphrase of it. Verified both directions: no output across this directory as it stands, and it still reports a file carrying a real `image: ...@sha256:` pin. Lesson worth the two commits: a documented check has to be executed in the form it is documented in. I had verified a variant twice and shipped a guard whose only hit was itself — which trains people to ignore it. Co-Authored-By: Claude Opus 5 (1M context) --- .../openbao/rotation/kustomization.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/kubernetes/infrastructure/openbao/rotation/kustomization.yaml b/kubernetes/infrastructure/openbao/rotation/kustomization.yaml index 15a055c2..dc26fe3f 100644 --- a/kubernetes/infrastructure/openbao/rotation/kustomization.yaml +++ b/kubernetes/infrastructure/openbao/rotation/kustomization.yaml @@ -30,12 +30,13 @@ kind: Kustomization # is easy to reintroduce silently and the failure only shows up months later on # the first scheduled run: # -# grep -l 'image:.*@sha256' kubernetes/infrastructure/openbao/rotation/*.yaml +# grep -l 'image:.*@sha256' \ +# kubernetes/infrastructure/openbao/rotation/*.yaml | grep -v kustomization.yaml # -# (want: no output. Match on `image:` rather than the bare digest string, or the -# check flags this file for merely documenting the pattern — verified: the bare -# form returns kustomization.yaml, the anchored form returns nothing here and -# still catches a real pin.) +# (want: no output. The trailing filter is not padding — this file documents the +# pattern, so it necessarily matches its own grep. Verified both directions: +# silent across this directory as it stands, and it still reports a file that +# actually carries an `image: …@sha256:` pin.) # # BUMPING the tag: verify the new one exists BEFORE committing — a tag that was # never built fails exactly like the digests did, and just as quietly: -- 2.54.0