mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-10 20:56:14 +00:00
markdown-link-check has no concept of "warning" vs "error" — it's a binary alive/dead per link, so a transient 429 from a rate-limiting site we don't control (recently: blogspot.com, izndgroup.com) fails the whole CI job exactly like a genuine dead link, with no way to tell them apart from the job's exit code. New scripts/check-doc-links.sh wraps the tool per file, parses its "[✖] <url> → Status: <code>" output, and re-decides pass/fail per link: 404 still fails the build (a real dead link, worth fixing), other 4xx (429, 401, 403, ...) become a GitHub Actions ::warning:: annotation instead, and anything else (5xx, timeouts, DNS failures) still fails the build same as before. De-dupes markdown-link-check's own doubled -v output. Written for bash 3.2 (macOS's default /bin/bash) so it's testable locally, not just on the ubuntu-latest runner. Existing retry config in .github/markdown-link-check.json (retryOn429, 3 retries, 30s backoff) is untouched; this only changes what happens once retries are exhausted. Verified locally: 6 synthetic scenarios (404/429/500/mixed/clean/ duplicate-line) via a stubbed markdown-link-check, plus a real run against the docs tree with the actual tool.
483 lines
16 KiB
YAML
483 lines
16 KiB
YAML
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||
|
||
- name: Set up Go
|
||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||
with:
|
||
go-version-file: "go.mod"
|
||
|
||
- name: Cache Go modules
|
||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||
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@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||
|
||
- name: Set up Go
|
||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.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@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
|
||
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||
|
||
- name: Set up Go
|
||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||
with:
|
||
go-version-file: "go.mod"
|
||
|
||
- name: Cache Go modules
|
||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||
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
|
||
|
||
# soundtouch-web is now a transitional alias of soundtouch-player
|
||
# (same source); building the player is enough to verify both.
|
||
for binary in soundtouch-cli soundtouch-service soundtouch-player 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||
|
||
- name: Set up Go
|
||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||
|
||
- name: Check documentation links
|
||
run: |
|
||
npm install -g markdown-link-check
|
||
./scripts/check-doc-links.sh
|
||
|
||
- 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||
|
||
- name: Set up Go
|
||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||
|
||
- name: Set up Docker Buildx
|
||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.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@dbcb813823bdd20940b903addbd779551569679f # v4.6.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@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.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@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.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-player
|
||
id: meta-player
|
||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||
with:
|
||
images: ghcr.io/${{ github.repository }}-player
|
||
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-player Docker image
|
||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||
with:
|
||
context: .
|
||
target: soundtouch-player
|
||
platforms: linux/amd64,linux/arm64,linux/arm64/v8,linux/arm/v7
|
||
push: ${{ steps.push-check.outputs.should-push == 'true' }}
|
||
tags: ${{ steps.meta-player.outputs.tags }}
|
||
labels: ${{ steps.meta-player.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 }}
|
||
PLAYER_TAGS: ${{ steps.meta-player.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-player"
|
||
echo ""
|
||
echo '```bash'
|
||
while IFS= read -r tag; do
|
||
[[ -n "$tag" ]] && echo "docker pull $tag"
|
||
done <<< "$PLAYER_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
|
||
}
|