This is an automated email from the ASF dual-hosted git repository.

janhoy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-operator.git


The following commit(s) were added to refs/heads/main by this push:
     new afd1f80  Add a dependency-upgrade agent skill (#864)
afd1f80 is described below

commit afd1f80f15839e36a905a153debcb81bf812a40a
Author: Jan Høydahl <[email protected]>
AuthorDate: Fri Nov 6 13:48:16 2026 +0100

    Add a dependency-upgrade agent skill (#864)
---
 .agents/skills/dependency-upgrade/SKILL.md | 290 ++++++++++++++++++++
 .github/dependabot.yml                     |  89 ++++++
 .gitignore                                 |   3 +
 AGENTS.md                                  |  19 ++
 Makefile                                   |   4 +
 dev-docs/dependency-upgrades.md            | 167 ++++++++++++
 hack/list-upgrades.sh                      | 418 +++++++++++++++++++++++++++++
 hack/release/artifacts/bundle_source.sh    |   3 +
 8 files changed, 993 insertions(+)

diff --git a/.agents/skills/dependency-upgrade/SKILL.md 
b/.agents/skills/dependency-upgrade/SKILL.md
new file mode 100644
index 0000000..73ccee6
--- /dev/null
+++ b/.agents/skills/dependency-upgrade/SKILL.md
@@ -0,0 +1,290 @@
+---
+name: dependency-upgrade
+description: Perform a sweeping dependency upgrade of the Solr Operator - bump 
Go modules and pinned tool versions together, regenerate CRDs, RBAC, deepcopy 
code and the license list, fix the test fixtures that k8s API bumps break, and 
record the user-visible effects in both Helm charts' artifacthub.io/changes 
changelog. Use this instead of accepting many small Dependabot PRs, and 
especially before cutting a release.
+when_to_use: The user asks to upgrade dependencies, bump Go modules, update 
k8s/controller-runtime/Helm libraries, refresh the dependency license list, 
handle a major-version or breaking dependency change, or prepare dependencies 
ahead of a release. Also when a dependency PR fails "make check-licenses" or 
"make check-git".
+---
+
+# Sweeping dependency upgrade
+
+Upgrade everything in one reviewed pass: one CRD regeneration, one 
license-list regeneration, one
+set of fixture repairs, one changelog entry. The changelog is the step most 
often forgotten and the
+main reason this skill exists. Read `AGENTS.md` first — where it and this 
skill disagree, the repo
+wins.
+
+## Who owns what
+
+| Tier | What | Owner |
+|---|---|---|
+| A | GitHub Actions, the release wizard's `requirements.txt` | Dependabot 
(`.github/dependabot.yml`). Not yours: leave its PRs alone, never bump a 
`uses:` SHA or a wizard pin during a sweep. |
+| B | Go modules, Makefile tool pins, envtest assets | This skill. `make 
list-upgrades` detects them. |
+| C | Base images, kind/Kubernetes versions in the test scripts, the Zookeeper 
server image, docs matrices | This skill, but nothing upstream to check 
against. `make list-upgrades` locates them by pattern and prints current 
values; bump what is clearly stale. `PATTERN NOT FOUND` or `MISSING FILE` means 
the table in the script needs updating. |
+
+Out of scope: `DefaultSolrVersion`, chart `appVersion`, the operator's image 
tag — all release
+process (`hack/release/version/propagate_version.sh`). Also the declared 
minimum Kubernetes
+version, below.
+
+## Step 0: branch from upstream main
+
+```bash
+git remote -v | awk '/apache\/solr-operator(\.git)? \(fetch\)/ {print $1}'
+git status --short
+git fetch <remote> main
+git switch -c dependency-upgrade-<yyyy-mm> <remote>/main
+```
+
+The remote is `upstream` in some clones, `apache` or `origin` in others; if 
the first command prints
+nothing, ask rather than guess. Stop and ask if tracked files are modified 
(untracked scratch files
+are fine). Branch from `<remote>/main`, never local `main`. Do not `git pull`. 
Do not commit to
+`main`.
+
+## Preflight
+
+All five. Each one silently ruins the run.
+
+1. **Delete `vendor/`.** Not vendored, not gitignored, so stale copies linger. 
Breaks three things:
+   Go switches to `-mod=vendor` (`inconsistent vendoring`); 
`hack/check_license.sh` has no `vendor/`
+   exclusion so `make lint` exits 255 on thousands of third-party files; `make 
check-git` sees only
+   tracked files, so CI stays green and warns you of nothing. Also check `go 
env GOFLAGS`.
+2. **Match the Go toolchain to `go.mod`.** The Makefile fails at parse time, 
blocking *every*
+   target including `make help`, unless `major.minor` from the `go` directive 
is a substring of
+   `go version`. Newer fails as hard as older. Install it, or set 
`GOTOOLCHAIN=go1.<minor>.0`.
+3. **`make clean`.** Tool installation is a bare file-existence check, so 
stale binaries in `bin/`
+   survive a version bump and regenerate CRDs with the old `controller-gen`.
+4. **Docker running, network available.** `make lint` needs Docker 
(`check-helm`, `check-docs`),
+   `make test` downloads envtest assets, `mod-tidy` and `fetch-licenses-list` 
reach the network.
+5. **`helm` and `yq` on `PATH`.** `make install-dependencies` installs neither.
+
+## Decide the scope
+
+Run `make list-upgrades`, or `./hack/list-upgrades.sh` when the Go-version 
gate blocks make. It
+writes nothing, exits 0, and prints every section even when one of them fails.
+
+**Take every patch and minor bump without asking.** No interview, no 
per-module approval, and never
+hold one back because its release notes are long.
+
+### Resolve the Go toolchain first
+
+Decide the target Go version before judging any other candidate. A 
dependency's newest release may
+require a newer Go than `go.mod` pins; taking it anyway silently pulls a 
second toolchain, and the
+Makefile's gate then rejects the mismatch. `make list-upgrades` marks those 
candidates
+`[needs Go X.Y]` — compared against the *current* directive, so if you raise 
`go` in `go.mod`,
+re-run it and judge everything else against the new floor.
+
+Raising Go is its own decision: it moves `build/Dockerfile` and the 
`go-version` matrix in CI, and
+forces every contributor to update. If Go stays put, take the newest 
dependency version that still
+builds on it rather than skipping the module — e.g. kustomize v5.8.3 needs Go 
1.27, while v5.8.0
+needs only 1.24.
+
+### Linked sets
+
+These move together — bumping one alone produces a tidy that resolves and a 
build that fails:
+
+- **The Kubernetes set:** `k8s.io/api`, `apimachinery`, `client-go`, plus the 
indirect `k8s.io/*`
+  entries (`apiextensions-apiserver`, `apiserver`, `cli-runtime`, 
`component-base`, `kubectl`) — all
+  on one version. `helm.sh/helm/v3` pulls several, so Helm moves in lockstep.
+- **`sigs.k8s.io/controller-runtime`** — also decides the `setup-envtest` 
release branch.
+- **`github.com/onsi/ginkgo/v2`** — the Makefile derives the installed ginkgo 
version from it.
+- **`github.com/pravega/zookeeper-operator`** — see below.
+
+Read each candidate's release notes for the span crossed.
+
+### A new major is the user's call
+
+Never fold one in unilaterally, and never take the minor without saying the 
major exists. Cost it
+out in minutes:
+
+1. Migration notes for the major boundary only, not every release in the span.
+2. Blast radius: `grep -rln '<module path>' --include='*.go' .`
+3. What it drags along: a `controller-runtime` major usually forces the 
Kubernetes set, and
+   `helm.sh/helm` pulls `k8s.io/*`, so one major becomes four.
+4. User-visible effects: CRD schema, generated RBAC, minimum Kubernetes 
version.
+5. Whether the old major is still maintained. EOL means staying put is the 
risk.
+
+Present per module: current → available major, which breaking changes touch 
this codebase, file
+count, knock-on bumps, recommendation. Offer three options — take it now, take 
the minor and leave
+the major to a dedicated PR, or hold the module.
+
+On a toss-up recommend the minor plus a separate PR — a module-path change is 
unreviewable buried
+in regenerated CRDs. Recommend taking it now when the migration is mechanical, 
the old major is
+unmaintained, or a wanted bump requires it; say which. A major taken gets its 
own commit and its own
+PR paragraph.
+
+## The Zookeeper Operator is pinned in a dozen places
+
+`versions.props` is the source of truth and `make check-zk-op-version` 
enforces most, not all, of
+the copies. If it moves, update every one of `versions.props`, `go.mod`,
+`config/dependencies/zookeeper_cluster_crd.yaml` (both the `tree/` comment and 
the
+`operator.zookeeper.pravega.io/version` annotation), 
`helm/solr-operator/Chart.yaml` (the
+`dependencies:` entry), `helm/solr-operator/README.md`, `NOTICE` and
+`docs/modules/development/pages/development.adoc`, then run that target.
+
+- The **`v` prefix is inconsistent on purpose**: `v0.0.0` in `versions.props`, 
`go.mod`, the CRD
+  file, `NOTICE` and the chart README; bare `0.0.0` in `Chart.yaml` and 
`development.adoc`. The
+  check enforces both forms and hints when you get it wrong.
+- The ZookeeperCluster CRD is regenerated by **no** make target. Run 
`hack/zk-operator/update-crd.sh`
+  by hand; it needs network.
+- `tests/scripts/manage_e2e_tests.sh` pins the Zookeeper *server* image 
separately, unchecked.
+
+## The minimum Kubernetes version is not a dependency
+
+The `k8s.io/*` libraries are an ordinary dependency, but the declared minimum 
supported Kubernetes
+is a support promise: `kubeVersion` in both charts plus the matrix in
+`docs/modules/upgrade-notes/pages/upgrade-notes.adoc`, enforced by Helm at 
install time, so raising
+it stops existing users from upgrading. Nothing links the two, so a library 
bump cannot raise the
+floor by accident — but it widens the gap between what the operator is built 
against and what it
+claims to support. `make list-upgrades` prints declared, built and tested side 
by side: report that
+gap, never close it. **Never edit `kubeVersion` or the matrix during a 
sweep.** Raising the floor is
+its own PR, timed to a release, with an upgrade-notes entry and a `changed` 
entry in both charts. The
+exception is a bump that *forces* the floor — new libraries dropping an API 
the operator uses, or CRD
+schema older API servers reject — and then you stop, because the supported 
range is the first
+decision.
+
+## Tier C notes
+
+Two that need care rather than a bump:
+
+- The `golang` builder image in `build/Dockerfile` moves together with the 
`go` directive in
+  `go.mod` and the `go-version` matrix in CI, because the Makefile's gate 
rejects a mismatch either
+  way.
+- `artifacthub/ah` in the Makefile is **untagged**, so `make lint` pulls 
`:latest`. Pinning it is
+  worthwhile but a deliberate change of its own.
+
+Some Tier C pins have already drifted (kind and Kubernetes differ between the 
e2e and smoke-test
+scripts; `hack/test-upgrade.sh` names an old Zookeeper Operator). Do not fold 
a drift cleanup into a
+sweep — file it separately, or the diff stops being reviewable.
+
+## Do the upgrade
+
+1. `make list-upgrades`. Settle the Go toolchain target first, then the rest 
of the scope,
+   including any major-version interview.
+2. Edit `go.mod`, and `versions.props` if the Zookeeper Operator moves.
+3. `make mod-tidy`.
+4. If the Zookeeper Operator moved: `hack/zk-operator/update-crd.sh`, update 
the files listed above,
+   then `make check-zk-op-version` until it passes.
+5. `make install-dependencies` — installs the tools at the new pinned and 
derived versions.
+6. `make prepare` — the one command that makes a PR pass lint. Runs `fmt`, 
`generate`, `manifests`,
+   `fetch-licenses-list` and `mod-tidy` in the right order.
+7. `make test`, then repair fixtures.
+8. Add or consolidate the changelog entries.
+9. `make lint`, then `make test` again.
+
+### Regenerate, never hand-edit
+
+`make manifests` writes `config/` **and** `helm/` from one `controller-gen` 
run, transforming on the
+way, and `make check-manifests` diffs both trees recursively — a partial 
regeneration fails. Never
+hand-edit `helm/solr-operator/crds/crds.yaml` (the three `config/crd/bases` 
files concatenated under
+a header) or `helm/solr-operator/templates/role.yaml` (`config/rbac/role.yaml` 
in Helm conditionals
+with templated names). A `controller-gen` bump alone rewrites every CRD, 
because the annotation
+script stamps a version line into each; several thousand changed lines is 
normal.
+
+### Lint targets run serially
+
+`check-mod`, `check-manifests` and `check-generated` each delete a shared 
scratch directory, so
+`make -j` has them destroy each other's work. `check-mod` also moves 
`go.mod`/`go.sum` aside,
+re-tidies, then moves the originals back — interrupt it mid-way and the tree 
keeps the tidied files
+while the originals sit in a scratch directory `make clean` deletes. After 
interrupting, check
+`git diff go.mod` first.
+
+### The license list is platform-sensitive
+
+`fetch-licenses-list` is prefixed `GOOS=linux GOARCH=amd64` deliberately: 
`go-licenses` walks only
+the current platform's build graph, so regenerating on macOS or ARM silently 
omits Linux-only
+dependencies and CI's byte-exact diff then fails. Always use the make target, 
never `go-licenses`
+directly. Do not re-sort the CSV; it is in the generator's native order.
+
+`make check-licenses` fails two ways. A `<`/`>` diff means the CSV is stale — 
regenerate it. A
+disallowed-license complaint means a new transitive dependency landed under a 
license outside the
+allow-list, and **there is no override**: find a version that avoids it, or 
stop and raise it with
+the PMC. Never edit the allow-list to make a build pass.
+
+## Fixing what a Kubernetes bump breaks
+
+The controller tests boot a real API server through envtest and load the real 
CRDs, so an API bump
+changes behaviour, not just signatures. Expect to repair test fixtures, not 
production code — past
+sweeps needed a toleration operator changed from `Exists` to `Equal`, and 
storage resource requests
+added to PVC templates. One failure at a time, and for each ask whether it is 
a fixture encoding an
+old default or a real regression. A regression means fixing the controller and 
a changelog entry; so
+does any change to generated RBAC, which is user-visible.
+
+All controller and e2e specs use focused Ginkgo nodes (`FDescribe`, 
`FContext`, `FIt`), so
+`GINKGO_EDITOR_INTEGRATION=true` is load-bearing. `make test` sets it; `go 
test ./controllers/...`
+by hand without it fails confusingly on programmatic focus.
+
+## Changelog: both charts
+
+Add entries to `artifacthub.io/changes` in **both** 
`helm/solr-operator/Chart.yaml` and
+`helm/solr/Chart.yaml` — a YAML block scalar holding a list. `kind` accepts 
exactly `added`,
+`changed`, `deprecated`, `removed`, `fixed`, `security`. Follow the existing 
entries, including the
+`links:` convention of a `Github Issue` and/or `Github PR` pair. Malformed 
YAML fails
+`make check-helm`.
+
+Write for a user deciding whether to upgrade, not a reviewer reading the diff:
+
+- Tight wording. One line per entry, no prose, no restating the diff.
+- Never list library bumps individually. One `changed` entry summarising the 
sweep.
+- Several sweeps land in one release cycle. If that grouped entry is already 
there from an earlier
+  one, update it instead of adding a second — the net span since the last 
release, not this PR's
+  delta — and add this PR to its `links:`. The separate-entry rules below 
still apply.
+- A separate entry for anything changing behaviour or requiring action: new 
RBAC permissions, a
+  bundled Zookeeper Operator bump, a changed default.
+- A CVE fix uses `kind: security` **and** flips 
`artifacthub.io/containsSecurityUpdates` to `"true"`
+  in that chart. Once set in a cycle it stays set; a later CVE-free sweep must 
not flip it back.
+- A sweep with no user-visible effect still gets one `changed` entry.
+
+If the bundled Zookeeper Operator moved, also add an entry to
+`docs/modules/upgrade-notes/pages/upgrade-notes.adoc`; its matrices are 
hand-maintained.
+
+Release-time interaction: the release process wipes `artifacthub.io/changes` 
to a placeholder just
+after a release branch is cut, so an entry added to `main` after that lands in 
the *next* release.
+An upgrade belonging to the release being cut must merge before that step. 
That placeholder is two
+dummy entries (`Addition 1`, `Change 2`), not an empty list — replace them, 
don't append.
+
+## Verify before you commit
+
+1. `make lint`, serially. Covers zk version consistency, tidy, formatting, 
licenses, manifests,
+   generated code, charts, docs.
+2. `make test`.
+3. `git status --short`, then stage deliberately by naming paths. **Never `git 
add -A`** — the tree
+   often still holds an untracked `vendor/` (6500+ files) or scratch notes. 
Confirm **both**
+   `git diff --exit-code` and `git status --short` are clean; CI's `check-git` 
only checks the
+   former, so an untracked generated file passes CI and breaks the next person.
+
+Treat any failure as unfinished work. Add `make smoke-test` if packaging could 
be affected; it needs
+Docker, kind and network, unpacks the source release and runs `make check` 
inside it.
+
+CI coverage: build-and-check runs on every PR; smoke-test skips doc-only paths 
but runs for
+`go.mod`, `go.sum`, `Makefile`, the license CSV and `helm/`. **The e2e 
workflow is permanently
+disabled** and reports as skipped, so it is evidence of nothing — if runtime 
behaviour could be
+affected, run `make e2e-tests` locally and say so in the PR.
+
+## Commit, then ask before pushing
+
+One commit, or a few logically separate ones, with generated output in the 
same commit as the change
+that generated it. Message is a line or two: imperative subject under ~70 
characters matching the
+existing log (`Upgrade almost all operator dependencies`), at most a sentence 
of body. Detail belongs
+in the PR description.
+
+The fork is the remote whose URL is not `apache/solr-operator`:
+
+```bash
+git remote -v | grep -v apache/solr-operator
+```
+
+Several candidates or none means ask which. **Never push without the user 
agreeing in this
+conversation**, and never push to the apache remote. After pushing, propose 
the PR description and
+let the user open the PR unless they ask you to.
+
+### PR description
+
+A reviewer cannot read thousands of lines of regenerated CRDs, so the 
description says what to check
+instead. Well under a screenful:
+
+- One sentence: which dependency sets moved, and why now — routine sweep, 
pre-release, CVE.
+- The sets and version spans crossed, not one bullet per module.
+- Everything not mechanical: each major bump and what it required; each 
changed fixture, and whether
+  it was stale or a real behaviour change; any RBAC or default that moved.
+- One line naming the changelog entries added.
+- What you ran: `make lint`, `make test`, and `make e2e-tests` if you did — 
CI's e2e is disabled.
+
+Leave out anything visible from the file list, and never paste regenerated 
diffs. Write each
+paragraph and bullet as one unwrapped line; GitHub reflows them.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..59201a3
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,89 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Dependabot owns GitHub Actions and the release wizard's Python requirements,
+# and nothing else in this repository.
+#
+# There is deliberately no "gomod" entry here. Dependabot only runs the
+# ecosystems listed in this file, so leaving it out is the enforcement.
+# Go modules, the Makefile's pinned tool versions, the envtest assets and
+# the hand-maintained image pins are upgraded in one reviewed sweep by the
+# dependency-upgrade skill (.agents/skills/dependency-upgrade/SKILL.md),
+# because each bump also needs the CRDs regenerated, dependency_licenses.csv
+# refreshed, test fixtures repaired and an artifacthub.io/changes entry
+# written - none of which Dependabot can do. Run "make list-upgrades" to see
+# what has moved. Please do not add a gomod ecosystem here.
+#
+# There is no "docker" entry either. The only Dockerfile pins the Go builder
+# image, which has to move in lockstep with the "go" directive in go.mod and
+# the CI matrix, so it belongs to the same sweep.
+#
+# The wizard's requirements.txt is the opposite case, and that is why it is
+# here: those packages are only used by the release wizard itself, never built
+# into the operator or its image, so a bump needs no regeneration, no license
+# list and no changelog entry. Reviewing it is reading the diff and running
+# the wizard once.
+version: 2
+updates:
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "monthly"
+    open-pull-requests-limit: 2
+    # Keep brand-new releases out until they have had a week to settle. This
+    # applies to version updates only; security updates are never delayed.
+    # The github-actions ecosystem supports default-days only - the
+    # semver-major-days / semver-minor-days / semver-patch-days variants are
+    # not honoured here, so do not add them.
+    cooldown:
+      default-days: 7
+    # One PR for all actions rather than one per action.
+    groups:
+      github-actions:
+        patterns:
+          - "*"
+    labels:
+      - "dependencies"
+      - "github-actions"
+    commit-message:
+      prefix: "ci"
+    # Review note: the ASF allowlists third-party actions by commit SHA, in
+    # actions.yml in the apache/infrastructure-actions repository. A SHA that
+    # is not on that list fails the workflow at startup, before any step runs,
+    # so check a proposed SHA there before merging. Of the actions used here
+    # only azure/setup-helm is third-party; the actions/* ones are first-party
+    # and not gated. See the comment in publish-helm-oci.yaml.
+
+  # Python requirements of the release wizard (hack/release/wizard)
+  #
+  # The requirements are pinned exactly, so every upgrade needs a commit. One
+  # group collects them into a single PR covering the whole file. Only the
+  # packages the wizard imports are pinned; pip resolves their dependencies.
+  - package-ecosystem: "pip"
+    directory: "/hack/release/wizard"
+    schedule:
+      interval: "monthly"
+    open-pull-requests-limit: 2
+    cooldown:
+      default-days: 7
+    groups:
+      release-wizard:
+        patterns:
+          - "*"
+    labels:
+      - "dependencies"
+      - "python"
+    commit-message:
+      prefix: "deps"
diff --git a/.gitignore b/.gitignore
index 212e712..adf39c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,6 +36,9 @@ release-artifacts
 
 # editor and IDE paraphernalia
 .idea
+
+# Local AI assistant config. Skills live in .agents/, see 
dev-docs/dependency-upgrades.md
+.claude/
 *.swp
 *.swo
 *~
diff --git a/AGENTS.md b/AGENTS.md
index 0ff338c..d085b65 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -22,6 +22,25 @@ under the License.
 This file is read by automated agents (security scanners, code
 analyzers, AI assistants) operating on this repository.
 
+## Skills
+
+Reusable procedures for this repository are published as [Agent 
Skills](https://agentskills.io) in
+`.agents/skills/`, the vendor-neutral location that most assistants read. 
Before doing one of these
+tasks by hand, read its `SKILL.md`:
+
+| Skill | Use it for |
+|---|---|
+| `dependency-upgrade` | Upgrading Go modules and pinned tool versions in one 
sweeping pass, including CRD/RBAC regeneration, the dependency license list, 
and the Helm chart changelogs. |
+
+To see what has moved before starting an upgrade, run `make list-upgrades`. It 
reports the Go
+modules, pinned tool versions and envtest assets that the skill owns, and 
lists the image tags no
+tool can check. GitHub Actions and the release wizard's Python requirements 
are Dependabot's
+(`.github/dependabot.yml`), not the skill's.
+
+Most assistants read `.agents/skills/` directly. Claude Code does not, and 
needs a small local shim
+that is deliberately not kept in this repository - 
`dev-docs/dependency-upgrades.md` explains the
+options, including a session-start hook that mirrors the skills automatically.
+
 ## Security
 
 Security model: [SECURITY.md](./SECURITY.md)
diff --git a/Makefile b/Makefile
index c8e9095..28807a4 100644
--- a/Makefile
+++ b/Makefile
@@ -393,6 +393,10 @@ $(LOCALBIN):
 
 install-dependencies: controller-gen kustomize go-licenses setup-envtest kind 
ginkgo ## Install necessary dependencies for building and testing the Solr 
Operator
 
+.PHONY: list-upgrades
+list-upgrades: ## List dependencies with newer versions available (requires 
network)
+       ./hack/list-upgrades.sh
+
 CONTROLLER_GEN = $(LOCALBIN)/controller-gen
 .PHONY: controller-gen
 controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if 
necessary.
diff --git a/dev-docs/dependency-upgrades.md b/dev-docs/dependency-upgrades.md
new file mode 100644
index 0000000..ee6ade1
--- /dev/null
+++ b/dev-docs/dependency-upgrades.md
@@ -0,0 +1,167 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Dependency upgrades
+
+Dependencies are upgraded in **one sweeping pass** rather than through a 
stream of single-dependency
+pull requests. One pass means one round of CRD regeneration, one license-list 
regeneration, one set
+of test-fixture repairs and one changelog entry, instead of repeating all of 
that per bump.
+
+A release manager should do a sweep before cutting a release.
+
+## The agent skill
+
+The procedure is written up as an [Agent Skill](https://agentskills.io) at
+`.agents/skills/dependency-upgrade/SKILL.md`, so an AI coding assistant can 
carry it out.
+
+`.agents/skills/` is the vendor-neutral convention, read directly by Codex, 
Cursor, Copilot and
+Gemini CLI among others. For those, nothing needs setting up: ask for a 
dependency upgrade and the
+assistant finds the skill from its `description` and `when_to_use` frontmatter.
+
+The skill is instructions, not automation. It contains no scripts; it names 
the `make` targets to
+run, in what order, and what each is for. You can read it and follow it by 
hand — it is written to
+be useful to a human too.
+
+### Claude Code needs a local shim
+
+Claude Code only discovers skills under `.claude/skills/`. It does not scan 
`.agents/skills/`, and
+there is no setting to register another directory, so some local glue is 
needed until it follows the
+convention.
+
+`.claude/` is deliberately **gitignored** in this repository. We do not keep 
per-skill pointer files
+in tree, because that means a hand-maintained duplicate for every skill we 
ever add, each with a copy
+of the frontmatter to keep in sync. Set up whichever of the following suits 
you; all of them are
+local and none are committed.
+
+**Copy on session start (recommended).** Install a `SessionStart` hook that 
mirrors every skill from
+`.agents/skills/` into `.claude/skills/`. Because it copies the real file, the 
frontmatter and body
+are always correct, and because it re-runs every session the copy cannot go 
stale. Put the hook in
+your own `.claude/settings.local.json` (or `~/.claude/settings.json` to get it 
in every repo):
+
+```json
+{
+  "hooks": {
+    "SessionStart": [
+      {
+        "matcher": "startup|clear|resume|compact",
+        "hooks": [
+          { "type": "command", "command": 
"${CLAUDE_PROJECT_DIR}/.claude/hooks/sync-agent-skills.sh" }
+        ]
+      }
+    ]
+  }
+}
+```
+
+The script it points at is yours to write and lives outside version control. 
It needs to do only
+this: for each directory under `.agents/skills/` that contains a `SKILL.md`, 
create the matching
+directory under `.claude/skills/` and copy the file in, replacing any previous 
copy. Keep it fast —
+the hook runs on every session start, including after `/clear` and compaction.
+
+**Copy by hand.** The same thing, without the hook: copy
+`.agents/skills/<name>/SKILL.md` to `.claude/skills/<name>/SKILL.md` and 
repeat whenever the skill
+changes. Fine if you rarely touch it, easy to forget if you do.
+
+**Do not use a symlink.** Claude Code's skill discovery and validation do not 
resolve symlinks even
+though execution does, so a symlinked skill can appear to be missing while 
still working when
+invoked by name. Symlinks also do not survive a Windows checkout with 
`core.symlinks=false`.
+
+### Invoking it
+
+Once Claude Code can see the skill, `/dependency-upgrade` runs it. In any 
assistant, you can instead
+just say what you want — "upgrade all the Go dependencies", "bump 
controller-runtime" — and let the
+`description` and `when_to_use` fields do their job. Or open the file and 
follow it yourself.
+
+Note that neither this repository's `SKILL.md` nor any copy of it carries an 
ASF license header, and
+neither should: the YAML frontmatter has to be the very first thing in the 
file or the skill does not
+parse. `hack/check_license.sh` only enforces headers on `*.go`, `*.yaml` and 
`*.sh`, so this is not a
+lint failure waiting to happen — but please do not "fix" it by adding one.
+
+## Doing it by hand
+
+The short version, with the detail and the reasoning in the skill:
+
+1. `make list-upgrades` to see what has moved, and decide the scope. Patch and 
minor bumps are
+   always wanted; a new major is a decision, not a default.
+2. Delete any `vendor/` directory. The project is not vendored, `vendor/` is 
not gitignored, and a
+   stale copy breaks `make lint` locally while CI stays green.
+3. Match your Go toolchain to the `go` directive in `go.mod`. The `Makefile` 
refuses to run
+   otherwise, and a *newer* toolchain fails just as hard as an older one.
+4. `make clean`. Tool installs are skipped if the binary already exists, so 
stale tools in `bin/`
+   would otherwise regenerate code with the wrong versions.
+5. Edit `go.mod`, plus `versions.props` if the Zookeeper Operator moves.
+6. `make mod-tidy`, then `make install-dependencies`, then `make prepare`.
+7. `make test` and repair test fixtures. An API bump changes envtest 
behaviour, so expect fixtures,
+   not production code, to need updating.
+8. Add `artifacthub.io/changes` entries to **both** 
`helm/solr-operator/Chart.yaml` and
+   `helm/solr/Chart.yaml`.
+9. `make lint` (serially, not with `make -j`) and confirm both `git diff 
--exit-code` and
+   `git status --short` are clean.
+
+## What Dependabot owns, and what it does not
+
+Dependabot handles **GitHub Actions and the release wizard's Python 
requirements**
+(`.github/dependabot.yml`): one grouped PR a month for each. Actions are cheap 
to review, already
+pinned to commit SHAs, and need no follow-up work, so automating them costs 
nothing. The same goes
+for `hack/release/wizard/requirements.txt`: those packages are only used by 
the wizard itself, never
+built into the operator or its image, so a bump needs no regeneration, no 
license list and no
+changelog entry. Reviewing one is reading the diff and running the wizard once.
+
+It deliberately has **no `gomod` entry**, and Dependabot only runs the 
ecosystems a config lists, so
+leaving it out is the enforcement. A Go module bump is not self-contained 
here: Dependabot cannot run
+`make fetch-licenses-list`, so any bump that moves a transitive dependency's 
license URL fails
+`make check-licenses` until the list is regenerated by hand, and it cannot 
regenerate CRDs, repair
+the test fixtures a Kubernetes API bump invalidates, or write a changelog 
entry. The result would be
+a monthly stream of PRs each needing the same manual follow-up. A sweeping 
upgrade concentrates that
+work into one reviewed change, and the skill makes sure the easily forgotten 
parts — the license list
+and the chart changelogs — actually happen.
+
+There is no `docker` entry either. The only Dockerfile pins the Go builder 
image, which has to move
+in lockstep with the `go` directive in `go.mod` and the CI matrix, so it 
belongs to the sweep.
+
+The wizard's requirements are pinned exactly (`==`), so a bump is always a 
commit and never a silent
+resolution change. Only the packages the wizard imports are listed; pip 
resolves the rest.
+
+### Reviewing a Dependabot Actions PR
+
+The ASF allowlists third-party actions **by commit SHA**, in `actions.yml` in 
the
+`apache/infrastructure-actions` repository. A SHA that is not on that list 
fails the workflow at
+startup, before any step runs. Check a proposed SHA there before merging. This 
has already bitten the
+project once: commit `bbb5a3d` had to move `azure/setup-helm` *forward* to an 
INFRA-approved SHA.
+
+Of the actions in use only `azure/setup-helm` is third-party; the `actions/*` 
ones are first-party
+and not gated. If these PRs turn out to need that check too often, one 
`ignore:` entry in
+`.github/dependabot.yml` takes `azure/setup-helm` back to being a manual bump.
+
+## Finding out what has moved
+
+`make list-upgrades` (or `./hack/list-upgrades.sh` directly, when the 
Makefile's Go-version gate is in
+the way) reports everything in the skill's half of the split: direct Go 
modules with a newer version
+in the same major, modules with a whole new major available, the Makefile's 
pinned tool versions, and
+the envtest Kubernetes assets. It then lists the image tags and version 
strings that no tool can see,
+so they can be checked by hand.
+
+It reads the Go module proxy rather than scraping release pages, writes 
nothing, and always exits 0.
+It is deliberately not part of `make lint` or `make check`: it needs the 
network, and its output
+changes whenever an upstream project cuts a release.
+
+The major-version probe is the part worth knowing about. `go list -u` only 
reports updates within a
+module's current major, because under semantic import versioning a new major 
is a different module
+path and therefore not an update to the current one. Without an explicit probe 
for the successor
+path, a dependency can sit a whole major behind indefinitely without anything 
saying so.
diff --git a/hack/list-upgrades.sh b/hack/list-upgrades.sh
new file mode 100755
index 0000000..b91d004
--- /dev/null
+++ b/hack/list-upgrades.sh
@@ -0,0 +1,418 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# only exit with zero if all commands of the pipeline exit successfully
+set -o pipefail
+# error on unset variables
+set -u
+# Deliberately NO "set -e", unlike the other scripts in hack/. This one's
+# contract is that it prints every section and always exits 0. A sweep is
+# usually run from a tree where go.mod has just been hand-edited, so "go list"
+# failing is an expected state rather than an emergency - and the hand-check
+# list at the end needs no network, so it is exactly what is still useful
+# then. Failures are reported in place instead of aborting the run.
+
+# Report which dependencies have newer versions available. Read-only: this
+# script changes nothing, and always exits 0, so it can never fail a build.
+#
+# It covers what can be detected reproducibly from the Go module proxy, which
+# is the tier the dependency-upgrade skill owns. GitHub Actions are 
Dependabot's
+# (see .github/dependabot.yml) and are not reported here. The pins that no tool
+# can see are listed at the end as a reminder to check them by hand.
+#
+# Runnable either as "make list-upgrades" or directly as 
./hack/list-upgrades.sh.
+# The direct form matters: the Makefile refuses to run at all when the 
installed
+# Go toolchain does not match the "go" directive in go.mod, which is exactly 
the
+# situation you are in when you are about to bump it.
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SCRIPT_PATH="${SCRIPT_DIR}/$(basename "${BASH_SOURCE[0]}")"
+PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
+cd "${PROJECT_DIR}"
+
+# A stale, untracked vendor/ directory switches Go to -mod=vendor and makes
+# every command below fail with "inconsistent vendoring". Detection has to work
+# before that has been cleaned up, so override it for this script only.
+# readonly rather than mod: it escapes vendor mode just the same, but also
+# forbids Go from writing go.mod or go.sum, which "-mod=mod" does as a side
+# effect of resolving the upgrade candidates.
+export GOFLAGS="-mod=readonly"
+export GO111MODULE=on
+
+# Newest non-prerelease version from "go list -m -versions" output, or nothing.
+# Field 1 is the module path; the rest are versions, oldest first.
+newest_stable() {
+  tr ' ' '\n' <<<"$1" | tail -n +2 | awk '!/-/' | tail -1
+}
+
+# Resolve a path to the module that actually publishes versions, and echo it.
+# The Makefile installs some tools by package path (.../cmd/controller-gen)
+# rather than by module path, and only the module has versions, so walk up
+# until one does. Returns 1 if nothing on the way up is a versioned module.
+resolve_module_path() {
+  local path="$1" versions parent
+  while [[ "${path}" == *"/"* ]]; do
+    if versions="$(go list -m -versions "${path}" 2>/dev/null)" &&
+      [[ -n "$(newest_stable "${versions}")" ]]; then
+      printf '%s' "${path}"
+      return 0
+    fi
+    parent="${path%/*}"
+    [[ "${parent}" != *"/"* ]] && return 1
+    path="${parent}"
+  done
+  return 1
+}
+
+# The Go version go.mod pins. Resolve the toolchain target BEFORE judging any
+# other candidate: a dependency's newest release may require a newer Go than
+# this, in which case taking it silently pulls a second toolchain.
+GO_DIRECTIVE="$(grep -E '^go [0-9]' go.mod 2>/dev/null | awk '{print $2}' | 
head -1)"
+
+# Go version a specific module version requires, or nothing. One proxy call.
+go_requirement() {
+  go list -m -f '{{.GoVersion}}' "$1@$2" 2>/dev/null
+}
+
+# " [needs Go X.Y]" when $1@$2 requires a newer Go than go.mod pins, else "".
+go_req_note() {
+  local req
+  req="$(go_requirement "$1" "$2")"
+  if [[ -n "${req}" && -n "${GO_DIRECTIVE}" ]] && version_gt "${req}" 
"${GO_DIRECTIVE}"; then
+    printf '  [needs Go %s]' "${req}"
+  fi
+}
+
+# True when $1 is a strictly newer version than $2. Version-aware, so a
+# transient resolution failure or a retracted version can never present a
+# downgrade as an upgrade.
+version_gt() {
+  [[ "$1" != "$2" ]] && [[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail 
-1)" == "$1" ]]
+}
+
+# Emit "VAR|package path|pinned version" for each tool the Makefile installs.
+# Single source of truth for the two sections that need it. Versions derived
+# from go.mod with $(shell ...) follow their module automatically, and are
+# emitted with an empty version so callers can skip them.
+each_tool_pin() {
+  local line tool_path tool_var tool_def tool_version
+  while IFS= read -r line; do
+    [[ -z "${line}" ]] && continue
+    # $(call go-get-tool,$(BINARY),some.host/mod/path@$(SOME_VERSION))
+    if [[ "${line}" =~ 
go-get-tool,[^,]*,([^@]+)@\$\(([A-Z0-9_]+)\)\)[[:space:]]*$ ]]; then
+      tool_path="${BASH_REMATCH[1]}"
+      tool_var="${BASH_REMATCH[2]}"
+      tool_def="$(grep -E "^${tool_var} *[:?]?= *" Makefile | head -1 || true)"
+      if [[ "${tool_def}" == *'$(shell'* ]]; then
+        printf '%s|%s|\n' "${tool_var}" "${tool_path}"
+        continue
+      fi
+      tool_version="${tool_def#*=}"
+      printf '%s|%s|%s\n' "${tool_var}" "${tool_path}" "${tool_version// /}"
+    fi
+  done < <(grep -E '\$\(call go-get-tool,' Makefile || true)
+}
+
+# Report a command failure where it happened, rather than aborting the run.
+report_failure() {
+  local what="$1" consequence="$2" details="$3"
+  printf '  ERROR: %s failed.\n' "${what}"
+  if [[ -n "${details}" ]]; then
+    printf '%s\n' "${details}" | head -6 | sed 's/^/    /'
+  fi
+  printf '  %s\n' "${consequence}"
+}
+
+# Is the module proxy actually reachable? With GOPROXY=off, or no network,
+# "go list -m -u" still exits 0 and simply reports no updates available - a
+# clean bill of health that means nothing. Resolving a module we depend on
+# anyway distinguishes "nothing has moved" from "nothing could be checked".
+proxy_reachable() {
+  [[ -n "$(newest_stable "$(go list -m -versions 
sigs.k8s.io/controller-runtime 2>/dev/null)")" ]]
+}
+
+# --- internal re-entry, used to run the major probes in parallel through xargs
+if [[ "${1:-}" == "--probe-major" ]]; then
+  probe_path="$2"
+  probe_current="$3"
+
+  # Resolve to the real module first. Without this, a tool pinned by package
+  # path would be probed at .../cmd/controller-gen/v2, a path that can never
+  # exist, and a genuine major move would be reported as "none".
+  if ! probe_module="$(resolve_module_path "${probe_path}")"; then
+    printf '  %-50s %s\n' "${probe_path}" "LOOKUP FAILED - majors unchecked 
for this module"
+    exit 0
+  fi
+
+  # Split the resolved module path into its base and its current major. Paths
+  # without a /vN suffix are major 0 or 1, whose successor is /v2 either way.
+  if [[ "${probe_module}" =~ ^(.*)/v([0-9]+)$ ]]; then
+    probe_base="${BASH_REMATCH[1]}"
+    probe_major="${BASH_REMATCH[2]}"
+  else
+    probe_base="${probe_module}"
+    probe_major=1
+  fi
+
+  found_path=""
+  found_version=""
+  # Probing stops at the first major that does not exist. A project that
+  # skipped a major number outright would be missed, which is rare enough to
+  # live with and cheaper than probing a fixed range every time.
+  for ((n = probe_major + 1; n <= probe_major + 5; n++)); do
+    if ! versions="$(go list -m -versions "${probe_base}/v${n}" 2>/dev/null)"; 
then
+      # Non-zero means either "no such module" or "the lookup failed", and the
+      # exit code is 1 for both. Re-probe a path known to exist: if that now
+      # fails too, the proxy is the problem and this is an unchecked module,
+      # not a module without a successor.
+      if go list -m -versions "${probe_module}" >/dev/null 2>&1; then
+        break
+      fi
+      printf '  %-50s %s\n' "${probe_base}" "LOOKUP FAILED - majors unchecked 
for this module"
+      exit 0
+    fi
+    stable="$(newest_stable "${versions}")"
+    [[ -z "${stable}" ]] && break
+    found_path="${probe_base}/v${n}"
+    found_version="${stable}"
+  done
+
+  if [[ -n "${found_path}" ]]; then
+    printf '  %-50s %-28s -> %s %s%s\n' "${probe_base}" "${probe_current}" \
+      "${found_version}" "(${found_path})" "$(go_req_note "${found_path}" 
"${found_version}")"
+  fi
+  exit 0
+fi
+
+NETWORK_OK=true
+if ! proxy_reachable; then
+  NETWORK_OK=false
+  printf '\n  WARNING: the Go module proxy (GOPROXY=%s) could not be 
reached.\n' "$(go env GOPROXY)"
+  printf '  Everything below that needs the network is unchecked, not 
confirmed current.\n'
+fi
+
+# --- Go modules, within the current major ------------------------------------
+printf '\n=== Go modules (direct, within the current major) ===\n\n'
+modules_ok=true
+module_err="$(mktemp)"
+trap 'rm -f "${module_err}"' EXIT
+module_updates="$(go list -m -u -f '{{if and (not .Indirect) 
.Update}}{{.Path}} {{.Version}} {{.Update.Version}}{{end}}' all 
2>"${module_err}")"
+module_rc=$?
+if ((module_rc != 0)); then
+  modules_ok=false
+  report_failure '"go list -m -u all"' \
+    'Cannot report Go module updates. Fix go.mod, or check network access.' \
+    "$(cat "${module_err}")"
+else
+  module_updates="$(grep . <<<"${module_updates}" || true)"
+  if [[ -n "${module_updates}" ]]; then
+    while read -r m_path m_cur m_new; do
+      [[ -z "${m_path}" ]] && continue
+      printf '  %-50s %-28s -> %s%s\n' \
+        "${m_path}" "${m_cur}" "${m_new}" "$(go_req_note "${m_path}" 
"${m_new}")"
+    done <<<"${module_updates}"
+  elif [[ "${NETWORK_OK}" == true ]]; then
+    printf '  Everything is current.\n'
+  else
+    printf '  UNKNOWN - the proxy is unreachable, so no update could be 
seen.\n'
+  fi
+fi
+
+# --- new major versions 
-------------------------------------------------------
+printf '\n=== New MAJOR versions available (each one needs a decision) ===\n\n'
+# "go list -u" can never report these: under semantic import versioning a new
+# major is a different module path, so it is not an update to the current one.
+if [[ "${modules_ok}" != true ]]; then
+  printf '  SKIPPED - the module list could not be read.\n'
+else
+  probe_input="$(
+    go list -m -f '{{if and (not .Indirect) (not .Main)}}{{.Path}} 
{{.Version}}{{end}}' all 2>/dev/null |
+      # Keep only complete "path version" pairs. xargs -n 2 below splits on
+      # whitespace, so a line missing its version would consume the next line's
+      # path as its version and misalign every probe that follows.
+      awk 'NF == 2' |
+      # Pseudo-versions mean the module publishes no tags, so /vN is 
meaningless.
+      awk '$2 !~ /^v0\.0\.0-[0-9]{14}-/'
+  )"
+  # The tools the Makefile installs are modules too, and are just as capable of
+  # having moved a major (kustomize/v4 -> /v5, for instance).
+  while IFS='|' read -r _ tool_path tool_version; do
+    [[ -z "${tool_version}" ]] && continue
+    probe_input+=$'\n'"${tool_path} ${tool_version}"
+  done < <(each_tool_pin)
+
+  # Invoked through "bash" rather than directly: a missing execute bit would
+  # otherwise make every probe fail and silently report "None.", which is the
+  # worst possible outcome for the one check nothing else performs.
+  major_output="$(printf '%s\n' "${probe_input}" | awk 'NF == 2' |
+    xargs -P 8 -n 2 bash "${SCRIPT_PATH}" --probe-major 2>/dev/null | sort)"
+  major_rc=$?
+  if ((major_rc != 0)); then
+    report_failure 'the major-version probe' \
+      'Majors could not be checked. Re-run, or check network access.' ''
+  elif [[ -n "${major_output}" ]]; then
+    printf '%s\n' "${major_output}"
+    printf '\n  A new major is never taken automatically. Assess the migration 
cost\n'
+    printf '  and put the choice to the user - see the skill for what to 
present.\n'
+  elif [[ "${NETWORK_OK}" == true ]]; then
+    printf '  None.\n'
+  else
+    printf '  UNKNOWN - the proxy is unreachable, so no major could be seen.\n'
+  fi
+fi
+
+# --- Makefile tool pins 
-------------------------------------------------------
+printf '\n=== Makefile tool pins ===\n\n'
+while IFS='|' read -r tool_var tool_path tool_version; do
+  if [[ -z "${tool_version}" ]]; then
+    printf '  %-26s %-12s derived from go.mod, nothing to do\n' "${tool_var}" 
""
+    continue
+  fi
+  tool_module="$(resolve_module_path "${tool_path}")"
+  if [[ -z "${tool_module}" ]]; then
+    printf '  %-26s %-12s (could not resolve %s)\n' "${tool_var}" 
"${tool_version}" "${tool_path}"
+    continue
+  fi
+  # Never compare across a major boundary. Walking .../yq/v4 up to .../yq would
+  # otherwise offer the newest v3 as an "upgrade" from a v4 pin.
+  if [[ "${tool_path}" =~ /v[0-9]+$ && "${tool_module}" != "${tool_path}" ]]; 
then
+    printf '  %-26s %-12s (only %s resolved; majors are reported above)\n' \
+      "${tool_var}" "${tool_version}" "${tool_module}"
+    continue
+  fi
+  tool_latest="$(newest_stable "$(go list -m -versions "${tool_module}" 
2>/dev/null)")"
+  if [[ -z "${tool_latest}" ]]; then
+    printf '  %-26s %-12s (could not resolve %s)\n' "${tool_var}" 
"${tool_version}" "${tool_path}"
+  elif version_gt "${tool_latest}" "${tool_version}"; then
+    printf '  %-26s %-12s -> %s%s\n' "${tool_var}" "${tool_version}" 
"${tool_latest}" \
+      "$(go_req_note "${tool_module}" "${tool_latest}")"
+  else
+    printf '  %-26s %-12s current\n' "${tool_var}" "${tool_version}"
+  fi
+done < <(each_tool_pin)
+
+# --- envtest assets 
-----------------------------------------------------------
+printf '\n=== envtest Kubernetes assets ===\n\n'
+ENVTEST_PINNED="$(grep -E '^ENVTEST_K8S_VERSION' Makefile | head -1 | sed -E 
's/.*= *//' | tr -d ' ' || true)"
+SETUP_ENVTEST="${PROJECT_DIR}/bin/setup-envtest"
+if [[ -z "${ENVTEST_PINNED}" ]]; then
+  printf '  %-26s %s\n' "ENVTEST_K8S_VERSION" "not found in the Makefile - has 
it been renamed?"
+elif [[ ! -x "${SETUP_ENVTEST}" ]]; then
+  printf '  %-26s %-12s (run "make install-dependencies" to check for newer 
assets)\n' \
+    "ENVTEST_K8S_VERSION" "${ENVTEST_PINNED}"
+else
+  # Asset versions are plain x.y.z, and the list is not ordered newest-first.
+  envtest_raw="$("${SETUP_ENVTEST}" list 2>/dev/null)"
+  envtest_rc=$?
+  envtest_latest="$(grep -o -E '[0-9]+\.[0-9]+\.[0-9]+' <<<"${envtest_raw}" | 
sort -V -u | tail -1)"
+  if ((envtest_rc != 0)) || [[ -z "${envtest_latest}" ]]; then
+    # An empty or failed listing is not evidence the pin is current.
+    printf '  %-26s %-12s UNCHECKED - "setup-envtest list" returned nothing\n' 
\
+      "ENVTEST_K8S_VERSION" "${ENVTEST_PINNED}"
+  elif version_gt "${envtest_latest}" "${ENVTEST_PINNED}"; then
+    printf '  %-26s %-12s -> %s\n' "ENVTEST_K8S_VERSION" "${ENVTEST_PINNED}" 
"${envtest_latest}"
+  else
+    printf '  %-26s %-12s current\n' "ENVTEST_K8S_VERSION" "${ENVTEST_PINNED}"
+  fi
+fi
+
+# --- Kubernetes support contract 
----------------------------------------------
+printf '\n=== Kubernetes support contract ===\n\n'
+# The declared minimum is NOT a dependency: it is a support promise that Helm
+# enforces at install time, and nothing links it to the libraries. A sweep can
+# therefore never raise it by accident - but it can quietly leave it behind,
+# which is why all four numbers are printed side by side rather than checked.
+k8s_lib="$(grep -E '^[[:space:]]+k8s\.io/api ' go.mod | awk '{print $2}' | 
head -1)"
+k8s_lib_release=""
+[[ "${k8s_lib}" =~ ^v0\.([0-9]+)\. ]] && k8s_lib_release="1.${BASH_REMATCH[1]}"
+declared_operator="$(grep -E '^kubeVersion' helm/solr-operator/Chart.yaml 
2>/dev/null | sed -E 's/.*"(.*)".*/\1/' || true)"
+declared_solr="$(grep -E '^kubeVersion' helm/solr/Chart.yaml 2>/dev/null | sed 
-E 's/.*"(.*)".*/\1/' || true)"
+oldest_tested="$(grep -hoE 'KUBERNETES_VERSION="v[0-9]+\.[0-9]+\.[0-9]+"' \
+  tests/scripts/manage_e2e_tests.sh hack/release/smoke_test/test_cluster.sh 
2>/dev/null |
+  grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | sort -V | head -1 || true)"
+
+printf '  %-34s %s\n' "declared minimum (both charts)" 
"${declared_operator:-not found}"
+if [[ "${declared_operator}" != "${declared_solr}" ]]; then
+  printf '  %-34s %s\n' "  MISMATCH, helm/solr says" "${declared_solr:-not 
found}"
+fi
+printf '  %-34s %s\n' "k8s.io/* libraries built against" "${k8s_lib:-not 
found}${k8s_lib_release:+ (Kubernetes ${k8s_lib_release})}"
+printf '  %-34s %s\n' "envtest API server" "${ENVTEST_PINNED:-not found}"
+printf '  %-34s %s\n' "oldest Kubernetes actually tested" 
"${oldest_tested:-not found}"
+printf '\n  Raising the declared minimum is a breaking change for users and is 
NOT\n'
+printf '  part of a sweep. Leave kubeVersion and the compatibility matrix 
alone;\n'
+printf '  if the gap above has grown too wide, that is its own PR against a\n'
+printf '  release. See the skill.\n'
+
+# --- pins nothing watches 
-----------------------------------------------------
+printf '\n=== Not detectable automatically - check these by hand ===\n\n'
+
+# Image tags and version strings that no tool watches. Located by pattern at
+# run time rather than by line number, so an edit above them cannot silently
+# make this list point at the wrong line. A pattern that stops matching is
+# reported, because that means the pin moved or the file was renamed and this
+# table needs updating.
+#
+# Format: description|file|extended regex
+while IFS='|' read -r pin_desc pin_file pin_re; do
+  [[ -z "${pin_desc}" ]] && continue
+  if [[ ! -f "${pin_file}" ]]; then
+    printf '  %-46s %s\n' "MISSING FILE - update this list" "${pin_file}"
+    continue
+  fi
+  pin_match="$(grep -nE "${pin_re}" "${pin_file}" 2>/dev/null | head -1 || 
true)"
+  if [[ -z "${pin_match}" ]]; then
+    printf '  %-46s %s\n' "PATTERN NOT FOUND - update this list" "${pin_file}"
+    continue
+  fi
+  pin_line="${pin_match%%:*}"
+  # Strip the line number, then leading and trailing whitespace.
+  pin_text="${pin_match#*:}"
+  pin_text="$(printf '%s' "${pin_text}" | sed -E 's/^[[:space:]]+//; 
s/[[:space:]]+$//')"
+  # Some matches are whole command lines; keep the column readable.
+  if ((${#pin_text} > 72)); then
+    pin_text="${pin_text:0:69}..."
+  fi
+  printf '  %-46s %s\n' "${pin_file}:${pin_line}" "${pin_text}"
+  printf '  %-46s %s\n' "" "${pin_desc}"
+done <<'PINS'
+must move together with go.mod and the CI matrix|build/Dockerfile|^FROM golang:
+a floating tag, no digest|build/Dockerfile|^FROM gcr.io/distroless
+the docs toolchain|Makefile|^ANTORA_IMAGE
+untagged, so "make lint" pulls :latest|Makefile|artifacthub/ah
+the Zookeeper server image|tests/scripts/manage_e2e_tests.sh|pravega/zookeeper:
+Kubernetes for the e2e kind 
cluster|tests/scripts/manage_e2e_tests.sh|KUBERNETES_VERSION="v[0-9]
+Kubernetes for the smoke-test kind 
cluster|hack/release/smoke_test/test_cluster.sh|KUBERNETES_VERSION="v[0-9]
+a second kind pin, independent of the 
Makefile|hack/release/smoke_test/test_cluster.sh|sigs.k8s.io/kind@
+left over from kubebuilder 
scaffolding|config/default/manager_auth_proxy_patch.yaml|kube-rbac-proxy:
+PINS
+
+for pin_file in docs/modules/upgrade-notes/pages/upgrade-notes.adoc; do
+  if [[ -f "${pin_file}" ]]; then
+    printf '  %-46s %s\n' "${pin_file}" "several pins, read the file"
+  else
+    printf '  %-46s %s\n' "MISSING FILE - update this list" "${pin_file}"
+  fi
+done
+
+cat <<'EOF'
+
+  GitHub Actions and the release wizard's requirements.txt are Dependabot's, 
not
+  this script's. See .github/dependabot.yml. The Solr and operator image
+  versions belong to the release process, not to a dependency sweep.
+
+EOF
+
+exit 0
diff --git a/hack/release/artifacts/bundle_source.sh 
b/hack/release/artifacts/bundle_source.sh
index 0800c60..e2085f0 100755
--- a/hack/release/artifacts/bundle_source.sh
+++ b/hack/release/artifacts/bundle_source.sh
@@ -80,5 +80,8 @@ COPYFILE_DISABLE=true "${TAR}" --exclude-vcs 
--exclude-vcs-ignores \
   --exclude .asf.yaml \
   --exclude .github \
   --exclude .run \
+  --exclude .agents \
+  --exclude AGENTS.md \
+  --exclude .claude \
   --transform "s,^,solr-operator-${VERSION}/," \
   -czf "${ARTIFACTS_DIR}/solr-operator-${VERSION}.tgz" .

Reply via email to