name: wifi-densepose sensing-server → Docker Hub + ghcr.io # Build + publish the `wifi-densepose` sensing-server image to both Docker Hub # (`ruvnet/wifi-densepose`) and ghcr.io (`ghcr.io/ruvnet/wifi-densepose`) on: # - push to main affecting the Dockerfile, the server crate, the UI assets, # or this workflow itself, # - tag push matching v* (release builds), # - manual workflow_dispatch. # # Closes #520 and #514: the stale `:latest` is rebuilt and pushed automatically # whenever the surface that produces it changes, and the Dockerfile fails the # build if the observatory/pose-fusion UI assets ever go missing again. # # Secrets: # DOCKERHUB_USERNAME — `ruvnet` (Docker Hub login name) # DOCKERHUB_TOKEN — Docker Hub access token with read/write/delete scope # (ghcr.io uses the workflow's GITHUB_TOKEN — no secret needed.) on: push: branches: [main] paths: - 'docker/Dockerfile.rust' - 'docker/docker-entrypoint.sh' - 'v2/crates/wifi-densepose-sensing-server/**' - 'v2/crates/wifi-densepose-signal/**' - 'v2/crates/wifi-densepose-vitals/**' - 'v2/crates/wifi-densepose-wifiscan/**' - 'v2/Cargo.toml' - 'v2/Cargo.lock' - 'ui/**' - '.github/workflows/sensing-server-docker.yml' tags: ['v*'] workflow_dispatch: {} permissions: contents: read packages: write concurrency: group: sensing-server-docker-${{ github.ref }} cancel-in-progress: true jobs: build-and-publish: name: build · push · smoke-test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: docker/setup-buildx-action@v3 - name: Log in to Docker Hub uses: docker/login-action@v3 with: registry: docker.io username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Log in to ghcr.io uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Compute tags id: meta uses: docker/metadata-action@v6 with: images: | docker.io/ruvnet/wifi-densepose ghcr.io/ruvnet/wifi-densepose tags: | type=ref,event=branch type=ref,event=tag type=sha,format=short type=raw,value=latest,enable={{is_default_branch}} - name: Build + push id: build uses: docker/build-push-action@v7 with: context: . file: docker/Dockerfile.rust push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max platforms: linux/amd64 # --------------------------------------------------------------------- # Smoke-test the freshly-pushed image: # 1. UI assets that closed #520 are inside `/app/ui` (the Dockerfile's # RUN guard catches missing ones at build time, this re-checks the # pushed artifact post-hoc as belt-and-braces). # 2. /health is up. # 3. /api/v1/info returns 200 with no auth (LAN-mode default). # 4. With RUVIEW_API_TOKEN set, /api/v1/info returns 401 without a # Bearer header, 200 with the correct one (the #443 auth middleware). # --------------------------------------------------------------------- - name: Smoke-test image assets + LAN-mode HTTP run: | set -euo pipefail IMAGE="ghcr.io/ruvnet/wifi-densepose:sha-${GITHUB_SHA::7}" docker pull "$IMAGE" docker run --rm "$IMAGE" sh -c \ 'ls /app/ui/observatory.html /app/ui/pose-fusion.html /app/ui/index.html /app/ui/viz.html >/dev/null' docker run --rm "$IMAGE" sh -c 'ls -d /app/ui/observatory /app/ui/pose-fusion >/dev/null' docker run -d --name sm -p 3000:3000 -e CSI_SOURCE=simulated "$IMAGE" # Wait up to 30 s for /health. for _ in $(seq 1 30); do if curl -fsS http://127.0.0.1:3000/health >/dev/null 2>&1; then break; fi sleep 1 done curl -fsS http://127.0.0.1:3000/health curl -fsS http://127.0.0.1:3000/api/v1/info >/dev/null curl -fsS http://127.0.0.1:3000/ui/observatory.html >/dev/null curl -fsS http://127.0.0.1:3000/ui/pose-fusion.html >/dev/null docker stop sm - name: Smoke-test the bearer-token auth path run: | set -euo pipefail IMAGE="ghcr.io/ruvnet/wifi-densepose:sha-${GITHUB_SHA::7}" docker run -d --name auth \ -p 3000:3000 \ -e CSI_SOURCE=simulated \ -e RUVIEW_API_TOKEN=smoke-test-token-do-not-use \ "$IMAGE" for _ in $(seq 1 30); do if curl -fsS http://127.0.0.1:3000/health >/dev/null 2>&1; then break; fi sleep 1 done # /health stays unauthenticated. curl -fsS http://127.0.0.1:3000/health >/dev/null # /api/v1/info without a bearer → 401. code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/api/v1/info) test "$code" = "401" || { echo "expected 401, got $code"; exit 1; } # Wrong bearer → 401. code=$(curl -s -o /dev/null -w '%{http_code}' -H 'Authorization: Bearer wrong' http://127.0.0.1:3000/api/v1/info) test "$code" = "401" || { echo "expected 401 (wrong token), got $code"; exit 1; } # Correct bearer → 200. curl -fsS -H 'Authorization: Bearer smoke-test-token-do-not-use' http://127.0.0.1:3000/api/v1/info >/dev/null docker stop auth - name: Summary if: always() run: | { echo "## sensing-server image published" echo echo "Tags:" echo '```' echo "${{ steps.meta.outputs.tags }}" echo '```' echo echo "Closes #520 (missing observatory/pose-fusion UI assets) and #514 (stale `:latest` for the v0.6+ packet format)." echo "The Dockerfile fails the build if those UI assets ever disappear again, and this workflow rebuilds + pushes automatically on every change to the surface." } >> "$GITHUB_STEP_SUMMARY"