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:
| File | Purpose |
|---|---|
go.mod | Go module dependencies — authoritative for the build version (the pin) |
versions.yaml | Version metadata: supported range, notes (no build version) |
docs/compatibility.md | Generated from versions.yaml + go.mod — never edit directly |
pkg/versions/versions_gen.go | Generated from versions.yaml — never edit directly; run mise run versions:generate |
renovate.json | Bot 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:
- Range — each dependency’s build version, read from
go.mod, falls within thesupported_rangedeclared inversions.yaml. For a dependency pinned to a Go pseudo-version (no semver tags upstream) that also declaresupstream_release(see “Release-pinned dependencies” below), the declared release is substituted for the range check instead of being skipped —supported_rangeis enforced for these too. Skipped entirely for a dependency declaringfloor_module(see “MVS-floor dependencies” below) — guard 6 already enforces the invariant that actually applies to it. Each bound ofsupported_rangemust be exactlymajor.minor("1.26 - 1.28"or"1.26"):check,generateandwidenall reject a malformed one such as"1.x - 3.x"rather than compare it or publish it aspkg/versionsMin/Maxmetadata. - go.mod pin comment — the
// Current pin: vX.Y.Z (Kubernetes 1.N)comment above thek8s.io/apireplace directive matches that directive’s actual version. A drifted comment used to be caught only by AI review, on every single Kubernetes bump. - No raw commit SHAs in
versions.yamlnotes — anotes: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 ago.modpseudo-version), reference “the pseudo-version pinned ingo.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. - Drift — the committed
docs/compatibility.mdis byte-identical to whatgeneratewould 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 whatversions.yaml+go.modcurrently imply) and tells you to rungenerate. - Go API drift — the committed
pkg/versions/versions_gen.gois byte-identical to whatgenerate_go_apiwould produce right now. Same regenerate-into-a-temp-file-and-diff shape as item 4 above. - MVS-floor equality — for a dependency declaring
floor_module(see “MVS-floor dependencies” below), the go.mod pin exactly equals whatfloor_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 checkasserts 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— theowner/repothe tag lives in. Used only by the second, best-effort check below; a dependency with noupstream_reposkips it and keeps only the offline digest check.supported_rangeis then enforced for the dependency for the first time, againstupstream_releaserather 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-remoteenumerates the repo’s refs with none matching) — error: likely a typo inupstream_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 tidyre-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 checkmechanically asserts this: withfloor_moduleset, it readsfloor_module’s owngo.modand asserts it requiresgo_moduleat 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+tidyrecipe above.validate_gomod’s ownmajor.minorrange check is skipped entirely for any entry declaringfloor_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. Asupported_rangehere 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. Declaringsupported_rangeon afloor_moduleentry anyway is harmless but pointless: it is never read.docs/compatibility.md’s generated table rendersderived from <floor_module>in the Deployment Compatibility column for these entries instead of a range (seegenerate_docs). This was originally a range-membership check applied by accident — the loop iterates every.infrastructurekey 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-cloudis directly imported (pkg/kubernetes/cnpg/create.go,pkg/kubernetes/cnpg/update.go), so an incompatible upstream API change still surfaces — as atestcompile failure, not alintrange 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, afloor_moduledependency is entirely unguarded on an offline dev machine (both CI checks always run there).renovate.jsondisables the standalonegomodbump for the module (matchPackageNames+enabled: false, same shape as the release-pinned rule above). The shared preset’spostUpdateOptions: ["gomodTidy"]re-raises the require to the new floor automatically whenever the floor-setting dependency’s own PR runsgo mod tidy— so the pin still moves, inside that dependency’s own reviewable PR, never on its own.- Without the
versions.yamlentry,validate_gomod()’s loop — keyed on.infrastructurekeys — never visits the module at all: not skipped, not warned, invisible. That silence is what letbarman-clouddigest-bump unreviewed for months before this pattern existed (the shared preset’sgo-patchgroup automergesgomodpatch/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:testIt’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 generateThe 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_moduleentry (its range is never checked — see below — so there’s nothing to widen; dropsupported_rangeinstead if it still has a stale one) - a
--notecontaining a|(would break the Markdown table cell it’s rendered into indocs/compatibility.md) or a raw 12- or 40-character commit SHA (the same restrictioncheck’svalidate_no_sha_in_notesguard enforces — see above). Quotes and backslashes are fine: the note is passed toyqas data, not spliced into its expression - an entry with no
supported_rangedeclared yet (add one by hand first) - a new upper bound that isn’t exactly
major.minor(e.g.2.1.0is rejected — matchingsupported_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.7to1.6would 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 tidyNo 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.
- Review the upstream changelog for breaking changes
- Check if Kure uses any deprecated APIs
- Update
go.mod; if the new version lands outsidesupported_range, widen the range and updatenotesinversions.yaml(only after confirming API compatibility) - Run
make verifyto catch compile-time breakage
Major Updates (High Risk)
Major bumps (e.g., v1 → v2) likely have breaking API changes.
- Review the migration guide thoroughly
- Assess impact on all callers (check with
grep -rfor imports) - Update code to use new APIs
- Update
versions.yamland documentation - 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/v2helm-controller/apikustomize-controller/apinotification-controller/apisource-controller/apiimage-automation-controller/apiimage-reflector-controller/apipkg/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.shWhat 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 -uCNPG 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:
- Create a feature branch:
git checkout -b chore/bundle-dependency-updates main - Run
go getfor all dependencies (Flux packages first for coordinated upgrades) - Run
go mod tidy - Update
versions.yamlsupported_range/notesfor any bump that lands outside its range - 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 - Validate:
./scripts/sync-versions.sh checkand./scripts/gen-builders.sh check - Run full verification:
make verify && make test-race - Commit, push, and create PR
- 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:resourcemarkers in the Go source, and theCustomResourceDefinitionmanifests the module ships. A kind that a bump leaves with neither is a hard generation failure on that PR, not a silent default — seepkg/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.golives in the doc-gatedpkg/kubernetespackage. When a bump’s only effect on a row is itsModuleVersionprovenance field — pure version churn — the doc-gate exempts it automatically: no paired doc edit, nodocs-skiplabel. 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
| Dependency | Risk | Watch For |
|---|---|---|
| cert-manager major (v1 → v2) | Breaking | API group changes, CRD schema changes |
| k8s.io major (e.g., v0.35 → v0.36) | Breaking | API removals, type changes, replace directive updates |
| Flux major (v2 → v3) | Breaking | API version removals (v1beta1 → v1 migrations) |
| controller-runtime major | Breaking | Interface changes affecting all CRD-based packages |
Validation Checklist
Before merging any dependency update:
-
./scripts/sync-versions.sh check— build versions withinsupported_range, thego.modpin comment andversions.yamlnotes match (rungeneratefirst if it reports drift; reword any raw commit SHA innotes:to referencego.modinstead) -
./scripts/gen-builders.sh check— the constructor wrappers and the API tables match the pinned modules (rungeneratefirst 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
- Development Guide § Renovate Management — dashboard workflow for managing Renovate PRs
- Compatibility Matrix — Generated compatibility matrix
- versions.yaml — Version source of truth