name: Go Pull Request on: workflow_call: inputs: go-version: description: "Go version to use" required: false type: string default: ">=1.24" coverage-threshold: description: "Minimum coverage percentage required (0 to disable)" required: false type: number default: 0 lfs: description: "Enable Git LFS checkout (for repos with large files)" required: false type: boolean default: false # Caller must declare these permissions: # permissions: # contents: read # pull-requests: write # security-events: write jobs: static-analysis: name: Static Analysis runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 lfs: ${{ inputs.lfs }} - name: Run workflow prepare script shell: bash run: | if [ -f "./workflow-prepare.sh" ]; then chmod +x ./workflow-prepare.sh ./workflow-prepare.sh fi - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ inputs.go-version }} cache: true - name: Install staticcheck run: go install honnef.co/go/tools/cmd/staticcheck@latest - name: Run go vet run: go vet ./... - name: Run staticcheck run: staticcheck ./... security-scan: name: Security Scan runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 lfs: ${{ inputs.lfs }} - name: Run workflow prepare script shell: bash run: | if [ -f "./workflow-prepare.sh" ]; then chmod +x ./workflow-prepare.sh ./workflow-prepare.sh fi - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ inputs.go-version }} cache: true - name: Run TruffleHog uses: trufflesecurity/trufflehog@main with: extra_args: --only-verified - name: Install govulncheck run: go install golang.org/x/vuln/cmd/govulncheck@latest - name: Run govulncheck run: | # Run govulncheck and capture output set +e OUTPUT=$(govulncheck -show verbose ./... 2>&1) EXIT_CODE=$? echo "$OUTPUT" # If there are vulnerabilities, check if they're only in stdlib if [ $EXIT_CODE -ne 0 ]; then # Check if there are any third-party package vulnerabilities # Stdlib vulnerabilities show "Found in: @go" (e.g., crypto/x509@go1.25.4) # Third-party vulns show "Found in: @v" (e.g., github.com/foo/bar@v1.2.3) if echo "$OUTPUT" | grep -q "Found in:"; then # If no "Found in:" lines contain @v (third-party version), only stdlib affected if ! echo "$OUTPUT" | grep "Found in:" | grep -qE "@v[0-9]"; then echo "" echo "⚠️ Only stdlib vulnerabilities found - these require a Go version upgrade" echo "✅ No vulnerabilities in third-party dependencies" exit 0 fi fi exit $EXIT_CODE fi gosec: name: Gosec SARIF runs-on: ubuntu-latest permissions: contents: read security-events: write steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Run workflow prepare script shell: bash run: | if [ -f "./workflow-prepare.sh" ]; then chmod +x ./workflow-prepare.sh ./workflow-prepare.sh fi - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ inputs.go-version }} cache: true - name: Install gosec run: go install github.com/securego/gosec/v2/cmd/gosec@latest - name: Run gosec run: gosec -no-fail -fmt sarif -out gosec-results.sarif ./... || true - name: Upload gosec SARIF if: always() && hashFiles('gosec-results.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: gosec-results.sarif category: gosec continue-on-error: true goreleaser-check: name: GoReleaser Config Validation runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 lfs: ${{ inputs.lfs }} - name: Run workflow prepare script shell: bash run: | if [ -f "./workflow-prepare.sh" ]; then chmod +x ./workflow-prepare.sh ./workflow-prepare.sh fi - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ inputs.go-version }} cache: true - name: Check goreleaser config if: hashFiles('.goreleaser.yml', '.goreleaser.yaml') != '' uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: "~> v2" args: check codeql: name: CodeQL Analysis runs-on: ubuntu-latest permissions: contents: read security-events: write steps: - name: Checkout uses: actions/checkout@v4 - name: Run workflow prepare script shell: bash run: | if [ -f "./workflow-prepare.sh" ]; then chmod +x ./workflow-prepare.sh ./workflow-prepare.sh fi - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ inputs.go-version }} cache: true - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: go - name: Autobuild uses: github/codeql-action/autobuild@v4 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 test: name: Tests & Coverage runs-on: ubuntu-latest outputs: coverage: ${{ steps.coverage.outputs.coverage }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 lfs: ${{ inputs.lfs }} - name: Run workflow prepare script shell: bash run: | if [ -f "./workflow-prepare.sh" ]; then chmod +x ./workflow-prepare.sh ./workflow-prepare.sh fi - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ inputs.go-version }} cache: true - name: Run tests with coverage run: | go test -race -coverprofile=coverage.out -covermode=atomic ./... go tool cover -func=coverage.out -o=coverage.txt - name: Calculate coverage id: coverage run: | COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//') echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT echo "Total Coverage: $COVERAGE%" coverage-report: name: Coverage Report runs-on: ubuntu-latest needs: test if: always() && needs.test.result == 'success' steps: - name: Comment coverage on PR if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const coverage = '${{ needs.test.outputs.coverage }}'; const threshold = ${{ inputs.coverage-threshold }}; const coverageNum = parseFloat(coverage); const hasThreshold = threshold > 0; const meetsThreshold = !hasThreshold || coverageNum >= threshold; const emoji = meetsThreshold ? '✅' : '⚠️'; const status = hasThreshold ? (meetsThreshold ? 'meets' : 'below') + ' threshold' : 'reported'; let body = `## ${emoji} Test Coverage Report\n\n| Metric | Value |\n|--------|-------|\n| **Total Coverage** | ${coverage}% |`; if (hasThreshold) { body += `\n| **Threshold** | ${threshold}% |\n| **Status** | ${emoji} Coverage ${status} |`; } const { data: comments } = await github.rest.issues.listComments({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, }); const botComment = comments.find(comment => comment.user.type === 'Bot' && comment.body.includes('Test Coverage Report') ); if (botComment) { await github.rest.issues.updateComment({ comment_id: botComment.id, owner: context.repo.owner, repo: context.repo.repo, body: body }); } else { await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: body }); } - name: Check coverage threshold if: inputs.coverage-threshold > 0 run: | COVERAGE=${{ needs.test.outputs.coverage }} THRESHOLD=${{ inputs.coverage-threshold }} echo "Coverage: $COVERAGE%" echo "Threshold: $THRESHOLD%" if (( $(echo "$COVERAGE < $THRESHOLD" | bc -l) )); then echo "❌ Coverage $COVERAGE% is below threshold $THRESHOLD%" exit 1 fi echo "✅ Coverage $COVERAGE% meets threshold $THRESHOLD%"