diff --git a/.github/workflows/actions-audit.yaml b/.github/workflows/actions-audit.yaml index e3efc27..123e78f 100644 --- a/.github/workflows/actions-audit.yaml +++ b/.github/workflows/actions-audit.yaml @@ -48,6 +48,11 @@ jobs: run: go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 - name: Run actionlint + env: + # Gate on shellcheck warning+ only. The info/style nits (SC2012 "use + # find not ls", SC2035 "use ./*glob*") in the canonical pr.yaml are + # not worth failing every PR over; warnings and errors still gate. + SHELLCHECK_OPTS: --severity=warning run: actionlint -color zizmor: @@ -76,9 +81,9 @@ jobs: GH_TOKEN: ${{ github.token }} # Report-only for now: don't fail the job on findings, upload them to # Code Scanning instead. Remove `|| true` to turn this into a gate. - run: zizmor --format sarif .github/workflows/ > zizmor.sarif || true + run: zizmor --config .zizmor.yml --format sarif .github/workflows/ > zizmor.sarif || true - name: Upload zizmor SARIF - uses: github/codeql-action/upload-sarif@e0647621c2984b5ed2f768cb892365bf2a616ad1 # v4 + uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4 with: sarif_file: zizmor.sarif diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 29a43fc..7309a7b 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -77,7 +77,7 @@ jobs: - name: Initialize CodeQL if: steps.check-csharp.outputs.has-csharp == 'true' - uses: github/codeql-action/init@e0647621c2984b5ed2f768cb892365bf2a616ad1 # v4 + uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4 with: languages: ${{ matrix.language }} # security-extended adds the broader security query pack on top of the @@ -159,7 +159,7 @@ jobs: - name: Perform CodeQL Analysis id: perform-codeql-analysis if: steps.check-csharp.outputs.has-csharp == 'true' - uses: github/codeql-action/analyze@e0647621c2984b5ed2f768cb892365bf2a616ad1 # v4 + uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/perf-regression.yaml b/.github/workflows/perf-regression.yaml new file mode 100644 index 0000000..52c0a10 --- /dev/null +++ b/.github/workflows/perf-regression.yaml @@ -0,0 +1,87 @@ +name: Perf Regression + +# Per-PR performance-regression detection (#144). +# +# Runs the BenchmarkDotNet suite on the PR and compares it against the baseline +# published to gh-pages (dev/bench) by benchmarks.yaml. If a benchmark regresses +# beyond the alert threshold, github-action-benchmark comments on the PR. +# +# Report-only for now: BenchmarkDotNet on shared CI runners is noisy, so this +# does NOT fail the build (`fail-on-alert: false`) and does NOT update the +# baseline (`auto-push: false`). Flip `fail-on-alert` to true — with a threshold +# tuned to the observed noise — to turn it into a gate. + +on: + pull_request: + paths: + - 'src/**' + - 'benchmarks/**' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: perf-regression-${{ github.ref }} + cancel-in-progress: true + +jobs: + benchmark: + name: Benchmark vs base + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + pull-requests: write # post the regression alert comment + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - name: Setup .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + dotnet-version: '10.0.x' + + - name: Restore + run: dotnet restore benchmarks/Wolfgang.Etl.TestKit.Benchmarks/Wolfgang.Etl.TestKit.Benchmarks.csproj + + - name: Build (Release) + run: dotnet build -c Release --no-restore benchmarks/Wolfgang.Etl.TestKit.Benchmarks/Wolfgang.Etl.TestKit.Benchmarks.csproj + + - name: Run benchmarks + working-directory: benchmarks/Wolfgang.Etl.TestKit.Benchmarks + run: dotnet run -c Release --no-build -- --filter "*" --job short --memory --exporters json + + - name: Merge per-class BDN reports + id: locate + working-directory: benchmarks/Wolfgang.Etl.TestKit.Benchmarks + run: | + shopt -s nullglob + reports=(BenchmarkDotNet.Artifacts/results/*-report-full-compressed.json) + if [ ${#reports[@]} -eq 0 ]; then + echo "::error::No BenchmarkDotNet JSON report found" + exit 1 + fi + jq -s '{ + Title: "BenchmarkDotNet combined report", + HostEnvironmentInfo: .[0].HostEnvironmentInfo, + Benchmarks: [.[].Benchmarks[]] + }' "${reports[@]}" > benchmarks-result.json + echo "report=benchmarks/Wolfgang.Etl.TestKit.Benchmarks/benchmarks-result.json" >> "$GITHUB_OUTPUT" + + - name: Compare against gh-pages baseline + uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1 + with: + name: BenchmarkDotNet + tool: 'benchmarkdotnet' + output-file-path: ${{ steps.locate.outputs.report }} + gh-pages-branch: gh-pages + benchmark-data-dir-path: dev/bench + github-token: ${{ secrets.GITHUB_TOKEN }} + alert-threshold: '200%' + comment-on-alert: true + fail-on-alert: false + auto-push: false + save-data-file: false diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 177debb..a9e6b3a 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -275,7 +275,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: ref: refs/pull/${{ github.event.pull_request.number }}/head persist-credentials: false @@ -337,7 +337,7 @@ jobs: echo "✅ Configuration files secured - using versions from main branch" - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 with: # Match the test stages' SDK list so the solution's older-TFM projects # (test projects target netcoreapp3.1 / net5.0-net7.0) restore and build @@ -394,7 +394,7 @@ jobs: --no-build - name: Upload SARIF to Code Scanning - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4 with: sarif_file: inspect.sarif diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yaml index 38e7269..5246e26 100644 --- a/.github/workflows/scorecard.yaml +++ b/.github/workflows/scorecard.yaml @@ -45,6 +45,6 @@ jobs: publish_results: true - name: Upload SARIF to Code Scanning - uses: github/codeql-action/upload-sarif@e0647621c2984b5ed2f768cb892365bf2a616ad1 # v4 + uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4 with: sarif_file: results.sarif diff --git a/.github/workflows/semgrep.yaml b/.github/workflows/semgrep.yaml index 54c85f3..3845a86 100644 --- a/.github/workflows/semgrep.yaml +++ b/.github/workflows/semgrep.yaml @@ -57,6 +57,6 @@ jobs: --error || true - name: Upload Semgrep SARIF - uses: github/codeql-action/upload-sarif@e0647621c2984b5ed2f768cb892365bf2a616ad1 # v4 + uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4 with: sarif_file: semgrep.sarif diff --git a/.zizmor.yml b/.zizmor.yml index f9d2374..efcc5db 100644 --- a/.zizmor.yml +++ b/.zizmor.yml @@ -12,3 +12,11 @@ rules: config: policies: "*": hash-pin + dangerous-triggers: + # pr.yaml deliberately uses `pull_request_target` as a *gated* workflow: it + # runs from the trusted main branch, checks out PR code via refs/pull/*/head + # (never executing untrusted workflow YAML), and re-fetches analyzers/config + # from main so a malicious PR cannot disable checks. This is the canonical + # secure pattern, not the vulnerability the rule warns about. + ignore: + - pr.yaml