diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0363d07 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,33 @@ +# .dockerignore + +# Exclude large firmware files and archives +firmware/ +data/ + +# Exclude local build artifacts +build/ +soundtouch-cli +soundtouch-service + +# Exclude Go specific files that aren't needed for build context +# (go.mod and go.sum ARE needed, but other local stuff isn't) +.cache/ +vendor/ + +# Exclude IDE and system files +.idea/ +.vscode/ +.DS_Store + +# Exclude Git history +.git/ +.gitignore + +# Exclude documentation and other non-essential files for the binary build +docs/ +examples/ +scripts/ +CONTRIBUTING.md +CODE_OF_CONDUCT.md +LICENSE +README.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cf237a0..fee6b0d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -77,3 +77,24 @@ updates: - "*scan*" - "securecodewarrior/*" - "codecov/*" + + # Docker dependency updates + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + day: "wednesday" + time: "09:00" + timezone: "UTC" + open-pull-requests-limit: 3 + reviewers: + - "gesellix" + assignees: + - "gesellix" + commit-message: + prefix: "docker" + include: "scope" + labels: + - "dependencies" + - "docker" + rebase-strategy: "auto" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ed1c93..14b9fcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -218,10 +218,51 @@ jobs: 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@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,value=edge,enable=${{ github.ref == 'refs/heads/main' }} + type=ref,event=pr + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + notify: name: Notify Status runs-on: ubuntu-latest - needs: [test, lint, build, security, docs] + needs: [test, lint, build, security, docs, docker] if: always() permissions: statuses: write @@ -234,7 +275,8 @@ jobs: "${{ needs.lint.result }}" == "success" && \ "${{ needs.build.result }}" == "success" && \ "${{ needs.security.result }}" == "success" && \ - "${{ needs.docs.result }}" == "success" ]]; then + "${{ needs.docs.result }}" == "success" && \ + "${{ needs.docker.result }}" == "success" ]]; then echo "✅ All CI checks passed!" echo "status=success" >> $GITHUB_OUTPUT else @@ -244,6 +286,7 @@ jobs: 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be133a4..e14af7a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,7 @@ on: permissions: contents: write actions: read + packages: write env: GO_VERSION_FILE: "go.mod" @@ -477,17 +478,57 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + docker: + name: Build and Push Docker Image + runs-on: ubuntu-latest + needs: validate + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}},value=v${{ needs.validate.outputs.version }} + type=semver,pattern={{major}}.{{minor}},value=v${{ needs.validate.outputs.version }} + type=raw,value=latest,enable=${{ needs.validate.outputs.is_prerelease == 'false' }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + notify: name: Post-Release Notifications runs-on: ubuntu-latest - needs: [validate, create_release, update_release] - if: always() && (needs.create_release.result == 'success' || needs.update_release.result == 'success') + needs: [validate, create_release, update_release, docker] + if: always() && (needs.create_release.result == 'success' || needs.update_release.result == 'success' || needs.docker.result == 'success') steps: - name: Notify success run: | echo "🎉 Release ${{ needs.validate.outputs.version }} completed successfully!" echo "📦 Binaries built for 7 platforms (CLI and Service)" + echo "🐳 Docker image published to ghcr.io" echo "🔐 Checksums generated and verified" echo "📋 Release notes automatically generated" echo "" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..66176f8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# Build stage +FROM golang:1.25.7-alpine AS builder + +WORKDIR /app + +# Copy go mod and sum files +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the rest of the source code +COPY . . + +# Build the soundtouch-service +RUN CGO_ENABLED=0 GOOS=linux go build -o /soundtouch-service ./cmd/soundtouch-service + +# Final stage +FROM alpine:3.21 + +# Install necessary runtime dependencies +RUN apk add --no-cache ca-certificates tzdata + +WORKDIR /app + +# Copy the binary from the builder stage +COPY --from=builder /soundtouch-service /app/soundtouch-service + +# Create data directory for persistence +RUN mkdir -p /app/data + +# Set environment variables with defaults +ENV PORT=8000 +ENV DATA_DIR=/app/data +ENV LOG_PROXY_BODY=false +ENV REDACT_PROXY_LOGS=true + +# Expose the service port +EXPOSE 8000 + +# Run the service +ENTRYPOINT ["/app/soundtouch-service"] diff --git a/Makefile b/Makefile index 59a82f1..e13a27c 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,8 @@ GOFMT=gofmt # Build parameters BINARY_NAME=soundtouch-cli BINARY_PATH=./cmd/$(BINARY_NAME) +SERVICE_NAME=soundtouch-service +SERVICE_PATH=./cmd/$(SERVICE_NAME) EXAMPLE_MDNS_NAME=example-mdns EXAMPLE_MDNS_PATH=./cmd/$(EXAMPLE_MDNS_NAME) EXAMPLE_UPNP_NAME=example-upnp @@ -25,13 +27,18 @@ BUILD_DIR=./build all: check build -build: build-cli build-examples +build: build-cli build-service build-examples build-cli: @echo "Building $(BINARY_NAME)..." @mkdir -p $(BUILD_DIR) $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME) $(BINARY_PATH) +build-service: + @echo "Building $(SERVICE_NAME)..." + @mkdir -p $(BUILD_DIR) + $(GOBUILD) -o $(BUILD_DIR)/$(SERVICE_NAME) $(SERVICE_PATH) + build-examples: @echo "Building $(EXAMPLE_MDNS_NAME)..." @mkdir -p $(BUILD_DIR) @@ -47,17 +54,21 @@ build-linux: @echo "Building for Linux..." @mkdir -p $(BUILD_DIR) GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 $(BINARY_PATH) + GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(SERVICE_NAME)-linux-amd64 $(SERVICE_PATH) build-darwin: @echo "Building for macOS..." @mkdir -p $(BUILD_DIR) GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 $(BINARY_PATH) GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 $(BINARY_PATH) + GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(SERVICE_NAME)-darwin-amd64 $(SERVICE_PATH) + GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(SERVICE_NAME)-darwin-arm64 $(SERVICE_PATH) build-windows: @echo "Building for Windows..." @mkdir -p $(BUILD_DIR) GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(BINARY_PATH) + GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(SERVICE_NAME)-windows-amd64.exe $(SERVICE_PATH) build-examples-all: @echo "Building examples for all platforms..." @@ -108,6 +119,18 @@ dev: build-cli @echo "Starting development CLI..." $(BUILD_DIR)/$(BINARY_NAME) -help +dev-service: build-service + @echo "Starting development service..." + $(BUILD_DIR)/$(SERVICE_NAME) + +dev-service-proxy: build-service + @echo "Starting development service with proxy..." + @if [ -z "$(PROXY_URL)" ]; then \ + echo "Usage: make dev-service-proxy PROXY_URL=http://localhost:8001"; \ + exit 1; \ + fi + PYTHON_BACKEND_URL=$(PROXY_URL) $(BUILD_DIR)/$(SERVICE_NAME) + dev-discover: build-cli @echo "Running device discovery..." $(BUILD_DIR)/$(BINARY_NAME) -discover @@ -164,9 +187,10 @@ dev-scan-http: build-examples @echo "Scanning for HTTP mDNS services..." $(BUILD_DIR)/$(SCANNER_NAME) -service _http._tcp -v -install: build-cli - @echo "Installing $(BINARY_NAME) to $(GOPATH)/bin..." +install: build-cli build-service + @echo "Installing binaries to $(GOPATH)/bin..." cp $(BUILD_DIR)/$(BINARY_NAME) $(GOPATH)/bin/ + cp $(BUILD_DIR)/$(SERVICE_NAME) $(GOPATH)/bin/ clean: @echo "Cleaning..." @@ -177,7 +201,7 @@ clean: release: clean check build-all @echo "Creating release archive..." @mkdir -p $(BUILD_DIR)/release - @for binary in $(BUILD_DIR)/$(BINARY_NAME)-*; do \ + @for binary in $(BUILD_DIR)/$(BINARY_NAME)-* $(BUILD_DIR)/$(SERVICE_NAME)-*; do \ if [ -f "$$binary" ]; then \ cp "$$binary" $(BUILD_DIR)/release/; \ fi \ @@ -186,16 +210,22 @@ release: clean check build-all docker-build: @echo "Building Docker image..." - docker build -t soundtouch-go:$(VERSION) . + docker build -t soundtouch-service . -docker-dev: docker-build - @echo "Running development container..." - docker run --rm -it --network host soundtouch-go:$(VERSION) +docker-run-host: + @echo "Running Docker container..." + @echo "Note: --network host is used for discovery (Linux only). For macOS/Windows use port mapping." + docker run --rm -it --network host -v $$(pwd)/data:/app/data soundtouch-service + +docker-run-ports: + @echo "Running Docker container with port mapping (discovery will be manual)..." + docker run --rm -it -p 8000:8000 -v $$(pwd)/data:/app/data soundtouch-service help: @echo "Available targets:" - @echo " build - Build the CLI tool and examples" + @echo " build - Build the CLI tool, service, and examples" @echo " build-cli - Build only the CLI tool" + @echo " build-service - Build only the service" @echo " build-examples - Build only the example programs" @echo " build-all - Build for all platforms" @echo " test - Run tests" @@ -206,6 +236,8 @@ help: @echo " lint - Run golangci-lint" @echo " tidy - Tidy dependencies" @echo " dev - Build and show CLI help" + @echo " dev-service - Build and run service locally" + @echo " dev-service-proxy - Build and run service with proxy (PROXY_URL=url required)" @echo " dev-discover - Build and run device discovery" @echo " dev-info - Build and get device info (HOST=ip required)" @echo " dev-mdns - Build and run mDNS discovery example" @@ -217,14 +249,17 @@ help: @echo " dev-scan-all - Scan all mDNS services on network" @echo " dev-scan-soundtouch - Scan specifically for SoundTouch mDNS services" @echo " dev-scan-http - Scan for HTTP mDNS services" - @echo " install - Install binary to GOPATH/bin" + @echo " install - Install binaries to GOPATH/bin" @echo " clean - Clean build artifacts" @echo " release - Create release binaries" @echo " docker-build - Build Docker image" - @echo " docker-dev - Run development container" + @echo " docker-run-host - Run container with host networking (Linux discovery)" + @echo " docker-run-ports - Run container with port mapping (macOS/Windows/No discovery)" @echo " help - Show this help message" @echo "" @echo "Examples:" + @echo " make dev-service" + @echo " make dev-service-proxy PROXY_URL=http://192.168.1.50:8001" @echo " make dev-discover" @echo " make dev-info HOST=192.168.1.10" @echo " make dev-mdns" diff --git a/README.md b/README.md index 79457b6..db90ffb 100644 --- a/README.md +++ b/README.md @@ -97,13 +97,69 @@ The `soundtouch-service` is a local server that emulates Bose's cloud services, # Install the service go install github.com/gesellix/bose-soundtouch/cmd/soundtouch-service@latest -# Start with default settings (http://localhost:8000) +# Start with default settings (http://localhost:8000, proxying to http://localhost:8001) soundtouch-service # Or configure with environment variables -PORT=9000 DATA_DIR=/my/data soundtouch-service +PORT=9000 PYTHON_BACKEND_URL=http://your-python-backend:8001 DATA_DIR=/my/data soundtouch-service ``` +#### Running with Docker + +You can also run the SoundTouch service using Docker or Docker Compose. + +> **Note for macOS and Windows users**: The `--net host` option is only supported on Linux. On macOS and Windows, service discovery (mDNS, UPnP) will not work automatically within the container. You will need to manually enter your device's IP address in the management UI, and the service will communicate with it directly. + +##### Using Docker + +**Linux (with host networking for discovery):** +```bash +docker run -d \ + --name soundtouch-service \ + --network host \ + -v $(pwd)/data:/app/data \ + ghcr.io/gesellix/bose-soundtouch:latest +``` + +**macOS / Windows (with port mapping):** +```bash +docker run -d \ + --name soundtouch-service \ + -p 8000:8000 \ + -v $(pwd)/data:/app/data \ + ghcr.io/gesellix/bose-soundtouch:latest +``` + +##### Using Docker Compose + +Create a `docker-compose.yml` file: + +```yaml +services: + soundtouch-service: + image: ghcr.io/gesellix/bose-soundtouch:latest + container_name: soundtouch-service + # Linux users: use host networking for device discovery + # network_mode: host + # macOS/Windows users: use port mapping (discovery will be manual) + ports: + - "8000:8000" + environment: + - PORT=8000 + - DATA_DIR=/app/data + volumes: + - ./data:/app/data + restart: unless-stopped +``` + +And run: + +```bash +docker-compose up -d +``` + +> **Note**: `--network host` is required for device discovery via UPnP and mDNS to work correctly within the container. + #### Device Migration Example ```bash diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f83fa8b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + soundtouch-service: + build: . + container_name: soundtouch-service + # network_mode: host # Linux only, required for discovery + ports: + - "8000:8000" + environment: + - PORT=8000 + - DATA_DIR=/app/data + - LOG_PROXY_BODY=false + - REDACT_PROXY_LOGS=true + volumes: + - ./data:/app/data + restart: unless-stopped