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" .