diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2e099f1..098ccc2 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 @@ -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 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/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) 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"