name: CI on: push: branches: [master, main] pull_request: branches: [master, main] types: [opened, synchronize, reopened] permissions: contents: read checks: write pull-requests: write concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true env: GO_VERSION: '1.24.x' jobs: # Stage 1: Format Check (fastest - fail immediately if code isn't formatted) fmt: name: Format Check runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ env.GO_VERSION }} - name: Check formatting run: | unformatted=$(gofmt -s -l . | grep -v vendor || true) if [ -n "$unformatted" ]; then echo "❌ The following files are not properly formatted:" echo "$unformatted" echo "" echo "Run 'gofmt -s -w .' to fix formatting issues" exit 1 fi echo "✅ All files are properly formatted" # Stage 2: Lint (depends on fmt) lint: name: Lint runs-on: ubuntu-latest needs: fmt steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ env.GO_VERSION }} - name: Cache Go modules uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go-${{ env.GO_VERSION }}- - name: Download dependencies run: go mod download - name: Run go vet run: go vet ./... - name: Run golangci-lint uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v6.5.0 with: version: v1.64.8 args: --timeout=5m # Stage 3: Test with Coverage (depends on lint) test: name: Test & Coverage runs-on: ubuntu-latest needs: lint steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 # Full history for SonarCloud - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ env.GO_VERSION }} - name: Cache Go modules uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go-${{ env.GO_VERSION }}- - name: Download dependencies run: go mod download - name: Run tests with coverage run: | go test -v -race -covermode=atomic -coverprofile=coverage.out -json ./... > test-report.json 2>&1 || true # Ensure coverage file exists even if tests fail if [ ! -f coverage.out ]; then echo "mode: atomic" > coverage.out fi - name: Display coverage summary run: | echo "📊 Coverage Summary:" go tool cover -func=coverage.out | tail -20 - name: Upload coverage artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: coverage-reports path: | coverage.out test-report.json retention-days: 7 - name: Upload to Codecov uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v4.6.0 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.out flags: unittests name: codecov-onvif-go # Don't fail on PRs from forks where token may not be available fail_ci_if_error: ${{ github.event_name == 'push' }} verbose: true # Stage 4: SonarCloud Analysis (depends on test) # Only runs on push to master/main when SONAR_TOKEN is available # Skipped for PRs from forks where secrets are not accessible sonarcloud: name: SonarCloud Analysis runs-on: ubuntu-latest needs: test if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') && github.repository == '0x524a/onvif-go' steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 # Full history for accurate blame information - name: Download coverage reports uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: coverage-reports - name: Verify coverage file run: | echo "📁 Downloaded files:" ls -la if [ -f coverage.out ]; then echo "✅ Coverage file found" head -5 coverage.out else echo "âš ī¸ Coverage file not found, creating empty one" echo "mode: atomic" > coverage.out fi - name: SonarCloud Scan uses: SonarSource/sonarcloud-github-action@4006f663ecaf1f8093e8e4abb9227f6041f52216 # v3.1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Stage 5: Build Verification (depends on test, runs in parallel with sonarcloud) build: name: Build Verification runs-on: ubuntu-latest needs: test steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ env.GO_VERSION }} - name: Cache Go modules uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go-${{ env.GO_VERSION }}- - name: Download dependencies run: go mod download - name: Build library run: go build -v ./... - name: Build CLI tools run: | echo "🔨 Building CLI tools..." go build -v -o bin/onvif-cli ./cmd/onvif-cli go build -v -o bin/onvif-quick ./cmd/onvif-quick go build -v -o bin/onvif-server ./cmd/onvif-server go build -v -o bin/onvif-diagnostics ./cmd/onvif-diagnostics echo "✅ All CLI tools built successfully" # Final status check ci-success: name: CI Success runs-on: ubuntu-latest needs: [fmt, lint, test, sonarcloud, build] if: always() steps: - name: Check all jobs status run: | if [[ "${{ needs.fmt.result }}" != "success" ]]; then echo "❌ Format check failed" exit 1 fi if [[ "${{ needs.lint.result }}" != "success" ]]; then echo "❌ Lint check failed" exit 1 fi if [[ "${{ needs.test.result }}" != "success" ]]; then echo "❌ Tests failed" exit 1 fi # SonarCloud is optional - only fails if it ran and failed (not if skipped) if [[ "${{ needs.sonarcloud.result }}" == "failure" ]]; then echo "❌ SonarCloud analysis failed" exit 1 fi if [[ "${{ needs.sonarcloud.result }}" == "skipped" ]]; then echo "â„šī¸ SonarCloud analysis skipped (only runs on push to master/main)" fi if [[ "${{ needs.build.result }}" != "success" ]]; then echo "❌ Build verification failed" exit 1 fi echo "✅ All CI checks passed successfully!"