diff --git a/.github/workflows/chaos.yml b/.github/workflows/chaos.yml index 8be5b6a2..6cf711aa 100644 --- a/.github/workflows/chaos.yml +++ b/.github/workflows/chaos.yml @@ -48,14 +48,14 @@ jobs: matrix: arch: [amd64, arm64] steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: true - name: Set up QEMU if: matrix.arch != 'amd64' - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 with: platforms: ${{ matrix.arch }} - name: Cross-build failure-inject for ${{ matrix.arch }} @@ -107,8 +107,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: true @@ -120,8 +120,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: true diff --git a/.github/workflows/chart.yml b/.github/workflows/chart.yml index 733af194..f38996dd 100644 --- a/.github/workflows/chart.yml +++ b/.github/workflows/chart.yml @@ -33,8 +33,8 @@ jobs: name: render runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: true @@ -198,8 +198,8 @@ jobs: runs-on: ubuntu-latest needs: render steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: true diff --git a/.github/workflows/ci-hardware.yml.staged b/.github/workflows/ci-hardware.yml.staged index 048f3a6d..22697bed 100644 --- a/.github/workflows/ci-hardware.yml.staged +++ b/.github/workflows/ci-hardware.yml.staged @@ -48,8 +48,8 @@ jobs: - "dcgm" - "dcgm hardware" steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c258d8ca..b35c2137 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,8 +23,8 @@ jobs: name: verify runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: true @@ -59,8 +59,8 @@ jobs: name: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: true @@ -90,11 +90,11 @@ jobs: -ldflags "-s -w -X github.com/tracecoreai/tracecore/internal/version.Version=$VERSION" \ -o tracecore-linux-arm64 \ ./cmd/tracecore - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: tracecore-linux-amd64 path: tracecore-linux-amd64 - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: tracecore-linux-arm64 path: tracecore-linux-arm64 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 42ffd40b..51640485 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -22,15 +22,15 @@ jobs: matrix: language: [go] steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: true - - uses: github/codeql-action/init@v4 + - uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: languages: ${{ matrix.language }} - - uses: github/codeql-action/autobuild@v4 - - uses: github/codeql-action/analyze@v4 + - uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + - uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index fa63bf5e..eca2f708 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -16,12 +16,12 @@ jobs: name: govulncheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: true - - uses: golang/govulncheck-action@v1 + - uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4 with: go-version-file: go.mod repo-checkout: false diff --git a/.github/workflows/kernelevents-integration.yml b/.github/workflows/kernelevents-integration.yml index 4582f300..7a96ef8a 100644 --- a/.github/workflows/kernelevents-integration.yml +++ b/.github/workflows/kernelevents-integration.yml @@ -39,8 +39,8 @@ jobs: # the older entry — we want exactly two adjacent kernels. runner: [ubuntu-22.04, ubuntu-24.04] steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: true @@ -78,7 +78,7 @@ jobs: - name: Upload benchmark results if: always() - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: kernelevents-bench-results-${{ matrix.runner }} path: bench.txt @@ -89,8 +89,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: true @@ -138,7 +138,7 @@ jobs: ignore-unfixed: true - name: kernelevents-trivy — upload SARIF if: always() - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 continue-on-error: true with: sarif_file: trivy-otelcol.sarif @@ -210,8 +210,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: true @@ -225,8 +225,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: true diff --git a/.github/workflows/nccl-fr-fuzz-nightly.yml b/.github/workflows/nccl-fr-fuzz-nightly.yml index 6b44ce5e..8115bea8 100644 --- a/.github/workflows/nccl-fr-fuzz-nightly.yml +++ b/.github/workflows/nccl-fr-fuzz-nightly.yml @@ -20,8 +20,8 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: true @@ -35,7 +35,7 @@ jobs: # to f.Add() seeds. - name: Upload fuzz crashers (if any) if: failure() - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: fuzz-crashers path: pkg/nccl/fr_parser/testdata/fuzz/ diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index af86d183..6c486f67 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -13,7 +13,7 @@ jobs: name: pr-lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 085a0f22..f2986caa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -273,12 +273,19 @@ jobs: # without this, any workflow on any branch in the repo could # produce a bundle that passes the verifier's identity check. IDENTITY_REGEXP: "^https://github.com/${{ github.repository }}/\\.github/workflows/release\\.yml@refs/tags/" + TAG: ${{ needs.build.outputs.tag }} run: | set -euo pipefail + # --certificate-github-workflow-ref pins the verify to this exact + # tag (not any tag-ref the regex would also match). --trigger pins + # the OIDC claim that this fired from a tag push, not workflow_dispatch. + # Both narrow strictly inside what IDENTITY_REGEXP already accepts. cosign verify-blob \ --bundle "$BINARY_BASENAME.cosign.bundle" \ --certificate-identity-regexp "$IDENTITY_REGEXP" \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ + --certificate-github-workflow-ref "refs/tags/$TAG" \ + --certificate-github-workflow-trigger 'push' \ "$BINARY_BASENAME" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -384,11 +391,23 @@ jobs: if [ "$IS_PRERELEASE" = "true" ]; then prerelease_flag="--prerelease" fi + # Compose release notes: reproducibility walkthrough + a Rekor + # transparency-log pointer for after-the-fact audit. Extracting + # logIndex from the bundle in-job beats hand-pulling it later. + notes=$(mktemp) + cat docs/reproducibility.md > "$notes" + bundle="release/tracecore_${TAG}.cosign.bundle" + if [ -f "$bundle" ]; then + logIndex=$(jq -r '.rekorBundle.Payload.logIndex // empty' "$bundle") + if [ -n "$logIndex" ]; then + printf '\n## Transparency log\n\nRekor entry: https://search.sigstore.dev/?logIndex=%s\n' "$logIndex" >> "$notes" + fi + fi if gh release view "$TAG" >/dev/null 2>&1; then gh release upload "$TAG" --clobber release/* else gh release create "$TAG" $prerelease_flag \ --title "$TAG" \ - --notes-file docs/reproducibility.md \ + --notes-file "$notes" \ release/* fi diff --git a/Makefile b/Makefile index 50a1f96f..a437559b 100644 --- a/Makefile +++ b/Makefile @@ -193,7 +193,7 @@ doc-check: ## Verify test identifiers referenced in rot-prone docs exist in the @scripts/doc-check.sh @scripts/alert-check.sh -ci: license-check generate-check vet build-tags tidy-check lint nccl-fr-rce-gate coverage-check ci-fuzz-nccl-fr govulncheck doc-check build ## Everything CI runs. Run before opening a PR. +ci: license-check generate-check vet build-tags tidy-check mod-verify lint nccl-fr-rce-gate coverage-check ci-fuzz-nccl-fr govulncheck doc-check build ## Everything CI runs. Run before opening a PR. smoke: build ## End-to-end smoke test: validate the dcgm example config, run the binary for 1.5s, kill, assert lifecycle logs appear. No hardware required; receiver degrades cleanly on macOS/CI. @scripts/smoke.sh diff --git a/components/exporters/stdoutexporter/stdoutexporter_test.go b/components/exporters/stdoutexporter/stdoutexporter_test.go index c5d4d87d..470b0774 100644 --- a/components/exporters/stdoutexporter/stdoutexporter_test.go +++ b/components/exporters/stdoutexporter/stdoutexporter_test.go @@ -152,7 +152,7 @@ func TestExporter_ConcurrentWrites_DoNotInterleave(t *testing.T) { errs := make(chan error, goroutines) var wg sync.WaitGroup wg.Add(goroutines) - for i := 0; i < goroutines; i++ { + for range goroutines { go func() { defer wg.Done() errs <- exp.ConsumeMetrics(t.Context(), newMetricsWith("concurrent")) diff --git a/components/receivers/dcgm/cardinality_fuzz_test.go b/components/receivers/dcgm/cardinality_fuzz_test.go index 28cc3d1c..c8a2b28e 100644 --- a/components/receivers/dcgm/cardinality_fuzz_test.go +++ b/components/receivers/dcgm/cardinality_fuzz_test.go @@ -51,7 +51,7 @@ func TestEmit_CardinalityCap_InvariantsAcrossRandomInputs(t *testing.T) { pkgdcgm.FieldProfPCIeTxBytes, } - for trial := 0; trial < 200; trial++ { + for trial := range 200 { numGPUs := 1 + rng.IntN(8) // 1..8 GPUs numFieldsPerGPU := 1 + rng.IntN(10) // 1..10 fields capacity := 1 + rng.IntN(numGPUs*numFieldsPerGPU+5) // 1..(total+5) @@ -62,7 +62,7 @@ func TestEmit_CardinalityCap_InvariantsAcrossRandomInputs(t *testing.T) { samples := make([]pkgdcgm.Sample, 0, numGPUs*numFieldsPerGPU) now := time.Unix(1_700_000_000, 0) - for g := 0; g < numGPUs; g++ { + for g := range numGPUs { entity := pkgdcgm.Entity{ Kind: pkgdcgm.EntityGPU, ID: uint32(g), diff --git a/components/receivers/dcgm/e2e_test.go b/components/receivers/dcgm/e2e_test.go index 2f1be8a3..842a98cd 100644 --- a/components/receivers/dcgm/e2e_test.go +++ b/components/receivers/dcgm/e2e_test.go @@ -161,7 +161,7 @@ func TestEndToEnd_ConsumerGPU_PartialFieldSupport(t *testing.T) { func indexMetricsByName(t *testing.T, ms pmetric.MetricSlice) map[string]pmetric.Metric { t.Helper() out := make(map[string]pmetric.Metric, ms.Len()) - for i := 0; i < ms.Len(); i++ { + for i := range ms.Len() { m := ms.At(i) out[m.Name()] = m } diff --git a/components/receivers/dcgm/metrics.go b/components/receivers/dcgm/metrics.go index 708e92fc..515d1a58 100644 --- a/components/receivers/dcgm/metrics.go +++ b/components/receivers/dcgm/metrics.go @@ -539,7 +539,7 @@ func sanitizeEntityString(s string) string { if len(s) > maxEntityAttrLen { s = s[:maxEntityAttrLen] } - for i := 0; i < len(s); i++ { + for i := range len(s) { c := s[i] if c < 0x20 || c == 0x7F { b := make([]byte, 0, len(s)) diff --git a/components/receivers/dcgm/metrics_test.go b/components/receivers/dcgm/metrics_test.go index dcf47b63..8f66126d 100644 --- a/components/receivers/dcgm/metrics_test.go +++ b/components/receivers/dcgm/metrics_test.go @@ -259,7 +259,7 @@ func TestEmit_CardinalityCapIsDeterministic(t *testing.T) { } first := dropMetricSnapshot(t, r, samples) - for i := 0; i < 49; i++ { + for i := range 49 { got := dropMetricSnapshot(t, r, samples) require.Equal(t, first, got, "scrape %d: drop selection diverged from first run", i+2) } @@ -272,12 +272,12 @@ func dropMetricSnapshot(t *testing.T, r *receiver, samples []pkgdcgm.Sample) []s t.Helper() _, _, _, md := r.emit(samples) out := make([]string, 0) - for i := 0; i < md.ResourceMetrics().Len(); i++ { + for i := range md.ResourceMetrics().Len() { rm := md.ResourceMetrics().At(i) hwid, _ := rm.Resource().Attributes().Get(AttrHWID) - for j := 0; j < rm.ScopeMetrics().Len(); j++ { + for j := range rm.ScopeMetrics().Len() { sm := rm.ScopeMetrics().At(j) - for k := 0; k < sm.Metrics().Len(); k++ { + for k := range sm.Metrics().Len() { out = append(out, hwid.AsString()+":"+sm.Metrics().At(k).Name()) } } diff --git a/components/receivers/dcgm/pattern_replay_test.go b/components/receivers/dcgm/pattern_replay_test.go index d48496f9..97d1ca6e 100644 --- a/components/receivers/dcgm/pattern_replay_test.go +++ b/components/receivers/dcgm/pattern_replay_test.go @@ -193,7 +193,7 @@ func TestPattern5_PCIeAER(t *testing.T) { // The degraded GPU's resource attributes must carry the BDF // so operators can join with dmesg AER entries. foundDegradedBDF := false - for i := 0; i < md.ResourceMetrics().Len(); i++ { + for i := range md.ResourceMetrics().Len() { rm := md.ResourceMetrics().At(i) hwid, _ := rm.Resource().Attributes().Get(AttrHWID) if hwid.AsString() != "GPU-PCI-B" { @@ -215,13 +215,13 @@ func collectNVLinkValues(t *testing.T, md pmetric.Metrics) map[string]int64 { t.Helper() out := make(map[string]int64) rms := md.ResourceMetrics() - for i := 0; i < rms.Len(); i++ { + for i := range rms.Len() { rm := rms.At(i) hwid, _ := rm.Resource().Attributes().Get(AttrHWID) sms := rm.ScopeMetrics() - for j := 0; j < sms.Len(); j++ { + for j := range sms.Len() { ms := sms.At(j).Metrics() - for k := 0; k < ms.Len(); k++ { + for k := range ms.Len() { m := ms.At(k) if m.Name() != MetricHWGPUNVLinkIO { continue diff --git a/components/receivers/dcgm/perf_test.go b/components/receivers/dcgm/perf_test.go index 48a6668e..50b5f226 100644 --- a/components/receivers/dcgm/perf_test.go +++ b/components/receivers/dcgm/perf_test.go @@ -43,7 +43,7 @@ func TestEmit_StaysUnderBudget(t *testing.T) { const budget = 4 * time.Millisecond result := testing.Benchmark(func(b *testing.B) { - for i := 0; i < b.N; i++ { + for range b.N { _, _, _, _ = r.emit(samples) } }) @@ -79,7 +79,7 @@ func BenchmarkEmit_TypicalScrape(b *testing.B) { b.ReportAllocs() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { emitted, _, _, _ := r.emit(samples) if emitted == 0 { b.Fatalf("emit produced no metrics; benchmark input is wrong") @@ -104,7 +104,7 @@ func buildBenchmarkSamples(numGPUs int, now time.Time) []pkgdcgm.Sample { } samples := make([]pkgdcgm.Sample, 0, numGPUs*len(fieldSet)) - for i := 0; i < numGPUs; i++ { + for i := range numGPUs { // fmt.Sprintf with %d scales to any GPU count; the previous // "GPU-bench-" form produced garbage above 26. entity := pkgdcgm.Entity{ diff --git a/components/receivers/dcgm/stress_test.go b/components/receivers/dcgm/stress_test.go index 554025c5..10fd49de 100644 --- a/components/receivers/dcgm/stress_test.go +++ b/components/receivers/dcgm/stress_test.go @@ -38,7 +38,7 @@ func TestReceiver_StartShutdown_NoGoroutineLeak(t *testing.T) { cfg.CollectionInterval = 5 * time.Millisecond cfg.InitialDelay = 0 - for i := 0; i < 100; i++ { + for range 100 { r := newReceiver(context.Background(), newTestSettings(t), &cfg, nullConsumer{}, WithClient(unavailableClient()), ) @@ -80,7 +80,7 @@ func TestReceiver_RepeatedShutdown_IsIdempotent(t *testing.T) { r := newReceiver(context.Background(), newTestSettings(t), &cfg, nullConsumer{}, WithClient(unavailableClient())) ctx := context.Background() - for i := 0; i < 10; i++ { + for i := range 10 { require.NoError(t, r.Shutdown(ctx), "shutdown #%d (no preceding Start) must be a clean no-op", i+1, ) diff --git a/components/receivers/k8sevents/bench_test.go b/components/receivers/k8sevents/bench_test.go index d02da1a4..cda667c8 100644 --- a/components/receivers/k8sevents/bench_test.go +++ b/components/receivers/k8sevents/bench_test.go @@ -31,7 +31,7 @@ func BenchmarkEmitOne(b *testing.B) { logs := plog.NewLogs() scope := logs.ResourceLogs().AppendEmpty().ScopeLogs().AppendEmpty() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { lr := scope.LogRecords().AppendEmpty() _ = k8sevents.BuildLogRecordForBench(lr, rec, k8sevents.DefaultMaxAttributes, 0) } @@ -51,13 +51,13 @@ func BenchmarkConvertOne(b *testing.B) { Action: "Killing", Reason: "Evicted", Note: "memory pressure", - Type: "Warning", + Type: k8sevents.EventTypeWarning, Regarding: corev1.ObjectReference{ Kind: "Pod", Namespace: "default", Name: "pod-x", UID: types.UID("pod-uid-9"), }, } b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { _ = k8sevents.ConvertEventForBench(e) } } @@ -72,7 +72,7 @@ func buildBenchRecord(_ *testing.B) k8sevents.Record { Hint: k8sevents.HintPodEvicted, ReportingController: "kubelet", Note: "memory pressure", - Type: "Warning", + Type: k8sevents.EventTypeWarning, SeriesCount: 3, EventTime: time.Date(2026, 5, 15, 2, 30, 0, 0, time.UTC), Regarding: k8sevents.ObjectRef{ diff --git a/components/receivers/k8sevents/convert_test.go b/components/receivers/k8sevents/convert_test.go index 0c4043c3..b1b516fc 100644 --- a/components/receivers/k8sevents/convert_test.go +++ b/components/receivers/k8sevents/convert_test.go @@ -29,7 +29,7 @@ func TestConvertEvent_FullFixturePopulatesAllFields(t *testing.T) { Action: "Killing", Reason: "Evicted", Note: "memory pressure", - Type: "Warning", + Type: EventTypeWarning, Regarding: corev1.ObjectReference{ Kind: "Pod", Namespace: "default", @@ -52,7 +52,7 @@ func TestConvertEvent_FullFixturePopulatesAllFields(t *testing.T) { require.Equal(t, "memory pressure", rec.Note) require.Equal(t, int32(3), rec.SeriesCount) require.WithinDuration(t, now, rec.EventTime, time.Microsecond) - require.Equal(t, "Warning", rec.Type) + require.Equal(t, EventTypeWarning, rec.Type) } func TestConvertEvent_NoHintForUnknownReason(t *testing.T) { @@ -93,7 +93,7 @@ func TestBuildLogRecord_PopulatesPinnedAttributes(t *testing.T) { Reason: "Evicted", Hint: HintPodEvicted, Note: "memory pressure", - Type: "Warning", + Type: EventTypeWarning, ReportingController: "kubelet", SeriesCount: 3, EventTime: time.Date(2026, 5, 15, 2, 30, 0, 0, time.UTC), @@ -125,7 +125,7 @@ func TestBuildLogRecord_PopulatesPinnedAttributes(t *testing.T) { require.Equal(t, "pod-uid-9", get(AttrRegardingUID)) require.Equal(t, "kubelet", get(AttrReportingController)) require.Equal(t, "Killing", get(AttrEventAction)) - require.Equal(t, "Warning", get(AttrEventType)) + require.Equal(t, EventTypeWarning, get(AttrEventType)) require.Equal(t, "memory pressure", get(AttrNote)) require.Equal(t, "memory pressure", lr.Body().Str()) require.Equal(t, plog.SeverityNumberWarn, lr.SeverityNumber()) @@ -179,7 +179,7 @@ func TestBuildLogRecord_DropsPastCap(t *testing.T) { }, ReportingController: "ctrl", Action: "a", - Type: "Normal", + Type: EventTypeNormal, Note: "n", SeriesCount: 1, EventTime: time.Now(), @@ -206,7 +206,7 @@ func TestBuildLogRecord_CapPreservesJoinKeys(t *testing.T) { }, ReportingController: "ctrl", Action: "a", - Type: "Normal", + Type: EventTypeNormal, Note: "n", } diff --git a/components/receivers/k8sevents/factory.go b/components/receivers/k8sevents/factory.go index 69505256..ac1e3fba 100644 --- a/components/receivers/k8sevents/factory.go +++ b/components/receivers/k8sevents/factory.go @@ -11,9 +11,14 @@ import ( "github.com/tracecoreai/tracecore/internal/selftelemetry" ) +// ComponentType is the canonical receiver-factory ID. Centralized so +// tests and operator-facing config can reference the same symbol +// instead of duplicating the string literal. +const ComponentType = "k8s_events" + // componentType is wrapped in a function so the pipeline.MustNewType // call is not a top-level side effect (mirrors kernelevents pattern). -func componentType() pipeline.Type { return pipeline.MustNewType("k8s_events") } +func componentType() pipeline.Type { return pipeline.MustNewType(ComponentType) } // Factory is the package-scoped ReceiverFactory for k8sevents. // Only CreateLogs returns a real Receiver; CreateMetrics and diff --git a/components/receivers/k8sevents/filter_test.go b/components/receivers/k8sevents/filter_test.go index cf0aed94..9c675ba7 100644 --- a/components/receivers/k8sevents/filter_test.go +++ b/components/receivers/k8sevents/filter_test.go @@ -11,9 +11,9 @@ import ( func TestFilter_MinEventTypeWarningDropsNormal(t *testing.T) { t.Parallel() - spec := buildFilterSpec(&Config{MinEventType: "Warning"}) - require.True(t, spec.dropByFilter(Record{Type: "Normal"})) - require.False(t, spec.dropByFilter(Record{Type: "Warning"})) + spec := buildFilterSpec(&Config{MinEventType: EventTypeWarning}) + require.True(t, spec.dropByFilter(Record{Type: EventTypeNormal})) + require.False(t, spec.dropByFilter(Record{Type: EventTypeWarning})) } func TestFilter_ReasonRegexDropsNonMatch(t *testing.T) { @@ -58,5 +58,5 @@ func TestFilter_IncludeAndExcludeBothApplied(t *testing.T) { func TestFilter_NoConfigPassesEverything(t *testing.T) { t.Parallel() spec := buildFilterSpec(&Config{}) - require.False(t, spec.dropByFilter(Record{Type: "Normal", Reason: "Pulled"})) + require.False(t, spec.dropByFilter(Record{Type: EventTypeNormal, Reason: "Pulled"})) } diff --git a/components/receivers/k8sevents/integration_test.go b/components/receivers/k8sevents/integration_test.go index 16a9c7e3..c46cd3bc 100644 --- a/components/receivers/k8sevents/integration_test.go +++ b/components/receivers/k8sevents/integration_test.go @@ -42,11 +42,11 @@ func (c *captureConsumer) Capabilities() consumer.Capabilities { func (c *captureConsumer) ConsumeLogs(_ context.Context, ld plog.Logs) error { c.mu.Lock() defer c.mu.Unlock() - for i := 0; i < ld.ResourceLogs().Len(); i++ { + for i := range ld.ResourceLogs().Len() { rl := ld.ResourceLogs().At(i) - for j := 0; j < rl.ScopeLogs().Len(); j++ { + for j := range rl.ScopeLogs().Len() { sl := rl.ScopeLogs().At(j) - for k := 0; k < sl.LogRecords().Len(); k++ { + for k := range sl.LogRecords().Len() { c.records = append(c.records, sl.LogRecords().At(k)) c.emitted.Add(1) } @@ -82,7 +82,7 @@ func TestReceiver_AgainstFakeAPIServer(t *testing.T) { Action: "Killing", Reason: "Evicted", Note: "memory pressure", - Type: "Warning", + Type: k8sevents.EventTypeWarning, Regarding: corev1.ObjectReference{ Kind: "Pod", Namespace: "default", @@ -102,7 +102,7 @@ func TestReceiver_AgainstFakeAPIServer(t *testing.T) { } set := pipeline.CreateSettings{ - ID: pipeline.MustNewID(pipeline.MustNewType("k8s_events"), "primary"), + ID: pipeline.MustNewID(pipeline.MustNewType(k8sevents.ComponentType), "primary"), } r := k8sevents.NewReceiverForTestWithFactory(set, cfg, cc, client, func(c kubernetes.Interface, resync time.Duration, ns []string) informers.SharedInformerFactory { opts := []informers.SharedInformerOption{} @@ -164,7 +164,7 @@ func TestReceiver_NoteMaxBytesTruncatesBodyAndAttribute(t *testing.T) { Action: "Killing", Reason: "Evicted", Note: longNote, - Type: "Warning", + Type: k8sevents.EventTypeWarning, Regarding: corev1.ObjectReference{ Kind: "Pod", Namespace: "default", Name: "pod-y", }, @@ -181,7 +181,7 @@ func TestReceiver_NoteMaxBytesTruncatesBodyAndAttribute(t *testing.T) { Burst: k8sevents.PinnedBurst, } set := pipeline.CreateSettings{ - ID: pipeline.MustNewID(pipeline.MustNewType("k8s_events"), "primary"), + ID: pipeline.MustNewID(pipeline.MustNewType(k8sevents.ComponentType), "primary"), } r := k8sevents.NewReceiverForTestWithFactory(set, cfg, cc, client, func(c kubernetes.Interface, resync time.Duration, ns []string) informers.SharedInformerFactory { return informers.NewSharedInformerFactoryWithOptions(c, resync) diff --git a/components/receivers/k8sevents/pattern_consumer_test.go b/components/receivers/k8sevents/pattern_consumer_test.go index af1146cb..c75888f1 100644 --- a/components/receivers/k8sevents/pattern_consumer_test.go +++ b/components/receivers/k8sevents/pattern_consumer_test.go @@ -30,7 +30,7 @@ func TestPatternConsumer_RecordTypeCompiles(t *testing.T) { ReportingController: "kubelet", Note: "memory pressure", SeriesCount: 3, - Type: "Warning", + Type: k8sevents.EventTypeWarning, Regarding: k8sevents.ObjectRef{ Kind: "Pod", Namespace: "default", diff --git a/components/receivers/k8sevents/receiver_test.go b/components/receivers/k8sevents/receiver_test.go index b210a74f..012c0133 100644 --- a/components/receivers/k8sevents/receiver_test.go +++ b/components/receivers/k8sevents/receiver_test.go @@ -112,7 +112,7 @@ func TestReceiver_BackPressureDropsPastChannelCap(t *testing.T) { Burst: k8sevents.PinnedBurst, } set := pipeline.CreateSettings{ - ID: pipeline.MustNewID(pipeline.MustNewType("k8s_events"), "primary"), + ID: pipeline.MustNewID(pipeline.MustNewType(k8sevents.ComponentType), "primary"), } client := fake.NewSimpleClientset() @@ -127,13 +127,13 @@ func TestReceiver_BackPressureDropsPastChannelCap(t *testing.T) { // Flood 10k synthetic Events into deliver — the run loop is // blocked on the consumer, so anything past cap drops. - for i := 0; i < 10_000; i++ { + for range 10_000 { k8sevents.DeliverForTest(r, &eventsv1.Event{ ObjectMeta: metav1.ObjectMeta{ UID: types.UID("e"), }, Reason: "Evicted", - Type: "Warning", + Type: k8sevents.EventTypeWarning, Regarding: corev1.ObjectReference{ Kind: "Pod", Namespace: "default", Name: "x", }, @@ -165,7 +165,7 @@ func TestReceiver_GoroutineDeferRecover_KeepsProcessAlive(t *testing.T) { Burst: k8sevents.PinnedBurst, } set := pipeline.CreateSettings{ - ID: pipeline.MustNewID(pipeline.MustNewType("k8s_events"), "primary"), + ID: pipeline.MustNewID(pipeline.MustNewType(k8sevents.ComponentType), "primary"), } client := fake.NewSimpleClientset() @@ -204,7 +204,7 @@ func TestReceiver_WatchErrorIncrementsDegradedAndCounter(t *testing.T) { Burst: k8sevents.PinnedBurst, } set := pipeline.CreateSettings{ - ID: pipeline.MustNewID(pipeline.MustNewType("k8s_events"), "primary"), + ID: pipeline.MustNewID(pipeline.MustNewType(k8sevents.ComponentType), "primary"), } client := fake.NewSimpleClientset() @@ -238,7 +238,7 @@ func TestReceiver_ShutdownIdempotent(t *testing.T) { Burst: k8sevents.PinnedBurst, } set := pipeline.CreateSettings{ - ID: pipeline.MustNewID(pipeline.MustNewType("k8s_events"), "primary"), + ID: pipeline.MustNewID(pipeline.MustNewType(k8sevents.ComponentType), "primary"), } client := fake.NewSimpleClientset() @@ -278,18 +278,18 @@ func TestReceiver_GoleakNoLeakAfterShutdown(t *testing.T) { Burst: k8sevents.PinnedBurst, } set := pipeline.CreateSettings{ - ID: pipeline.MustNewID(pipeline.MustNewType("k8s_events"), "primary"), + ID: pipeline.MustNewID(pipeline.MustNewType(k8sevents.ComponentType), "primary"), } client := fake.NewSimpleClientset() r := k8sevents.NewReceiverForTestWithFactory(set, cfg, cc, client, identityFactory(), nil) require.NoError(t, r.Start(t.Context(), pipelineHost{})) - for i := 0; i < 10_000; i++ { + for range 10_000 { k8sevents.DeliverForTest(r, &eventsv1.Event{ ObjectMeta: metav1.ObjectMeta{UID: types.UID("e")}, Reason: "Evicted", - Type: "Warning", + Type: k8sevents.EventTypeWarning, Regarding: corev1.ObjectReference{ Kind: "Pod", Namespace: "default", Name: "x", }, diff --git a/components/receivers/k8sevents/rusage_linux_test.go b/components/receivers/k8sevents/rusage_linux_test.go index 4fe0fa23..aab11365 100644 --- a/components/receivers/k8sevents/rusage_linux_test.go +++ b/components/receivers/k8sevents/rusage_linux_test.go @@ -74,7 +74,7 @@ func TestReceiver_OverheadUnderBudget(t *testing.T) { Burst: k8sevents.PinnedBurst, } set := pipeline.CreateSettings{ - ID: pipeline.MustNewID(pipeline.MustNewType("k8s_events"), "primary"), + ID: pipeline.MustNewID(pipeline.MustNewType(k8sevents.ComponentType), "primary"), } client := fake.NewSimpleClientset() r := k8sevents.NewReceiverForTestWithFactory(set, cfg, cc, client, func(c kubernetes.Interface, resync time.Duration, ns []string) informers.SharedInformerFactory { @@ -92,11 +92,11 @@ func TestReceiver_OverheadUnderBudget(t *testing.T) { require.NoError(t, r.Shutdown(ctx)) }) - for i := 0; i < 1000; i++ { + for range 1000 { k8sevents.DeliverForTest(r, &eventsv1.Event{ ObjectMeta: metav1.ObjectMeta{UID: types.UID("e")}, Reason: "Evicted", - Type: "Warning", + Type: k8sevents.EventTypeWarning, Regarding: corev1.ObjectReference{ Kind: "Pod", Namespace: "default", Name: "x", }, diff --git a/components/receivers/kernelevents/bench_test.go b/components/receivers/kernelevents/bench_test.go index 0c6bc834..b235c98d 100644 --- a/components/receivers/kernelevents/bench_test.go +++ b/components/receivers/kernelevents/bench_test.go @@ -189,11 +189,11 @@ type countingSink struct { func (s *countingSink) ConsumeLogs(_ context.Context, ld plog.Logs) error { now := time.Now() rls := ld.ResourceLogs() - for i := 0; i < rls.Len(); i++ { + for i := range rls.Len() { sls := rls.At(i).ScopeLogs() - for j := 0; j < sls.Len(); j++ { + for j := range sls.Len() { lrs := sls.At(j).LogRecords() - for k := 0; k < lrs.Len(); k++ { + for k := range lrs.Len() { ts := lrs.At(k).ObservedTimestamp().AsTime() if !ts.IsZero() { s.mu.Lock() diff --git a/components/receivers/kernelevents/journald.go b/components/receivers/kernelevents/journald.go index 82b81128..88bc056e 100644 --- a/components/receivers/kernelevents/journald.go +++ b/components/receivers/kernelevents/journald.go @@ -365,7 +365,7 @@ func (s *journaldSource) resolveBinary() (string, error) { // hardcoded ceiling. func currentBackoff(initial time.Duration, attempt int) time.Duration { d := initial - for i := 0; i < attempt; i++ { + for range attempt { d *= 2 if d >= backoffCeiling { return backoffCeiling diff --git a/components/receivers/kernelevents/parser.go b/components/receivers/kernelevents/parser.go index 71dcad0f..8e46346f 100644 --- a/components/receivers/kernelevents/parser.go +++ b/components/receivers/kernelevents/parser.go @@ -412,7 +412,7 @@ func sanitizeAttrValue(s string) string { // isPrintable returns true if every byte of s is either tab/newline/ // CR or an ASCII printable. func isPrintable(s string) bool { - for i := 0; i < len(s); i++ { + for i := range len(s) { c := s[i] if c == '\t' || c == '\n' || c == '\r' { continue diff --git a/components/receivers/kernelevents/parser_test.go b/components/receivers/kernelevents/parser_test.go index b1edb4bb..209fa0d0 100644 --- a/components/receivers/kernelevents/parser_test.go +++ b/components/receivers/kernelevents/parser_test.go @@ -71,7 +71,7 @@ func TestSourceCommon_WarnOnce_GatesByKey(t *testing.T) { c := &sourceCommon{name: "test", logger: logger} // 100 calls with the same key → 1 log line. - for i := 0; i < 100; i++ { + for i := range 100 { c.warnOnce("parse", "parse error", "iter", i) } require.Equal(t, 1, strings.Count(buf.String(), "parse error"), @@ -143,7 +143,7 @@ func TestAddPassthrough_DeterministicDropOrder(t *testing.T) { } var first []string - for i := 0; i < trials; i++ { + for i := range trials { lr := plog.NewLogRecord() ab := newAttrBudget(lr.Attributes(), 3) addPassthrough(ab, fields) @@ -444,7 +444,7 @@ func TestBuildLogRecord_CardinalityCap(t *testing.T) { t.Parallel() lr := plog.NewLogRecord() fields := make(map[string]string, 50) - for i := 0; i < 50; i++ { + for i := range 50 { fields[strings.ToUpper("EXTRA_KEY_"+string(rune('A'+i%26)))+"_"+itoa(i)] = "v" } r := parsedRecord{ diff --git a/components/receivers/kernelevents/runbook_test.go b/components/receivers/kernelevents/runbook_test.go index 6a94d233..49f32ce9 100644 --- a/components/receivers/kernelevents/runbook_test.go +++ b/components/receivers/kernelevents/runbook_test.go @@ -95,7 +95,7 @@ func buildTracecoreBinary(t *testing.T) string { pkgDir, err := os.Getwd() require.NoError(t, err) root := pkgDir - for i := 0; i < 8; i++ { + for range 8 { if _, statErr := os.Stat(filepath.Join(root, "go.mod")); statErr == nil { break } diff --git a/components/receivers/nccl_fr/nccl_fr_test.go b/components/receivers/nccl_fr/nccl_fr_test.go index 04beeac3..9b3a0470 100644 --- a/components/receivers/nccl_fr/nccl_fr_test.go +++ b/components/receivers/nccl_fr/nccl_fr_test.go @@ -307,9 +307,9 @@ func (s *logsSink) recordCount() int { defer s.mu.Unlock() total := 0 for _, ld := range s.pushed { - for i := 0; i < ld.ResourceLogs().Len(); i++ { + for i := range ld.ResourceLogs().Len() { rl := ld.ResourceLogs().At(i) - for j := 0; j < rl.ScopeLogs().Len(); j++ { + for j := range rl.ScopeLogs().Len() { total += rl.ScopeLogs().At(j).LogRecords().Len() } } diff --git a/docs/FOLLOWUPS.md b/docs/FOLLOWUPS.md index 4e3a5745..22eb98d7 100644 --- a/docs/FOLLOWUPS.md +++ b/docs/FOLLOWUPS.md @@ -22,9 +22,13 @@ silently carried. Highest-leverage opportunistic items, in order: -1. **`make doc-check`** — assert every test name referenced in - `docs/**.md` actually exists. ~30 minutes. Doc rot risk grows - with every `git mv` of a test file. + + +1. *Closed (see above): `make doc-check` shipped and CI-gated.* 2. **Machine-readable stability tier per component.** Today stability is declared in two places (per-component README header + CHANGELOG entry). When `tracecore components` (a future CLI @@ -289,7 +293,7 @@ reviewer suggested. | `EnableOpenMetrics: true` config toggle (current default `false` matches Prometheus) | M5 (backend-compat milestone) | Mimir/Cortex 2024+ want exemplars + `_created` lines; needs explicit operator opt-in and a compat-matrix test. | | Recording rules `tracecore:exporter_failure_rate:rate5m` + Grafana dashboard JSON | M4 (observability-platform integration milestone) | Operator-platform glue, not collector-side work. | | Programmatic `tracecore.RunCollect(...)` API for embedding the collector in another Go binary | post-v1 | Pre-v1 the surface is still settling; locking a Go API into the binary now would force re-cuts. | -| Server `Server:` header strip | opportunistic | Trivial; do it the next time someone touches server.go. | +| Server `Server:` header strip | ~~opportunistic~~ closed | Verified Go's `net/http.Server` does not emit a Server header in any default path (handler responses or ServeMux's 404). Locked the invariant with `TestServer_NoServerHeader` in `internal/telemetry/server_test.go`. No middleware needed. | | Rate limit on `/metrics` (defensive against pathological scrapers) | M5 (security hardening) | Slowloris timeouts already bound single-attacker resource burn; rate-limit is a layered defense. | | SBOM human-readable summary attached to release artifacts | M3 (release-process milestone) | The CI scan already runs; the human-readable manifest is operator-friendliness work. | @@ -435,13 +439,14 @@ the milestone-tracked "Carry-forward from M8" section of `filepath.Glob("*.go")` minus `*_test.go`, union the walks. *Trigger:* `receiver.go` split into multiple files (already listed under "split 475-LOC receiver.go" — these are paired). -- [ ] **Promote `make build-tags` runs into the `pr-validation` - CI workflow.** `make ci` now includes `build-tags` so the - gate fires locally and in any full-CI run; the GitHub - Actions `ci.yml` already runs `make ci`, but the - `pr-validation.yml` shortcut path doesn't. *Trigger:* - `pr-validation` jobs miss a future build-tag-conditional - regression (or just opportunistic when next touching CI). + +- *Closed (see comment above): already satisfied by ci.yml running + `make build-tags` directly; no separate pr-validation.yml exists.* - [ ] **`warnOnce` oscillating-kinds set-keyed gate.** The current `lastErrorKind == kind` check re-logs on every kind transition; an oscillating receiver (Connect ↔ Watch ↔ @@ -775,16 +780,22 @@ implementation gated on an upstream package that has not landed yet. check inside the loop honors PRINCIPLES §1 ("shutdown is fast and bounded"). *Trigger:* first operator hitting Ctrl-C on a `--count=10000` invocation that takes >100ms to interrupt. -- **`for i := range n` Go 1.22+ idiom sweep.** xidgen / podevict / - chaos_test still use C-style `for i := 0; i < n; i++`. Repo pins - Go 1.26.3, so the idiom is available. Cosmetic; do as a - repo-wide sweep, not piecemeal. *Trigger:* a separate - modernization PR. -- **Pin GitHub Actions by SHA, not `@v6`.** chaos.yml + - ci.yml + every other workflow uses tag shorthand. Aligns with - NORTHSTARS O3 SLSA L3 hardening; do as a repo-wide sweep (M3 - reproducible-build is the natural home). *Trigger:* M3 lands - the sweep. + +- *Closed (see comment above): `for i := range n` sweep applied + repo-wide; remaining C-style loops have non-convertible conditions.* + +- *Closed (see comment above): all GitHub Actions SHA-pinned; + upload-artifact unified on v7.0.1.* - **`LockOSThread` falsifier in cpusteal.** Today the call sits in `cpusteal.go:40` with no test that would fail if removed. A mutation-tested falsifier closes the gap. *Trigger:* an @@ -981,14 +992,16 @@ deferred are phased here. `string`-typed enums). Wire `exhaustive` into `.golangci.yml` so a downstream switch over `Hint` fails CI if a constant is missed. -- **Backfill `EventTypeNormal` / `EventTypeWarning` usage in tests.** - Production paths (emit.go, filter.go, config.go) reference the - constants; ~12 test sites still use raw `"Normal"` / `"Warning"`. - Cosmetic, not load-bearing. -- **k8sevents `ComponentType = "k8s_events"` exported const.** The - factory ID is duplicated as a literal across factory.go and 5+ - test files. Centralise once kernelevents follows the same - pattern (likely with the type-naming consistency milestone). + +- *Closed (see comment above): `EventType*` constants backfilled + across k8sevents test files.* + +- *Closed (see comment above): `k8sevents.ComponentType` exported + and adopted by test sites.* - **Receiver-alert ↔ M2 self-telemetry contract reconciliation.** diff --git a/docs/reproducibility.md b/docs/reproducibility.md index 496d6708..81cdae91 100644 --- a/docs/reproducibility.md +++ b/docs/reproducibility.md @@ -79,27 +79,34 @@ diffoscope src/tracecore "$PUBLISHED" ```bash # 5. Verify the cosign keyless signature. The certificate identity is -# pinned to this exact workflow file on a tag-ref so a sibling -# workflow on a non-protected branch cannot mint a passing bundle. +# pinned to this exact tag (not just any tag-ref) and to the `push` +# trigger, so a sibling workflow on a non-protected branch or a manual +# workflow_dispatch run cannot mint a passing bundle. BUNDLE=$(ls release/tracecore_*.cosign.bundle | head -n1) cosign verify-blob \ --bundle "$BUNDLE" \ --certificate-identity-regexp "^https://github.com/${REPO}/\.github/workflows/release\.yml@refs/tags/" \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ + --certificate-github-workflow-ref "refs/tags/$TAG" \ + --certificate-github-workflow-trigger 'push' \ "$PUBLISHED" ``` ```bash # 6. Verify the SLSA v1.0 provenance attestation. Reading the bundle -# from disk (--bundle) makes this work offline and pins the signer -# to this exact workflow on a tag-ref — a sibling workflow elsewhere -# in the repo cannot produce an attestation that passes. +# from disk (--bundle) makes this work offline and pins the signer to +# this exact workflow on a tag-ref. --source-ref + --source-digest +# additionally bind the attestation to the tag commit you cloned in +# step 1 — a sibling workflow producing an attestation against a +# different commit cannot pass. ATTEST=$(ls release/tracecore_*.intoto.jsonl | head -n1) gh attestation verify "$PUBLISHED" \ --bundle "$ATTEST" \ --owner "${REPO%/*}" \ --predicate-type 'https://slsa.dev/provenance/v1' \ - --signer-workflow "${REPO}/.github/workflows/release.yml" + --signer-workflow "${REPO}/.github/workflows/release.yml" \ + --source-ref "refs/tags/$TAG" \ + --source-digest "$(git -C src rev-parse HEAD)" ``` ```bash diff --git a/internal/config/bench_test.go b/internal/config/bench_test.go index 3d281190..a4c6d7b1 100644 --- a/internal/config/bench_test.go +++ b/internal/config/bench_test.go @@ -13,7 +13,7 @@ import ( // cheapest hot-path parser in the tree; pinning a baseline now lets // future regex / string-handling changes be assessed quantitatively. func BenchmarkParsePipelineID(b *testing.B) { - for i := 0; i < b.N; i++ { + for range b.N { _, _, err := config.ParsePipelineID("metrics/primary") if err != nil { b.Fatal(err) diff --git a/internal/fanout/bench_test.go b/internal/fanout/bench_test.go index dae7add4..b31f9082 100644 --- a/internal/fanout/bench_test.go +++ b/internal/fanout/bench_test.go @@ -32,10 +32,10 @@ func BenchmarkFanout_NewMetrics(b *testing.B) { for _, tc := range cases { b.Run(tc.name, func(b *testing.B) { consumers := make([]consumer.Metrics, 0, tc.mutable+tc.readonly) - for i := 0; i < tc.mutable; i++ { + for range tc.mutable { consumers = append(consumers, noopBenchMetrics{mutates: true}) } - for i := 0; i < tc.readonly; i++ { + for range tc.readonly { consumers = append(consumers, noopBenchMetrics{}) } f := fanout.NewMetrics(consumers) @@ -45,7 +45,7 @@ func BenchmarkFanout_NewMetrics(b *testing.B) { ctx := b.Context() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { if err := f.ConsumeMetrics(ctx, md); err != nil { b.Fatal(err) } diff --git a/internal/fanout/fanout.go b/internal/fanout/fanout.go index 86a0503d..5c5fd585 100644 --- a/internal/fanout/fanout.go +++ b/internal/fanout/fanout.go @@ -49,7 +49,7 @@ func (g *genericFanout[T, C]) consumeAll(ctx context.Context, d T) error { if len(g.mutable) > 0 { // All mutators except the last get a fresh clone. - for i := 0; i < len(g.mutable)-1; i++ { + for i := range len(g.mutable) - 1 { clone := g.ops.new() g.ops.copyTo(d, clone) errs = errors.Join(errs, g.consume(g.mutable[i], ctx, clone)) diff --git a/internal/fanout/fanout_test.go b/internal/fanout/fanout_test.go index 38ebf1aa..6b03f1fe 100644 --- a/internal/fanout/fanout_test.go +++ b/internal/fanout/fanout_test.go @@ -209,13 +209,13 @@ func TestMetrics_NestedPayload_DeepCloneIsolation(t *testing.T) { } var aCap, bCap captured a := &mutateMetricsRecorder{mutates: true, capture: func(md pmetric.Metrics) { - for i := 0; i < md.ResourceMetrics().Len(); i++ { + for i := range md.ResourceMetrics().Len() { rm := md.ResourceMetrics().At(i) - for j := 0; j < rm.ScopeMetrics().Len(); j++ { + for j := range rm.ScopeMetrics().Len() { sm := rm.ScopeMetrics().At(j) - for k := 0; k < sm.Metrics().Len(); k++ { + for k := range sm.Metrics().Len() { m := sm.Metrics().At(k) - for p := 0; p < m.Gauge().DataPoints().Len(); p++ { + for p := range m.Gauge().DataPoints().Len() { dp := m.Gauge().DataPoints().At(p) dp.Attributes().PutStr("mutated-by-a", "yes") } @@ -258,16 +258,16 @@ func TestMetrics_NestedPayload_DeepCloneIsolation(t *testing.T) { func buildNestedMetrics(t *testing.T, nRMs, nSMs, nDPs int) pmetric.Metrics { t.Helper() md := pmetric.NewMetrics() - for i := 0; i < nRMs; i++ { + for i := range nRMs { rm := md.ResourceMetrics().AppendEmpty() rm.Resource().Attributes().PutStr("rm.index", strconv.Itoa(i)) - for j := 0; j < nSMs; j++ { + for j := range nSMs { sm := rm.ScopeMetrics().AppendEmpty() sm.Scope().SetName("test/" + strconv.Itoa(j)) m := sm.Metrics().AppendEmpty() m.SetName("test.gauge") g := m.SetEmptyGauge() - for k := 0; k < nDPs; k++ { + for k := range nDPs { dp := g.DataPoints().AppendEmpty() dp.Attributes().PutStr("service", "test") dp.SetIntValue(int64(k)) @@ -351,12 +351,12 @@ func runCloningContract[T any, C any](t *testing.T, kit fanoutKit[T, C]) { if tc.wantErrInOutput { injectErr = errors.New("recorder-injected") } - for i := 0; i < tc.nMutating; i++ { + for range tc.nMutating { c, gc := kit.newRecorder(true, nil) consumers = append(consumers, c) getCalls = append(getCalls, gc) } - for i := 0; i < tc.nReadonly; i++ { + for range tc.nReadonly { c, gc := kit.newRecorder(false, injectErr) consumers = append(consumers, c) getCalls = append(getCalls, gc) diff --git a/internal/pipeline/bench_test.go b/internal/pipeline/bench_test.go index 52c924fd..7e98e319 100644 --- a/internal/pipeline/bench_test.go +++ b/internal/pipeline/bench_test.go @@ -29,7 +29,7 @@ func BenchmarkRuntime_StartShutdown(b *testing.B) { } b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { rt := pipeline.NewRuntime([]pipeline.Pipeline{{ ID: pipeline.MustNewID(pipeline.MustNewType("metrics"), "bench"), Exporters: components, @@ -60,7 +60,7 @@ func BenchmarkWrapFirstDataMetrics_ConsumeMetrics(b *testing.B) { ctx := b.Context() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { if err := wrap.ConsumeMetrics(ctx, md); err != nil { b.Fatal(err) } diff --git a/internal/pipeline/chaos_test.go b/internal/pipeline/chaos_test.go index c383b574..8d32accd 100644 --- a/internal/pipeline/chaos_test.go +++ b/internal/pipeline/chaos_test.go @@ -44,7 +44,7 @@ func TestChaos_PanicReceiverPanicExporter(t *testing.T) { consumeErrors atomic.Int64 ) - for i := 0; i < iterations; i++ { + for i := range iterations { seed := uint64(i + 1) rng := rand.New(rand.NewPCG(seed, seed^0x9E3779B97F4A7C15)) diff --git a/internal/pipeline/componentstate_test.go b/internal/pipeline/componentstate_test.go index d90f8080..c70c8cf1 100644 --- a/internal/pipeline/componentstate_test.go +++ b/internal/pipeline/componentstate_test.go @@ -59,7 +59,7 @@ func TestComponentState_ConcurrentAccess(t *testing.T) { const n = 64 var wg sync.WaitGroup wg.Add(n * 4) - for i := 0; i < n; i++ { + for range n { go func() { defer wg.Done(); _ = s.Start(t.Context(), nil) }() go func() { defer wg.Done(); _ = s.Shutdown(t.Context()) }() go func() { defer wg.Done(); _ = s.Started() }() diff --git a/internal/pipeline/firstdata_test.go b/internal/pipeline/firstdata_test.go index ed0bc3c4..cbfefc5d 100644 --- a/internal/pipeline/firstdata_test.go +++ b/internal/pipeline/firstdata_test.go @@ -30,7 +30,7 @@ func TestWrapFirstDataMetrics_LogsOnlyOnce(t *testing.T) { next := &metricsSink{} wrapped := pipeline.WrapFirstDataMetrics("metrics/primary", logger, next) - for i := 0; i < 5; i++ { + for range 5 { require.NoError(t, wrapped.ConsumeMetrics(t.Context(), pmetric.NewMetrics())) } require.Equal(t, int32(5), next.calls.Load(), "every push must reach next") @@ -84,7 +84,7 @@ func TestWrapFirstDataMetrics_ConcurrentFirstPushes_LogOnce(t *testing.T) { const goroutines = 32 var wg sync.WaitGroup wg.Add(goroutines) - for i := 0; i < goroutines; i++ { + for range goroutines { go func() { defer wg.Done() _ = wrapped.ConsumeMetrics(t.Context(), pmetric.NewMetrics()) diff --git a/internal/runtime/lifecycle/lifecycle_test.go b/internal/runtime/lifecycle/lifecycle_test.go index 71a6e86e..cb80eaaa 100644 --- a/internal/runtime/lifecycle/lifecycle_test.go +++ b/internal/runtime/lifecycle/lifecycle_test.go @@ -229,9 +229,9 @@ func TestLifecycle_StartShutdownConcurrent_NoPanic(t *testing.T) { const iterations = 200 const parallel = 8 - for i := 0; i < iterations; i++ { + for range iterations { var wg sync.WaitGroup - for j := 0; j < parallel; j++ { + for range parallel { wg.Add(1) go func() { defer wg.Done() diff --git a/internal/telemetry/leak_test.go b/internal/telemetry/leak_test.go index a985b2d7..27c64065 100644 --- a/internal/telemetry/leak_test.go +++ b/internal/telemetry/leak_test.go @@ -31,7 +31,7 @@ func TestServer_RepeatedStartShutdown_NoLeaks(t *testing.T) { fdBefore, fdSupported := countOpenFDs() - for i := 0; i < 10; i++ { + for i := range 10 { srv, err := telemetry.NewServer(telemetry.ServerConfig{ Listen: freePort(t), MeterProvider: mp, diff --git a/internal/telemetry/server_test.go b/internal/telemetry/server_test.go index d5766249..809fd2db 100644 --- a/internal/telemetry/server_test.go +++ b/internal/telemetry/server_test.go @@ -159,6 +159,28 @@ func TestServer_HealthzReadyz_JSONContentNegotiation(t *testing.T) { } } +// TestServer_NoServerHeader pins the no-fingerprinting invariant. +// Go's net/http does not emit a default Server header today; this test +// fires if a future middleware or handler accidentally adds one (e.g., +// a reverse-proxy shim or a debug instrument). The surface is exposed +// on the operator's pod network, so version fingerprinting is a real +// adversary win even when the value is generic. +func TestServer_NoServerHeader(t *testing.T) { + t.Parallel() + var ready atomic.Bool + ready.Store(true) + _, base := newServer(t, telemetry.WithReadyFn(ready.Load)) + + for _, path := range []string{"/metrics", "/healthz", "/readyz", "/not-a-route"} { + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, base+path, http.NoBody) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err, "GET %s", path) + _ = resp.Body.Close() + require.Empty(t, resp.Header.Values("Server"), "GET %s leaked Server header", path) + } +} + // TestServer_RecoversFromHandlerPanic pins PRINCIPLES §1 ("trust // under load is the product — never crash the workload"): a panic // inside any handler — whether from a buggy SLOSource callback, a diff --git a/internal/telemetry/slo_bench_test.go b/internal/telemetry/slo_bench_test.go index 065afd5b..14b8c042 100644 --- a/internal/telemetry/slo_bench_test.go +++ b/internal/telemetry/slo_bench_test.go @@ -43,7 +43,7 @@ func benchAggregateSLO(b *testing.B, nReaders int) { // Warm the sample buffer so we're benchmarking steady-state, // not the first-scrape "warming up" path. - for i := 0; i < 3; i++ { + for range 3 { _ = src.ExporterFailureRate() } @@ -58,7 +58,7 @@ func benchAggregateSLO(b *testing.B, nReaders int) { // reuse the primitive without an ExporterRegistry layer. func BenchmarkWindowedRate_Observe(b *testing.B) { w := telemetry.NewWindowedRate(telemetry.DefaultSLOWindow) - for i := 0; i < 3; i++ { + for i := range 3 { _ = w.Observe(uint64(i), uint64(i*10)) } b.ResetTimer() diff --git a/internal/telemetry/slo_test.go b/internal/telemetry/slo_test.go index 4f60ef64..2f13c894 100644 --- a/internal/telemetry/slo_test.go +++ b/internal/telemetry/slo_test.go @@ -122,7 +122,7 @@ func TestAggregateSLOSource_WindowedRate_LifetimeRatioNotReflected(t *testing.T) time.Sleep(200 * time.Millisecond) // Many successes since. - for i := 0; i < 10; i++ { + for range 10 { reader.success++ src.ExporterFailureRate() time.Sleep(20 * time.Millisecond) diff --git a/pkg/nccl/fr_parser/fuzz_test.go b/pkg/nccl/fr_parser/fuzz_test.go index 6a9daf14..2546fcd3 100644 --- a/pkg/nccl/fr_parser/fuzz_test.go +++ b/pkg/nccl/fr_parser/fuzz_test.go @@ -141,7 +141,7 @@ func TestFuzzSeeds_AllRejectGracefully(t *testing.T) { func allOpcode(op byte, n int) []byte { out := []byte{opPROTO, 0x05} - for i := 0; i < n; i++ { + for range n { out = append(out, op) } out = append(out, opSTOP) @@ -150,10 +150,10 @@ func allOpcode(op byte, n int) []byte { func deeplyNestedListSeed(n int) []byte { out := []byte{opPROTO, 0x05} - for i := 0; i < n; i++ { + for range n { out = append(out, opEmptyList, opMARK) } - for i := 0; i < n; i++ { + for range n { out = append(out, opAPPENDS) } out = append(out, opSTOP) @@ -176,7 +176,7 @@ func fourGiBDeclaredLength() []byte { // aborts with ErrDepthExceeded. func tupleNestingBypassSeed(n int) []byte { out := []byte{opPROTO, 0x05, opEmptyList} - for i := 0; i < n; i++ { + for range n { out = append(out, opTUPLE1) } out = append(out, opSTOP) diff --git a/pkg/nccl/fr_parser/limits_test.go b/pkg/nccl/fr_parser/limits_test.go index 5f4c399f..bc482e8f 100644 --- a/pkg/nccl/fr_parser/limits_test.go +++ b/pkg/nccl/fr_parser/limits_test.go @@ -47,7 +47,7 @@ func TestParse_MaxBytesEnforced(t *testing.T) { // honor the cap. We trigger via APPENDS with MaxItems=4 and 5 items. func TestParse_MaxItemsEnforced(t *testing.T) { in := []byte{opPROTO, 0x05, opEmptyList, opMARK} - for i := 0; i < 5; i++ { + for i := range 5 { in = append(in, opBININT1, byte(i)) } in = append(in, opAPPENDS, opSTOP) @@ -65,10 +65,10 @@ func TestParse_MaxDepthEnforced(t *testing.T) { // Build: PROTO 5, then N × (EMPTY_LIST MARK), then N × APPENDS, STOP. const N = 300 // > DefaultMaxDepth (256) in := []byte{opPROTO, 0x05} - for i := 0; i < N; i++ { + for range N { in = append(in, opEmptyList, opMARK) } - for i := 0; i < N; i++ { + for range N { in = append(in, opAPPENDS) } in = append(in, opSTOP) @@ -86,7 +86,7 @@ func TestParse_MaxDepthEnforced(t *testing.T) { func TestParse_MaxDepth_TupleNestingBypass(t *testing.T) { const N = 300 // > DefaultMaxDepth (256) in := []byte{opPROTO, 0x05, opEmptyList} - for i := 0; i < N; i++ { + for range N { in = append(in, opTUPLE1) } in = append(in, opSTOP) @@ -107,7 +107,7 @@ func TestParse_MaxDepth_AppendItemTooDeep(t *testing.T) { // Wrap the second list in TUPLE1 enough times that its depth // reaches DefaultMaxDepth exactly (one below the cap, since // TUPLE1 itself is depth-checked at MaxDepth+1). - for i := 0; i < DefaultMaxDepth-1; i++ { + for range DefaultMaxDepth - 1 { in = append(in, opTUPLE1) } // Now APPEND it to the bottom list. List depth would become diff --git a/pkg/nccl/fr_parser/parser.go b/pkg/nccl/fr_parser/parser.go index 19eee3b7..0a827945 100644 --- a/pkg/nccl/fr_parser/parser.go +++ b/pkg/nccl/fr_parser/parser.go @@ -268,7 +268,7 @@ func (p *parser) popN(n int) ([]any, int) { from := len(p.stack) - n out := make([]any, n) maxD := 0 - for i := 0; i < n; i++ { + for i := range n { out[i] = p.stack[from+i] if d := p.stackDepth[from+i]; d > maxD { maxD = d diff --git a/pkg/nccl/fr_parser/parser_test.go b/pkg/nccl/fr_parser/parser_test.go index 25610ab2..8b8cdfb4 100644 --- a/pkg/nccl/fr_parser/parser_test.go +++ b/pkg/nccl/fr_parser/parser_test.go @@ -129,7 +129,7 @@ func TestOpcodeTables_Pinned(t *testing.T) { // either pinned table aborts with ErrUnknownOpcode. This is the // default-deny invariant. func TestParse_UnknownOpcode_DefaultDeny(t *testing.T) { - for op := 0; op < 256; op++ { + for op := range 256 { if _, safe := safeOpcodesPinned[byte(op)]; safe { continue } diff --git a/pkg/nccl/fr_parser/synthesize_test.go b/pkg/nccl/fr_parser/synthesize_test.go index 616e2a76..2672a5a9 100644 --- a/pkg/nccl/fr_parser/synthesize_test.go +++ b/pkg/nccl/fr_parser/synthesize_test.go @@ -72,7 +72,7 @@ func TestSynthesize_RoundTrip(t *testing.T) { // of Go map-iteration order. func TestSynthesize_DictKeysSorted(t *testing.T) { in := map[string]any{"c": int64(3), "a": int64(1), "b": int64(2)} - for i := 0; i < 8; i++ { + for range 8 { bs, err := Synthesize(in) if err != nil { t.Fatalf("Synthesize: %v", err) @@ -80,7 +80,7 @@ func TestSynthesize_DictKeysSorted(t *testing.T) { // First SETITEMS payload should begin with "a" key // (SHORT_BINUNICODE 0x8c, length 1, 'a' = 0x61). idx := -1 - for i := 0; i < len(bs); i++ { + for i := range len(bs) { if bs[i] == opShortBinUnicode { idx = i break diff --git a/tools/failure-inject/xidgen/xidgen.go b/tools/failure-inject/xidgen/xidgen.go index ec43d1c5..fd09b86e 100644 --- a/tools/failure-inject/xidgen/xidgen.go +++ b/tools/failure-inject/xidgen/xidgen.go @@ -75,7 +75,7 @@ func emitKmsg(ctx context.Context, opts Options, bdf string, rng *rand.Rand, w i var sb strings.Builder baseSeq := rng.Uint64N(1_000_000) baseTS := rng.Uint64N(1_000_000_000) - for i := 0; i < opts.Count; i++ { + for i := range opts.Count { seq := baseSeq + uint64(i) ts := baseTS + uint64(i)*100 fmt.Fprintf(&sb, @@ -100,7 +100,7 @@ func emitJournald(ctx context.Context, opts Options, bdf string, rng *rand.Rand, machineID := deterministicHex32(rng) enc := json.NewEncoder(w) enc.SetEscapeHTML(false) - for i := 0; i < opts.Count; i++ { + for i := range opts.Count { rec := map[string]string{ "__REALTIME_TIMESTAMP": strconv.FormatUint(baseRealtime+uint64(i), 10), "__MONOTONIC_TIMESTAMP": strconv.FormatUint(baseMonotonic+uint64(i), 10),