From 0cbe88ecf7347b3d4dd1b8cdbca075f278c0656a Mon Sep 17 00:00:00 2001 From: Pedro Almeida Date: Tue, 1 Sep 2026 13:00:55 +0100 Subject: [PATCH 1/2] fix: source CI workflow func version from build The func CLI version in generated GitHub workflows was hardcoded to a specific release (knative-v1.22.0), meaning it would silently go stale as new releases shipped. FuncCliVersion is now sourced from version.Kver, which is set via ldflags at build time to the current release tag. The git describe suffix (e.g. "-22-g2871d3bf") is stripped so that dev builds between releases still emit a real, installable tag rather than a ref that does not exist on GitHub releases. When version.Kver is empty (plain go build without ldflags, or the package used as a library), the field falls back to DefaultFuncCliVersion. --- cmd/config_ci.go | 7 +++++++ cmd/config_ci_int_test.go | 2 +- pkg/ci/github/generator_test.go | 5 +++-- pkg/ci/github/workflow.go | 15 ++++++++++----- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/cmd/config_ci.go b/cmd/config_ci.go index 2c51dbd803..155458685d 100644 --- a/cmd/config_ci.go +++ b/cmd/config_ci.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "io" + "regexp" "strings" "github.com/ory/viper" @@ -11,8 +12,13 @@ import ( "knative.dev/func/cmd/common" "knative.dev/func/pkg/ci/github" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/version" ) +// gitDescribeSuffix matches the "-N-gHASH" suffix produced by git describe +// on commits that are not exactly on a release tag. +var gitDescribeSuffix = regexp.MustCompile(`-\d+-g[0-9a-f]+$`) + // ciGeneratorFactory creates a CIGenerator from resolved CLI flag values. // Using a factory allows tests to capture the resolved config and inject // a mock generator without running the real implementation. @@ -250,6 +256,7 @@ func newConfigAndLoadedFunc( RegistryUserVar: viper.GetString(registryUserVariableNameFlag), RegistryPassSecret: viper.GetString(registryPassSecretNameFlag), RegistryUrlVar: viper.GetString(registryUrlVariableNameFlag), + FuncCliVersion: gitDescribeSuffix.ReplaceAllString(version.Kver, ""), RegistryLogin: viper.GetBool(registryLoginFlag), SelfHostedRunner: viper.GetBool(selfHostedRunnerFlag), RemoteBuild: viper.GetBool(remoteBuildFlag), diff --git a/cmd/config_ci_int_test.go b/cmd/config_ci_int_test.go index e606eec6a9..1831e51dc5 100644 --- a/cmd/config_ci_int_test.go +++ b/cmd/config_ci_int_test.go @@ -231,7 +231,7 @@ func assertDefaultWorkflow(t *testing.T, actualGw string) { assert.Assert(t, yamlContains(actualGw, "Install func cli")) assert.Assert(t, yamlContains(actualGw, "functions-dev/action@main")) - assert.Assert(t, yamlContains(actualGw, "version: knative-v1.22.0")) + assert.Assert(t, yamlContains(actualGw, "version: "+github.DefaultFuncCliVersion)) assert.Assert(t, yamlContains(actualGw, "name: func")) assert.Assert(t, yamlContains(actualGw, "Deploy function")) diff --git a/pkg/ci/github/generator_test.go b/pkg/ci/github/generator_test.go index fb458431f7..6f8902a96a 100644 --- a/pkg/ci/github/generator_test.go +++ b/pkg/ci/github/generator_test.go @@ -566,6 +566,7 @@ func defaultOpts() opts { RegistryUserVar: github.DefaultRegistryUserVariableName, RegistryPassSecret: github.DefaultRegistryPassSecretName, RegistryUrlVar: github.DefaultRegistryUrlVariableName, + FuncCliVersion: github.DefaultFuncCliVersion, RegistryLogin: github.DefaultRegistryLogin, SelfHostedRunner: github.DefaultSelfHostedRunner, RemoteBuild: github.DefaultRemoteBuild, @@ -643,7 +644,7 @@ func assertDefaultWorkflow(t *testing.T, actualGw string) { assert.Assert(t, yamlContains(actualGw, "Install func cli")) assert.Assert(t, yamlContains(actualGw, "functions-dev/action@main")) - assert.Assert(t, yamlContains(actualGw, "version: knative-v1.22.0")) + assert.Assert(t, yamlContains(actualGw, "version: "+github.DefaultFuncCliVersion)) assert.Assert(t, yamlContains(actualGw, "name: func")) assert.Assert(t, yamlContains(actualGw, "Deploy function")) @@ -676,7 +677,7 @@ func assertSemiDefaultWorkflow(t *testing.T, actualGw string) { assert.Assert(t, yamlContains(actualGw, "Install func cli")) assert.Assert(t, yamlContains(actualGw, "functions-dev/action@main")) - assert.Assert(t, yamlContains(actualGw, "version: knative-v1.22.0")) + assert.Assert(t, yamlContains(actualGw, "version: "+github.DefaultFuncCliVersion)) assert.Assert(t, yamlContains(actualGw, "name: func")) assert.Assert(t, yamlContains(actualGw, "Deploy function")) diff --git a/pkg/ci/github/workflow.go b/pkg/ci/github/workflow.go index 85403760e2..e4eae9d90d 100644 --- a/pkg/ci/github/workflow.go +++ b/pkg/ci/github/workflow.go @@ -14,7 +14,7 @@ import ( var ErrWorkflowExists = errors.New("existing GitHub workflow detected, overwrite using the --force option") const ( - defaultFuncCliVersion = "knative-v1.22.0" + DefaultFuncCliVersion = "knative-v1.23.0" DefaultPlatform = "github" DefaultGitHubWorkflowDir = ".github/workflows" DefaultGitHubWorkflowFilename = "func-deploy.yaml" @@ -45,7 +45,8 @@ type WorkflowConfig struct { RegistryLoginUrlVar, RegistryUserVar, RegistryPassSecret, - RegistryUrlVar string + RegistryUrlVar, + FuncCliVersion string RegistryLogin, SelfHostedRunner, RemoteBuild, @@ -74,6 +75,7 @@ func defaultWorkflowConfig() WorkflowConfig { RegistryUserVar: DefaultRegistryUserVariableName, RegistryPassSecret: DefaultRegistryPassSecretName, RegistryUrlVar: DefaultRegistryUrlVariableName, + FuncCliVersion: DefaultFuncCliVersion, RegistryLogin: DefaultRegistryLogin, SelfHostedRunner: DefaultSelfHostedRunner, RemoteBuild: DefaultRemoteBuild, @@ -111,6 +113,9 @@ func setEmptyFieldsToDefaults(defaults WorkflowConfig) WorkflowConfig { if defaults.RegistryUrlVar == "" { defaults.RegistryUrlVar = DefaultRegistryUrlVariableName } + if defaults.FuncCliVersion == "" { + defaults.FuncCliVersion = DefaultFuncCliVersion + } return defaults } @@ -149,7 +154,7 @@ func newGitHubWorkflow(cfg WorkflowConfig, runtime string, messageWriter io.Writ steps = createRuntimeTestStep(cfg, runtime, messageWriter, steps) steps = createK8ContextStep(cfg, steps) steps = createRegistryLoginStep(cfg, steps) - steps = createFuncCLIInstallStep(steps) + steps = createFuncCLIInstallStep(cfg, steps) steps, err := createFuncDeployStep(cfg, runtime, steps) if err != nil { @@ -223,10 +228,10 @@ func createRegistryLoginStep(opts WorkflowConfig, steps []step) []step { return append(steps, *loginToContainerRegistry) } -func createFuncCLIInstallStep(steps []step) []step { +func createFuncCLIInstallStep(cfg WorkflowConfig, steps []step) []step { installFuncCli := newStep("Install func cli"). withUses("functions-dev/action@main"). - withActionConfig("version", defaultFuncCliVersion). + withActionConfig("version", cfg.FuncCliVersion). withActionConfig("name", "func") return append(steps, *installFuncCli) From 5d9ec295007fd917f70c597a6fd7e9c3d5a5d849 Mon Sep 17 00:00:00 2001 From: Pedro Almeida Date: Tue, 1 Sep 2026 17:17:02 +0100 Subject: [PATCH 2/2] fix: honor func.yaml builder in CI workflow generation The generated GitHub Actions workflow always determined FUNC_BUILDER from the runtime, silently ignoring any builder configured in func.yaml. This made it impossible for library consumers to force a specific builder without patching the generated YAML after the fact. f.Build.Builder is now threaded through the workflow generator. When set, it takes precedence over the runtime default. Runtime validation still runs regardless, so unsupported runtimes are rejected even when an explicit builder is provided. Explicit builder values are also validated against the known set. --- pkg/ci/github/common.go | 24 +++++++++++- pkg/ci/github/generator.go | 4 +- pkg/ci/github/generator_test.go | 65 +++++++++++++++++++++++++++++++++ pkg/ci/github/printer.go | 4 +- pkg/ci/github/printer_test.go | 8 ++-- pkg/ci/github/workflow.go | 8 ++-- pkg/ci/github/workflow_test.go | 2 +- 7 files changed, 100 insertions(+), 15 deletions(-) diff --git a/pkg/ci/github/common.go b/pkg/ci/github/common.go index 5fd105c86c..d41a95a874 100644 --- a/pkg/ci/github/common.go +++ b/pkg/ci/github/common.go @@ -1,8 +1,28 @@ package github -import "fmt" +import ( + "fmt" + "slices" -func determineBuilder(runtime string, remote bool) (string, error) { + "knative.dev/func/pkg/builders" +) + +func determineBuilder(runtime, builder string, remote bool) (string, error) { + defaultBuilder, err := runtimeDefaultBuilder(runtime, remote) + if err != nil { + return "", err + } + if builder == "" { + return defaultBuilder, nil + } + all := builders.All() + if !slices.Contains(all, builder) { + return "", builders.ErrUnknownBuilder{Name: builder, Known: all} + } + return builder, nil +} + +func runtimeDefaultBuilder(runtime string, remote bool) (string, error) { switch runtime { case "go": if remote { diff --git a/pkg/ci/github/generator.go b/pkg/ci/github/generator.go index 6d5ed696d2..9278ade3b0 100644 --- a/pkg/ci/github/generator.go +++ b/pkg/ci/github/generator.go @@ -78,7 +78,7 @@ func (g *workflowGenerator) Generate(ctx context.Context, f fn.Function) error { return fmt.Errorf("function root path can not be empty") } - githubWorkflow, err := newGitHubWorkflow(g.cfg, f.Runtime, g.messageWriter) + githubWorkflow, err := newGitHubWorkflow(g.cfg, f.Runtime, f.Build.Builder, g.messageWriter) if err != nil { return err } @@ -89,7 +89,7 @@ func (g *workflowGenerator) Generate(ctx context.Context, f fn.Function) error { if g.verbose { // best-effort user message; errors are non-critical - _ = PrintConfiguration(g.cfg, f.Runtime, g.messageWriter) + _ = PrintConfiguration(g.cfg, f.Runtime, f.Build.Builder, g.messageWriter) return nil } diff --git a/pkg/ci/github/generator_test.go b/pkg/ci/github/generator_test.go index 6f8902a96a..f52ce368e4 100644 --- a/pkg/ci/github/generator_test.go +++ b/pkg/ci/github/generator_test.go @@ -518,6 +518,46 @@ func TestCIGenerator_BuilderForRuntime(t *testing.T) { } } +func TestCIGenerator_BuilderConfiguredInFuncYaml(t *testing.T) { + testCases := []struct { + name string + runtime string + builder string + }{ + { + name: "node function with s2i builder configured", + runtime: "node", + builder: "s2i", + }, + { + name: "python function with pack builder configured", + runtime: "python", + builder: "pack", + }, + { + name: "quarkus function with s2i builder configured", + runtime: "quarkus", + builder: "s2i", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // GIVEN + opts := defaultOpts() + opts.goFn.Runtime = tc.runtime + opts.goFn.Build.Builder = tc.builder + + // WHEN + result := runGenerateWorkflow(t, opts) + + // THEN + assert.NilError(t, result.executeErr) + assert.Assert(t, strings.Contains(result.gwYamlString, "FUNC_BUILDER: "+tc.builder)) + }) + } +} + func TestCIGenerator_BuilderForRuntimeError(t *testing.T) { // GIVEN opts := defaultOpts() @@ -530,6 +570,31 @@ func TestCIGenerator_BuilderForRuntimeError(t *testing.T) { assert.Error(t, result.executeErr, "no builder support for runtime: zig") } +func TestCIGenerator_UnsupportedRuntimeErrorsEvenWithExplicitBuilder(t *testing.T) { + // GIVEN + opts := defaultOpts() + opts.goFn.Runtime = "zig" + opts.goFn.Build.Builder = "pack" + + // WHEN + result := runGenerateWorkflow(t, opts) + + // THEN + assert.Error(t, result.executeErr, "no builder support for runtime: zig") +} + +func TestCIGenerator_InvalidBuilderErrors(t *testing.T) { + // GIVEN + opts := defaultOpts() + opts.goFn.Build.Builder = "pakc" + + // WHEN + result := runGenerateWorkflow(t, opts) + + // THEN + assert.Error(t, result.executeErr, `"pakc" is not a known builder. Available builders are "host", "pack" and "s2i"`) +} + // --------------------- // END: Broad Unit Tests diff --git a/pkg/ci/github/printer.go b/pkg/ci/github/printer.go index 962801d373..4412c77709 100644 --- a/pkg/ci/github/printer.go +++ b/pkg/ci/github/printer.go @@ -49,8 +49,8 @@ Create the following Secret on github.com: %s ` ) -func PrintConfiguration(cfg WorkflowConfig, runtime string, w io.Writer) error { - builder, err := determineBuilder(runtime, cfg.RemoteBuild) +func PrintConfiguration(cfg WorkflowConfig, runtime, builder string, w io.Writer) error { + builder, err := determineBuilder(runtime, builder, cfg.RemoteBuild) if err != nil { return err } diff --git a/pkg/ci/github/printer_test.go b/pkg/ci/github/printer_test.go index 7b718b78fa..f06c30763c 100644 --- a/pkg/ci/github/printer_test.go +++ b/pkg/ci/github/printer_test.go @@ -29,7 +29,7 @@ func TestPrintConfigurationFail(t *testing.T) { t.Run("main layout write fails", func(t *testing.T) { w := &failWriter{failOnCall: 1, err: errWrite} - err := PrintConfiguration(WorkflowConfig{}, "go", w) + err := PrintConfiguration(WorkflowConfig{}, "go", "", w) assert.Error(t, err, errWrite.Error()) }) @@ -38,7 +38,7 @@ func TestPrintConfigurationFail(t *testing.T) { w := &failWriter{failOnCall: 2, err: errWrite} opts := WorkflowConfig{RegistryLogin: true} - err := PrintConfiguration(opts, "go", w) + err := PrintConfiguration(opts, "go", "", w) assert.Error(t, err, errWrite.Error()) }) @@ -47,7 +47,7 @@ func TestPrintConfigurationFail(t *testing.T) { w := &failWriter{failOnCall: 2, err: errWrite} opts := WorkflowConfig{RegistryLogin: false} - err := PrintConfiguration(opts, "go", w) + err := PrintConfiguration(opts, "go", "", w) assert.Error(t, err, errWrite.Error()) }) @@ -56,7 +56,7 @@ func TestPrintConfigurationFail(t *testing.T) { runtime := "ruby" expectedErr := fmt.Errorf("no builder support for runtime: %s", runtime) - err := PrintConfiguration(WorkflowConfig{}, runtime, &bytes.Buffer{}) + err := PrintConfiguration(WorkflowConfig{}, runtime, "", &bytes.Buffer{}) assert.Error(t, err, expectedErr.Error()) }) diff --git a/pkg/ci/github/workflow.go b/pkg/ci/github/workflow.go index e4eae9d90d..fd20aed1f7 100644 --- a/pkg/ci/github/workflow.go +++ b/pkg/ci/github/workflow.go @@ -148,7 +148,7 @@ type step struct { With map[string]string `yaml:"with,omitempty"` } -func newGitHubWorkflow(cfg WorkflowConfig, runtime string, messageWriter io.Writer) (*workflow, error) { +func newGitHubWorkflow(cfg WorkflowConfig, runtime, builder string, messageWriter io.Writer) (*workflow, error) { var steps []step steps = createCheckoutStep(steps) steps = createRuntimeTestStep(cfg, runtime, messageWriter, steps) @@ -156,7 +156,7 @@ func newGitHubWorkflow(cfg WorkflowConfig, runtime string, messageWriter io.Writ steps = createRegistryLoginStep(cfg, steps) steps = createFuncCLIInstallStep(cfg, steps) - steps, err := createFuncDeployStep(cfg, runtime, steps) + steps, err := createFuncDeployStep(cfg, runtime, builder, steps) if err != nil { return nil, err } @@ -237,11 +237,11 @@ func createFuncCLIInstallStep(cfg WorkflowConfig, steps []step) []step { return append(steps, *installFuncCli) } -func createFuncDeployStep(opts WorkflowConfig, runtime string, steps []step) ([]step, error) { +func createFuncDeployStep(opts WorkflowConfig, runtime, builder string, steps []step) ([]step, error) { deployFuncStep := newStep("Deploy function"). withEnv("FUNC_VERBOSE", "true") - builder, err := determineBuilder(runtime, opts.RemoteBuild) + builder, err := determineBuilder(runtime, builder, opts.RemoteBuild) if err != nil { return nil, err } diff --git a/pkg/ci/github/workflow_test.go b/pkg/ci/github/workflow_test.go index 9c1fca1708..1a9e943d3c 100644 --- a/pkg/ci/github/workflow_test.go +++ b/pkg/ci/github/workflow_test.go @@ -18,7 +18,7 @@ func TestGitHubWorkflow_Export(t *testing.T) { // WHEN opts := WorkflowConfig{} - gw, workflowErr := newGitHubWorkflow(WorkflowConfig{}, "go", &bytes.Buffer{}) + gw, workflowErr := newGitHubWorkflow(WorkflowConfig{}, "go", "", &bytes.Buffer{}) assert.NilError(t, workflowErr, "unexpected error when creating GitHub Workflow") exportErr := gw.Export(opts.fnGitHubWorkflowFilepath("path/to/functions"), bufferWriter, true, &bytes.Buffer{})