From d64c7e63e1ec705e0051f17ac9d41b727473d594 Mon Sep 17 00:00:00 2001 From: Fredrik Date: Wed, 13 May 2026 08:04:32 +0200 Subject: [PATCH] ci(release): publish multi-arch Docker image to ghcr.io on tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unblocks bundling hugin-agent as a sidecar in other Docker-native products (forty-two-watts being the immediate consumer). What lands: Dockerfile (repo root) Multi-stage build (golang:1.25-alpine → alpine:3.21). Runs as non-root `hugin` user. Container-friendly defaults: HUGIN_AGENT_HOST=0.0.0.0 (reachable from outside the container) HUGIN_AGENT_PORT=19090 HUGIN_AGENT_CREDS=/var/lib/hugin-agent/creds.json CMD --no-browser because there's no browser to open inside the container. The pairing URL is printed to stderr; the parent product's UI or `docker logs` surfaces it. /var/lib/hugin-agent is a named volume so creds.json survives container restarts. .goreleaser.yml dockers: + docker_manifests: blocks for linux/amd64 + linux/arm64. arm64 explicitly for Raspberry Pi installs (a big 42W use case). Three tag aliases per release: vX.Y.Z, vX.Y, latest. .github/workflows/release.yml Adds packages: write permission and three pre-goreleaser steps: QEMU setup, buildx setup, GHCR login (using GITHUB_TOKEN — no extra secret needed for same-org GHCR publish). README.md New Docker section between Scoop and "From source", with a docker-compose snippet showing the sidecar pattern. The brew tap + scoop bucket auto-publish PR (#2) is independent; both can land in either order. Same release tag drives all three publishers (tarballs, image, brew/scoop) when both PRs are merged. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 17 +++++++++ .goreleaser.yml | 66 +++++++++++++++++++++++++++++++++++ Dockerfile | 66 +++++++++++++++++++++++++++++++++++ README.md | 28 +++++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 Dockerfile diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed5b657..468940e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,7 @@ on: permissions: contents: write # goreleaser uploads to GitHub Releases + packages: write # goreleaser pushes to ghcr.io/srcfl/hugin-agent jobs: goreleaser: @@ -21,6 +22,22 @@ jobs: go-version: "1.25" cache: true + # Multi-arch Docker build for ghcr.io/srcfl/hugin-agent (linux/amd64 + # + linux/arm64). buildx + qemu enable cross-arch compilation in the + # x86 GitHub runner. + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: goreleaser/goreleaser-action@v6 with: version: "~> v2" diff --git a/.goreleaser.yml b/.goreleaser.yml index ec411ff..d2482f8 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -54,3 +54,69 @@ changelog: - "^test:" - "^chore:" - "Merge pull request" + +# Multi-arch container image publish to GHCR. Same release tag drives +# tarballs (above) and ghcr.io/srcfl/hugin-agent:vX.Y.Z. The release +# workflow logs in to ghcr.io before goreleaser runs (see +# .github/workflows/release.yml). +# +# linux/amd64 + linux/arm64 cover Raspberry Pi (arm64) and most x86 +# servers — the primary 42W bundling targets. Add more if a host +# platform shows up later. +dockers: + - id: hugin-agent-amd64 + goos: linux + goarch: amd64 + use: buildx + dockerfile: Dockerfile + image_templates: + - "ghcr.io/srcfl/hugin-agent:{{ .Version }}-amd64" + - "ghcr.io/srcfl/hugin-agent:v{{ .Major }}.{{ .Minor }}-amd64" + - "ghcr.io/srcfl/hugin-agent:latest-amd64" + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.title=hugin-agent" + - "--label=org.opencontainers.image.description=Local agent for the Hugin driver workbench" + - "--label=org.opencontainers.image.url=https://github.com/srcfl/hugin-agent" + - "--label=org.opencontainers.image.source=https://github.com/srcfl/hugin-agent" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.revision={{ .FullCommit }}" + - "--label=org.opencontainers.image.created={{ .Date }}" + - "--label=org.opencontainers.image.licenses=MIT" + + - id: hugin-agent-arm64 + goos: linux + goarch: arm64 + use: buildx + dockerfile: Dockerfile + image_templates: + - "ghcr.io/srcfl/hugin-agent:{{ .Version }}-arm64" + - "ghcr.io/srcfl/hugin-agent:v{{ .Major }}.{{ .Minor }}-arm64" + - "ghcr.io/srcfl/hugin-agent:latest-arm64" + build_flag_templates: + - "--platform=linux/arm64" + - "--label=org.opencontainers.image.title=hugin-agent" + - "--label=org.opencontainers.image.description=Local agent for the Hugin driver workbench" + - "--label=org.opencontainers.image.url=https://github.com/srcfl/hugin-agent" + - "--label=org.opencontainers.image.source=https://github.com/srcfl/hugin-agent" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.revision={{ .FullCommit }}" + - "--label=org.opencontainers.image.created={{ .Date }}" + - "--label=org.opencontainers.image.licenses=MIT" + +# Combine the per-arch images into one multi-arch manifest so +# `docker pull ghcr.io/srcfl/hugin-agent:v0.2.1` works on either +# host architecture and Docker picks the right one. +docker_manifests: + - name_template: "ghcr.io/srcfl/hugin-agent:{{ .Version }}" + image_templates: + - "ghcr.io/srcfl/hugin-agent:{{ .Version }}-amd64" + - "ghcr.io/srcfl/hugin-agent:{{ .Version }}-arm64" + - name_template: "ghcr.io/srcfl/hugin-agent:v{{ .Major }}.{{ .Minor }}" + image_templates: + - "ghcr.io/srcfl/hugin-agent:v{{ .Major }}.{{ .Minor }}-amd64" + - "ghcr.io/srcfl/hugin-agent:v{{ .Major }}.{{ .Minor }}-arm64" + - name_template: "ghcr.io/srcfl/hugin-agent:latest" + image_templates: + - "ghcr.io/srcfl/hugin-agent:latest-amd64" + - "ghcr.io/srcfl/hugin-agent:latest-arm64" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d6198e6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,66 @@ +# syntax=docker/dockerfile:1.6 +# +# Dockerfile for hugin-agent — the local helper the workbench talks +# to. Published as ghcr.io/srcfl/hugin-agent:vX.Y.Z on every release +# tag via goreleaser (see .goreleaser.yml). +# +# Primary use case: bundling inside another product's docker-compose +# stack (e.g. forty-two-watts) as a sidecar. Single static Go binary, +# ~12 MB final image, no daemon, no telemetry. +# +# Browser auto-open is disabled by default in the container — there +# is no browser; the user opens the pairing URL on their host +# manually (or via the parent product's UI). + +# ---- Build stage ----------------------------------------------------------- +FROM golang:1.25-alpine AS builder + +WORKDIR /src + +RUN apk add --no-cache git + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 go build \ + -ldflags "-s -w -X main.version=$(git describe --tags --always --dirty 2>/dev/null || echo docker)" \ + -o /out/hugin-agent ./cmd/hugin-agent/ + +# ---- Runtime stage --------------------------------------------------------- +FROM alpine:3.21 + +RUN apk add --no-cache ca-certificates tzdata \ + && addgroup -S hugin \ + && adduser -S -G hugin -h /home/hugin hugin + +COPY --from=builder /out/hugin-agent /usr/local/bin/hugin-agent + +# Container-friendly defaults: +# - HUGIN_AGENT_HOST=0.0.0.0 so the agent is reachable from outside +# the container (the parent compose stack does port mapping or +# network_mode: host). The pairing token is still the auth boundary. +# - --no-browser passed via CMD because there's no browser to open. +# - Creds persist under /var/lib/hugin-agent so docker-compose +# volumes can keep them across container restarts. +ENV HUGIN_AGENT_HOST=0.0.0.0 \ + HUGIN_AGENT_PORT=19090 \ + HUGIN_AGENT_CREDS=/var/lib/hugin-agent/creds.json + +RUN mkdir -p /var/lib/hugin-agent && chown hugin:hugin /var/lib/hugin-agent + +USER hugin +WORKDIR /home/hugin +VOLUME ["/var/lib/hugin-agent"] + +EXPOSE 19090 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD wget -qO- "http://127.0.0.1:${HUGIN_AGENT_PORT}/v1/health" \ + | grep -q '"status":"ok"' || exit 1 +# /v1/health is intentionally unauthenticated (see internal/server/server.go) — +# the agent's pairing-token gate sits on the work endpoints (scan, probe, +# run-lua), not on liveness probes. + +ENTRYPOINT ["hugin-agent"] +CMD ["--no-browser"] diff --git a/README.md b/README.md index b35d245..0d2f091 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,34 @@ scoop install hugin-agent hugin-agent ``` +### Docker + +Published as a multi-arch image on every release tag. Use as a +sidecar in your existing compose stack (e.g. forty-two-watts): + +```yaml +# docker-compose.hugin.yml +services: + hugin-agent: + image: ghcr.io/srcfl/hugin-agent:latest + restart: unless-stopped + network_mode: host # see Modbus devices on the host LAN + volumes: + - hugin-agent-data:/var/lib/hugin-agent +volumes: + hugin-agent-data: +``` + +```bash +docker compose -f docker-compose.hugin.yml up -d +docker logs hugin-agent # the pairing URL is printed on stderr +``` + +`/var/lib/hugin-agent/creds.json` persists NATS pairing across +restarts. Without `network_mode: host` the agent can still talk to +the workbench (publish port 19090) but won't see Modbus devices on +the user's LAN — pick whichever fits your security model. + ### From source (any platform with Go 1.25+) ```bash