From f4c70c9ca41c33358323bb3c8bbb22012052cdcd Mon Sep 17 00:00:00 2001 From: stackkit-ci Date: Mon, 26 Oct 2026 19:28:34 +0000 Subject: [PATCH 1/3] fix: align CI lint with Go 1.25+ module bumps golangci-lint v2.1.6 is built with Go 1.24 and refuses the go 1.25 and 1.26 directives that the open module bumps write into go.mod. Pin v2.13.0, which is built with Go 1.27. Use reflect.Pointer so the Go 1.25 inline check stays quiet, build telemetry resources on the SDK schema so the otel 1.46 bump can merge, and assert shutdown cancellation directly instead of racing a channel. Co-authored-by: froppa-agent --- .github/workflows/test.yml | 2 +- Makefile | 2 +- kits/configkit/discovery.go | 18 +++++++++--------- kits/shutdownkit/fx_test.go | 13 +------------ kits/telemetry/fx.go | 13 +++++++++---- kits/telemetry/fx_test.go | 3 +++ mise.toml | 2 +- 7 files changed, 25 insertions(+), 28 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2e099f1..9d316f7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: - name: Run Lint uses: golangci/golangci-lint-action@v9 with: - version: v2.1.6 + version: v2.13.0 test: name: Test diff --git a/Makefile b/Makefile index 3cc3fe4..ca189fa 100644 --- a/Makefile +++ b/Makefile @@ -68,6 +68,6 @@ clean: ## Clean up build artifacts and coverage reports. tools: ## Install required development tools. @echo "==> Installing development tools..." - @go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 + @go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.0 .PHONY: all lint update-deps test tidy fmt vet ci help cover-html clean tools diff --git a/kits/configkit/discovery.go b/kits/configkit/discovery.go index 3fb9c4c..4c4c72b 100644 --- a/kits/configkit/discovery.go +++ b/kits/configkit/discovery.go @@ -77,7 +77,7 @@ func RegisterRequirementType(key string, tt reflect.Type) { func registerRequirementType(key string, tt reflect.Type) { // Unwrap pointers for introspection. base := tt - for base.Kind() == reflect.Ptr { + for base.Kind() == reflect.Pointer { base = base.Elem() } @@ -161,7 +161,7 @@ func Spec(req Requirement) ([]FieldSpec, error) { } func walkStruct(t reflect.Type, prefix string, out *[]FieldSpec) { - if t.Kind() == reflect.Ptr { + if t.Kind() == reflect.Pointer { t = t.Elem() } if t.Kind() != reflect.Struct { @@ -193,7 +193,7 @@ func walkStruct(t reflect.Type, prefix string, out *[]FieldSpec) { // If struct, recurse; otherwise record leaf ft := f.Type base := ft - for base.Kind() == reflect.Ptr { + for base.Kind() == reflect.Pointer { base = base.Elem() } switch base.Kind() { @@ -280,7 +280,7 @@ func RegisterKnown(key string, sample any) { if t == nil { return } - for t.Kind() == reflect.Ptr { + for t.Kind() == reflect.Pointer { t = t.Elem() } knownMu.Lock() @@ -443,7 +443,7 @@ func extractFieldAndRule(s string) (field, rule string) { // yamlPathFromStructNS maps a validator StructNamespace (Go struct path) to a yaml-like path. func yamlPathFromStructNS(ns string, root reflect.Type) string { // Unwrap pointer - for root.Kind() == reflect.Ptr { + for root.Kind() == reflect.Pointer { root = root.Elem() } if root.Kind() != reflect.Struct || ns == "" { @@ -471,7 +471,7 @@ func yamlPathFromStructNS(ns string, root reflect.Type) string { } // next t := f.Type - for t.Kind() == reflect.Ptr { + for t.Kind() == reflect.Pointer { t = t.Elem() } cur = t @@ -485,7 +485,7 @@ func yamlPathFromStructNS(ns string, root reflect.Type) string { // --- Unknown key detection --- func findUnknownKeys(y interface{}, t reflect.Type, prefix string) []string { - for t.Kind() == reflect.Ptr { + for t.Kind() == reflect.Pointer { t = t.Elem() } if t.Kind() != reflect.Struct { @@ -526,7 +526,7 @@ func findUnknownKeys(y interface{}, t reflect.Type, prefix string) []string { // Merge inline struct fields into allowed space for name existence checks. for _, f := range inlineFields { ft := f.Type - for ft.Kind() == reflect.Ptr { + for ft.Kind() == reflect.Pointer { ft = ft.Elem() } if ft.Kind() != reflect.Struct { @@ -561,7 +561,7 @@ func findUnknownKeys(y interface{}, t reflect.Type, prefix string) []string { // Recurse only into struct fields; maps/slices allow arbitrary nested keys f := allowed[ks] ft := f.Type - for ft.Kind() == reflect.Ptr { + for ft.Kind() == reflect.Pointer { ft = ft.Elem() } if ft.Kind() == reflect.Struct { diff --git a/kits/shutdownkit/fx_test.go b/kits/shutdownkit/fx_test.go index 47b53ba..d728212 100644 --- a/kits/shutdownkit/fx_test.go +++ b/kits/shutdownkit/fx_test.go @@ -65,18 +65,12 @@ func TestModule_ProvidesShutdownContexts(t *testing.T) { func TestShutdownLifecycle(t *testing.T) { var sd ShutdownDeps - gracefulObserved := make(chan struct{}, 1) app := fx.New( shutdownkit.Module(), fx.Provide(func() *zap.Logger { return zaptest.NewLogger(t) }), fx.Invoke(func(d ShutdownDeps) { sd = d - // observe graceful cancellation outside OnStop to avoid ordering issues - go func() { - <-sd.Graceful.Done() - gracefulObserved <- struct{}{} - }() // run a managed goroutine sd.WG.Add(1) go func() { @@ -94,10 +88,5 @@ func TestShutdownLifecycle(t *testing.T) { defer cancelStop() require.NoError(t, app.Stop(ctxStop)) - select { - case <-gracefulObserved: - // ok - default: - t.Fatal("expected graceful context to be cancelled during Stop") - } + require.ErrorIs(t, sd.Graceful.Err(), context.Canceled) } diff --git a/kits/telemetry/fx.go b/kits/telemetry/fx.go index 51fd146..4f0d3c5 100644 --- a/kits/telemetry/fx.go +++ b/kits/telemetry/fx.go @@ -236,6 +236,11 @@ func applyConfigDefaults(cfg *Config) { // buildResource creates the OTEL resource by merging attributes from the default // resource, configuration, and runtime metadata package. func buildResource(cfg Config) (*sdkresource.Resource, error) { + // Use the SDK default's schema URL. Pinning semconv.SchemaURL here conflicts + // once resource.Default() moves to a newer semantic convention. + base := sdkresource.Default() + schemaURL := base.SchemaURL() + // Standard attributes attrs := []attribute.KeyValue{ semconv.ServiceName(cfg.ServiceName), @@ -246,11 +251,11 @@ func buildResource(cfg Config) (*sdkresource.Resource, error) { if *cfg.Disabled { attrs = append(attrs, attribute.Bool("otel.sdk.disabled", true)) } - mainAttrs := sdkresource.NewWithAttributes(semconv.SchemaURL, attrs...) + mainAttrs := sdkresource.NewWithAttributes(schemaURL, attrs...) // Extra attributes from runtime metadata package metaAttrs := sdkresource.NewWithAttributes( - semconv.SchemaURL, + schemaURL, runtimeinfo.OTELAttributes()..., ) @@ -259,10 +264,10 @@ func buildResource(cfg Config) (*sdkresource.Resource, error) { for k, v := range cfg.ResourceAttributes { extraConfigAttrs = append(extraConfigAttrs, attribute.String(k, v)) } - extraAttrs := sdkresource.NewWithAttributes(semconv.SchemaURL, extraConfigAttrs...) + extraAttrs := sdkresource.NewWithAttributes(schemaURL, extraConfigAttrs...) // Merge all resource sources. - res, err := sdkresource.Merge(sdkresource.Default(), mainAttrs) + res, err := sdkresource.Merge(base, mainAttrs) if err != nil { return nil, err } diff --git a/kits/telemetry/fx_test.go b/kits/telemetry/fx_test.go index 2244db7..5e61099 100644 --- a/kits/telemetry/fx_test.go +++ b/kits/telemetry/fx_test.go @@ -182,6 +182,9 @@ func TestBuildResourceIncludesAttributes(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } + if res.SchemaURL() != sdkresource.Default().SchemaURL() { + t.Fatalf("resource schema URL = %q, want SDK default %q", res.SchemaURL(), sdkresource.Default().SchemaURL()) + } attrs := res.Attributes() if !attrEquals(attrs, semconv.ServiceNameKey, "svc") { t.Fatalf("missing service name attribute") diff --git a/mise.toml b/mise.toml index b69e5a5..fa0f3dd 100644 --- a/mise.toml +++ b/mise.toml @@ -1,3 +1,3 @@ [tools] go = "1.24.0" -golangci-lint = "2.1.6" +golangci-lint = "2.13.0" From 8c7196a94f0638d96f48f0ba2bad5f46eec72e56 Mon Sep 17 00:00:00 2001 From: froppa <5844892+froppa@users.noreply.github.com> Date: Mon, 26 Oct 2026 22:04:09 +0100 Subject: [PATCH 2/3] test(signals): keep self-sent SIGTERM from killing the test binary TestGracefulThenSignalIgnored_NoForce sends SIGTERM to the test process after canceling graceful. The handler loop exits on graceful Done and calls signal.Stop, so whenever it wins that race SIGTERM falls back to its default action and the whole package dies with "signal: terminated" (reproduced locally on master and on this branch: 2 of 5 -count=20 runs). Hold a test-owned SIGTERM subscription for the duration of the test so the signal is always consumed; the assertions on Wait latency and the force context are unchanged. Signed-off-by: froppa <5844892+froppa@users.noreply.github.com> --- kits/signals/signal_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/kits/signals/signal_test.go b/kits/signals/signal_test.go index f2295cb..42fdaa2 100644 --- a/kits/signals/signal_test.go +++ b/kits/signals/signal_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "os/exec" + "os/signal" "sync" "syscall" "testing" @@ -275,6 +276,15 @@ func TestGracefulThenSignalIgnored_NoForce(t *testing.T) { // Ensure that after graceful is canceled programmatically, a subsequent OS // signal is effectively ignored by the handler loop (which exits on // graceful Done) and does not flip the force context. + // The handler loop exits and calls signal.Stop once graceful is canceled, + // so SIGTERM would fall back to its default action and kill the test + // binary ("signal: terminated") whenever the loop wins that race. Keep a + // test-owned subscription so the self-sent signal is always consumed here + // and the assertion below is about the Shutdown, not process survival. + guard := make(chan os.Signal, 1) + signal.Notify(guard, syscall.SIGTERM) + defer signal.Stop(guard) + var wg sync.WaitGroup s := sig.NewWithSignals(context.Background(), &wg) From 780754ac804ee36f80b2fd934757a70b67d77915 Mon Sep 17 00:00:00 2001 From: froppa <5844892+froppa@users.noreply.github.com> Date: Mon, 26 Oct 2026 22:23:07 +0100 Subject: [PATCH 3/3] ci: report the required "Test and Lint" status check The master ruleset requires a status check named "Test and Lint", but that is only the workflow name; the jobs report "Lint" and "Test", so no pull request can satisfy the rule. Add a "Test and Lint" job that needs both and fails unless both succeeded (including when either is skipped or cancelled). Lint and Test are unchanged. Signed-off-by: froppa <5844892+froppa@users.noreply.github.com> --- .github/workflows/test.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9d316f7..098ccc2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,3 +55,21 @@ jobs: name: coverage-report path: coverage.out if-no-files-found: error + + # The master ruleset requires a status check named exactly "Test and Lint" + # (the workflow name). Report it from one job that passes only when both + # Lint and Test succeeded, so the required context reflects both jobs. + test-and-lint: + name: Test and Lint + needs: [lint, test] + if: always() + runs-on: ubuntu-latest + steps: + - name: Require Lint and Test to succeed + env: + LINT_RESULT: ${{ needs.lint.result }} + TEST_RESULT: ${{ needs.test.result }} + run: | + echo "Lint: $LINT_RESULT, Test: $TEST_RESULT" + test "$LINT_RESULT" = success + test "$TEST_RESULT" = success