Skip to content

✅ CI: Verify — PR #133 #390

✅ CI: Verify — PR #133

✅ CI: Verify — PR #133 #390

Workflow file for this run

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