Files
RuView/.github/workflows/sensing-server-docker.yml
T
Dan Alec d45a04381d fix(ci): make the container image pipelines version-safe and fork-safe
nvsim-server -> ghcr.io was failing for three separate reasons, each one
masked by the previous. The Dockerfile carried two literals that had drifted
from their source of truth, and both workflows hard-coded the `ruvnet`
registry namespace that only the upstream can write to.

nvsim-server/Dockerfile

  1. Stub workspace version. The Dockerfile builds a throwaway workspace
     containing only nvsim and nvsim-server. That stub hard-coded
     version = "0.3.0" (PR #440); the real workspace is now 0.3.1 (PR
     #1439) and nvsim-server requires nvsim = { path = "../nvsim",
     version = "0.3.1" }. The build died with "failed to select a version
     for the requirement nvsim". The stub now extracts the version from
     the real Cargo.toml and fails loudly if it cannot read one.

  2. Builder toolchain. FROM was pinned to rust:1.81 while
     rust-toolchain.toml declares channel = "1.89", so async-compression
     0.4.42 and compression-codecs 0.4.38 (both MSRV 1.83, reached through
     tower-http's compression-gzip feature) aborted the release build with
     "rustc 1.81.0 is not supported". The builder is now 1.89.

     Rather than re-pinning another literal that will drift again, the
     declared channel travels into the build context and is compared
     against the effective rustc before compiling. Bumping one without the
     other now fails the build loudly instead of resurfacing as an MSRV
     error three steps later. It is copied under a separate name on
     purpose: letting rustup resolve it would silently download a
     toolchain and hide the drift.

nvsim-server-docker.yml

  The image name was hard-coded to ghcr.io/ruvnet/nvsim-server, so the push
  failed with `denied: The requested installation does not exist` outside
  the upstream namespace. It now derives from github.repository_owner,
  which resolves to ruvnet for the upstream and to the fork owner for
  every fork, so the workflow keeps working unchanged here.

  The smoke test also pulled :sha-<short> but ran :latest. `latest` is
  emitted with `enable={{is_default_branch}}`, so a tag build never had
  it, and the `|| latest` fallback masked a failed push by testing a stale
  image. It now pulls and runs the sha tag, which type=sha emits on every
  event.

sensing-server-docker.yml

  Same namespace problem: the image was fixed to ruvnet/wifi-densepose on
  both Docker Hub and ghcr.io, which a fork cannot push to. Publishing
  under github.repository_owner fixes that.

  docker login was also unconditional, so a repository without
  DOCKERHUB_USERNAME/DOCKERHUB_TOKEN died at "Must provide --username with
  --password-stdin" before building anything. ghcr.io needs no secret, so
  it stays the always-on target and Docker Hub is added only when both
  credentials are present.

Verified: cargo check -p nvsim-server --release under rustc 1.89.0
compiles nvsim v0.3.1, nvsim-server v0.3.1 and async-compression 0.4.42;
the resulting image builds, pushes and its /api/health reports
{"nvsim_version":"0.3.1"}. CI on this branch runs the full pipeline
green.
2026-10-16 16:06:56 -03:00

213 lines
9.1 KiB
YAML

name: wifi-densepose sensing-server → Docker Hub + ghcr.io
# Build + publish the `wifi-densepose` sensing-server image under the repository
# owner (`docker.io/<owner>/wifi-densepose` and `ghcr.io/<owner>/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.
#
# Docker Hub is only published when DOCKERHUB_USERNAME/DOCKERHUB_TOKEN exist;
# ghcr.io always publishes.
#
# 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/crates/wifi-densepose-bfld/**'
- 'v2/crates/cog-ha-matter/**'
- 'v2/crates/homecore*/**'
- '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@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
# QEMU is required so the amd64 GitHub runner can cross-build the
# linux/arm64 layer below (Dockerfile.rust is arch-agnostic — no `--target`
# flag — so buildx + QEMU is all that's needed; arm64 builds are emulated
# by the runner, not built on a separate arm64 host).
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
# Publish under the repository owner instead of a hard-coded `ruvnet`
# namespace: a fork cannot push to the upstream's Docker Hub or ghcr.io
# namespaces, and `github.repository_owner` resolves correctly for both
# the upstream and every fork.
#
# Docker Hub is optional. The login was previously hard-required, so a
# repository without DOCKERHUB_USERNAME/DOCKERHUB_TOKEN failed at
# `docker login` ("Must provide --username with --password-stdin") before
# it ever built anything. ghcr.io needs no secret, so it stays the
# always-on target.
#
# Bypassing docker/login-action@v3: the action kept emitting
# "malformed HTTP Authorization header" against a known-good
# dckr_pat_* token (verified by direct curl against the Hub API).
# `docker login --password-stdin` is the documented credential
# path and avoids whatever encoding step the action injects.
- name: Resolve publish targets
id: targets
env:
DH_USER: ${{ secrets.DOCKERHUB_USERNAME }}
DH_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
OWNER: ${{ github.repository_owner }}
run: |
set -uo pipefail
image="wifi-densepose"
images="ghcr.io/${OWNER}/${image}"
if [ -n "$DH_USER" ] && [ -n "$DH_TOKEN" ]; then
images="docker.io/${OWNER}/${image}"$'\n'"${images}"
printf '%s' "$DH_TOKEN" | docker login docker.io -u "$DH_USER" --password-stdin
else
echo "::warning::DOCKERHUB_USERNAME/DOCKERHUB_TOKEN are not configured; this run publishes to ghcr.io only."
fi
echo "images<<EOF" >> "$GITHUB_OUTPUT"
echo "$images" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
echo "ghcr_image=ghcr.io/${OWNER}/${image}" >> "$GITHUB_OUTPUT"
- name: Log in to ghcr.io
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute tags
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302
with:
images: ${{ steps.targets.outputs.images }}
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@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a
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
# README badge advertises `amd64 + arm64`, and #547 promised multi-arch
# as part of the docker publish refresh; arm64 was never actually wired
# in, so Apple Silicon Macs hit `no matching manifest for linux/arm64/v8`
# on `docker pull ruvnet/wifi-densepose:latest` (#136, #625). Build both.
platforms: linux/amd64,linux/arm64
# ---------------------------------------------------------------------
# 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 the explicit trusted-LAN opt-in.
# 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="${{ steps.targets.outputs.ghcr_image }}:sha-${GITHUB_SHA::7}"
docker pull "$IMAGE"
docker run --rm --entrypoint sh "$IMAGE" -c \
'ls /app/ui/observatory.html /app/ui/pose-fusion.html /app/ui/index.html /app/ui/viz.html >/dev/null'
docker run --rm --entrypoint sh "$IMAGE" -c 'ls -d /app/ui/observatory /app/ui/pose-fusion >/dev/null'
docker run -d --name sm -p 3000:3000 \
-e CSI_SOURCE=simulated \
-e RUVIEW_ALLOW_UNAUTHENTICATED=1 \
"$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="${{ steps.targets.outputs.ghcr_image }}: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"