⚠️ Work in Progress
You are viewing development documentation built from the latest commit on main. APIs and features are subject to change.

Dependency Updates

Dependency Updates Guide

This guide covers the process for updating Kure’s dependencies, including version tracking, risk assessment, and coordinated upgrades.

Version Management Overview

Kure tracks dependency versions across the following files:

FilePurpose
go.modGo module dependencies — authoritative for the build version (the pin)
versions.yamlVersion metadata: supported range, notes (no build version)
docs/compatibility.mdGenerated from versions.yaml + go.mod — never edit directly
pkg/versions/versions_gen.goGenerated from versions.yaml — never edit directly; run mise run versions:generate
renovate.jsonBot policy: update gates and caps, on top of the shared go-kure/.github preset

The sync-versions.sh check command performs six assertions. It runs in CI’s validate job, which is gated on the go paths-filter in .github/workflows/ci.yml — that filter includes versions.yaml, docs/compatibility.md and scripts/sync-versions.sh precisely so a version-metadata-only PR cannot skip it:

  1. Range — each dependency’s build version, read from go.mod, falls within the supported_range declared in versions.yaml. For a dependency pinned to a Go pseudo-version (no semver tags upstream) that also declares upstream_release (see “Release-pinned dependencies” below), the declared release is substituted for the range check instead of being skipped — supported_range is enforced for these too. Skipped entirely for a dependency declaring floor_module (see “MVS-floor dependencies” below) — guard 6 already enforces the invariant that actually applies to it. Each bound of supported_range must be exactly major.minor ("1.26 - 1.28" or "1.26"): check, generate and widen all reject a malformed one such as "1.x - 3.x" rather than compare it or publish it as pkg/versions Min/Max metadata.
  2. go.mod pin comment — the // Current pin: vX.Y.Z (Kubernetes 1.N) comment above the k8s.io/api replace directive matches that directive’s actual version. A drifted comment used to be caught only by AI review, on every single Kubernetes bump.
  3. No raw commit SHAs in versions.yaml notes — a notes: block may not contain a bare 40- or 12-hex commit SHA literal. When a dependency has no semver tags (e.g. external-secrets, pinned to a go.mod pseudo-version), reference “the pseudo-version pinned in go.mod” instead of writing out the commit by hand — the literal drifts silently the next time the pin moves. This is not a ban on patch-level semver in justification prose (e.g. metallb’s “v0.16.0 is a minor release…” stays fine). upstream_release/upstream_release_commit (below) are structured fields, not prose, so this ban does not apply to them.
  4. Drift — the committed docs/compatibility.md is byte-identical to what generate would produce right now. It regenerates into a temp file and diffs; your working tree is never touched. On failure it prints the diff (- is what is committed, + is what versions.yaml + go.mod currently imply) and tells you to run generate.
  5. Go API drift — the committed pkg/versions/versions_gen.go is byte-identical to what generate_go_api would produce right now. Same regenerate-into-a-temp-file-and-diff shape as item 4 above.
  6. MVS-floor equality — for a dependency declaring floor_module (see “MVS-floor dependencies” below), the go.mod pin exactly equals what floor_module’s own go.mod currently requires. Degrades to a warning when Go or the module cache is unavailable (offline dev machine); a reachable mismatch is always an error.

Release-pinned dependencies (untagged Go submodules)

Some upstream Go modules version their CRD types in a submodule (e.g. external-secrets' /apis) that upstream never tags. Go can then only express the dependency as a pseudo-version, and @latest resolves to upstream main HEAD — every upstream commit looks like a new version, and supported_range is unverifiable and unenforceable (there is no semver to compare).

Such a dependency’s versions.yaml entry adds structured fields to pin it to a named release instead of main HEAD:

external-secrets:
  go_module: "github.com/external-secrets/external-secrets/apis"
  upstream_repo: "external-secrets/external-secrets"
  upstream_release: "vX.Y.Z"          # the upstream tag this pin tracks
  upstream_release_commit: "<40-char commit that tag resolves to>"
  supported_range: "X.Y"

(Values shown as placeholders deliberately — sync-eso-pin.sh only rewrites versions.yaml and regenerates docs/compatibility.md, never this guide, so a literal example version here would go stale the next time Renovate advances the pin. See the live entry in versions.yaml for the current values.)

  • upstream_release — the upstream tag the pin tracks.
  • upstream_release_commit — the full commit that tag resolves to. This is what makes the check work offline: sync-versions.sh check asserts that go.mod’s pseudo-version’s embedded 12-char commit digest is a prefix of this field, on every run, with no network access. If the pin ever drifted off the declared release (hand edit, bad rebase, …), this assertion fails — the actual drift guard, separate from and in addition to the range check.
  • upstream_repo — the owner/repo the tag lives in. Used only by the second, best-effort check below; a dependency with no upstream_repo skips it and keeps only the offline digest check.
  • supported_range is then enforced for the dependency for the first time, against upstream_release rather than being skipped as unparseable.

The offline digest check proves go.mod matches upstream_release_commit, but not that upstream_release_commit is actually the commit upstream_release names upstream — a hand edit that bumps upstream_release (and supported_range) without re-running sync-eso-pin.sh would satisfy it while upstream_release_commit (and go.mod) stay on the old release. sync-versions.sh check closes that gap with a second, best-effort online check: it resolves upstream_repo@upstream_release live via the GitHub API (falling back to git ls-remote, same resolution as sync-eso-pin.sh below, including the annotated-vs-lightweight tag peel) and compares it to upstream_release_commit. It distinguishes three outcomes, not two — collapsing “server unreachable” and “tag definitively doesn’t exist” into one signal would let a typo’d upstream_release downgrade to a mere warning:

  • resolved and matches upstream_release_commit — pass.
  • resolved and differs — error: the field pair is stale independent of go.mod.
  • a reachable server confirms the tag does not exist (API 404, or git ls-remote enumerates the repo’s refs with none matching) — error: likely a typo in upstream_release, not a network problem, so must not be downgraded to a warning.
  • neither path could reach the server at all (offline dev machine, DNS/connect failure, GitHub rate limit) — warning only. The offline digest check above already covers the common case, and this must never break an otherwise-valid offline run.

scripts/sync-eso-pin.sh re-pins such a dependency: it reads upstream_release, resolves the tag to a commit via the GitHub API (falling back to git ls-remote), go gets that exact commit (never hand-constructs the pseudo-version string — Go verifies its embedded timestamp against the commit), runs go mod tidy, writes the resolved commit back to upstream_release_commit, and regenerates docs/compatibility.md and the API tables (gen-builders.sh generate — the tables record the module version each kind and field came from, so re-pinning this module changes them). It is idempotent: a re-run with upstream_release unchanged leaves the tree untouched. renovate.json disables the raw gomod manager for this module (its @latest is main HEAD, not useful) and instead tracks upstream_release via a customManagers regex entry on versions.yaml, filtered to plain vX.Y.Z release tags (upstream also cuts non-matching helm-chart-X.Y.Z tags), wired to run sync-eso-pin.sh as a postUpgradeTasks command — so a PR opens when upstream cuts a release, rather than on every upstream commit.

Renovate’s image ships no Go, and it installs Go on demand only while its gomod manager runs. This dependency arrives through the regex manager, so nothing has put go on PATH when the command starts; sync-eso-pin.sh therefore installs the toolchain that go.mod’s go directive names, through containerbase’s install-tool, when go is missing and install-tool exists (a developer machine has go and no install-tool, so it is a no-op there). Before that step existed, the command exited 127 at go get and the bot’s branch carried only the one-line upstream_release edit — the upstream_release_commit mismatch then failed sync-versions.sh check in CI.

With the re-pin done, a patch release arrives re-pinned and range-checked. A minor release still arrives red: supported_range is a compatibility judgement no bot can make, so a maintainer widens it (and adds the notes sentence) on the PR branch before merging.

generate regenerates docs/compatibility.md and the go.mod pin comment in place. There is no other hand-maintained “current” version to keep in sync (see #593 ).

The drift assertion exists because the matrix is generated but committed: before it, any bump that moved a build version or a supported_range left the committed matrix silently stale until someone happened to re-run generate, and the staleness was caught only by human review — on three separate dependency PRs.

MVS-floor dependencies (tagged upstream, but never chosen directly)

A different shape of pseudo-version problem: the upstream repo does cut releases, but nothing in kure ever picks one — the pin is set by Go’s minimum-version selection from a different dependency’s go.mod. github.com/cloudnative-pg/barman-cloud is the example: kure imports it directly, but github.com/cloudnative-pg/plugin-barman-cloud also requires it, and the version that requirement lands on — a tag, or a pseudo-version past barman-cloud’s last real tag — is whatever the plugin’s own go.mod says, not kure’s choice. Either way, a hand-picked tag that Go’s minimum-version selection would then have to override is a downgrade the build cannot use.

This needs no release-tracking machinery, because there is no release to track — the pin should simply always equal the floor:

barman-cloud:
  go_module: "github.com/cloudnative-pg/barman-cloud"
  floor_module: "github.com/cloudnative-pg/plugin-barman-cloud"
  # No supported_range -- see below.
  • The pin itself is derived, never hand-written: go mod edit -droprequire=<module> && go mod tidy re-computes it from whatever the floor-setting dependency currently requires. Re-run this whenever the floor-setting dependency (here, plugin-barman-cloud) bumps.
  • sync-versions.sh check mechanically asserts this: with floor_module set, it reads floor_module’s own go.mod and asserts it requires go_module at exactly the pin’s version. It degrades to a warning when Go or the module cache is unavailable (offline dev machine), since that is “could not check,” not “checked and it’s wrong” — but never when the check actually runs and finds a mismatch. A plugin downgrade will legitimately fail this guard until the pin is re-derived with the -droprequire + tidy recipe above.
  • validate_gomod’s own major.minor range check is skipped entirely for any entry declaring floor_module (scripts/sync-versions.sh, right after the module lookup, before the pseudo-version branch), printing <dep>: range check not applicable (pin vX.Y derived from <floor_module>'s own go.mod, ...) instead of a pass/fail line. A supported_range here can only ever demand a forced-yes edit — refusing to widen it means refusing the bump that raised the floor, which is not kure’s choice to refuse. Declaring supported_range on a floor_module entry anyway is harmless but pointless: it is never read. docs/compatibility.md’s generated table renders derived from <floor_module> in the Deployment Compatibility column for these entries instead of a range (see generate_docs). This was originally a range-membership check applied by accident — the loop iterates every .infrastructure key with no exemption for floor deps — not a deliberate gate on this class of dependency; see go-kure/kure#703 , which asked for the floor-equality check above to be added (it was: this is that check). Removing the accidental range check on the same entry was the other half. Caveats this creates, not papered over: barman-cloud is directly imported (pkg/kubernetes/cnpg/create.go, pkg/kubernetes/cnpg/update.go), so an incompatible upstream API change still surfaces — as a test compile failure, not a lint range error, which is the correct detector for a types-only module that is never itself deployed. And because the floor-equality guard above degrades to a warning offline, a floor_module dependency is entirely unguarded on an offline dev machine (both CI checks always run there).
  • renovate.json disables the standalone gomod bump for the module (matchPackageNames + enabled: false, same shape as the release-pinned rule above). The shared preset’s postUpdateOptions: ["gomodTidy"] re-raises the require to the new floor automatically whenever the floor-setting dependency’s own PR runs go mod tidy — so the pin still moves, inside that dependency’s own reviewable PR, never on its own.
  • Without the versions.yaml entry, validate_gomod()’s loop — keyed on .infrastructure keys — never visits the module at all: not skipped, not warned, invisible. That silence is what let barman-cloud digest-bump unreviewed for months before this pattern existed (the shared preset’s go-patch group automerges gomod patch/digest bumps).

Testing the guards

scripts/sync-versions.sh has six guards (validate_gomod, validate_gomod_pin_comment, validate_no_sha_in_notes, validate_mvs_floors, validate_docs_drift, validate_go_api_drift), and CI only ever runs them against a repo state where they all pass. Nothing proves a guard actually fails when it should — a guard that stops guarding (a dropped flag, a loosened regex, a silently-skipped branch) looks identical to one still doing its job. scripts/test/ closes that gap with a hermetic, network-free mutation-matrix harness: each scripts/test/cases/*.sh file builds a synthetic fixture tree, sets up exactly one scenario — a fixture mutation, a stub go/curl/git mode, or the unmutated baseline — runs sync-versions.sh, and asserts both the exit code and the specific message the guard must emit (assert_err_contains for a failure case, assert_out_contains for one that must stay green).

Run it locally with:

mise run versions:test

It’s part of mise run verify and runs in CI as its own step, gated on scripts/test/**/scripts/sync-versions.sh/versions.yaml/docs/compatibility.md changes (same path filter as sync-versions.sh check itself).

The same harness also covers scripts/sync-flux-operator-pin.sh (the flux-operator-pin cases, using new_pin_fixture/run_pin and the stub curl in scripts/test/fixtures/stub-pin/): that script is a Renovate postUpgradeTasks command with no other coverage, and its contract is failure-shaped — leave the tree untouched on any bad input.

Adding a case: every case follows the same shape — see scripts/test/lib.sh for the full helper reference (new_fixture, yq_set, gomod_sub, run_check/run_generate, the assert_* family, with_stub_go/with_stub_net):

source "$(dirname "${BASH_SOURCE[0]}")/../lib.sh"

new_fixture
yq_set '.infrastructure.<dep>.<field>' '<mutated value>'   # or gomod_sub for go.mod
run_check
assert_rc 1
assert_err_contains '<the exact error substring the guard must emit>'

A new guard lands with a case proving it fails. A guard with no case exercising its failure path is, by this harness’s own standard, unproven — the whole point is that CI should never again be able to report green while a guard silently stopped guarding.

Portable-environment prerequisites (timeout, mktemp). The harness itself needs no network. Without a timeout/gtimeout binary on PATH, sync-versions.sh check runs its two bounded probes (the MVS-floor go list lookup and the tag-resolution git ls-remote fallback) unbounded instead, and says so once at startup — generate never reaches either probe, so it is unaffected either way. This is production-runtime graceful degradation, not a hard requirement — see DEVELOPMENT.md’s Prerequisites block for the full split.

That degradation does not extend to the test harness itself: pre-existing case 09-mvs-floor-hang-timeout.sh and this PR’s own 36-timeout-present-no-warning.sh both need a real timeout/gtimeout on the machine running the test suite to pass — this is a test-harness-only prerequisite, narrower than and separate from production’s graceful degradation, where a real stalled module proxy or git remote has no such bound.

For case 09: it stubs go list to sleep 5s (faking a hang) and asserts SYNC_VERSIONS_PROBE_TIMEOUT bounds it to ~1s. On a host with neither binary, the probe isn’t bounded, so the stub’s 5s sleep runs to completion and go list “succeeds” (echoing a go.mod path) — but the case does not then fail on its warning-text assertion. The unbounded success reaches step 4 (go mod edit -json against that path), which the same stub fails for every mode but ok/norequire/editfail; validate_mvs_floors reports this as a hard error (“could not read … go.mod requirements … rc=3”), not the “could not resolve … skipping” warning the case asserts, so check itself exits 1. The case therefore fails on its earlier assert_rc 0, not on the warning-text assertion, in ~6-11s (the 5s sleep plus the rest of check’s own guards) — confirmed by direct reproduction, not by hanging indefinitely. For case 36: with no real timeout/gtimeout, resolve_timeout_bin cannot resolve TIMEOUT_BIN and emits its startup warning, which is exactly the text 36-timeout-present-no-warning.sh asserts is absent — so the case fails outright rather than skipping.

mise registry maps coreutils to aqua:uutils/coreutils; checked directly against a local install, that backend provisions one multi-call coreutils binary (a timeout applet is reachable only via coreutils timeout ...), never a standalone binary literally named timeout — so it is not pinned in mise.toml for this. yq remains the only externally-required tool mise provisions for this script.

Widening a supported_range

When sync-versions.sh check fails a dependency for being outside supported_range, and you’ve confirmed the new version is actually API-compatible, widen automates the mechanical edit to versions.yaml — raising supported_range and adding your --note to notes — while leaving the judgment (is it actually compatible?) entirely to the note you write. It does not itself regenerate docs/compatibility.md or pkg/versions/versions_gen.go; run generate afterward as a separate step:

./scripts/sync-versions.sh widen <dep> <new-upper-major.minor> --note "<compatibility assessment>"
./scripts/sync-versions.sh generate

The note is appended to the existing notes as a new line, and the block is written as a | literal block. notes is the audit trail for the whole declared range, accumulated one widen at a time: a widen that replaced it would delete the reasons for the part of the range that still stands. Write the note about the new version only. To replace the notes instead, pass --replace-notes explicitly. If the existing notes is not a plain string, the default append refuses and leaves versions.yaml untouched rather than overwrite it.

It refuses:

  • a dependency that isn’t in .infrastructure
  • a floor_module entry (its range is never checked — see below — so there’s nothing to widen; drop supported_range instead if it still has a stale one)
  • a --note containing a | (would break the Markdown table cell it’s rendered into in docs/compatibility.md) or a raw 12- or 40-character commit SHA (the same restriction check’s validate_no_sha_in_notes guard enforces — see above). Quotes and backslashes are fine: the note is passed to yq as data, not spliced into its expression
  • an entry with no supported_range declared yet (add one by hand first)
  • a new upper bound that isn’t exactly major.minor (e.g. 2.1.0 is rejected — matching supported_range’s own format)
  • a new upper bound that is not strictly above the current upper bound — widen only ever raises the ceiling; it never narrows a range (e.g. widening 1.5 - 1.7 to 1.6 would silently drop declared support for 1.7) and never touches the floor

A pin that falls below the declared lower bound is a different situation — the pin moved backward for some reason widen can’t diagnose — and check’s error says so explicitly without suggesting a widen command, since one would only be refused.

Update Risk Levels

Patch Updates (Low Risk)

Patch bumps (e.g., v1.5.0 → v1.5.1) contain bug fixes only.

go get <module>@v<new-version>
go mod tidy

No versions.yaml change is needed for an in-range patch — the build version comes from go.mod, and sync-versions.sh check passes as long as the new version stays within supported_range. (This is what lets in-range Renovate patch bumps go green untouched.)

Minor Updates (Medium Risk)

Minor bumps (e.g., v1.19 → v1.20) may add new APIs or deprecate existing ones.

  1. Review the upstream changelog for breaking changes
  2. Check if Kure uses any deprecated APIs
  3. Update go.mod; if the new version lands outside supported_range, widen the range and update notes in versions.yaml (only after confirming API compatibility)
  4. Run make verify to catch compile-time breakage

Major Updates (High Risk)

Major bumps (e.g., v1 → v2) likely have breaking API changes.

  1. Review the migration guide thoroughly
  2. Assess impact on all callers (check with grep -r for imports)
  3. Update code to use new APIs
  4. Update versions.yaml and documentation
  5. Consider impact on external consumers (see AGENTS.md § Consumer Compatibility)

Coordinated Upgrade Rules

Some dependencies must be upgraded together to avoid version conflicts.

Flux Ecosystem

All github.com/fluxcd/* packages must be upgraded together. Flux releases coordinate versions across:

  • flux2/v2
  • helm-controller/api
  • kustomize-controller/api
  • notification-controller/api
  • source-controller/api
  • image-automation-controller/api
  • image-reflector-controller/api
  • pkg/apis/meta, pkg/apis/kustomize

Renovate enforces this with the fluxcd group in the shared go-kure/.github preset, which covers github.com/fluxcd/* and github.com/controlplaneio-fluxcd/*. Ungrouped, one upstream release arrived as five separate PRs whose go.mod changes conflicted with each other in the merge queue and had to be consolidated by hand. Flux minors are additionally dashboard-gated by renovate.json (majors are gated org-wide by the preset), so the group only ever carries patches unless a pending minor is deliberately approved on the dashboard.

The vendored flux-operator install bundle

pkg/stack/fluxcd embeds the upstream install manifest (flux_operator_install.yaml) and names its release in FluxOperatorVersion, so the FluxInstance type kure compiles against and the operator it installs stay in lockstep. A flux-operator bump in go.mod therefore has three companion edits: the bundle, the constant, and the “currently vX.Y.Z” mention in pkg/stack/fluxcd/README.md.

scripts/sync-flux-operator-pin.sh makes all three. It reads the module version from go.mod, downloads that release’s install.yaml asset, validates it looks like an install bundle, and only then writes anything — so a failed download, an empty or non-bundle response, or a rewrite target that is missing or ambiguous leaves the tree untouched rather than half-applied. It then stages all three files before renaming any of them into place, so a failed write cannot leave a new bundle beside an old constant. It refuses a go.mod pin that is not a plain vX.Y.Z tag (a pseudo-version has no release asset). It is idempotent, and exits before any network call when the constant and README already name the go.mod version, because Renovate runs it after every gomod/mise bump and most have nothing to do with flux-operator. That also means it does not overwrite a hand-edited bundle that is already at the right version; to force a refresh, follow the manual procedure in the FluxOperatorVersion doc comment.

It is wired as a command of the shared gomod/mise rule’s postUpgradeTasks in renovate.json, not as its own rule (postUpgradeTasks has no merge semantics, so a second matching rule would replace the command list), and the three paths it writes are in that rule’s fileFilters — an unlisted path is dropped from the bot’s commit silently. Run it by hand after bumping the module yourself:

./scripts/sync-flux-operator-pin.sh

What this does not automate: the supported_range widen. It records a compatibility assessment (Widening a supported_range ), so a flux-operator minor beyond the declared range still arrives red on lint until someone runs sync-versions.sh widen. The bundle re-vendor is the mechanical part; the widen is the judgment.

The vendored gotk manifests base

gotk bootstrap mode builds the Flux controllers from the flux2 release’s install manifests base, embedded as internal/gotk/manifests.tar.gz and named in gotk.Version (re-exported as pkg/stack/fluxcd.GotkVersion), so generation with FluxVersion empty or naming that release downloads nothing (go-kure/kure#794); any other FluxVersion is an explicit opt-in to the upstream download. The same bundle is where the test suite reads the Flux toolkit CustomResourceDefinitions it validates kure’s Flux output against (internal/kuretest), so a bump also moves the schemas the fixtures are held to. A flux2/v2 bump in go.mod therefore has three companion edits: the tarball (the release’s manifests.tar.gz asset), the constant, and the “currently vX.Y.Z” mention in pkg/stack/fluxcd/README.md; the gotk.Version doc comment lists the steps. No script makes them yet, so the Flux group’s bump PR fails TestVendoredPinsMatchGoMod until they are made: the test compares GotkVersion and FluxOperatorVersion with go.mod, and each controller image in the vendored gotk bundle that has a controller API module with that module’s version. A Flux release moves those images, so a bundle left on the old release fails; it does not fingerprint the archive, so a bump that leaves every checked controller unchanged still relies on the refresh procedure.

Kubernetes (k8s.io/*)

All k8s.io/ packages must stay at the same patch release. Kure uses replace directives in go.mod to enforce this. See the comment block in go.mod for details.

k8s.io/kube-aggregator is one of them. Kure requires it for the APIService type alone and imports only its API package (pkg/apis/apiregistration/v1), so the aggregator’s server dependencies stay out of go.mod. It is pinned in the same replace block and moves with the other k8s.io/ packages in the kubernetes update group.

When can replace directives be removed? Only when ALL direct and transitive dependencies converge on the same k8s.io/ minor version. Check with:

go mod graph | grep 'k8s.io/' | awk '{print $2}' | sort -u

CNPG Ecosystem

cloudnative-pg, barman-cloud, machinery, and plugin-barman-cloud are related but versioned independently. Check compatibility notes in versions.yaml before upgrading.

Kustomize (sigs.k8s.io/kustomize/*)

sigs.k8s.io/kustomize/api and sigs.k8s.io/kustomize/kyaml are direct requirements for two reasons. The pkg/stack/fluxcd tests build every written tree with the real kustomize library (krusty), to prove each Flux spec.path names a directory kustomize can build. And pkg/stack/fluxcd uses kustomize’s own patch-target matching at runtime (kustomize/api/types selectors, kyaml/resid effective namespaces) to refuse a patch in a shared directory that would reach another bundle’s objects. A bump can therefore change which patches kure refuses: keep the two on the same version, and review a bump against the patch-scope tests (TestGenerateFromLayout_Patch*) as well as the written-tree builds.

Vendored go-kure/.github Files

.github/workflows/ci.yml’s forbidden-terms job byte-compares three vendored copies against their canonical files in go-kure/.github: site/scripts/check-forbidden-terms.sh (from scripts/check-forbidden-terms.sh), the release guide docs/releasing.md (from standards/release-process.md) and the shared workflows guide docs/shared-workflows.md (from standards/github-workflows.md). All three are checked out at a ref the job resolves from the check-forbidden-terms action’s own uses:@<sha> pin — the single pin Renovate’s github-actions manager already tracks. No second, independently-tracked pin exists to drift out of step with it. Edit the guides in go-kure/.github, never here.

renovate.json’s matchDepNames: ["go-kure/.github"] packageRules entry runs postUpgradeTasks: ./scripts/vendor-guard.sh on the bot’s branch whenever that dependency bumps. vendor-guard.sh extracts the same uses:@<sha> pin the CI resolver step uses, re-fetches all three files from go-kure/.github at that revision, and re-vendors them. The script is idempotent — a re-run against an already-synced tree makes no further change.

See go-kure/.github’s docs/standards.md § “Adopting the trusted Actions lane” for the full mechanism, and go-kure/kure#813 for the original migration off a two-pin design.

Bundling Renovate PRs

Renovate’s ecosystem groups already land related bumps as one PR, so bundling is rarely needed. When separate PRs still accumulate (e.g. several groups at once), bundle them into a single PR:

  1. Create a feature branch: git checkout -b chore/bundle-dependency-updates main
  2. Run go get for all dependencies (Flux packages first for coordinated upgrades)
  3. Run go mod tidy
  4. Update versions.yaml supported_range / notes for any bump that lands outside its range
  5. Regenerate docs: ./scripts/sync-versions.sh generate; regenerate the constructor wrappers and the API tables if an API module moved: ./scripts/gen-builders.sh generate
  6. Validate: ./scripts/sync-versions.sh check and ./scripts/gen-builders.sh check
  7. Run full verification: make verify && make test-race
  8. Commit, push, and create PR
  9. Reference all Renovate PR numbers in the PR body to auto-close them

The Generated API Tables

An API module bump can change what kure publishes about that API, so scripts/gen-builders.sh generate runs in Renovate’s postUpgradeTasks and rewrites four generated artifacts: the per-kind constructor wrappers, pkg/kubernetes/zz_generated_tables.go, docs/api-tables.json and docs/api-tables.md. CI’s validate job runs scripts/gen-builders.sh check, which fails on any of them being stale. All four are in the fileFilters of that postUpgradeTasks block — a path missing there is dropped from the bot’s commit silently, never as an error, and surfaces only as a red check.

That block matches the gomod and mise managers, which is every API-module bump but one: external-secrets arrives through the customManagers regex entry on versions.yaml (above), so it matches by manager name nowhere in that rule. Its own rule therefore carries the same artifacts in fileFilters, and sync-eso-pin.sh chains gen-builders.sh generate itself, so a manual re-pin regenerates them too.

Two things about those tables specifically:

  • They are derived from two upstream sources, both read out of the pinned module as unpacked in the local module cache: the +kubebuilder:resource markers in the Go source, and the CustomResourceDefinition manifests the module ships. A kind that a bump leaves with neither is a hard generation failure on that PR, not a silent default — see pkg/kubernetes/README.md § 9. If a bump fails to generate for that reason, the fix is to establish the kind’s real scope from upstream, not to make the generator guess.
  • A table change may trip the doc gate. zz_generated_tables.go lives in the doc-gated pkg/kubernetes package. When a bump’s only effect on a row is its ModuleVersion provenance field — pure version churn — the doc-gate exempts it automatically: no paired doc edit, no docs-skip label. A bump that adds, removes or re-scopes a kind is not version churn, is not exempt, and should update the prose too.

Dangerous Upgrades to Watch For

DependencyRiskWatch For
cert-manager major (v1 → v2)BreakingAPI group changes, CRD schema changes
k8s.io major (e.g., v0.35 → v0.36)BreakingAPI removals, type changes, replace directive updates
Flux major (v2 → v3)BreakingAPI version removals (v1beta1 → v1 migrations)
controller-runtime majorBreakingInterface changes affecting all CRD-based packages

Validation Checklist

Before merging any dependency update:

  • ./scripts/sync-versions.sh check — build versions within supported_range, the go.mod pin comment and versions.yaml notes match (run generate first if it reports drift; reword any raw commit SHA in notes: to reference go.mod instead)
  • ./scripts/gen-builders.sh check — the constructor wrappers and the API tables match the pinned modules (run generate first if it reports drift)
  • make verify — tidy + lint + test
  • make test-race — race condition detection
  • k8s.io replace directives unchanged (unless intentionally bumping)

See Also