From 4a71a01d1c1d84e79fb3a299a1c08fb68a5ac8cf Mon Sep 17 00:00:00 2001 From: Roman Dmytrenko Date: Wed, 20 May 2026 14:06:50 +0100 Subject: [PATCH] chore: bump Go to 1.26.3 and centralize CLI build in integration tests - Bump go to 1.26.3 and update GoBaseImage accordingly - Use go 1.25.0 for integration go test as lowest version for compatibility - Move CLI build out of individual tests into shared buildOpenFeatureCLI - Bump golangci-lint to v2.12.2 and fix linter issues - Fix test/README.md and update new-generator.md Signed-off-by: Roman Dmytrenko --- .github/workflows/pr-lint.yml | 4 +-- Makefile | 7 ++++-- go.mod | 2 +- internal/generators/csharp/csharp.go | 2 +- internal/generators/golang/golang.go | 2 +- internal/generators/java/java.go | 2 +- internal/generators/python/python.go | 2 +- internal/manifest/compare.go | 3 +-- internal/manifest/validate.go | 6 ++--- renovate.json | 31 +++++++++++++++++------ test/README.md | 2 +- test/integration/cmd/angular/run.go | 22 ++++++----------- test/integration/cmd/csharp/run.go | 25 +++++++++---------- test/integration/cmd/go/run.go | 37 ++++++++++------------------ test/integration/cmd/nodejs/run.go | 18 ++++++-------- test/integration/integration.go | 34 ++++++++++++++++++++----- test/new-generator.md | 37 +++++++++++++--------------- 17 files changed, 125 insertions(+), 111 deletions(-) diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 84c4c512..acb4f1ab 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -24,7 +24,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-go@v5 with: - go-version-file: 'go.mod' + go-version-file: "go.mod" - name: Check Go formatting run: | unformatted=$(go fmt ./...) @@ -48,4 +48,4 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: v2.7.1 + version: v2.12.2 diff --git a/Makefile b/Makefile index b889d858..0fe22e58 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,9 @@ help: @echo " fmt - Format Go code" @echo " ci - Run all CI checks locally (fmt, lint, test, verify-generate)" +# Tool versions +GOLANGCI_LINT_VERSION := v2.12.2 + # Build variables VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") @@ -105,7 +108,7 @@ lint: @echo "Running golangci-lint..." @if ! command -v golangci-lint &> /dev/null; then \ echo "Installing golangci-lint..."; \ - go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.1; \ + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION); \ fi @golangci-lint run @echo "Linting completed successfully!" @@ -115,7 +118,7 @@ lint-fix: @echo "Running golangci-lint with auto-fix..." @if ! command -v golangci-lint &> /dev/null; then \ echo "Installing golangci-lint..."; \ - go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.1; \ + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION); \ fi @golangci-lint run --fix @echo "Linting with auto-fix completed successfully!" diff --git a/go.mod b/go.mod index d7a07f37..a089bbfd 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/open-feature/cli -go 1.25.0 +go 1.26.3 require ( dagger.io/dagger v0.20.6 diff --git a/internal/generators/csharp/csharp.go b/internal/generators/csharp/csharp.go index 5ee9b226..d0cd7f61 100644 --- a/internal/generators/csharp/csharp.go +++ b/internal/generators/csharp/csharp.go @@ -69,7 +69,7 @@ func toCSharpDict(value any) string { for _, key := range keys { val := assertedMap[key] - builder.WriteString(fmt.Sprintf(".Set(%q, %s)", key, formatNestedValue(val))) + fmt.Fprintf(&builder, ".Set(%q, %s)", key, formatNestedValue(val)) } builder.WriteString(".Build())") diff --git a/internal/generators/golang/golang.go b/internal/generators/golang/golang.go index c45d0a81..3ef7944f 100644 --- a/internal/generators/golang/golang.go +++ b/internal/generators/golang/golang.go @@ -90,7 +90,7 @@ func toMapLiteral(value any) string { } val := assertedMap[key] - builder.WriteString(fmt.Sprintf(`%q: %s`, key, formatNestedValue(val))) + fmt.Fprintf(&builder, `%q: %s`, key, formatNestedValue(val)) } builder.WriteString("}") diff --git a/internal/generators/java/java.go b/internal/generators/java/java.go index 4ce8f734..e6a590bc 100644 --- a/internal/generators/java/java.go +++ b/internal/generators/java/java.go @@ -73,7 +73,7 @@ func toMapLiteral(value any) string { } val := assertedMap[key] - builder.WriteString(fmt.Sprintf("%q, %s", key, formatNestedValue(val))) + fmt.Fprintf(&builder, "%q, %s", key, formatNestedValue(val)) } builder.WriteString(")") diff --git a/internal/generators/python/python.go b/internal/generators/python/python.go index 8f84c060..2d9b41e3 100644 --- a/internal/generators/python/python.go +++ b/internal/generators/python/python.go @@ -98,7 +98,7 @@ func toPythonDict(value any) string { } val := assertedMap[key] - builder.WriteString(fmt.Sprintf(`%q: %s`, key, formatNestedValue(val))) + fmt.Fprintf(&builder, `%q: %s`, key, formatNestedValue(val)) } builder.WriteString("}") diff --git a/internal/manifest/compare.go b/internal/manifest/compare.go index 31173baa..c8e71f48 100644 --- a/internal/manifest/compare.go +++ b/internal/manifest/compare.go @@ -68,8 +68,7 @@ func getKnownFlagProperties() map[string]bool { // Extract fields from BaseFlag struct t := reflect.TypeFor[BaseFlag]() - for i := range t.NumField() { - field := t.Field(i) + for field := range t.Fields() { jsonTag := field.Tag.Get("json") if jsonTag != "" { // Extract field name from json tag (e.g., "flagType,omitempty" -> "flagType") diff --git a/internal/manifest/validate.go b/internal/manifest/validate.go index c09bd34e..873796c2 100644 --- a/internal/manifest/validate.go +++ b/internal/manifest/validate.go @@ -199,12 +199,10 @@ func FormatValidationError(issues []ValidationError) string { if flagType == "" { flagType = "missing" } - sb.WriteString(fmt.Sprintf( - "- flagType: %s\n flagPath: %s\n errors:\n ~ %s\n \tSuggestions:\n \t- flagType: boolean\n \t- defaultValue: true\n\n", + fmt.Fprintf(&sb, "- flagType: %s\n flagPath: %s\n errors:\n ~ %s\n \tSuggestions:\n \t- flagType: boolean\n \t- defaultValue: true\n\n", flagType, path, - strings.Join(entry.messages, "\n ~ "), - )) + strings.Join(entry.messages, "\n ~ ")) } return sb.String() } diff --git a/renovate.json b/renovate.json index 466fa1d2..2cd0e66b 100644 --- a/renovate.json +++ b/renovate.json @@ -5,9 +5,7 @@ { "customType": "regex", "description": "Update the GoBaseImage constant when a newer golang Docker image is available", - "managerFilePatterns": [ - "test/integration/integration.go" - ], + "managerFilePatterns": ["test/integration/integration.go"], "matchStrings": [ "GoBaseImage = \"golang:(?\\d+\\.\\d+(?:\\.\\d+)?)-alpine\"" ], @@ -18,10 +16,7 @@ { "customType": "regex", "description": "Update minimum Go version mentioned in README and CONTRIBUTING", - "managerFilePatterns": [ - "README.md", - "CONTRIBUTING.md" - ], + "managerFilePatterns": ["README.md", "CONTRIBUTING.md"], "matchStrings": [ "Go >= (?\\d+\\.\\d+(?:\\.\\d+)?)", "Go (?\\d+\\.\\d+(?:\\.\\d+)?) or later" @@ -29,6 +24,24 @@ "depNameTemplate": "go", "datasourceTemplate": "golang-version", "extractVersionTemplate": "^(?\\d+\\.\\d+)" + }, + { + "customType": "regex", + "managerFilePatterns": ["Makefile"], + "matchStrings": [ + "GOLANGCI_LINT_VERSION\\s*:?=\\s*(?v?\\d+\\.\\d+\\.\\d+)" + ], + "depNameTemplate": "golangci/golangci-lint", + "datasourceTemplate": "github-releases" + }, + { + "customType": "regex", + "managerFilePatterns": [".github/workflows/pr-lint.yml"], + "matchStrings": [ + "uses:\\s*golangci/golangci-lint-action@v\\d+[\\s\\S]*?version:\\s*(?v?\\d+\\.\\d+\\.\\d+)" + ], + "depNameTemplate": "golangci/golangci-lint", + "datasourceTemplate": "github-releases" } ], "packageRules": [ @@ -37,6 +50,10 @@ "matchDatasources": ["docker", "golang-version"], "matchPackageNames": ["golang", "go"], "groupName": "go toolchain" + }, + { + "matchDepNames": ["golangci/golangci-lint"], + "groupName": "golangci-lint" } ] } diff --git a/test/README.md b/test/README.md index 6d11ebbd..afa159d7 100644 --- a/test/README.md +++ b/test/README.md @@ -25,7 +25,7 @@ make test-integration ```bash # For C# tests -make test-csharp-dagger +make test-integration-csharp ``` ## Adding a New Integration Test diff --git a/test/integration/cmd/angular/run.go b/test/integration/cmd/angular/run.go index 8da1ac44..175571a7 100644 --- a/test/integration/cmd/angular/run.go +++ b/test/integration/cmd/angular/run.go @@ -11,21 +11,18 @@ import ( ) type Test struct { - ProjectDir string + projectDir string TestDir string } func New(projectDir, testDir string) *Test { return &Test{ - ProjectDir: projectDir, + projectDir: projectDir, TestDir: testDir, } } -func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) { - // Mount the project source - source := client.Host().Directory(t.ProjectDir) - +func (t *Test) Run(ctx context.Context, client *dagger.Client, cli *dagger.Container) (*dagger.Container, error) { // Mount the test files testFiles := client.Host().Directory(t.TestDir, dagger.HostDirectoryOpts{ Include: []string{ @@ -37,14 +34,7 @@ func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Containe }, }) - // Build the CLI in a Go container - cli := client.Container(). - From(integration.GoBaseImage). - WithDirectory("/src", source). - WithWorkdir("/src"). - WithExec([]string{"go", "build", "-o", "cli", "./cmd/openfeature"}) - - // Generate the Angular code + // Generate the Angular code using the pre-built CLI generated := cli.WithExec([]string{ "./cli", "generate", "angular", "--manifest=/src/sample/sample_manifest.json", @@ -76,6 +66,10 @@ func (t *Test) Name() string { return "angular" } +func (t *Test) ProjectDir() string { + return t.projectDir +} + func main() { ctx := context.Background() diff --git a/test/integration/cmd/csharp/run.go b/test/integration/cmd/csharp/run.go index a2f8337a..7e96e390 100644 --- a/test/integration/cmd/csharp/run.go +++ b/test/integration/cmd/csharp/run.go @@ -12,8 +12,8 @@ import ( // Test implements the integration test for the C# generator type Test struct { - // ProjectDir is the absolute path to the root of the project - ProjectDir string + // projectDir is the absolute path to the root of the project + projectDir string // TestDir is the absolute path to the test directory TestDir string } @@ -21,27 +21,19 @@ type Test struct { // New creates a new Test func New(projectDir, testDir string) *Test { return &Test{ - ProjectDir: projectDir, + projectDir: projectDir, TestDir: testDir, } } // Run executes the C# integration test using Dagger -func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) { - // Source code container - source := client.Host().Directory(t.ProjectDir) +func (t *Test) Run(ctx context.Context, client *dagger.Client, cli *dagger.Container) (*dagger.Container, error) { + // Test source files testFiles := client.Host().Directory(t.TestDir, dagger.HostDirectoryOpts{ Include: []string{"CompileTest.csproj", "Program.cs"}, }) - // Build the CLI - cli := client.Container(). - From(integration.GoBaseImage). - WithDirectory("/src", source). - WithWorkdir("/src"). - WithExec([]string{"go", "build", "-o", "cli", "./cmd/openfeature"}) - - // Generate C# client + // Generate C# client using the pre-built CLI generated := cli.WithExec([]string{ "./cli", "generate", "csharp", "--manifest=/src/sample/sample_manifest.json", @@ -70,6 +62,11 @@ func (t *Test) Name() string { return "csharp" } +// ProjectDir returns the absolute path to the project root +func (t *Test) ProjectDir() string { + return t.projectDir +} + func main() { ctx := context.Background() diff --git a/test/integration/cmd/go/run.go b/test/integration/cmd/go/run.go index 8c9d33e4..1ebb41d4 100644 --- a/test/integration/cmd/go/run.go +++ b/test/integration/cmd/go/run.go @@ -12,8 +12,8 @@ import ( // Test implements the integration test for the Go generator type Test struct { - // ProjectDir is the absolute path to the root of the project - ProjectDir string + // projectDir is the absolute path to the root of the project + projectDir string // TestDir is the absolute path to the test directory TestDir string } @@ -21,36 +21,19 @@ type Test struct { // New creates a new Test func New(projectDir, testDir string) *Test { return &Test{ - ProjectDir: projectDir, + projectDir: projectDir, TestDir: testDir, } } // Run executes the Go integration test using Dagger -func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) { - // Source code container - source := client.Host().Directory(t.ProjectDir) +func (t *Test) Run(ctx context.Context, client *dagger.Client, cli *dagger.Container) (*dagger.Container, error) { + // Test source files testFiles := client.Host().Directory(t.TestDir, dagger.HostDirectoryOpts{ Include: []string{"test.go", "go.mod"}, }) - // goBase returns a Go container with git installed, used both to build - // the CLI and to compile the generated client against the test fixture. - goBase := func() *dagger.Container { - return client.Container(). - From(integration.GoBaseImage). - WithExec([]string{"apk", "add", "--no-cache", "git"}) - } - - // Build the CLI - cli := goBase(). - WithDirectory("/src", source). - WithWorkdir("/src"). - WithExec([]string{"go", "mod", "tidy"}). - WithExec([]string{"go", "mod", "download"}). - WithExec([]string{"go", "build", "-o", "cli", "./cmd/openfeature"}) - - // Generate Go client + // Generate Go client using the pre-built CLI generated := cli.WithExec([]string{ "./cli", "generate", "go", "--manifest=/src/sample/sample_manifest.json", @@ -62,7 +45,8 @@ func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Containe generatedFiles := generated.Directory("/tmp/generated") // Test Go compilation with the generated files - goContainer := goBase(). + goContainer := client.Container(). + From(integration.GoGenerateMinCompatImage). WithWorkdir("/app"). WithDirectory("/app", testFiles). WithDirectory("/app/openfeature", generatedFiles). @@ -78,6 +62,11 @@ func (t *Test) Name() string { return "go" } +// ProjectDir returns the absolute path to the project root +func (t *Test) ProjectDir() string { + return t.projectDir +} + func main() { ctx := context.Background() diff --git a/test/integration/cmd/nodejs/run.go b/test/integration/cmd/nodejs/run.go index e4cda9f4..9468178f 100644 --- a/test/integration/cmd/nodejs/run.go +++ b/test/integration/cmd/nodejs/run.go @@ -11,29 +11,22 @@ import ( ) type Test struct { - ProjectDir string + projectDir string TestDir string } func New(projectDir, testDir string) *Test { return &Test{ - ProjectDir: projectDir, + projectDir: projectDir, TestDir: testDir, } } -func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) { - source := client.Host().Directory(t.ProjectDir) +func (t *Test) Run(ctx context.Context, client *dagger.Client, cli *dagger.Container) (*dagger.Container, error) { testFiles := client.Host().Directory(t.TestDir, dagger.HostDirectoryOpts{ Include: []string{"package.json", "test.ts"}, }) - cli := client.Container(). - From(integration.GoBaseImage). - WithDirectory("/src", source). - WithWorkdir("/src"). - WithExec([]string{"go", "build", "-o", "cli", "./cmd/openfeature"}) - generated := cli.WithExec([]string{ "./cli", "generate", "nodejs", "--manifest=/src/sample/sample_manifest.json", @@ -58,6 +51,10 @@ func (t *Test) Name() string { return "nodejs" } +func (t *Test) ProjectDir() string { + return t.projectDir +} + func main() { ctx := context.Background() @@ -72,6 +69,7 @@ func main() { fmt.Fprintf(os.Stderr, "Failed to get test dir: %v\n", err) os.Exit(1) } + test := New(projectDir, testDir) if err := integration.RunTest(ctx, test); err != nil { diff --git a/test/integration/integration.go b/test/integration/integration.go index 48609b55..97bd8db4 100644 --- a/test/integration/integration.go +++ b/test/integration/integration.go @@ -11,17 +11,35 @@ import ( // GoBaseImage is the Go container image used to build the CLI inside // integration test pipelines. Centralized here so the version is bumped // in a single place when go.mod's required Go version changes. -const GoBaseImage = "golang:1.25-alpine" +const GoBaseImage = "golang:1.26-alpine" + +// GoGenerateMinCompatImage is the minimum Go version the generated Go client +// code must compile against. Used to verify forward compatibility of the +// generate output. +const GoGenerateMinCompatImage = "golang:1.25-alpine" // Test defines the interface for all integration tests type Test interface { - // Run executes the integration test with the given Dagger client - Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) + // Run executes the integration test with the given Dagger client and pre-built CLI container + Run(ctx context.Context, client *dagger.Client, cli *dagger.Container) (*dagger.Container, error) // Name returns the name of the integration test Name() string + // ProjectDir returns the absolute path to the project root + ProjectDir() string +} + +// buildOpenFeatureCLI compiles the OpenFeature CLI binary inside a Dagger container. +// The returned container has the binary at /src/cli and the project source mounted at /src. +func buildOpenFeatureCLI(client *dagger.Client, source *dagger.Directory) *dagger.Container { + return client.Container(). + From(GoBaseImage). + WithDirectory("/src", source). + WithWorkdir("/src"). + WithExec([]string{"go", "mod", "download"}). + WithExec([]string{"go", "build", "-o", "cli", "./cmd/openfeature"}) } -// RunTest runs a single integration test +// RunTest builds the CLI once and runs a single integration test func RunTest(ctx context.Context, test Test) error { // Initialize Dagger client client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout)) @@ -32,8 +50,12 @@ func RunTest(ctx context.Context, test Test) error { fmt.Printf("=== Running %s integration test ===\n", test.Name()) - // Run the integration test - container, err := test.Run(ctx, client) + // Build the CLI once (shared across all tests) + source := client.Host().Directory(test.ProjectDir()) + cli := buildOpenFeatureCLI(client, source) + + // Run the integration test with the pre-built CLI + container, err := test.Run(ctx, client, cli) if err != nil { return fmt.Errorf("failed to run %s integration test: %w", test.Name(), err) } diff --git a/test/new-generator.md b/test/new-generator.md index 5cce1282..f35d987c 100644 --- a/test/new-generator.md +++ b/test/new-generator.md @@ -32,42 +32,33 @@ import ( "fmt" "os" "path/filepath" - + "dagger.io/dagger" "github.com/open-feature/cli/test/integration" ) // Test implements the integration test for the Python generator type Test struct { - ProjectDir string + projectDir string TestDir string } // New creates a new Test func New(projectDir, testDir string) *Test { return &Test{ - ProjectDir: projectDir, + projectDir: projectDir, TestDir: testDir, } } // Run executes the Python integration test -func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) { - // Source code container - source := client.Host().Directory(t.ProjectDir) +// The CLI is pre-built and provided by the framework. +func (t *Test) Run(ctx context.Context, client *dagger.Client, cli *dagger.Container) (*dagger.Container, error) { testFiles := client.Host().Directory(t.TestDir, dagger.HostDirectoryOpts{ Include: []string{"test_openfeature.py", "requirements.txt"}, }) - // Build the CLI. Use integration.GoBaseImage so the Go version is - // bumped in a single place when go.mod changes. - cli := client.Container(). - From(integration.GoBaseImage). - WithDirectory("/src", source). - WithWorkdir("/src"). - WithExec([]string{"go", "build", "-o", "cli"}) - - // Generate Python client + // Generate Python client using the pre-built CLI generated := cli.WithExec([]string{ "./cli", "generate", "python", "--manifest=/src/sample/sample_manifest.json", @@ -95,6 +86,11 @@ func (t *Test) Name() string { return "python" } +// ProjectDir returns the absolute path to the project root +func (t *Test) ProjectDir() string { + return t.projectDir +} + func main() { ctx := context.Background() @@ -138,7 +134,7 @@ import ( func main() { // Run the generator-specific tests fmt.Println("=== Running all integration tests ===") - + // Run the C# integration test csharpCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/csharp") csharpCmd.Stdout = os.Stdout @@ -147,7 +143,7 @@ func main() { fmt.Fprintf(os.Stderr, "Error running C# integration test: %v\n", err) os.Exit(1) } - + // Run the Python integration test pythonCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/python") pythonCmd.Stdout = os.Stdout @@ -156,9 +152,9 @@ func main() { fmt.Fprintf(os.Stderr, "Error running Python integration test: %v\n", err) os.Exit(1) } - + // Add more tests here as they are available - + fmt.Println("=== All integration tests passed successfully ===") } ``` @@ -188,4 +184,5 @@ test-python-dagger: ## Step 5: Update the documentation -Update `test/README.md` to include your new test. \ No newline at end of file +Update `test/README.md` to include your new test. +