name: CI permissions: contents: read on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: jobs: test: name: Test runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: "go.mod" - name: Cache Go modules uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.mod') }}-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Install libpcap run: sudo apt-get install -y libpcap-dev - name: Download dependencies run: go mod download - name: Verify dependencies run: go mod verify - name: Run tests run: go test -v -race -coverprofile=coverage.out ./... - name: Build service run: make build-service - name: Run HTTP client integration tests run: make test-http-client - name: Upload coverage to Codecov uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 with: file: ./coverage.out flags: unittests name: codecov-umbrella fail_ci_if_error: false lint: name: Lint runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: "go.mod" - name: Install libpcap run: sudo apt-get install -y libpcap-dev - name: Run golangci-lint uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1 with: version: latest args: --timeout=5m build: name: Build runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - goos: linux goarch: amd64 - goos: linux goarch: arm64 - goos: linux goarch: arm goarm: 7 - goos: darwin goarch: amd64 - goos: darwin goarch: arm64 - goos: windows goarch: amd64 - goos: freebsd goarch: amd64 steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: "go.mod" - name: Cache Go modules uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.mod') }}-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Build binaries env: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} GOARM: ${{ matrix.goarm }} CGO_ENABLED: 0 run: | ARCH_SUFFIX="${{ matrix.goos }}-${{ matrix.goarch }}" if [[ -n "${{ matrix.goarm }}" ]]; then ARCH_SUFFIX="${ARCH_SUFFIX}v${{ matrix.goarm }}" fi EXT="" if [[ "${{ matrix.goos }}" == "windows" ]]; then EXT=".exe" fi mkdir -p build for binary in soundtouch-cli soundtouch-service soundtouch-web soundtouch-backup; do OUTPUT="build/${binary}-${ARCH_SUFFIX}${EXT}" echo "Building $OUTPUT" go build -trimpath -ldflags="-s -w" -o "$OUTPUT" "./cmd/$binary" done ls -la build/ - name: Upload build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: binaries-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm }} path: build/ security: name: Basic Security Check runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: "go.mod" - name: Install libpcap run: sudo apt-get install -y libpcap-dev - name: Run basic vulnerability check run: | go install golang.org/x/vuln/cmd/govulncheck@latest govulncheck ./... - name: Security scan reminder run: | echo "ℹ️ This is a basic security check for CI speed." echo "For comprehensive security scanning, see the Security workflow:" echo "https://github.com/${{ github.repository }}/actions/workflows/security.yml" docs: name: Documentation Check runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check documentation links run: | npm install -g markdown-link-check find . -name "*.md" -not -path "./tests/*" -not -path "./node_modules/*" -print0 | xargs -0 -n1 markdown-link-check -q -v -c .github/markdown-link-check.json - name: Warn on pending images run: | IMAGES=( "dashboard-home.png" "account-creation.png" "account-dashboard.png" "usb-remote-services.png" "device-discovery.png" "device-registration.png" "account-migration.png" "migration-setup.png" "migration-progress.png" "migration-health.png" "migration-complete.png" "backup-setup.png" ) for img in "${IMAGES[@]}"; do if [ ! -f "docs/static/images/$img" ]; then echo "::warning file=docs/content/docs/guides/MIGRATION-GUIDE.md::Pending image '$img' is missing from docs/static/images/" fi done - name: Validate API documentation run: | # Check that all documented endpoints exist in code echo "Validating API documentation consistency..." # Check API cookbook if [ -f "docs/content/docs/reference/API-COOKBOOK.md" ]; then echo "✓ API Cookbook exists" else echo "✗ API Cookbook missing" exit 1 fi # Check getting started guide if [ -f "docs/content/docs/guides/GETTING-STARTED.md" ]; then echo "✓ Getting Started guide exists" else echo "✗ Getting Started guide missing" exit 1 fi integration: name: Integration Test runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: "go.mod" - name: Test CLI build and help run: | go build -trimpath -ldflags="-s -w" -o soundtouch-cli ./cmd/soundtouch-cli ./soundtouch-cli -help - name: Test library imports run: | cat > test_import.go << 'EOF' package main import ( "fmt" "github.com/gesellix/bose-soundtouch/pkg/client" "github.com/gesellix/bose-soundtouch/pkg/models" "github.com/gesellix/bose-soundtouch/pkg/discovery" "github.com/gesellix/bose-soundtouch/pkg/config" ) func main() { // Test basic client creation c := client.NewClientFromHost("192.0.2.100") fmt.Printf("Client created for %s\n", c.BaseURL()) // Test models can be imported var info models.DeviceInfo fmt.Printf("DeviceInfo model available: %T\n", info) // Test discovery can be imported cfg := config.DefaultConfig() service := discovery.NewUnifiedDiscoveryService(cfg) fmt.Printf("Discovery service available: %T\n", service) fmt.Println("All imports successful!") } EOF go run test_import.go rm test_import.go docker: name: Docker Build runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Set build date id: build_date run: echo "date=$(date -u +%Y-%m-%d)" >> $GITHUB_OUTPUT - name: Determine push eligibility id: push-check run: | # Push on main, and on same-repo PRs (forks can't push to GHCR via GITHUB_TOKEN). SHOULD_PUSH="false" if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then SHOULD_PUSH="true" elif [[ "${{ github.event_name }}" == "pull_request" && \ "${{ github.event.pull_request.head.repo.full_name }}" == "${{ github.repository }}" ]]; then SHOULD_PUSH="true" fi echo "should-push=$SHOULD_PUSH" >> "$GITHUB_OUTPUT" echo "Will push: $SHOULD_PUSH" - name: Log in to GitHub Container Registry if: steps.push-check.outputs.should-push == 'true' uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for soundtouch-service id: meta-service uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: images: ghcr.io/${{ github.repository }} tags: | type=raw,value=edge,enable=${{ github.ref == 'refs/heads/main' }} type=ref,event=pr,prefix=preview-pr- type=sha,prefix=preview-sha-,format=short,enable=${{ github.event_name == 'pull_request' }} type=ref,event=branch,prefix=preview-branch-,enable=${{ github.event_name == 'push' && github.ref != 'refs/heads/main' }} - name: Build and push soundtouch-service Docker image uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . target: soundtouch-service platforms: linux/amd64,linux/arm64,linux/arm64/v8,linux/arm/v7 push: ${{ steps.push-check.outputs.should-push == 'true' }} tags: ${{ steps.meta-service.outputs.tags }} labels: ${{ steps.meta-service.outputs.labels }} build-args: | COMMIT=${{ github.sha }} DATE=${{ steps.build_date.outputs.date }} cache-from: type=gha cache-to: type=gha,mode=max - name: Extract metadata (tags, labels) for soundtouch-web id: meta-web uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: images: ghcr.io/${{ github.repository }}-web tags: | type=raw,value=edge,enable=${{ github.ref == 'refs/heads/main' }} type=ref,event=pr,prefix=preview-pr- type=sha,prefix=preview-sha-,format=short,enable=${{ github.event_name == 'pull_request' }} type=ref,event=branch,prefix=preview-branch-,enable=${{ github.event_name == 'push' && github.ref != 'refs/heads/main' }} - name: Build and push soundtouch-web Docker image uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . target: soundtouch-web platforms: linux/amd64,linux/arm64,linux/arm64/v8,linux/arm/v7 push: ${{ steps.push-check.outputs.should-push == 'true' }} tags: ${{ steps.meta-web.outputs.tags }} labels: ${{ steps.meta-web.outputs.labels }} build-args: | COMMIT=${{ github.sha }} DATE=${{ steps.build_date.outputs.date }} cache-from: type=gha cache-to: type=gha,mode=max - name: Summarize published images if: steps.push-check.outputs.should-push == 'true' env: SERVICE_TAGS: ${{ steps.meta-service.outputs.tags }} WEB_TAGS: ${{ steps.meta-web.outputs.tags }} EVENT_NAME: ${{ github.event_name }} PR_NUMBER: ${{ github.event.pull_request.number }} REF_NAME: ${{ github.ref_name }} run: | { echo "## 🐳 Published Docker Images" echo "" if [[ "$EVENT_NAME" == "pull_request" ]]; then echo "**Preview** images for PR #${PR_NUMBER}. These are not release builds." elif [[ "$REF_NAME" == "main" ]]; then echo "**Edge** images from \`main\`." else echo "**Preview** images from branch \`${REF_NAME}\`. These are not release builds." fi echo "" echo "### soundtouch-service" echo "" echo '```bash' while IFS= read -r tag; do [[ -n "$tag" ]] && echo "docker pull $tag" done <<< "$SERVICE_TAGS" echo '```' echo "" echo "### soundtouch-web" echo "" echo '```bash' while IFS= read -r tag; do [[ -n "$tag" ]] && echo "docker pull $tag" done <<< "$WEB_TAGS" echo '```' } >> "$GITHUB_STEP_SUMMARY" notify: name: Notify Status runs-on: ubuntu-latest needs: [test, lint, build, security, docs, docker] if: always() permissions: statuses: write contents: read steps: - name: Check overall status run: | if [[ "${{ needs.test.result }}" == "success" && \ "${{ needs.lint.result }}" == "success" && \ "${{ needs.build.result }}" == "success" && \ "${{ needs.security.result }}" == "success" && \ "${{ needs.docs.result }}" == "success" && \ "${{ needs.docker.result }}" == "success" ]]; then echo "✅ All CI checks passed!" echo "status=success" >> $GITHUB_OUTPUT else echo "❌ Some CI checks failed" echo "Test: ${{ needs.test.result }}" echo "Lint: ${{ needs.lint.result }}" echo "Build: ${{ needs.build.result }}" echo "Security: ${{ needs.security.result }}" echo "Docs: ${{ needs.docs.result }}" echo "Docker: ${{ needs.docker.result }}" echo "status=failure" >> $GITHUB_OUTPUT fi id: status - name: Update commit status if: always() uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | try { const state = '${{ steps.status.outputs.status }}' === 'success' ? 'success' : 'failure'; const description = state === 'success' ? 'All checks passed' : 'Some checks failed'; await github.rest.repos.createCommitStatus({ owner: context.repo.owner, repo: context.repo.repo, sha: context.sha, state: state, description: description, context: 'CI Pipeline' }); console.log(`✅ Successfully updated commit status to: ${state}`); } catch (error) { console.log(`⚠️ Failed to update commit status: ${error.message}`); // Don't fail the workflow if status update fails }