From 72491108cd4e7c2e8ec8b7a5a0cd871605b47d9d Mon Sep 17 00:00:00 2001 From: David Fridrich Date: Wed, 5 Aug 2026 17:31:16 +0200 Subject: [PATCH] feat: pin func-utils image per minor release Every released CLI embeds ghcr.io/knative/func-utils:v2, which main re-publishes on every green push, so changes on main reach the cluster side of old CLIs. For example v1.20 runs "scaffold ", but v2 needs two arguments since #3237. Main now carries the image of the minor in development: it embeds func-utils:1.24, and its green pushes publish that tag. A release-X.Y branch cut from main already embeds func-utils:X.Y, and the image exists, so the cut needs no change and the release can go at once. - The reference lives in one constant, version.FuncUtilsImage. The three variables func reads default to it, so plain go build and source tarballs embed it too. FUNC_UTILS_IMG and -X ldflags still override it. make func-utils-image prints what a build embeds; the workflows, hack/images.sh and hack/release.sh read it from there. - The Functions workflow publishes the tag the source embeds, from main and from release branches. A release branch must embed its own minor. Once release-X.Y is cut, main skips the publish until it moves to the next minor, unless its commit is the branch head (the cut commit's own run), so main never overwrites X.Y. - Cutting release-X.Y runs the new workflow update-func-utils-image.yaml, which opens a PR that moves main to X.(Y+1). - hack/release.sh refuses a vX.Y.Z release unless the build embeds func-utils:X.Y and that image exists. - Nothing publishes v2 any more, so CLIs up to 1.23 keep the image as of this change. - docs/func-utils-image.md describes the tags, the compatibility rule and the release cut. --- .github/workflows/functions.yaml | 31 ++++++++++- .../workflows/update-func-utils-image.yaml | 54 +++++++++++++++++++ Makefile | 12 ++++- docs/README.md | 1 + docs/func-utils-image.md | 42 +++++++++++++++ hack/images.sh | 7 ++- hack/release.sh | 16 ++++++ pkg/k8s/dialer.go | 4 +- pkg/k8s/persistent_volumes.go | 4 +- pkg/pipelines/tekton/tasks.go | 4 +- pkg/version/version.go | 7 +++ 11 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/update-func-utils-image.yaml create mode 100644 docs/func-utils-image.md diff --git a/.github/workflows/functions.yaml b/.github/workflows/functions.yaml index 8729dd4764..515da8f09f 100644 --- a/.github/workflows/functions.yaml +++ b/.github/workflows/functions.yaml @@ -536,7 +536,9 @@ jobs: - test-e2e-podman - test-e2e-runtimes - test-e2e-config-ci - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: >- + github.event_name == 'push' && + (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release-')) runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -595,8 +597,31 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Derive Image Tag + id: image-tag + # Publish the tag the source embeds: the next minor on main, X.Y on + # release-X.Y (see docs/func-utils-image.md). Once release-X.Y is cut, + # main skips the publish until it moves to the next minor, unless its + # commit is the branch head (the cut commit's own run). + run: | + ref="$(make -s --no-print-directory func-utils-image)" + tag="${ref##*:}" + if [[ "${GITHUB_REF_NAME}" == release-* && "${GITHUB_REF_NAME}" != "release-${tag}" ]]; then + echo "::error::${GITHUB_REF_NAME} embeds ${ref}; it must embed its own minor" + exit 1 + fi + if [[ "${GITHUB_REF_NAME}" == "main" ]]; then + head="$(git ls-remote origin "refs/heads/release-${tag}")" + head="${head%%$'\t'*}" + if [[ -n "${head}" && "${head}" != "${GITHUB_SHA}" ]]; then + echo "::notice::release-${tag} is cut, so only that branch publishes ${ref}; merge the PR that moves main to the next minor" + exit 0 + fi + fi + echo "tag=${tag}" >> "$GITHUB_OUTPUT" - name: Build and push id: build-images + if: steps.image-tag.outputs.tag != '' run: | for a in amd64 arm64 ppc64le s390x; do CGO_ENABLED=0 GOARCH="${a}" go build -buildvcs=false -o "func-util-$a" -trimpath -ldflags '-w -s' ./cmd/func-util @@ -608,7 +633,7 @@ jobs: --provenance=false --sbom=false \ --platform=linux/ppc64le,linux/s390x,linux/amd64,linux/arm64 \ --push \ - -t "ghcr.io/knative/func-utils:v2" \ + -t "ghcr.io/knative/func-utils:${{ steps.image-tag.outputs.tag }}" \ --annotation index:org.opencontainers.image.description="Knative Func Utils Image" \ --annotation index:org.opencontainers.image.source="https://github.com/knative/func" \ --annotation index:org.opencontainers.image.vendor="https://github.com/knative/func" \ @@ -617,6 +642,7 @@ jobs: DIGEST="$(jq -r '."containerimage.digest"' build-metadata.json)" echo "digest=${DIGEST}" >> "${GITHUB_OUTPUT}" - name: Attest Build Provenance + if: steps.image-tag.outputs.tag != '' uses: actions/attest-build-provenance@v3 with: subject-name: "ghcr.io/knative/func-utils" @@ -626,6 +652,7 @@ jobs: publish-image: name: Publish as Image needs: build + if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 30 steps: diff --git a/.github/workflows/update-func-utils-image.yaml b/.github/workflows/update-func-utils-image.yaml new file mode 100644 index 0000000000..f9060437d4 --- /dev/null +++ b/.github/workflows/update-func-utils-image.yaml @@ -0,0 +1,54 @@ +name: Update func-utils image to next release + +# Main carries the func-utils image of the next minor release, so a branch +# release-X.Y cut from it already embeds func-utils:X.Y, and main's pushes +# have published it (see docs/func-utils-image.md). Cutting release-X.Y opens +# a PR that moves main to the next minor, X.(Y+1). Until it merges, main +# publishes no func-utils image. + +on: + # create has no branch filter; the job's if drops every other branch and tag. + create: + +permissions: + contents: read + +jobs: + bump-utils-image: + name: Bump Utils Image + if: github.ref_type == 'branch' && startsWith(github.ref_name, 'release-') + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v5 + with: + ref: main + - name: Move to Next Minor + id: next + # release-1.24 moves main from func-utils:1.24 to 1.25. If main is on + # another minor (a re-cut or an old branch), sed changes nothing and no + # PR opens. + run: | + cut="${GITHUB_REF_NAME#release-}" + next="${cut%.*}.$(( ${cut#*.} + 1 ))" + sed -i "s|func-utils:${cut}\"|func-utils:${next}\"|" pkg/version/version.go + echo "tag=${next}" >> "$GITHUB_OUTPUT" + - name: Create Pull Request + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ github.token }} + commit-message: 'chore: bump func-utils image to ${{ steps.next.outputs.tag }} after new release branch cut' + title: 'Bump func-utils image to ${{ steps.next.outputs.tag }}' + body: | + `${{ github.ref_name }}` was cut, so main moves on to the func-utils image of the next minor release, `ghcr.io/knative/func-utils:${{ steps.next.outputs.tag }}`. Until this merges, main publishes no func-utils image. See [docs/func-utils-image.md](https://github.com/${{ github.repository }}/blob/main/docs/func-utils-image.md). + + This PR was automatically generated by the [Update func-utils image workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}). + branch: bump-func-utils-${{ steps.next.outputs.tag }} + delete-branch: true + committer: Knative Automation + author: Knative Automation + assignees: gauron99, matejvasek, lkingland + base: main diff --git a/Makefile b/Makefile index dae9f8d9d3..99a19f9f90 100644 --- a/Makefile +++ b/Makefile @@ -40,10 +40,13 @@ KVER ?= $(shell git describe --tags --match 'knative-*' 2>/dev/null) LDFLAGS := -X knative.dev/func/pkg/version.Vers=$(VERS) -X knative.dev/func/pkg/version.Kver=$(KVER) -X knative.dev/func/pkg/version.Hash=$(HASH) -FUNC_UTILS_IMG ?= ghcr.io/knative/func-utils:v2 +# Overrides the func-utils image; the default is version.FuncUtilsImage. +FUNC_UTILS_IMG ?= +ifneq ($(FUNC_UTILS_IMG),) LDFLAGS += -X knative.dev/func/pkg/k8s.SocatImage=$(FUNC_UTILS_IMG) LDFLAGS += -X knative.dev/func/pkg/k8s.TarImage=$(FUNC_UTILS_IMG) LDFLAGS += -X knative.dev/func/pkg/pipelines/tekton.FuncUtilImage=$(FUNC_UTILS_IMG) +endif GOFLAGS := "-ldflags=$(LDFLAGS)" export GOFLAGS @@ -377,6 +380,13 @@ func-instrumented-bin: # func binary instrumented with coverage reporting .PHONY: cross-platform cross-platform: darwin-arm64 darwin-amd64 linux-amd64 linux-arm64 linux-ppc64le linux-s390x windows ## Build all distributable (cross-platform) binaries +.PHONY: func-utils-image +func-utils-image: ## Print the func-utils image reference builds embed + @if [ -n "$(FUNC_UTILS_IMG)" ]; then echo "$(FUNC_UTILS_IMG)"; exit; fi; \ + ref="$$(sed -n 's/^const FuncUtilsImage = "\(.*\)"$$/\1/p' pkg/version/version.go)"; \ + if [ -z "$$ref" ]; then echo "no FuncUtilsImage constant in pkg/version/version.go" >&2; exit 1; fi; \ + echo "$$ref" + .PHONY: darwin-arm64 darwin-arm64: $(BIN_DARWIN_ARM64) ## Build for mac M1 diff --git a/docs/README.md b/docs/README.md index efaed440e2..cb3c863a03 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,6 +25,7 @@ Functions can be deployed on the following platforms: [Function Developer's Guide](https://knative.dev/docs/functions/) [Function Integrator's Guide](integrators_guide.md). [CLI Command Reference](reference/func.md) +[The func-utils Image](func-utils-image.md) ## Contributing diff --git a/docs/func-utils-image.md b/docs/func-utils-image.md new file mode 100644 index 0000000000..58a7113b52 --- /dev/null +++ b/docs/func-utils-image.md @@ -0,0 +1,42 @@ +# The func-utils image + +`ghcr.io/knative/func-utils` holds `cmd/func-util`, which func runs in the +cluster: Tekton build steps, the socat dialer and the volume upload. Each +func CLI embeds one tag of it. + +## Tags + +| Tag | Used by | +| --- | --- | +| `latest` | CLIs before 2025. Frozen. | +| `v2` | CLIs up to 1.23. Frozen. | +| `X.Y` | CLIs `vX.Y.*`. Published from `release-X.Y`, and from `main` while `X.Y` is in development. | + +Ideally we would not backport to `release-X.Y` a change that breaks how the CLI +calls the image: arguments, exit codes, parsed output or shared files. +In most cases the latest `X.Y` image will work with every `vX.Y.*` CLI. + +## Where the tag lives + +`FuncUtilsImage` in `pkg/version/version.go`. `make func-utils-image` +prints what a build embeds. To embed another image, set `FUNC_UTILS_IMG`. + +## Release cut + +`main` embeds the minor in development, say `1.24`, and every green push +publishes `:1.24`. When `release-1.24` is cut: + +1. The branch already embeds `:1.24`, and the image exists, so the + release can go. `hack/release.sh` refuses a `v1.24.z` release + otherwise. +2. `.github/workflows/update-func-utils-image.yaml` opens a PR that moves + `main` to `1.25`. Until it merges, `main` stops publishing `:1.24`, + except for the cut commit itself. +3. Every green push to `release-1.24` (a backport) publishes `:1.24`. + +If `:1.24` holds the wrong image, re-run the Functions run for the +newest commit on `release-1.24`. + +If `main` still embeds `1.24` when `release-1.25` is cut, set +`FuncUtilsImage` to `ghcr.io/knative/func-utils:1.25` on `release-1.25` +and to `:1.26` on `main`. diff --git a/hack/images.sh b/hack/images.sh index d509207c0f..b622fefaf7 100755 --- a/hack/images.sh +++ b/hack/images.sh @@ -4,7 +4,12 @@ set -o errexit set -o nounset set -o pipefail -FUNC_UTILS_IMG="registry.localtest.me/knative/func-utils:v2" +# Push the locally built func-util image under the reference the func binary +# embeds (see docs/func-utils-image.md), with the local registry in place of +# the registry host. The kind cluster mirrors ghcr.io and quay.io to the local +# registry (hack/cluster.sh), so in-cluster pulls of that reference find it. +EMBEDDED_IMG="$(make -C "$(dirname "$0")/.." -s --no-print-directory func-utils-image)" +FUNC_UTILS_IMG="registry.localtest.me/${EMBEDDED_IMG#*/}" CGO_ENABLED=0 go build -o "func-util" -trimpath -ldflags '-w -s' ./cmd/func-util diff --git a/hack/release.sh b/hack/release.sh index ac66e1e3a8..e37f60fa52 100755 --- a/hack/release.sh +++ b/hack/release.sh @@ -21,6 +21,21 @@ VALIDATION_TESTS="make test" source "$(go run knative.dev/hack/cmd/script release.sh)" +# A vX.Y.Z release must embed the func-utils image pinned for X.Y, and that +# image must exist. Both hold when release-X.Y is cut from main: main embeds +# the minor in development and its green pushes publish that image (see +# docs/func-utils-image.md). +function check_func_utils_image() { + local want actual + want="ghcr.io/knative/func-utils:$(major_minor_version "$1")" + actual="$(make -s --no-print-directory func-utils-image)" + [[ "${actual}" == "${want}" ]] \ + || abort "release $1 would embed func-utils image '${actual}', want '${want}'; see docs/func-utils-image.md" + go run github.com/google/go-containerregistry/cmd/crane digest "${want}" > /dev/null \ + || abort "func-utils image '${want}' does not exist" + echo "🧰 func-utils image: ${want}" +} + function build_release() { echo "🚧 🐧 Building cross platform binaries: Linux 🐧 (amd64 / arm64 / ppc64le / s390x), MacOS 🍏, and Windows 🎠" @@ -35,6 +50,7 @@ function build_release() { else # Release build: convert knative version to go module version go_module_version="v0.$(( $(minor_version "$TAG") + 27 )).$(patch_version "$TAG")" + check_func_utils_image "${TAG}" fi else knative_version="$(git describe --tags --match 'knative-*')" diff --git a/pkg/k8s/dialer.go b/pkg/k8s/dialer.go index 5fe559c794..01a25e834e 100644 --- a/pkg/k8s/dialer.go +++ b/pkg/k8s/dialer.go @@ -26,9 +26,11 @@ import ( restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/remotecommand" + + "knative.dev/func/pkg/version" ) -var SocatImage = "ghcr.io/knative/func-utils:v2" +var SocatImage = version.FuncUtilsImage // NewInClusterDialer creates context dialer that will dial TCP connections via POD running in k8s cluster. // This is useful when accessing k8s services that are not exposed outside cluster (e.g. openshift image registry). diff --git a/pkg/k8s/persistent_volumes.go b/pkg/k8s/persistent_volumes.go index 266af6a260..d98ba62a73 100644 --- a/pkg/k8s/persistent_volumes.go +++ b/pkg/k8s/persistent_volumes.go @@ -21,6 +21,8 @@ import ( "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" k8sclientcmd "k8s.io/client-go/tools/clientcmd" + + "knative.dev/func/pkg/version" ) func GetPersistentVolumeClaim(ctx context.Context, name, namespaceOverride string) (*corev1.PersistentVolumeClaim, error) { @@ -72,7 +74,7 @@ func DeletePersistentVolumeClaims(ctx context.Context, namespaceOverride string, return client.CoreV1().PersistentVolumeClaims(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOptions) } -var TarImage = "ghcr.io/knative/func-utils:v2" +var TarImage = version.FuncUtilsImage // UploadToVolume uploads files (passed in form of tar stream) into volume. func UploadToVolume(ctx context.Context, content io.Reader, claimName, namespace string) error { diff --git a/pkg/pipelines/tekton/tasks.go b/pkg/pipelines/tekton/tasks.go index 04faf8456d..db4e843c9d 100644 --- a/pkg/pipelines/tekton/tasks.go +++ b/pkg/pipelines/tekton/tasks.go @@ -5,10 +5,12 @@ import ( _ "embed" "strings" "text/template" + + "knative.dev/func/pkg/version" ) var ( - FuncUtilImage = "ghcr.io/knative/func-utils:v2" + FuncUtilImage = version.FuncUtilsImage DeployerImage string ScaffoldImage string S2IImage string diff --git a/pkg/version/version.go b/pkg/version/version.go index 114c9658bc..40b19d3916 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -1,3 +1,10 @@ package version var Vers, Kver, Hash string + +// FuncUtilsImage is the func-utils image that func runs in the cluster: the +// minor in development on main, X.Y on release-X.Y. Builds can override it +// (FUNC_UTILS_IMG in the Makefile). When release-X.Y is cut, the workflow +// update-func-utils-image.yaml moves main to the next minor; see +// docs/func-utils-image.md. +const FuncUtilsImage = "ghcr.io/knative/func-utils:1.24"