mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
ae59a5e88a
* Add ability to disable replay protection. - This is useful for runs with multiple traefik replicas to avoid false positives and tokens re-creation. * Enhance the CI/CD pipelines * Increase test coverage. * Update vendored dependencies. * Update behaviour on forceHTTPS as per issue #82
630 lines
18 KiB
YAML
630 lines
18 KiB
YAML
name: PR Validation
|
||
|
||
on:
|
||
pull_request:
|
||
branches: [ main ]
|
||
push:
|
||
branches: [ main ]
|
||
|
||
permissions:
|
||
contents: read
|
||
pull-requests: write
|
||
checks: write
|
||
security-events: write
|
||
|
||
jobs:
|
||
# Fast feedback - format and basic checks
|
||
quick-checks:
|
||
name: Quick Checks
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Format check
|
||
run: |
|
||
# Exclude vendor directory from format checks
|
||
UNFORMATTED=$(gofmt -s -l . | grep -v "^vendor/" || true)
|
||
if [ -n "$UNFORMATTED" ]; then
|
||
echo "Code is not formatted. Run: gofmt -s -w ."
|
||
echo "Unformatted files:"
|
||
echo "$UNFORMATTED"
|
||
gofmt -s -d $(echo "$UNFORMATTED")
|
||
exit 1
|
||
fi
|
||
|
||
- name: Go vet
|
||
run: go vet ./...
|
||
|
||
- name: Go mod verify
|
||
run: go mod verify
|
||
|
||
- name: Go mod tidy check
|
||
run: |
|
||
go mod tidy
|
||
git diff --exit-code go.mod go.sum
|
||
|
||
# Static analysis with golangci-lint (advisory - will not fail the build)
|
||
golangci-lint:
|
||
name: golangci-lint
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: golangci-lint
|
||
uses: golangci/golangci-lint-action@v8
|
||
with:
|
||
version: latest
|
||
args: --timeout=10m
|
||
continue-on-error: true # Allow pipeline to continue even with linting warnings
|
||
|
||
# Staticcheck analysis
|
||
staticcheck:
|
||
name: Staticcheck
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Install staticcheck
|
||
run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||
|
||
- name: Run staticcheck
|
||
run: staticcheck ./...
|
||
|
||
# Security scanning with gosec
|
||
gosec:
|
||
name: Gosec Security Scanner
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Run Gosec Security Scanner
|
||
run: |
|
||
go install github.com/securego/gosec/v2/cmd/gosec@latest
|
||
gosec -no-fail -fmt sarif -out results.sarif ./... || echo "Gosec completed with warnings"
|
||
continue-on-error: true
|
||
|
||
- name: Upload SARIF file
|
||
if: always() && hashFiles('results.sarif') != ''
|
||
uses: github/codeql-action/upload-sarif@v3
|
||
with:
|
||
sarif_file: results.sarif
|
||
continue-on-error: true
|
||
|
||
# Vulnerability scanning
|
||
govulncheck:
|
||
name: Vulnerability Scan
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Install govulncheck
|
||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||
|
||
- name: Run govulncheck
|
||
run: govulncheck ./...
|
||
|
||
# CodeQL analysis
|
||
codeql:
|
||
name: CodeQL Analysis
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Initialize CodeQL
|
||
uses: github/codeql-action/init@v3
|
||
with:
|
||
languages: go
|
||
continue-on-error: true
|
||
|
||
- name: Autobuild
|
||
uses: github/codeql-action/autobuild@v3
|
||
continue-on-error: true
|
||
|
||
- name: Perform CodeQL Analysis
|
||
uses: github/codeql-action/analyze@v3
|
||
continue-on-error: true
|
||
|
||
# Unit tests with race detection
|
||
test-race:
|
||
name: Unit Tests (Race Detector)
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Run tests with race detector
|
||
run: go test -race -timeout=15m -count=1 -v ./...
|
||
env:
|
||
GOMAXPROCS: 4
|
||
|
||
# Coverage analysis with threshold check
|
||
test-coverage:
|
||
name: Test Coverage
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Run tests with coverage
|
||
run: |
|
||
go test -coverprofile=coverage.out -covermode=atomic -timeout=15m ./...
|
||
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%"
|
||
|
||
# Get per-package coverage
|
||
echo "## Coverage by Package" >> coverage_report.md
|
||
echo "" >> coverage_report.md
|
||
go tool cover -func=coverage.out | grep -v "total:" | awk '{print "- " $1 ": " $3}' >> coverage_report.md || true
|
||
|
||
- name: Upload coverage to Codecov
|
||
uses: codecov/codecov-action@v4
|
||
with:
|
||
file: ./coverage.out
|
||
flags: unittests
|
||
name: codecov-umbrella
|
||
fail_ci_if_error: false
|
||
continue-on-error: true
|
||
|
||
- name: Comment coverage on PR
|
||
if: github.event_name == 'pull_request'
|
||
uses: actions/github-script@v8
|
||
with:
|
||
script: |
|
||
const fs = require('fs');
|
||
const coverage = '${{ steps.coverage.outputs.coverage }}';
|
||
let coverageReport = '';
|
||
|
||
try {
|
||
coverageReport = fs.readFileSync('coverage_report.md', 'utf8');
|
||
} catch (e) {
|
||
coverageReport = 'Coverage details not available';
|
||
}
|
||
|
||
const threshold = 70;
|
||
const coverageNum = parseFloat(coverage);
|
||
const emoji = coverageNum >= threshold ? '✅' : '⚠️';
|
||
|
||
const body = `## ${emoji} Test Coverage Report\n\n**Total Coverage:** ${coverage}%\n**Threshold:** ${threshold}%\n\n${coverageReport}`;
|
||
|
||
// Find existing comment
|
||
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
|
||
run: |
|
||
COVERAGE=${{ steps.coverage.outputs.coverage }}
|
||
THRESHOLD=70
|
||
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%"
|
||
|
||
# Memory leak detection
|
||
test-memory-leaks:
|
||
name: Memory Leak Detection
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Run goroutine leak tests
|
||
run: |
|
||
echo "Running goroutine leak detection tests..."
|
||
go test -v -timeout=20m -run='.*[Gg]oroutine.*[Ll]eak.*' ./... || echo "No goroutine leak tests found"
|
||
|
||
- name: Run memory leak tests
|
||
run: |
|
||
echo "Running memory leak detection tests..."
|
||
go test -v -timeout=20m -run='.*[Mm]emory.*[Ll]eak.*' ./... || echo "No memory leak tests found"
|
||
|
||
- name: Run cleanup tests
|
||
run: |
|
||
echo "Running cleanup and resource management tests..."
|
||
go test -v -timeout=20m -run='.*[Cc]leanup.*' ./... || echo "No cleanup tests found"
|
||
|
||
# Integration tests
|
||
test-integration:
|
||
name: Integration Tests
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Run integration tests
|
||
run: |
|
||
if [ -d "./integration" ]; then
|
||
go test -v -timeout=20m ./integration/...
|
||
else
|
||
echo "Running integration tests from all packages..."
|
||
go test -v -timeout=20m -run='.*[Ii]ntegration.*' ./...
|
||
fi
|
||
|
||
# Regression tests
|
||
test-regression:
|
||
name: Regression Tests
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Run regression tests
|
||
run: |
|
||
echo "Running regression tests..."
|
||
go test -v -timeout=20m -run='.*[Rr]egression.*' ./...
|
||
|
||
# Provider-specific tests (parallel matrix)
|
||
test-providers:
|
||
name: Provider Tests (${{ matrix.provider }})
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
provider:
|
||
- google
|
||
- azure
|
||
- auth0
|
||
- okta
|
||
- keycloak
|
||
- cognito
|
||
- gitlab
|
||
- github
|
||
- generic
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Run ${{ matrix.provider }} provider tests
|
||
run: |
|
||
PROVIDER_CAP=$(echo "${{ matrix.provider }}" | sed 's/.*/\u&/')
|
||
echo "Testing $PROVIDER_CAP provider..."
|
||
go test -v -timeout=10m -run=".*$PROVIDER_CAP.*" ./internal/providers/... || true
|
||
go test -v -timeout=10m -run=".*${{ matrix.provider }}.*" ./... || true
|
||
|
||
# Benchmark tests with performance tracking
|
||
benchmark:
|
||
name: Benchmark Tests
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Run benchmarks
|
||
run: |
|
||
echo "Running benchmark tests..."
|
||
go test -bench=. -benchmem -benchtime=1s -run=^$ ./... | tee benchmark.txt
|
||
|
||
- name: Upload benchmark results
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: benchmark-results
|
||
path: benchmark.txt
|
||
retention-days: 30
|
||
|
||
- name: Compare benchmarks
|
||
if: github.event_name == 'pull_request'
|
||
continue-on-error: true
|
||
run: |
|
||
echo "Benchmark results available in artifacts"
|
||
echo "To compare with main branch, download previous benchmark results"
|
||
|
||
# Build validation across platforms
|
||
build:
|
||
name: Build (${{ matrix.os }}/${{ matrix.arch }})
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
os: [linux, darwin]
|
||
arch: [amd64, arm64]
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Build for ${{ matrix.os }}/${{ matrix.arch }}
|
||
env:
|
||
GOOS: ${{ matrix.os }}
|
||
GOARCH: ${{ matrix.arch }}
|
||
run: |
|
||
echo "Building for $GOOS/$GOARCH..."
|
||
go build -v -ldflags="-s -w" ./...
|
||
|
||
# Security-specific edge case tests
|
||
test-security-edge-cases:
|
||
name: Security Edge Cases
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Run security edge case tests
|
||
run: |
|
||
echo "Running security edge case tests..."
|
||
go test -v -timeout=15m -run='.*[Ss]ecurity.*' ./...
|
||
|
||
# Session management tests
|
||
test-session:
|
||
name: Session Management Tests
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Run session tests
|
||
run: |
|
||
echo "Running session management tests..."
|
||
go test -v -timeout=15m -run='.*[Ss]ession.*' ./...
|
||
|
||
# Token validation tests
|
||
test-token:
|
||
name: Token Validation Tests
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Run token validation tests
|
||
run: |
|
||
echo "Running token validation tests..."
|
||
go test -v -timeout=15m -run='.*[Tt]oken.*' ./...
|
||
|
||
# CSRF and security tests
|
||
test-csrf:
|
||
name: CSRF and Security Tests
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: '1.24'
|
||
cache: true
|
||
|
||
- name: Run CSRF tests
|
||
run: |
|
||
echo "Running CSRF and security tests..."
|
||
go test -v -timeout=15m -run='.*[Cc][Ss][Rr][Ff].*' ./...
|
||
|
||
# Multi-Go version compatibility
|
||
test-go-versions:
|
||
name: Go ${{ matrix.go-version }} Compatibility
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
go-version: ['1.24']
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v5
|
||
|
||
- name: Setup Go ${{ matrix.go-version }}
|
||
uses: actions/setup-go@v6
|
||
with:
|
||
go-version: ${{ matrix.go-version }}
|
||
cache: true
|
||
|
||
- name: Run tests on Go ${{ matrix.go-version }}
|
||
run: go test -short -timeout=10m ./...
|
||
|
||
# Final validation - all checks must pass (golangci-lint is advisory)
|
||
all-checks-passed:
|
||
name: ✅ All Checks Passed
|
||
runs-on: ubuntu-latest
|
||
needs:
|
||
- quick-checks
|
||
- golangci-lint
|
||
- staticcheck
|
||
- gosec
|
||
- govulncheck
|
||
- codeql
|
||
- test-race
|
||
- test-coverage
|
||
- test-memory-leaks
|
||
- test-integration
|
||
- test-regression
|
||
- test-providers
|
||
- benchmark
|
||
- build
|
||
- test-security-edge-cases
|
||
- test-session
|
||
- test-token
|
||
- test-csrf
|
||
- test-go-versions
|
||
if: always()
|
||
steps:
|
||
- name: Check all jobs status
|
||
run: |
|
||
echo "Checking status of all jobs..."
|
||
|
||
# Check critical jobs (excluding golangci-lint which is advisory)
|
||
CRITICAL_FAILURES=false
|
||
|
||
if [ "${{ needs.quick-checks.result }}" == "failure" ] || \
|
||
[ "${{ needs.staticcheck.result }}" == "failure" ] || \
|
||
[ "${{ needs.test-race.result }}" == "failure" ] || \
|
||
[ "${{ needs.test-coverage.result }}" == "failure" ] || \
|
||
[ "${{ needs.build.result }}" == "failure" ]; then
|
||
CRITICAL_FAILURES=true
|
||
fi
|
||
|
||
if [ "$CRITICAL_FAILURES" == "true" ]; then
|
||
echo "❌ Critical checks failed"
|
||
exit 1
|
||
elif [ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]; then
|
||
echo "⚠️ Some checks were cancelled"
|
||
exit 1
|
||
else
|
||
echo "✅ All critical checks passed successfully!"
|
||
if [ "${{ needs.golangci-lint.result }}" != "success" ]; then
|
||
echo "ℹ️ Note: golangci-lint reported issues (advisory only)"
|
||
fi
|
||
fi
|
||
|
||
- name: Post summary
|
||
if: always()
|
||
run: |
|
||
echo "# PR Validation Summary" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "## Job Status" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Quick Checks: ${{ needs.quick-checks.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Linting (advisory): ${{ needs.golangci-lint.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Static Analysis: ${{ needs.staticcheck.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Security Scan (gosec): ${{ needs.gosec.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Vulnerability Scan: ${{ needs.govulncheck.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- CodeQL: ${{ needs.codeql.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Race Detection: ${{ needs.test-race.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Coverage: ${{ needs.test-coverage.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Memory Leaks: ${{ needs.test-memory-leaks.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Integration Tests: ${{ needs.test-integration.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Regression Tests: ${{ needs.test-regression.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Provider Tests: ${{ needs.test-providers.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Benchmarks: ${{ needs.benchmark.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Build: ${{ needs.build.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Security Edge Cases: ${{ needs.test-security-edge-cases.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Session Tests: ${{ needs.test-session.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Token Tests: ${{ needs.test-token.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- CSRF Tests: ${{ needs.test-csrf.result }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Go Version Compatibility: ${{ needs.test-go-versions.result }}" >> $GITHUB_STEP_SUMMARY
|