✅ CI: Verify — PR #133 #390
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "✅ CI: Verify" | |
| run-name: >- | |
| ${{ | |
| github.event_name == 'schedule' && '✅ CI: Verify — Weekly schedule' || | |
| github.event_name == 'pull_request' && format('✅ CI: Verify — PR #{0}', github.event.pull_request.number) || | |
| format('✅ CI: Verify — {0}', github.ref_name) | |
| }} | |
| on: | |
| push: | |
| branches: [main, 'release/*'] | |
| pull_request: | |
| branches: [main, 'dev/*'] | |
| merge_group: | |
| schedule: | |
| - cron: '30 6 * * 1' # Weekly on Monday at 06:30 UTC | |
| concurrency: | |
| group: ci-verify-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| env: | |
| GORELEASER_VERSION: v2.15.3 | |
| jobs: | |
| zizmor: | |
| name: "🔒 Workflow Security" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| actions: read # required by zizmor-action telemetry | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Run zizmor | |
| uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7 | |
| with: | |
| token: ${{ github.token }} | |
| # impostor-commit audit requires API access that harden-runner blocks | |
| # in the zizmor container. Skip online audits to keep the scan offline. | |
| online-audits: false | |
| # SARIF upload to code scanning requires GHAS, not available on free | |
| # private repos. Re-enable when sockguard goes public. | |
| advanced-security: false | |
| codeql: | |
| name: "🔍 CodeQL Analysis" | |
| # CodeQL SARIF upload requires GitHub Advanced Security (GHAS), which is not | |
| # available on free private repos. Skip unless the repo is public. | |
| if: github.event.repository.visibility == 'public' && (github.event_name == 'pull_request' || github.event_name == 'schedule' || (github.event_name == 'push' && github.ref == 'refs/heads/main')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| permissions: | |
| actions: read | |
| contents: read | |
| security-events: write | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Initialize CodeQL | |
| uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 | |
| with: | |
| languages: actions, go, javascript-typescript | |
| queries: security-and-quality | |
| - name: Setup Go | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| go-version-file: app/go.mod | |
| cache-dependency-path: app/go.sum | |
| - name: Build Go | |
| run: go build ./... | |
| working-directory: app | |
| - name: Perform CodeQL Analysis | |
| uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 | |
| dependency-review: | |
| name: "📦 Dependency Review" | |
| # Dependency Review requires GitHub Advanced Security on private repos. | |
| # Gated on public visibility so it auto-activates when the repo goes public. | |
| # Disabled until the repository Dependency Graph setting is enabled. | |
| if: ${{ false && github.event.repository.visibility == 'public' && github.event_name == 'pull_request' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Dependency Review | |
| uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 | |
| commit-message: | |
| name: "💬 Commit Message" | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| continue-on-error: true # advisory — does not block merge | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| - name: Validate commit messages | |
| run: | | |
| set -euo pipefail | |
| pattern='^[\xF0-\xF4].*|^[\xE2-\xEF].*' | |
| failed=0 | |
| while IFS= read -r msg; do | |
| first_line="$(echo "${msg}" | head -n1)" | |
| if ! echo "${first_line}" | grep -qP '^(\p{So}|\p{Emoji_Presentation})+\s+\w+(\(\w[\w-]*\))?!?:\s.+$'; then | |
| echo "::warning::Non-conventional commit: ${first_line}" | |
| failed=1 | |
| fi | |
| done < <(git log --format=%s origin/main..HEAD) | |
| if [ "${failed}" -eq 1 ]; then | |
| echo "::warning::Some commits don't follow emoji conventional format. Expected: <emoji> <type>(scope): description" | |
| fi | |
| goreleaser-check: | |
| name: "📦 GoReleaser Config" | |
| if: github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Go | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| go-version-file: app/go.mod | |
| cache-dependency-path: app/go.sum | |
| - name: Run GoReleaser check | |
| uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 | |
| with: | |
| distribution: goreleaser | |
| version: ${{ env.GORELEASER_VERSION }} | |
| args: check | |
| workdir: app | |
| go-lint: | |
| name: "🧹 Go Lint" | |
| if: github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Go | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| go-version-file: app/go.mod | |
| cache-dependency-path: app/go.sum | |
| - name: Check gofmt | |
| working-directory: app | |
| run: | | |
| unformatted="$(gofmt -l .)" | |
| if [ -n "${unformatted}" ]; then | |
| echo "::error::gofmt found unformatted files — run 'gofmt -w .' in app/ and commit:" | |
| echo "${unformatted}" | |
| exit 1 | |
| fi | |
| - name: Run golangci-lint | |
| uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1 | |
| with: | |
| # Pin the linter so Go Lint is deterministic. Leaving it unpinned lets | |
| # the action float to a newer golangci-lint whose staticcheck has | |
| # regressed SA5011: it false-positives "possible nil pointer | |
| # dereference" on `if x == nil { t.Fatal(...) }` guards (t.Fatal ends | |
| # the test via Goexit, so the deref below is unreachable). v2.12.2 | |
| # matches the local lefthook go-lint version and reports 0 issues. | |
| version: v2.12.2 | |
| working-directory: app | |
| go-test: | |
| name: "🧪 Go Test" | |
| if: github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # OIDC lets the Qlty coverage upload authenticate with no stored secret: | |
| # id-token:write mints the OIDC token, contents:read is for checkout. | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Go | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| go-version-file: app/go.mod | |
| cache-dependency-path: app/go.sum | |
| - name: Run tests | |
| run: | | |
| go list ./... | grep -v '/internal/testcert$' | xargs go test -race -coverprofile=coverage.txt -covermode=atomic | |
| working-directory: app | |
| - name: Upload coverage | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: go-coverage | |
| path: app/coverage.txt | |
| retention-days: 14 | |
| - name: Build production-only coverage profile | |
| # Drop non-production packages — those in NO binary's import closure: | |
| # differential (proxy-vs-daemon test harness), internal/testcert (test | |
| # certs), internal/testhelp (test helpers). Their *tests* still run as | |
| # regression checks; only their own statements are excluded so the gate | |
| # and dashboard reflect shipping code. coverage.prod.txt keeps the real | |
| # import paths for the gate (go tool cover needs them); coverage.qlty.txt | |
| # additionally rewrites the module path to app/ because Qlty resolves | |
| # paths against the repo root (sources under app/) and would otherwise | |
| # report 100% of files "missing on disk" and fail validation. | |
| working-directory: app | |
| run: | | |
| grep -vE 'github.com/codeswhat/sockguard/(differential|internal/testcert|internal/testhelp)/' coverage.txt > coverage.prod.txt | |
| sed 's#github.com/codeswhat/sockguard/#app/#g' coverage.prod.txt > coverage.qlty.txt | |
| - name: Publish coverage to Qlty | |
| # OIDC auth — no token/secret to store or rotate (verified working on | |
| # PR #115). Skipped on fork PRs, which GitHub does not grant | |
| # id-token:write. Non-blocking until a green run confirms coverage lands | |
| # in the dashboard, so a publish hiccup can't redden the release-gating | |
| # job during the rc soak; the coverage gate below still enforces the | |
| # floor regardless. | |
| if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} | |
| continue-on-error: true | |
| uses: qltysh/qlty-action/coverage@fd52dc852530a708d68c3b7342f8d33d1df4cd55 # v2.2.1 | |
| with: | |
| oidc: true | |
| files: app/coverage.qlty.txt | |
| format: coverprofile | |
| - name: Coverage gate | |
| # Vendor-free floor on PRODUCTION code: fails the job when the weighted | |
| # statement total of shipping packages drops below COVERAGE_MIN, | |
| # independent of Qlty Cloud being reachable. Uses coverage.prod.txt | |
| # (test harnesses/helpers excluded). Kept a touch under the real total | |
| # (96.7% production-only as of 2026-06) so ordinary churn does not trip | |
| # it; ratchet up as coverage climbs. | |
| working-directory: app | |
| env: | |
| COVERAGE_MIN: "96" | |
| run: | | |
| total=$(go tool cover -func=coverage.prod.txt | awk '/^total:/ {print $3}' | tr -d '%') | |
| echo "Production statement coverage: ${total}% (floor ${COVERAGE_MIN}%)" | |
| awk -v t="$total" -v m="$COVERAGE_MIN" 'BEGIN { exit (t+0 >= m+0) ? 0 : 1 }' || { | |
| echo "::error::production coverage ${total}% is below the ${COVERAGE_MIN}% floor" | |
| exit 1 | |
| } | |
| go-fuzz: | |
| # Tier 1 — per-PR smoke-level fuzzing. Each fuzzer gets a fixed 60 s | |
| # coverage budget in its own matrix job so the whole matrix runs in | |
| # parallel and the critical path stays at ~1–2 min wall time regardless | |
| # of how many fuzzers we ship. The deep-coverage tier lives in | |
| # .github/workflows/quality-fuzz-nightly.yml where each fuzzer gets 15 | |
| # minutes by default (configurable via workflow_dispatch input). | |
| # | |
| # `fail-fast: false` lets every fuzzer complete even if a sibling finds | |
| # a crash, so one failure surfaces alongside the other fuzzers' state | |
| # instead of aborting the run early. The Go test timeout stays above | |
| # the fuzz budget so a hung input produces a stack dump instead of | |
| # tying up the runner until the job-level ceiling fires. | |
| name: "🔀 Go Fuzz (${{ matrix.fuzzer.name }})" | |
| if: github.event_name == 'pull_request' || github.event_name == 'schedule' || (github.event_name == 'push' && github.ref == 'refs/heads/main') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| fuzzer: | |
| - { name: FuzzPathMatch, pkg: ./internal/filter/ } | |
| - { name: FuzzGlobToRegex, pkg: ./internal/filter/ } | |
| - { name: FuzzGlobToRegexString, pkg: ./internal/filter/ } | |
| - { name: FuzzNormalizePath, pkg: ./internal/filter/ } | |
| - { name: FuzzCompileRule, pkg: ./internal/filter/ } | |
| - { name: FuzzBuild, pkg: ./internal/filter/ } | |
| - { name: FuzzContainerCreate, pkg: ./internal/filter/ } | |
| - { name: FuzzExec, pkg: ./internal/filter/ } | |
| - { name: FuzzImagePull, pkg: ./internal/filter/ } | |
| - { name: FuzzVolume, pkg: ./internal/filter/ } | |
| - { name: FuzzSecret, pkg: ./internal/filter/ } | |
| - { name: FuzzConfigWrite, pkg: ./internal/filter/ } | |
| - { name: FuzzService, pkg: ./internal/filter/ } | |
| - { name: FuzzSwarm, pkg: ./internal/filter/ } | |
| - { name: FuzzPlugin, pkg: ./internal/filter/ } | |
| - { name: FuzzNetwork, pkg: ./internal/filter/ } | |
| - { name: FuzzProxyHeadersAndBody, pkg: ./internal/proxy/ } | |
| - { name: FuzzHijackHeadersAndBody, pkg: ./internal/proxy/ } | |
| - { name: FuzzPathRoutingDifferential, pkg: ./differential/ } | |
| - { name: FuzzVisibilityFilter, pkg: ./internal/visibility/ } | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Go | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| go-version-file: app/go.mod | |
| cache-dependency-path: app/go.sum | |
| - name: Fuzz ${{ matrix.fuzzer.name }} | |
| working-directory: app | |
| env: | |
| FUZZTIME: 60s | |
| TEST_TIMEOUT: 5m | |
| run: | | |
| go test -run=^$ \ | |
| -fuzz=^${{ matrix.fuzzer.name }}$ \ | |
| -fuzztime="${FUZZTIME}" \ | |
| -timeout="${TEST_TIMEOUT}" \ | |
| ${{ matrix.fuzzer.pkg }} | |
| - name: Upload fuzz corpus on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: fuzz-corpus-${{ matrix.fuzzer.name }}-${{ github.run_id }} | |
| path: app/**/testdata/fuzz/${{ matrix.fuzzer.name }}/ | |
| retention-days: 30 | |
| if-no-files-found: ignore | |
| ts-lint: | |
| name: "🎨 Biome Lint" | |
| if: github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Node | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Run Biome | |
| run: npx biome check . | |
| ts-test: | |
| name: "🧪 TS Test" | |
| if: github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Node | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Run tests | |
| run: npm test | |
| ts-build: | |
| name: "🏗️ Build Workspaces" | |
| if: github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Node | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build all workspaces | |
| run: npx turbo build | |
| docker: | |
| name: "🐳 Docker Build" | |
| if: github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| # Single-arch build to verify Dockerfile builds correctly. | |
| # Multi-arch (amd64 + arm64) is handled by the release workflow. | |
| - name: Build (single-arch) | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: . | |
| push: false | |
| load: true | |
| tags: sockguard:ci | |
| build-args: VERSION=ci | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max |