This is an automated email from the ASF dual-hosted git repository.
villebro pushed a commit to branch main
in repository
https://gitbox.apache.org/repos/asf/superset-kubernetes-operator.git
The following commit(s) were added to refs/heads/main by this push:
new 6703c0f ci(deps): matrix e2e across supported Kubernetes versions
(#77)
6703c0f is described below
commit 6703c0f9f0908f5bb720b43668f6362390e2ec2f
Author: Ville Brofeldt <[email protected]>
AuthorDate: Tue May 26 11:11:30 2026 -0700
ci(deps): matrix e2e across supported Kubernetes versions (#77)
* ci(test): matrix e2e across supported Kubernetes versions
* improve tooling for automatically bumping versions
* improve automation
---
.asf.yaml | 1 +
.github/supported-k8s.json | 16 +++++
.github/workflows/ci.yaml | 10 +++
.github/workflows/sync-supported-k8s.yaml | 66 ++++++++++++++++++
.github/workflows/test.yaml | 60 ++++++++++++++++-
.rat-excludes | 1 +
AGENTS.md | 15 +++++
Makefile | 24 ++++++-
README.md | 15 ++++-
docs/index.md | 13 ++++
docs/user-guide/installation.md | 15 ++++-
renovate.json | 30 ++++++++-
scripts/render-supported-versions.sh | 68 +++++++++++++++++++
scripts/sync-supported-versions.sh | 108 ++++++++++++++++++++++++++++++
14 files changed, 434 insertions(+), 8 deletions(-)
diff --git a/.asf.yaml b/.asf.yaml
index b37ac5c..e67329f 100644
--- a/.asf.yaml
+++ b/.asf.yaml
@@ -60,6 +60,7 @@ github:
- Build
- Lint
- Verify codegen
+ - Verify supported-versions table
- Docker build
- Helm lint
- Unit & Integration
diff --git a/.github/supported-k8s.json b/.github/supported-k8s.json
new file mode 100644
index 0000000..cb5a13d
--- /dev/null
+++ b/.github/supported-k8s.json
@@ -0,0 +1,16 @@
+{
+ "supported": [
+ {
+ "minor": "1.35",
+ "node_image":
"kindest/node:v1.35.0@sha256:452d707d4862f52530247495d180205e029056831160e22870e37e3f6c1ac31f"
+ },
+ {
+ "minor": "1.34",
+ "node_image":
"kindest/node:v1.34.3@sha256:08497ee19eace7b4b5348db5c6a1591d7752b164530a36f855cb0f2bdcbadd48"
+ }
+ ],
+ "next": {
+ "minor": "1.36",
+ "version": "v1.36.1"
+ }
+}
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 988c4b0..5b9e7d8 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -65,6 +65,16 @@ jobs:
exit 1
fi
+ verify-supported-versions:
+ name: Verify supported-versions table
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #
v6.0.2
+ - name: Verify supported-k8s.json matches kind release and docs are in
sync
+ run: make verify-supported-versions
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
docker:
name: Docker build
runs-on: ubuntu-latest
diff --git a/.github/workflows/sync-supported-k8s.yaml
b/.github/workflows/sync-supported-k8s.yaml
new file mode 100644
index 0000000..7494674
--- /dev/null
+++ b/.github/workflows/sync-supported-k8s.yaml
@@ -0,0 +1,66 @@
+# 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.
+
+name: Sync supported Kubernetes versions
+
+on:
+ schedule:
+ - cron: '0 6 * * *' # daily at 06:00 UTC
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ sync:
+ name: Sync supported-k8s.json
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #
v6.0.2
+ - name: Sync supported-k8s.json and regenerate docs
+ run: make sync-supported-versions
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Open or update PR if anything changed
+ uses:
peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 #
v8.1.1
+ with:
+ branch: chore/sync-supported-k8s
+ base: main
+ commit-message: |
+ chore(ci): sync supported Kubernetes versions
+
+ Auto-generated by .github/workflows/sync-supported-k8s.yaml.
+ title: 'chore(ci): sync supported Kubernetes versions'
+ body: |
+ Automated sync of `.github/supported-k8s.json` and the generated
+ supported-versions blocks in `README.md` and the docs.
+
+ The sync script (`scripts/sync-supported-versions.sh`) recomputes:
+
+ - `supported` — newest two Kubernetes minors with a `kindest/node`
+ image published by the currently pinned kind release.
+ - `next` — newest stable Kubernetes release if its minor isn't
+ already covered by kind; otherwise `null`.
+
+ **Heads up:** PRs opened by `GITHUB_TOKEN` don't trigger CI.
+ Close + reopen this PR (or push an empty commit) to run the
+ required checks before merging.
+ labels: |
+ dependencies
+ review:supported-k8s
+ delete-branch: true
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index a217dac..9f959ba 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -24,6 +24,11 @@ on:
permissions:
contents: read
+env:
+ # kind version + checksum are bumped in lockstep by Renovate (see
renovate.json).
+ KIND_VERSION: v0.31.0
+ KIND_CHECKSUM:
eb244cbafcc157dff60cf68693c14c9a75c4e6e6fedaf9cd71c58117cb93e3fa
+
jobs:
unit-integration:
name: Unit & Integration
@@ -50,9 +55,55 @@ jobs:
flags: integration
token: ${{ secrets.CODECOV_TOKEN }}
+ e2e-matrix:
+ name: E2E matrix
+ runs-on: ubuntu-latest
+ outputs:
+ supported: ${{ steps.load.outputs.supported }}
+ next_version: ${{ steps.load.outputs.next_version }}
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #
v6.0.2
+ - id: load
+ run: |
+ {
+ echo "supported=$(jq -c .supported .github/supported-k8s.json)"
+ echo "next_version=$(jq -r '.next.version // ""'
.github/supported-k8s.json)"
+ } >> "$GITHUB_OUTPUT"
+
e2e:
- name: E2E
+ name: E2E (${{ matrix.k8s.minor }})
+ needs: e2e-matrix
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ k8s: ${{ fromJSON(needs.e2e-matrix.outputs.supported) }}
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #
v6.0.2
+ - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c #
v6.4.0
+ with:
+ go-version-file: go.mod
+ - name: Install kind
+ run: |
+ curl -fsSLo ./kind
"https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-amd64"
+ echo "${KIND_CHECKSUM} ./kind" | sha256sum -c -
+ chmod +x ./kind
+ sudo mv ./kind /usr/local/bin/kind
+ kind version
+ - name: Run E2E tests
+ run: make test-e2e
+ env:
+ KIND_NODE_IMAGE: ${{ matrix.k8s.node_image }}
+ KIND_CLUSTER: superset-op-e2e-${{ matrix.k8s.minor }}
+
+ e2e-next:
+ name: E2E (next, best-effort)
+ needs: e2e-matrix
+ if: needs.e2e-matrix.outputs.next_version != ''
runs-on: ubuntu-latest
+ continue-on-error: true
+ env:
+ K8S_NEXT: ${{ needs.e2e-matrix.outputs.next_version }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #
v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c #
v6.4.0
@@ -60,12 +111,15 @@ jobs:
go-version-file: go.mod
- name: Install kind
run: |
- KIND_VERSION="v0.31.0"
-
KIND_CHECKSUM="eb244cbafcc157dff60cf68693c14c9a75c4e6e6fedaf9cd71c58117cb93e3fa"
curl -fsSLo ./kind
"https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-amd64"
echo "${KIND_CHECKSUM} ./kind" | sha256sum -c -
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
kind version
+ - name: Build node image for newest Kubernetes release
+ run: kind build node-image --type release "${K8S_NEXT}" --image
"kindest/node:${K8S_NEXT}-local"
- name: Run E2E tests
run: make test-e2e
+ env:
+ KIND_NODE_IMAGE: kindest/node:${{ env.K8S_NEXT }}-local
+ KIND_CLUSTER: superset-op-e2e-next
diff --git a/.rat-excludes b/.rat-excludes
index 9de36c4..5fd4242 100644
--- a/.rat-excludes
+++ b/.rat-excludes
@@ -47,3 +47,4 @@
.*rat-results\.txt
.*Dockerfile\.cross
.*codecov\.yml
+.*supported-k8s\.json
diff --git a/AGENTS.md b/AGENTS.md
index ba11822..ea8f82e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -137,6 +137,21 @@ All components use the reserved container name `superset`
for the main container
Superset CR names are validated via CEL to be valid DNS labels (lowercase
alphanumeric and hyphens, `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`, max 63
characters). DNS-label syntax is required because the operator derives Service
names from the parent name + component suffix. Per-component CEL rules enforce
that the parent name is short enough for each enabled component's suffix to fit
within the 63-char Service name limit (e.g., `-websocket-server` = 17 chars
limits parent to 46).
+## Automation Principle
+
+When committed state must stay in sync with something external (upstream
releases, generated docs, tables derived from a source of truth), prefer
**building a CI-enforced sync mechanism** over documenting a manual checklist.
Manual steps rot; CI failures don't.
+
+Concrete patterns to mirror:
+
+- **One source of truth, generated outputs.** Keep canonical data in a single
small file (`.github/supported-k8s.json`, Go type definitions, etc.) and derive
everything else — docs tables, CI matrices, defaults — from it.
+- **`make codegen` aggregates all generators.** Anything regenerable goes
through it. CI's `Verify codegen` job runs `make codegen` and fails on diff.
+- **Upstream drift checks.** When the source-of-truth file itself must track
something external (a pinned dependency's release notes, the latest upstream
minor), write a `make sync-<thing>` script that queries the upstream API and
rewrites the file, plus a `make verify-<thing>` that runs it in `--check` mode
in CI. See `scripts/sync-supported-versions.sh` for the canonical example.
+- **Scheduled auto-sync PRs.** Pair the sync script with a daily scheduled
GitHub Actions job that runs it and opens a PR if anything changed. Removes the
"every open PR breaks until someone syncs" failure mode when upstream ticks.
+- **Renovate for pinned versions.** Use `customManagers` for non-standard pin
sites (regex-matched JSON/YAML fields). Use `prBodyNotes` + `addLabels` to
steer the reviewer toward the right follow-up command on PRs that require
manual judgment.
+- **Sentinel-bounded generated blocks** in human-edited files (`<!-- BEGIN X
-->` / `<!-- END X -->`) so generators can rewrite specific regions without
disturbing surrounding hand-written content.
+
+The bar: every new "remember to also update X when Y changes" instruction is a
bug in the tooling. Fix the tooling instead.
+
## PR Conventions
- **Title format**: `type(scope): description` or `type: description` —
enforced by CI (`amannn/action-semantic-pull-request`). Scope is optional but
encouraged.
diff --git a/Makefile b/Makefile
index 71382d6..7e17781 100644
--- a/Makefile
+++ b/Makefile
@@ -128,6 +128,25 @@ docs-api: crd-ref-docs ## Generate API reference
documentation from Go types.
$(CRD_REF_DOCS) --source-path=api/v1alpha1
--config=hack/api-ref-config.yaml --renderer=markdown --max-depth=15
--output-path=docs/reference/api-reference.md
sed -f hack/fix-api-ref-links.sed docs/reference/api-reference.md >
docs/reference/api-reference.md.tmp && mv docs/reference/api-reference.md.tmp
docs/reference/api-reference.md
+.PHONY: supported-versions
+supported-versions: ## Regenerate the supported-Kubernetes-versions table in
README.md and docs.
+ ./scripts/render-supported-versions.sh
+
+.PHONY: sync-supported-versions
+sync-supported-versions: ## Sync .github/supported-k8s.json with the pinned
kind release's node images.
+ ./scripts/sync-supported-versions.sh --write
+ $(MAKE) supported-versions
+
+.PHONY: verify-supported-versions
+verify-supported-versions: ## Verify supported-k8s.json matches the pinned
kind release and docs are up to date.
+ ./scripts/sync-supported-versions.sh --check
+ $(MAKE) supported-versions
+ @if [ -n "$$(git status --porcelain README.md docs/index.md
docs/user-guide/installation.md)" ]; then \
+ echo "Supported-versions table is stale. Run 'make
supported-versions' and commit."; \
+ git diff README.md docs/index.md
docs/user-guide/installation.md; \
+ exit 1; \
+ fi
+
##@ Helm
HELM_CHART_DIR ?= charts/superset-operator
@@ -148,7 +167,7 @@ helm-lint: helm-sync-crds ## Lint the Helm chart (syncs
CRDs first).
##@ Development
.PHONY: codegen
-codegen: manifests generate helm-sync-crds docs-api ## Regenerate all
generated artifacts (CRDs, DeepCopy, Helm CRDs, API docs).
+codegen: manifests generate helm-sync-crds docs-api supported-versions ##
Regenerate all generated artifacts (CRDs, DeepCopy, Helm CRDs, API docs,
supported-versions table).
.PHONY: clean
clean: ## Remove build artifacts, downloaded tools, and test cache.
@@ -188,7 +207,8 @@ test-integration: manifests generate fmt vet setup-envtest
## Run integration te
# CertManager is installed by default; skip with:
# - CERT_MANAGER_INSTALL_SKIP=true
KIND_CLUSTER ?= superset-kubernetes-operator-test-e2e
-KIND_NODE_IMAGE ?= kindest/node:v1.35.1
+# Keep in sync with the first entry of .github/supported-k8s.json (Renovate
manages the digest).
+KIND_NODE_IMAGE ?=
kindest/node:v1.35.0@sha256:452d707d4862f52530247495d180205e029056831160e22870e37e3f6c1ac31f
.PHONY: setup-test-e2e
setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist
diff --git a/README.md b/README.md
index 7ecd5ff..350da4f 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,19 @@ A Kubernetes operator for deploying and managing [Apache
Superset](https://super
The operator manages the full Superset lifecycle: database migrations,
configuration rendering, component deployment, scaling, and networking.
Defaults are tuned for typical deployments; presets, deployment-template
fields, and a raw Python escape hatch let you override them where needed.
+## Supported Kubernetes versions
+
+<!-- BEGIN SUPPORTED-K8S -->
+<!-- generated by `make supported-versions`; do not edit -->
+- **Officially tested:** Kubernetes 1.35, 1.34
+- **Experimental (best-effort):** Kubernetes 1.36
+<!-- END SUPPORTED-K8S -->
+
+Official support covers the two most recent Kubernetes minor versions with a
+published [kind](https://kind.sigs.k8s.io/) node image. The newest Kubernetes
+release gets best-effort coverage via a non-blocking CI lane until kind ships
+its node image.
+
## Quick Start
Install the operator via Helm:
@@ -78,4 +91,4 @@ After editing type definitions in `api/v1alpha1/`, run `make
manifests generate`
## License
-Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for
details.
\ No newline at end of file
+Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for
details.
diff --git a/docs/index.md b/docs/index.md
index 2378ff6..e7f227d 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -38,6 +38,19 @@ The operator manages the full Superset lifecycle: database
migrations, configura
- **Scaling and resilience** — HPA with custom metrics, PodDisruptionBudgets,
NetworkPolicies, Prometheus ServiceMonitor
- **Flexible install scope** — cluster-scoped (default) or namespace-scoped;
the namespace-scoped Helm install renders no manager `ClusterRole` at all, so
it works on restricted clusters that forbid cluster-scoped RBAC
+## Supported Kubernetes versions
+
+<!-- BEGIN SUPPORTED-K8S -->
+<!-- generated by `make supported-versions`; do not edit -->
+- **Officially tested:** Kubernetes 1.35, 1.34
+- **Experimental (best-effort):** Kubernetes 1.36
+<!-- END SUPPORTED-K8S -->
+
+Official support covers the two most recent Kubernetes minor versions with a
+published [kind](https://kind.sigs.k8s.io/) node image. The newest Kubernetes
+release gets best-effort coverage via a non-blocking CI lane until kind ships
+its node image.
+
## What it looks like
A typical Superset deployment for getting started in dev mode:
diff --git a/docs/user-guide/installation.md b/docs/user-guide/installation.md
index 5657093..5d6f452 100644
--- a/docs/user-guide/installation.md
+++ b/docs/user-guide/installation.md
@@ -25,13 +25,26 @@ For development setup, see [Development
Setup](../contributing/development-setup
## Prerequisites
-- Kubernetes v1.28+ cluster
+- Kubernetes cluster (see [Supported versions](#supported-kubernetes-versions))
- Helm 3 (for Helm-based installation) or `kubectl` + `kustomize`
- PostgreSQL or MySQL database
- (Optional) [Valkey](https://valkey.io/) (or Redis) — required when enabling
caching, Celery task broker, or result backend
- (Optional) [Gateway API
CRDs](https://gateway-api.sigs.k8s.io/guides/#installing-gateway-api) for
HTTPRoute support — not included in Kubernetes, must be installed separately
- (Optional) prometheus-operator CRDs for ServiceMonitor support
+## Supported Kubernetes versions
+
+<!-- BEGIN SUPPORTED-K8S -->
+<!-- generated by `make supported-versions`; do not edit -->
+- **Officially tested:** Kubernetes 1.35, 1.34
+- **Experimental (best-effort):** Kubernetes 1.36
+<!-- END SUPPORTED-K8S -->
+
+Official support covers the two most recent Kubernetes minor versions with a
+published [kind](https://kind.sigs.k8s.io/) node image. The newest Kubernetes
+release gets best-effort coverage via a non-blocking CI lane until kind ships
+its node image.
+
## 1. Install the operator
```bash
diff --git a/renovate.json b/renovate.json
index 4051d9c..db18784 100644
--- a/renovate.json
+++ b/renovate.json
@@ -2,7 +2,7 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"minimumReleaseAge": "7 days",
- "schedule": ["before 7am on Monday"],
+ "schedule": ["before 7am"],
"labels": ["dependencies"],
"packageRules": [
{
@@ -13,6 +13,34 @@
{
"matchManagers": ["github-actions"],
"pinDigests": true
+ },
+ {
+ "matchPackageNames": ["kubernetes-sigs/kind"],
+ "addLabels": ["review:supported-k8s"],
+ "prBodyNotes": [
+ ":warning: After bumping kind, run `make sync-supported-versions &&
make codegen` and commit the result to update `.github/supported-k8s.json` (and
the generated docs). The script recomputes both `supported` (newest two
kind-published minors) and `next` (newest Kubernetes release not yet covered by
kind). The `Verify supported-versions table` CI check will fail until this is
done."
+ ]
+ }
+ ],
+ "customManagers": [
+ {
+ "customType": "regex",
+ "description": "Track kindest/node image digests in
.github/supported-k8s.json",
+ "managerFilePatterns": ["/^\\.github/supported-k8s\\.json$/"],
+ "matchStrings": [
+
"\"node_image\":\\s*\"(?<depName>kindest/node):(?<currentValue>[^@\"]+)@(?<currentDigest>sha256:[a-f0-9]+)\""
+ ],
+ "datasourceTemplate": "docker"
+ },
+ {
+ "customType": "regex",
+ "description": "Track sigs.k8s.io/kind release pin and matching sha256
in workflows",
+ "managerFilePatterns": ["/^\\.github/workflows/.+\\.ya?ml$/"],
+ "matchStrings": [
+
"KIND_VERSION:\\s*(?<currentValue>v[0-9.]+)\\s*\\n\\s*KIND_CHECKSUM:\\s*(?<currentDigest>[a-f0-9]+)"
+ ],
+ "datasourceTemplate": "github-releases",
+ "depNameTemplate": "kubernetes-sigs/kind"
}
]
}
diff --git a/scripts/render-supported-versions.sh
b/scripts/render-supported-versions.sh
new file mode 100755
index 0000000..ca7456e
--- /dev/null
+++ b/scripts/render-supported-versions.sh
@@ -0,0 +1,68 @@
+#!/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.
+#
+# Render the supported-Kubernetes-versions block from
.github/supported-k8s.json
+# into README.md and the docs, between sentinel comments:
+#
+# <!-- BEGIN SUPPORTED-K8S -->
+# ... generated block ...
+# <!-- END SUPPORTED-K8S -->
+
+set -euo pipefail
+
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+SOURCE="${REPO_ROOT}/.github/supported-k8s.json"
+TARGETS=(
+ "${REPO_ROOT}/README.md"
+ "${REPO_ROOT}/docs/index.md"
+ "${REPO_ROOT}/docs/user-guide/installation.md"
+)
+
+command -v jq >/dev/null || { echo "jq is required" >&2; exit 1; }
+
+supported="$(jq -r '[.supported[].minor] | join(", ")' "${SOURCE}")"
+next="$(jq -r '.next.minor // empty' "${SOURCE}")"
+
+block="$({
+ echo '<!-- BEGIN SUPPORTED-K8S -->'
+ echo '<!-- generated by `make supported-versions`; do not edit -->'
+ echo "- **Officially tested:** Kubernetes ${supported}"
+ if [ -n "${next}" ]; then
+ echo "- **Experimental (best-effort):** Kubernetes ${next}"
+ fi
+ echo '<!-- END SUPPORTED-K8S -->'
+})"
+
+for file in "${TARGETS[@]}"; do
+ if ! grep -q '<!-- BEGIN SUPPORTED-K8S -->' "${file}"; then
+ echo "no sentinel block found in ${file}" >&2
+ exit 1
+ fi
+ tmp="$(mktemp)"
+ BLOCK="${block}" awk '
+ /<!-- BEGIN SUPPORTED-K8S -->/ {
+ print ENVIRON["BLOCK"]
+ skip = 1
+ next
+ }
+ /<!-- END SUPPORTED-K8S -->/ {
+ skip = 0
+ next
+ }
+ !skip { print }
+ ' "${file}" > "${tmp}"
+ mv "${tmp}" "${file}"
+done
diff --git a/scripts/sync-supported-versions.sh
b/scripts/sync-supported-versions.sh
new file mode 100755
index 0000000..91bf94b
--- /dev/null
+++ b/scripts/sync-supported-versions.sh
@@ -0,0 +1,108 @@
+#!/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.
+#
+# Sync .github/supported-k8s.json with the kindest/node images published
+# by the currently pinned kind release (read from .github/workflows/test.yaml),
+# and compute `next` from upstream Kubernetes releases:
+#
+# - `supported` = the two newest Kubernetes minors that the pinned kind
+# release ships node images for (highest patch per minor).
+# - `next` = {minor, version} of the newest stable Kubernetes release if
+# its minor isn't already in `supported`; otherwise null.
+#
+# Usage:
+# sync-supported-versions.sh [--check|--write]
+#
+# --check (default): exit non-zero with a diff if the JSON is out of sync.
+# --write: rewrite the JSON in place.
+#
+# Set GITHUB_TOKEN to avoid GitHub API rate limits (60 req/hr unauth).
+
+set -euo pipefail
+
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+SOURCE="${REPO_ROOT}/.github/supported-k8s.json"
+WORKFLOW="${REPO_ROOT}/.github/workflows/test.yaml"
+
+mode="${1:---check}"
+case "${mode}" in --check|--write) ;; *) echo "usage: $0 [--check|--write]"
>&2; exit 2 ;; esac
+
+command -v jq >/dev/null || { echo "jq required" >&2; exit 1; }
+command -v curl >/dev/null || { echo "curl required" >&2; exit 1; }
+
+KIND_VERSION="$(awk '/^[[:space:]]*KIND_VERSION:/ {print $2; exit}'
"${WORKFLOW}")"
+[ -n "${KIND_VERSION}" ] || { echo "could not read KIND_VERSION from
${WORKFLOW}" >&2; exit 1; }
+
+api="https://api.github.com/repos/kubernetes-sigs/kind/releases/tags/${KIND_VERSION}"
+auth=()
+[ -n "${GITHUB_TOKEN:-}" ] && auth=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
+
+body="$(curl -fsSL ${auth[@]+"${auth[@]}"} -H 'Accept:
application/vnd.github+json' "${api}" | jq -r .body)"
+
+# Extract the highest-patch node image per Kubernetes minor, then take the
+# top two minors. Format per row: "MINOR FULL_IMAGE".
+top2="$(printf '%s\n' "${body}" \
+ | grep -oE 'kindest/node:v[0-9]+\.[0-9]+\.[0-9]+@sha256:[a-f0-9]{64}' \
+ | sort -u \
+ | sed -E 's|^kindest/node:v([0-9]+\.[0-9]+)\.([0-9]+)@.*|\1 \2 &|' \
+ | sort -k1,1V -k2,2n \
+ | awk '{ best[$1] = $3 } END { for (k in best) print k" "best[k] }' \
+ | sort -k1,1Vr \
+ | head -2)"
+
+[ -n "${top2}" ] || { echo "no kindest/node images found in release notes for
${KIND_VERSION}" >&2; exit 1; }
+[ "$(printf '%s\n' "${top2}" | wc -l | tr -d ' ')" -eq 2 ] \
+ || { echo "expected 2 minor versions, got:" >&2; printf '%s\n' "${top2}"
>&2; exit 1; }
+
+new_supported="$(printf '%s\n' "${top2}" \
+ | jq -R -s 'split("\n") | map(select(length>0)) | map(split(" ") | {minor:
.[0], node_image: .[1]})')"
+
+# Determine the newest stable Kubernetes release (skip prereleases/RCs).
+k8s_api='https://api.github.com/repos/kubernetes/kubernetes/releases?per_page=30'
+newest_k8s="$(curl -fsSL ${auth[@]+"${auth[@]}"} -H 'Accept:
application/vnd.github+json' "${k8s_api}" \
+ | jq -r '[.[] | select(.prerelease == false) | .tag_name |
select(test("^v[0-9]+\\.[0-9]+\\.[0-9]+$"))][0] // empty')"
+[ -n "${newest_k8s}" ] || { echo "could not determine newest Kubernetes
release" >&2; exit 1; }
+newest_k8s_minor="$(printf '%s' "${newest_k8s}" | sed -E
's|^v([0-9]+\.[0-9]+).*|\1|')"
+
+new_json="$(
+ jq --argjson sup "${new_supported}" \
+ --arg k8sMinor "${newest_k8s_minor}" \
+ --arg k8sVersion "${newest_k8s}" '
+ def to_v: split(".") | map(tonumber);
+ (.supported = $sup)
+ | ([.supported[].minor | to_v] | max) as $topSupported
+ | ($k8sMinor | to_v) as $k8s
+ | if $k8s > $topSupported
+ then .next = {minor: $k8sMinor, version: $k8sVersion}
+ else .next = null
+ end
+ ' "${SOURCE}"
+)"
+
+case "${mode}" in
+ --write)
+ printf '%s\n' "${new_json}" > "${SOURCE}"
+ echo "updated ${SOURCE} from kind ${KIND_VERSION}"
+ ;;
+ --check)
+ if ! diff -u <(jq -S . "${SOURCE}") <(printf '%s\n' "${new_json}" | jq -S
.); then
+ echo >&2
+ echo "supported-k8s.json is out of sync with kind ${KIND_VERSION}." >&2
+ echo "Run 'make sync-supported-versions' to update." >&2
+ exit 1
+ fi
+ ;;
+esac