diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..1c40a95 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,109 @@ +# GitHub Actions Workflows + +This directory contains all CI/CD workflows for the ONVIF Go library. + +## Workflows + +### ๐Ÿ”„ CI (`ci.yml`) +Main continuous integration workflow that runs on every push and pull request. + +**Jobs:** +- **validate** - Quick validation (formatting, vet, lint) +- **test** - Run tests with coverage on Go 1.23 +- **test-matrix** - Test on multiple Go versions (1.21, 1.22, 1.23) and platforms (Linux, macOS, Windows) +- **build** - Build verification for all packages and examples +- **sonarcloud** - Code quality analysis (runs on master/main only) + +**Triggers:** +- Push to `master`, `main`, `develop` +- Pull requests to `master`, `main`, `develop` + +### ๐Ÿงช Extended Tests (`test.yml`) +Extended testing workflow for comprehensive test coverage. + +**Jobs:** +- **test-older-versions** - Test on older Go versions (1.19, 1.20) +- **benchmark** - Run benchmark tests +- **race-detector** - Extended race detector tests + +**Triggers:** +- Manual dispatch +- Weekly schedule (Sunday 2 AM UTC) +- Push to `master`/`main` when Go files change + +### ๐Ÿ“Š Coverage Analysis (`coverage.yml`) +Post-CI coverage analysis and reporting. + +**Jobs:** +- **coverage-analysis** - Detailed coverage analysis with package breakdown + +**Triggers:** +- After successful CI workflow on `master`/`main` + +### ๐Ÿš€ Release (`release.yml`) +Automated release workflow for creating GitHub releases. + +**Jobs:** +- **build** - Build binaries for all platforms (Linux, Windows, macOS, multiple architectures) +- **release** - Create GitHub release with artifacts +- **docker** - Build and push Docker images to GHCR + +**Triggers:** +- Push tags matching `v*.*.*` +- Manual dispatch with version input + +### ๐Ÿ” Lint (`lint.yml`) +Dedicated linting workflow. + +**Triggers:** +- Push to `master`, `main`, `develop` +- Pull requests + +### ๐Ÿ”’ Security (`security.yml`) +Security scanning workflow. + +**Jobs:** +- **gosec** - Security scanner +- **govulncheck** - Vulnerability checker + +**Triggers:** +- Push to `master`/`main` +- Pull requests +- Weekly schedule + +### ๐Ÿ“š Documentation (`docs.yml`) +Documentation validation workflow. + +**Triggers:** +- Push to `master`/`main` when docs change +- Manual dispatch + +### ๐Ÿ” Dependency Review (`dependency-review.yml`) +Dependency vulnerability review. + +**Triggers:** +- Pull requests + +## Workflow Status + +All workflows use: +- โœ… Latest action versions +- โœ… Go 1.23 as primary version +- โœ… Caching for faster builds +- โœ… Matrix builds for multiple platforms +- โœ… Artifact uploads for coverage and releases + +## Required Secrets + +- `CODECOV_TOKEN` - For coverage reporting (optional) +- `SONAR_TOKEN` - For SonarCloud analysis (optional) +- `DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN` - For Docker Hub (optional) + +## Concurrency + +Workflows use concurrency groups to cancel in-progress runs when new commits are pushed, saving CI resources. + +--- + +*Last Updated: December 2, 2025* + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b3739f..79e00dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: push: branches: [ master, main, develop ] pull_request: - branches: [ master ] + branches: [ master, main, develop ] permissions: contents: read @@ -16,19 +16,10 @@ concurrency: cancel-in-progress: true jobs: - # Status check - always runs - status-check: - name: Workflow Status - runs-on: ubuntu-latest - steps: - - name: Workflow started - run: echo "โœ… CI workflow is running" - # Quick validation - fail fast on obvious issues validate: name: Quick Validation runs-on: ubuntu-latest - needs: status-check steps: - name: Checkout code @@ -42,7 +33,9 @@ jobs: - name: Cache Go modules uses: actions/cache@v4 with: - path: ~/go/pkg/mod + path: | + ~/.cache/go-build + ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- @@ -53,18 +46,22 @@ jobs: - name: Check formatting run: | if [ "$(gofmt -s -l . | grep -v vendor | wc -l)" -gt 0 ]; then - echo "Code formatting issues found:" + echo "โŒ Code formatting issues found:" gofmt -s -d . | grep -v vendor exit 1 fi + echo "โœ… Code formatting is correct" - - name: Lint - uses: golangci/golangci-lint-action@v4 + - name: Run go vet + run: go vet ./... + + - name: Lint with golangci-lint + uses: golangci/golangci-lint-action@v6 with: - version: v1.64 - skip-cache: true + version: latest + args: --timeout=5m - # Test on primary Go version + # Test on primary Go version with coverage test: name: Test (Go 1.23) runs-on: ubuntu-latest @@ -82,7 +79,9 @@ jobs: - name: Cache Go modules uses: actions/cache@v4 with: - path: ~/go/pkg/mod + path: | + ~/.cache/go-build + ~/go/pkg/mod key: ${{ runner.os }}-go-1.23-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go-1.23- @@ -103,7 +102,7 @@ jobs: files: ./coverage.out flags: unittests name: codecov-umbrella - fail_ci_if_error: true + fail_ci_if_error: false - name: Archive coverage if: always() @@ -115,17 +114,18 @@ jobs: coverage.html retention-days: 30 - # Test on multiple Go versions (after primary test passes) + # Test on multiple Go versions and platforms test-matrix: - name: Test (Go ${{ matrix.go-version }}) + name: Test (Go ${{ matrix.go-version }} on ${{ matrix.os }}) runs-on: ${{ matrix.os }} - needs: test + needs: validate strategy: - fail-fast: true # Stop on first failure + fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - go-version: ['1.21', '1.22'] + go-version: ['1.21', '1.22', '1.23'] exclude: + # Skip older Go versions on macOS and Windows to save CI time - os: macos-latest go-version: '1.21' - os: windows-latest @@ -156,40 +156,11 @@ jobs: - name: Run tests run: go test -v -race ./... - # Code quality - only run if tests pass - sonarcloud: - name: Code Quality (SonarCloud) - runs-on: ubuntu-latest - needs: test - if: github.event_name == 'push' && github.ref == 'refs/heads/master' && secrets.SONAR_TOKEN != '' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Download coverage from test job - uses: actions/download-artifact@v4 - with: - name: coverage-report - - - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - with: - args: > - -Dsonar.projectKey=0x524a_onvif-go - -Dsonar.organization=0x524a - -Dsonar.go.coverage.reportPaths=coverage.out - # Build verification build: - name: Build + name: Build Verification runs-on: ubuntu-latest - needs: test + needs: validate steps: - name: Checkout code @@ -203,7 +174,9 @@ jobs: - name: Cache Go modules uses: actions/cache@v4 with: - path: ~/go/pkg/mod + path: | + ~/.cache/go-build + ~/go/pkg/mod key: ${{ runner.os }}-go-1.23-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go-1.23- @@ -217,6 +190,44 @@ jobs: - name: Build examples run: | for dir in examples/*/; do - echo "Building $dir" - (cd "$dir" && go build -v .) + if [ -f "$dir/main.go" ] || [ -f "$dir/*.go" ]; then + echo "Building $dir" + (cd "$dir" && go build -v .) || echo "โš ๏ธ Failed to build $dir" + fi done + + - name: Build CLI tools + run: | + go build -v ./cmd/onvif-cli + go build -v ./cmd/onvif-quick + go build -v ./cmd/onvif-server + go build -v ./cmd/onvif-diagnostics + + # Code quality - only run if tests pass + sonarcloud: + name: Code Quality (SonarCloud) + runs-on: ubuntu-latest + needs: test + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') && secrets.SONAR_TOKEN != '' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download coverage from test job + uses: actions/download-artifact@v4 + with: + name: coverage-report + + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + args: > + -Dsonar.projectKey=0x524a_onvif-go + -Dsonar.organization=0x524a + -Dsonar.go.coverage.reportPaths=coverage.out diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 2262752..f8bd099 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,4 +1,4 @@ -name: Additional Coverage Reports +name: Coverage Analysis on: workflow_run: @@ -15,28 +15,55 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@v4 - - name: Download artifacts - uses: actions/download-artifact@fb7b1ae3fa6edf41bfe27490ab69d8657bea0656 # v4.1.7 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Download coverage artifacts + uses: actions/download-artifact@v4 with: name: coverage-report + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Check coverage percentage run: | if [ -f coverage.out ]; then + echo "๐Ÿ“Š Coverage Report:" + go tool cover -func=coverage.out | tail -1 + coverage=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//') - echo "Coverage: $coverage%" - # Set threshold to 40% - if (( $(echo "$coverage < 40" | bc -l) )); then - echo "โš ๏ธ Coverage below 40% threshold: $coverage%" + echo "Total Coverage: ${coverage}%" + + # Set threshold to 50% + threshold=50 + if (( $(echo "$coverage < $threshold" | bc -l) )); then + echo "โš ๏ธ Coverage below ${threshold}% threshold: ${coverage}%" + echo "::warning::Coverage is below ${threshold}% threshold" else - echo "โœ… Coverage above threshold: $coverage%" + echo "โœ… Coverage above ${threshold}% threshold: ${coverage}%" fi + + # Generate detailed coverage by package + echo "" + echo "๐Ÿ“ฆ Coverage by Package:" + go tool cover -func=coverage.out | grep -E "^github.com" | sort -k3 -nr | head -20 + else + echo "โŒ Coverage file not found" + exit 1 fi - - name: Upload coverage badge - continue-on-error: true - run: | - # Optional: Update badges or notifications - echo "Coverage analysis complete" + - name: Comment PR with coverage + if: github.event.workflow_run.event == 'pull_request' + uses: marocchino/sticky-pull-request-comment@v2 + with: + recreate: true + message: | + ## ๐Ÿ“Š Coverage Report + + Total Coverage: **${{ steps.coverage.outputs.percentage }}%** + + [View detailed coverage report](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..0e3b41a --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,23 @@ +name: Dependency Review + +on: + pull_request: + branches: [ master, main, develop ] + +permissions: + contents: read + +jobs: + dependency-review: + name: Review Dependencies + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Dependency Review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: moderate + diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..bc5f984 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,34 @@ +name: Documentation + +on: + push: + branches: [ master, main ] + paths: + - 'docs/**' + - '*.md' + workflow_dispatch: + +permissions: + contents: read + +jobs: + docs-check: + name: Documentation Check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Check for broken links + uses: peter-evans/link-checker@v1 + with: + args: -v -r -d docs/ + continue-on-error: true + + - name: Validate markdown + uses: DavidAnson/markdownlint-cli2-action@v16 + with: + globs: 'docs/**/*.md' + continue-on-error: true + diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..3d6e21e --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,31 @@ +name: Lint + +on: + push: + branches: [ master, main, develop ] + pull_request: + branches: [ master, main, develop ] + +permissions: + contents: read + +jobs: + golangci-lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: latest + args: --timeout=5m --out-format=github-actions + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f5af2f0..5d881ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,8 +3,12 @@ name: Release on: push: tags: - - 'v*' + - 'v*.*.*' workflow_dispatch: + inputs: + version: + description: 'Release version (e.g., v1.2.3)' + required: true permissions: contents: write @@ -39,20 +43,26 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@a4a2eec1d0ddf3f5835416e10cb208206f91ce91 # v5.0.0 + uses: actions/setup-go@v5 with: - go-version: '1.21' + go-version: '1.23' - name: Get version id: version run: | - echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + VERSION="${{ github.event.inputs.version }}" + else + VERSION=${GITHUB_REF#refs/tags/} + fi + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "Version: ${VERSION}" - name: Build binaries env: @@ -62,7 +72,8 @@ jobs: CGO_ENABLED: 0 run: | VERSION=${{ steps.version.outputs.VERSION }} - LDFLAGS="-s -w -X main.Version=${VERSION} -X main.Commit=${{ steps.version.outputs.SHORT_SHA }}" + SHORT_SHA=${{ steps.version.outputs.SHORT_SHA }} + LDFLAGS="-s -w -X main.Version=${VERSION} -X main.Commit=${SHORT_SHA}" # Set file extension for Windows EXT="" @@ -73,16 +84,16 @@ jobs: # Build all CLI tools mkdir -p dist - echo "Building onvif-cli..." + echo "๐Ÿ”จ Building onvif-cli..." go build -ldflags="${LDFLAGS}" -o "dist/onvif-cli-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}" ./cmd/onvif-cli - echo "Building onvif-quick..." + echo "๐Ÿ”จ Building onvif-quick..." go build -ldflags="${LDFLAGS}" -o "dist/onvif-quick-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}" ./cmd/onvif-quick - echo "Building onvif-server..." + echo "๐Ÿ”จ Building onvif-server..." go build -ldflags="${LDFLAGS}" -o "dist/onvif-server-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}" ./cmd/onvif-server - echo "Building onvif-diagnostics..." + echo "๐Ÿ”จ Building onvif-diagnostics..." go build -ldflags="${LDFLAGS}" -o "dist/onvif-diagnostics-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}" ./cmd/onvif-diagnostics - name: Create archive @@ -107,7 +118,7 @@ jobs: fi # Copy documentation - cp README.md LICENSE staging/ + cp README.md LICENSE staging/ 2>/dev/null || true # Create archive from staging directory if [ "${{ matrix.goos }}" = "windows" ]; then @@ -119,6 +130,8 @@ jobs: tar czf "../releases/${ARCHIVE_NAME}.tar.gz" . cd .. fi + + echo "โœ… Created ${ARCHIVE_NAME}.tar.gz" - name: Generate checksums run: | @@ -134,7 +147,7 @@ jobs: with: name: release-${{ matrix.goos }}-${{ matrix.goarch }} path: releases/* - retention-days: 5 + retention-days: 7 release: name: Create GitHub Release @@ -142,12 +155,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download all artifacts - uses: actions/download-artifact@fb7b1ae3fa6edf41bfe27490ab69d8657bea0656 # v4.1.7 + uses: actions/download-artifact@v4 with: path: all-releases pattern: release-* @@ -157,14 +170,18 @@ jobs: run: | cd all-releases # Combine all checksum files - cat checksums-*.txt > checksums.txt + cat checksums-*.txt > checksums.txt 2>/dev/null || true # Remove individual checksum files - rm checksums-*.txt + rm -f checksums-*.txt - name: Get version and changelog id: version run: | - VERSION=${GITHUB_REF#refs/tags/} + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + VERSION="${{ github.event.inputs.version }}" + else + VERSION=${GITHUB_REF#refs/tags/} + fi echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT # Generate changelog from commits since last tag @@ -174,21 +191,22 @@ jobs: git log --pretty=format:"- %s (%h)" ${PREV_TAG}..HEAD >> $GITHUB_OUTPUT echo "" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT + else + echo "CHANGELOG=Initial release" >> $GITHUB_OUTPUT fi - name: Create Release - uses: softprops/action-gh-release@d4c6436acb972979c89d42d294e19ddc00bdef6e # v2.0.1 + uses: softprops/action-gh-release@v2 with: files: all-releases/* - draft: true + draft: false prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-beta') || contains(github.ref, '-alpha') }} generate_release_notes: true - fail_on_unmatched_files: true make_latest: true body: | ## Release ${{ steps.version.outputs.VERSION }} - ### Installation + ### ๐Ÿ“ฆ Installation Download the appropriate binary for your platform below. @@ -211,11 +229,11 @@ jobs: go get github.com/${{ github.repository }}@${{ steps.version.outputs.VERSION }} ``` - ### Checksums + ### ๐Ÿ” Checksums SHA256 checksums are available in `checksums.txt` - ### Changes + ### ๐Ÿ“ Changes ${{ steps.version.outputs.CHANGELOG }} env: @@ -228,23 +246,16 @@ jobs: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') steps: - name: Checkout code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@2db740d56eb54d769da97c489bb369cf5d3dda6ec # v3.0.0 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa601d98bc5fc6 # v3.0.0 - - - name: Login to Docker Hub - uses: docker/login-action@8c334bdf38b3b7d57f1a2ab4dcb89e44d874e2a2 # v3.0.0 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - continue-on-error: true + uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry - uses: docker/login-action@8c334bdf38b3b7d57f1a2ab4dcb89e44d874e2a2 # v3.0.0 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} @@ -252,10 +263,12 @@ jobs: - name: Get version id: version - run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + run: | + VERSION=${GITHUB_REF#refs/tags/v} + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT - name: Build and push - uses: docker/build-push-action@5176660ba9f93254eda4d16d1a0beb4e32bd5a8e # v5.0.0 + uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64,linux/arm/v7 diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..6cfcc42 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,52 @@ +name: Security Scan + +on: + push: + branches: [ master, main ] + pull_request: + branches: [ master, main ] + schedule: + - cron: '0 0 * * 0' # Weekly on Sunday + +permissions: + contents: read + security-events: write + +jobs: + gosec: + name: Security Scan (gosec) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Gosec Security Scanner + uses: securego/gosec@master + with: + args: '-no-fail -fmt json -out gosec-report.json ./...' + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: gosec-report.json + + govulncheck: + name: Vulnerability Check (govulncheck) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Run govulncheck + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + govulncheck ./... + diff --git a/.github/workflows/test-simple.yml b/.github/workflows/test-simple.yml deleted file mode 100644 index 762c7b9..0000000 --- a/.github/workflows/test-simple.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Simple Test - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - simple: - runs-on: ubuntu-latest - steps: - - name: Echo test - run: echo "Hello from GitHub Actions" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bae7a09..759ceb9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,34 +1,42 @@ -name: Extra Tests +name: Extended Tests on: - workflow_dispatch: # Manual trigger only + workflow_dispatch: # Manual trigger schedule: - - cron: '0 2 * * *' # Daily at 2 AM UTC + - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC + push: + branches: [ master, main ] + paths: + - '**.go' + - 'go.mod' + - 'go.sum' jobs: - # Run tests on other Go versions as manual/scheduled job + # Run tests on older Go versions test-older-versions: name: Test on Go ${{ matrix.go-version }} runs-on: ${{ matrix.os }} strategy: - fail-fast: true + fail-fast: false matrix: os: [ubuntu-latest] go-version: ['1.20', '1.19'] steps: - name: Checkout code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@a4a2eec1d0ddf3f5835416e10cb208206f91ce91 # v5.0.0 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Cache Go modules - uses: actions/cache@e5f3f4dc664b57a06a2055cfc9b80cf9f20aba75 # v4.0.1 + uses: actions/cache@v4 with: - path: ~/go/pkg/mod + path: | + ~/.cache/go-build + ~/go/pkg/mod key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go-${{ matrix.go-version }}- @@ -38,3 +46,63 @@ jobs: - name: Run tests run: go test -v -race ./... + + # Run benchmarks + benchmark: + name: Benchmark Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-1.23-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-1.23- + + - name: Download dependencies + run: go mod download + + - name: Run benchmarks + run: go test -bench=. -benchmem ./... -run=^$ || echo "โš ๏ธ No benchmarks found" + + # Test with race detector + race-detector: + name: Race Detector Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-1.23-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-1.23- + + - name: Download dependencies + run: go mod download + + - name: Run tests with race detector + run: go test -race -timeout=10m ./... diff --git a/.golangci.yml b/.golangci.yml index 539f9cf..fe02696 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,13 +1,134 @@ +run: + timeout: 5m + tests: true + skip-dirs: + - vendor + - testdata + skip-files: + - ".*\\.pb\\.go$" + - ".*\\.gen\\.go$" + linters: enable: - errcheck - govet - staticcheck - unused + - gosimple + - ineffassign + - typecheck + - gofmt + - goimports + - misspell + - unconvert + - unparam + - gocritic + - gosec + - exportloopref + - goconst + - gocyclo + - dupl + - funlen + - gocognit + - lll + - nakedret + - prealloc + - stylecheck + - whitespace + - wrapcheck + - errname + - errorlint + - exhaustive + - godot + - goerr113 + - gomnd + - goprintffuncname + - nlreturn + - noctx + - nolintlint + - rowserrcheck + - sqlclosecheck + - thelper + - tparallel + - wastedassign -run: - timeout: 5m +linters-settings: + errcheck: + check-type-assertions: true + check-blank: true + + govet: + check-shadowing: true + enable-all: true + + gocyclo: + min-complexity: 15 + + funlen: + lines: 100 + statements: 50 + + lll: + line-length: 120 + + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + disabled-checks: + - dupImport + - ifElseChain + - octalLiteral + - whyNoLint + - wrapperFunc + + gosec: + severity: medium + confidence: medium + + godot: + scope: declarations + exclude: + - "^TODO:" + - "^FIXME:" + + goimports: + local-prefixes: github.com/0x524a/onvif-go + + misspell: + locale: US + +issues: + exclude-rules: + # Exclude some linters from test files + - path: _test\.go + linters: + - errcheck + - gosec + - funlen + - gocyclo + - gocognit + + # Exclude known false positives + - text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|.*Write|.*Read|.*Printf?|.*Fprintf?) is not checked" + linters: + - errcheck + + # Allow long lines in test files + - path: _test\.go + linters: + - lll + + max-issues-per-linter: 50 + max-same-issues: 10 + exclude-use-default: false output: formats: - colored-line-number + print-issued-lines: true + print-linter-name: true + uniq-by-line: false diff --git a/docs/CI_CD.md b/docs/CI_CD.md new file mode 100644 index 0000000..1d326b7 --- /dev/null +++ b/docs/CI_CD.md @@ -0,0 +1,190 @@ +# CI/CD Documentation + +## Overview + +The ONVIF Go library uses GitHub Actions for continuous integration and deployment. All workflows are located in `.github/workflows/`. + +## Workflow Summary + +| Workflow | Purpose | Triggers | Status | +|----------|---------|----------|--------| +| **CI** | Main CI pipeline | Push/PR to main branches | โœ… Active | +| **Test** | Extended testing | Manual/Weekly/Code changes | โœ… Active | +| **Coverage** | Coverage analysis | After CI success | โœ… Active | +| **Release** | Create releases | Tags/Manual | โœ… Active | +| **Lint** | Code linting | Push/PR | โœ… Active | +| **Security** | Security scanning | Push/PR/Weekly | โœ… Active | +| **Docs** | Documentation checks | Docs changes | โœ… Active | +| **Dependency Review** | Dependency security | PRs | โœ… Active | + +## Main CI Workflow + +The **CI** workflow (`ci.yml`) is the primary workflow that runs on every push and pull request. + +### Jobs + +1. **validate** - Quick validation (5-10 minutes) + - Code formatting check + - `go vet` + - Linting with golangci-lint + +2. **test** - Primary testing (10-15 minutes) + - Runs on Go 1.23 + - Race detector enabled + - Coverage report generation + - Uploads to Codecov + +3. **test-matrix** - Multi-platform testing (20-30 minutes) + - Tests on Go 1.21, 1.22, 1.23 + - Tests on Linux, macOS, Windows + - Parallel execution + +4. **build** - Build verification (5-10 minutes) + - Builds all packages + - Builds all examples + - Builds all CLI tools + +5. **sonarcloud** - Code quality (10-15 minutes) + - Only on master/main + - Requires SONAR_TOKEN secret + +### Performance + +- **Total CI time**: ~40-60 minutes (parallel jobs) +- **Fast feedback**: Validation job fails fast on formatting/lint issues +- **Caching**: Go modules and build cache for faster runs + +## Release Workflow + +The **Release** workflow (`release.yml`) creates GitHub releases with binaries for all platforms. + +### Supported Platforms + +- **Linux**: amd64, arm64, arm (v7) +- **Windows**: amd64, arm64 +- **macOS**: amd64, arm64 + +### Release Process + +1. **Tag creation**: Push a tag like `v1.2.3` +2. **Build**: Automatically builds for all platforms +3. **Archive**: Creates `.tar.gz` (Linux/macOS) and `.zip` (Windows) +4. **Checksums**: Generates SHA256 checksums +5. **Release**: Creates GitHub release with all artifacts +6. **Docker**: Builds and pushes multi-arch Docker image to GHCR + +### Manual Release + +You can also trigger a release manually: +1. Go to Actions โ†’ Release workflow +2. Click "Run workflow" +3. Enter version (e.g., `v1.2.3`) + +## Security Workflow + +The **Security** workflow (`security.yml`) scans for vulnerabilities. + +### Tools + +- **gosec**: Security scanner for Go code +- **govulncheck**: Vulnerability checker for dependencies + +### Schedule + +Runs weekly on Sundays to catch new vulnerabilities. + +## Coverage + +Coverage is tracked and reported to Codecov. The coverage workflow provides detailed analysis: + +- Total coverage percentage +- Coverage by package +- Coverage trends over time + +### Coverage Threshold + +Minimum coverage threshold: **50%** + +## Required Secrets + +### Optional Secrets + +- `CODECOV_TOKEN` - For Codecov integration +- `SONAR_TOKEN` - For SonarCloud integration +- `DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN` - For Docker Hub + +## Workflow Status Badges + +Add these badges to your README: + +```markdown +![CI](https://github.com/0x524a/onvif-go/workflows/CI/badge.svg) +![Test](https://github.com/0x524a/onvif-go/workflows/Extended%20Tests/badge.svg) +![Release](https://github.com/0x524a/onvif-go/workflows/Release/badge.svg) +``` + +## Best Practices + +1. **Always run CI locally first**: `make check test` +2. **Keep workflows fast**: Use caching and parallel jobs +3. **Fail fast**: Validation job catches issues early +4. **Test before release**: All tests must pass before tagging +5. **Review security scans**: Check security workflow results + +## Troubleshooting + +### CI Fails on Formatting + +```bash +# Fix formatting +make fmt + +# Or manually +gofmt -w . +``` + +### CI Fails on Linting + +```bash +# Run linter locally +make lint + +# Or manually +golangci-lint run ./... +``` + +### Tests Fail Locally but Pass in CI + +- Check Go version: CI uses Go 1.23 +- Check race detector: CI runs with `-race` +- Check environment differences + +### Release Fails + +- Ensure tag format: `v1.2.3` (not `1.2.3`) +- Check permissions: Need `contents: write` +- Verify all tests pass before tagging + +## Workflow Files + +All workflow files are in `.github/workflows/`: + +- `ci.yml` - Main CI pipeline +- `test.yml` - Extended tests +- `coverage.yml` - Coverage analysis +- `release.yml` - Release automation +- `lint.yml` - Linting +- `security.yml` - Security scanning +- `docs.yml` - Documentation checks +- `dependency-review.yml` - Dependency review + +## See Also + +- [GitHub Actions Documentation](https://docs.github.com/en/actions) +- [Workflow README](../.github/workflows/README.md) +- [Makefile](../Makefile) - Local development commands + +--- + +*Last Updated: December 2, 2025* +