Kaynağa Gözat

add-on: move to the sm121-ha-integration add-on store (cups-addon pattern)

The HA add-on now lives in github.com/kilork/sm121-ha-integration as a
proper HA add-on store (repository.json + sm121/ + GitHub Actions, the
same pattern as kilork/cups-addon):

- docker-images/sm121-addon/ removed (the sibling repo is the source of
  truth); the image is THIN (no sm121 binary, no toolchain — ~250 MB
  compressed, within GHCR's free tier): the entrypoint installs rustup
  on first boot and compiles sm121 for the container's own arch
- GitHub Actions builds aarch64 + amd64 natively and publishes
  ghcr.io/kilork/sm121-addon via OIDC (no docker login anywhere);
  make addon-push is gone — CI pushes
- make addon-image builds from the sibling repo; smoke-addon.sh does the
  same (verified: rustup install + compile from a file:// checkout +
  repo sync + /api/state 200)
- AGENTS.md: the add-on section follows the move
Alexander Korolev 1 hafta önce
ebeveyn
işleme
af893a3fe7

+ 21 - 21
AGENTS.md

@@ -780,27 +780,27 @@ the `sm121.checksum` label — a missing tag names the tag + hints to
 build/push first (or fall back to `--image-source repo`). Mutually
 exclusive with `--build-node`.
 
-**The add-on image** (`docker-images/sm121-addon/`, THIN — the Pi is the
-build host): `make addon-image` → `sm121-addon:latest` (debian-slim +
-git/jq + the pinned Rust toolchain, ~1.2 GB, no sm121 binary); the
-entrypoint **compiles sm121 from the launcher repo on first boot**
-(`sm121_launcher_url` default git.kilork.org/sm121/sm121-launcher,
-`sm121_launcher_rev` default main — set a commit to pin) and caches the
-binary + cargo target dir in the add-on's data folder ($HOME), so a
-reboot with an unchanged rev is fast and a rev change recompiles once.
-The arch always matches (no cross-toolchain, x86_64 HA hosts work with a
-multi-arch image push) and the binary never goes stale. First boot is a
-full release build (minutes on a Pi 5, longer on a Pi 4 —
-`sm121_build_jobs` defaults to 2 so a Pi 4 doesn't OOM under HA). This is
-the HAOS path (HAOS has no Docker to build with). `make addon-push` →
-`ghcr.io/kilork/sm121-addon:latest`; smoke test `scripts/smoke-addon.sh`
-(entrypoint + `file://` recipe repo + `file://` launcher checkout +
-curl /api/state). The entrypoint writes the effective config from the
-add-on options (env), drops the SSH key (base64 option), `sm121 repos
-sync`, then `sm121 web --port 3100 --bind 0.0.0.0` (a bearer token is
-required) with a background re-sync loop (`SM121_REPO_RESYNC_MINUTES`).
-The HA add-on manifest (`config.yaml` + README) lives in the sibling
-**sm121-ha-integration** repo — its `custom_components/` custom-integration
+**The add-on image** — the HA add-on store lives in the sibling
+**sm121-ha-integration** repo (`repository.json` + `sm121/` add-on dir +
+`.github/workflows/`, same pattern as `kilork/cups-addon`): a THIN image
+(debian-slim + git/jq/curl/gcc, no sm121 binary, no toolchain — small
+enough for GHCR's free tier). The **Pi is the build host**: the entrypoint
+installs rustup on first boot (cached in the add-on's data folder) and
+compiles sm121 from the launcher repo (`sm121_launcher_url` @
+`sm121_launcher_rev`, default main — set a commit to pin; `sm121_build_jobs`
+defaults to 2 so a Pi 4 doesn't OOM under HA). The arch always matches and
+the binary never goes stale; first boot = toolchain download + a release
+build (minutes on a Pi 5). **GitHub Actions builds + pushes**
+`ghcr.io/kilork/sm121-addon` (aarch64 + amd64 + manifest) on push to main
+via OIDC — no `docker login` anywhere (the GHCR package is created
+private: set it public once so HA pulls anonymously). Local dev:
+`make addon-image` (builds from the sibling repo) +
+`scripts/smoke-addon.sh` (entrypoint + `file://` recipe repo + `file://`
+launcher checkout + curl /api/state). The entrypoint writes the effective
+config from the add-on options (env), drops the SSH key (base64 option),
+`sm121 repos sync`, then `sm121 web --port 3100 --bind 0.0.0.0` (a bearer
+token is required) with a background re-sync loop
+(`SM121_REPO_RESYNC_MINUTES`). Its `custom_components/` custom-integration
 prototype is superseded (entities are a phase-2 nicety over the add-on's
 REST API). Per-install credentials that can't be flat options (e.g. a
 `[mikrotik]` block with its password) go in the `sm121_config_extra`

+ 8 - 12
Makefile

@@ -40,20 +40,16 @@ fmt:
 # docker build from the repo root (src/ compiled in-container — the binary
 # always matches the container's arch; .dockerignore trims the context).
 # Smoke test: scripts/smoke-addon.sh.
+# The add-on image lives in the sibling sm121-ha-integration repo (the HA
+# add-on store: repository.json + sm121/ + the GitHub Actions workflows)
+# and is built + pushed THERE by CI (ghcr.io/kilork/sm121-addon, OIDC —
+# no docker login). Local build for dev / smoke:
 addon-image:
-	docker build -t sm121-addon:latest -f docker-images/sm121-addon/Dockerfile docker-images/sm121-addon/
+	docker build -t sm121-addon:latest -f ../sm121-ha-integration/sm121/Dockerfile ../sm121-ha-integration/sm121/
 
-# Push the add-on image to the [registry] base (the HA add-on's config.yaml
-# references it). Requires `docker login ghcr.io`.
-# The image is THIN (no sm121 binary — the entrypoint compiles it on the Pi
-# from the launcher repo, so the arch always matches). Built on an aarch64
-# host for the Pi; for a multi-arch manifest (x86_64 too) use:
-#   docker buildx build --platform linux/aarch64,linux/x86_64 \
-#     -t ghcr.io/kilork/sm121-addon:latest --push -f docker-images/sm121-addon/Dockerfile docker-images/sm121-addon/
-# (needs qemu binfmt on the builder).
-addon-push:
-	docker tag sm121-addon:latest ghcr.io/kilork/sm121-addon:latest
-	docker push ghcr.io/kilork/sm121-addon:latest
+# No push target: GitHub Actions (sm121-ha-integration) publishes
+# ghcr.io/kilork/sm121-addon on push to main (aarch64 + amd64 + manifest).
+# The GHCR package must be set public (first time only) so HA can pull it.
 
 # Auto-fix lints (clippy --fix); review the diff before committing.
 clippy-fix:

+ 0 - 39
docker-images/sm121-addon/Dockerfile

@@ -1,39 +0,0 @@
-# sm121 HA add-on image — THIN: bakes in the Rust toolchain; the
-# entrypoint compiles sm121 from the launcher repo on first boot (cached
-# in the add-on's data folder). The Pi IS the build host: always the right
-# arch, the binary never goes stale, and the image carries no sm121
-# binary at all (specs/tech-architecture/ha-addon-recipe-repos.md —
-# "build on ha.lan" variant for HAOS, which has no Docker).
-#
-#   make addon-image      # context = docker-images/sm121-addon/
-#   scripts/smoke-addon.sh
-#
-# The image is arch-generic EXCEPT the baked rustup toolchain (built for
-# the build host's arch — build it on an aarch64 host for the Pi; a
-# multi-arch buildx push covers x86_64 HA hosts too).
-
-FROM debian:bookworm-slim
-
-# git (launcher-repo fetch + recipe-repo sync) + jq (env → config) +
-# ca-certs (https) + gcc/libc6-dev (ring's C backend, via russh)
-RUN apt-get update \
- && apt-get install -y --no-install-recommends \
-      git ca-certificates jq curl gcc libc6-dev \
- && rm -rf /var/lib/apt/lists/*
-
-# Rust toolchain, pinned (edition 2024 + let-chains need rust ≥ 1.88; the
-# launcher's Cargo.toml pins the exact version). `minimal` profile — no
-# docs/rustfmt/clippy (the Pi only builds, never lints).
-ENV RUSTUP_HOME=/usr/local/rustup \
-    CARGO_HOME=/usr/local/cargo \
-    PATH=/usr/local/cargo/bin:$PATH
-RUN curl -sSf https://sh.rustup.rs \
-    | sh -s -- -y --profile minimal --default-toolchain 1.98.1 \
- && rustup toolchain list
-
-COPY entrypoint.sh /entrypoint.sh
-RUN chmod 755 /entrypoint.sh
-
-ENV SM121_WEB_PORT=3100
-EXPOSE 3100
-ENTRYPOINT ["/entrypoint.sh"]

+ 0 - 141
docker-images/sm121-addon/entrypoint.sh

@@ -1,141 +0,0 @@
-#!/bin/sh
-# sm121 HA add-on entrypoint (specs/tech-architecture/ha-addon-recipe-repos.md):
-#
-#   1. write the effective /etc/sm121/config.toml from the add-on options
-#      (env vars); an optional base config at /etc/sm121/base.toml (e.g. a
-#      recipe repo's config.toml, mounted) carries the cluster definitions
-#      — the generated sections (recipe_repos / home_assistant / registry)
-#      are APPENDED, so the base must not define them
-#   2. drop the SSH key (a mounted secret) at SM121_SSH_KEY_PATH
-#   3. sync the recipe repos (clone/pull)
-#   4. run `sm121 web` on 3100; a supervisor loop re-syncs the repos every
-#      SM121_REPO_RESYNC_MINUTES so new recipes land without a restart
-#
-# Env (the add-on options — HA maps each `sm121_*` option to the
-# uppercased env var; the standalone flow sets the same names directly):
-#   SM121_RECIPE_REPOS        JSON list of {"url","token"} → [[recipe_repos]]
-#   SM121_SSH_USER            ssh_user
-#   SM121_SSH_KEY_B64         base64 private key (HA option; written to KEY_PATH)
-#   SM121_SSH_KEY             path to a mounted private key (docker secret mount)
-#   SM121_SSH_KEY_PATH        where the key lands (default /etc/sm121/ssh_key)
-#   SM121_HA_URL / SM121_HA_TOKEN   → [home_assistant]
-#   SM121_REGISTRY_BASE       → [registry] base (for --image-source registry)
-#   SM121_CONFIG_EXTRA        raw TOML appended verbatim (per-install
-#                             credentials that must not live in the shared
-#                             repo, e.g. a [mikrotik] block with its password)
-#   SM121_LAUNCHER_URL        launcher repo (default git.kilork.org/sm121/
-#                             sm121-launcher) — sm121 is COMPILED from it on
-#                             first boot (the Pi is the build host)
-#   SM121_LAUNCHER_REV        rev to compile (default main; a commit hash
-#                             pins the binary)
-#   SM121_BUILD_JOBS          cargo -j (default 2 — a Pi 4 must not OOM)
-#   SM121_REPO_RESYNC_MINUTES background re-sync interval (default 10)
-#   SM121_WEB_PORT            dashboard port (default 3100)
-#   SM121_WEB_BIND            dashboard bind (default 0.0.0.0)
-#   SM121_WEB_TOKEN           bearer token (REQUIRED for a non-loopback bind)
-set -eu
-
-CONFIG=/etc/sm121/config.toml
-mkdir -p /etc/sm121
-
-# --- 1. the effective config --------------------------------------------------
-if [ -f /etc/sm121/base.toml ]; then
-  cp /etc/sm121/base.toml "$CONFIG"
-else
-  : > "$CONFIG"
-fi
-
-{
-  # (if-form, not `&&`: a false condition must not trip `set -e`)
-  if [ -n "${SM121_SSH_USER:-}" ]; then printf 'ssh_user = "%s"\n' "$SM121_SSH_USER"; fi
-  if [ -n "${SM121_SSH_KEY_PATH:-}" ]; then printf 'ssh_key_path = "%s"\n' "$SM121_SSH_KEY_PATH"; fi
-} >> "$CONFIG"
-
-# JSON list of {"url","token"} → [[recipe_repos]] blocks (tojson keeps the
-# values properly escaped — JSON basic strings ARE valid TOML strings).
-if [ -n "${SM121_RECIPE_REPOS:-}" ]; then
-  printf '%s' "$SM121_RECIPE_REPOS" | jq -r '
-    .[] | ("[[recipe_repos]]\nurl = " + (.url | tojson) + "\n"
-           + (if .token then "token = " + (.token | tojson) + "\n" else "" end))
-  ' >> "$CONFIG"
-fi
-
-if [ -n "${SM121_HA_URL:-}" ] && [ -n "${SM121_HA_TOKEN:-}" ]; then
-  printf '[home_assistant]\nurl = "%s"\ntoken = "%s"\n' \
-    "$SM121_HA_URL" "$SM121_HA_TOKEN" >> "$CONFIG"
-fi
-if [ -n "${SM121_REGISTRY_BASE:-}" ]; then
-  printf '[registry]\nbase = "%s"\n' "$SM121_REGISTRY_BASE" >> "$CONFIG"
-fi
-
-# Raw per-install TOML, appended verbatim (the option is a multi-line
-# string). This is where credentials that can't be expressed as flat
-# options live — e.g. a [mikrotik] block (it carries a password, so it is
-# deliberately NOT in the shared recipe repo's config.toml). Never logged.
-if [ -n "${SM121_CONFIG_EXTRA:-}" ]; then
-  printf '%s\n' "$SM121_CONFIG_EXTRA" >> "$CONFIG"
-fi
-
-# --- 2. the SSH key ------------------------------------------------------------
-# HA add-on options cannot mount files: the key arrives base64-encoded as
-# SM121_SSH_KEY_B64 (the `sm121_ssh_key_b64` option) and is written to
-# SM121_SSH_KEY_PATH (default /etc/sm121/ssh_key). A plain file path
-# (SM121_SSH_KEY, e.g. a docker secret mount) is also accepted.
-KEY_PATH="${SM121_SSH_KEY_PATH:-/etc/sm121/ssh_key}"
-if [ -n "${SM121_SSH_KEY_B64:-}" ]; then
-  mkdir -p "$(dirname "$KEY_PATH")"
-  printf '%s' "$SM121_SSH_KEY_B64" | base64 -d > "$KEY_PATH"
-  chmod 600 "$KEY_PATH"
-elif [ -n "${SM121_SSH_KEY:-}" ] && [ -f "$SM121_SSH_KEY" ]; then
-  cp "$SM121_SSH_KEY" "$KEY_PATH"
-  chmod 600 "$KEY_PATH"
-fi
-
-# --- 2b. compile sm121 if needed (the Pi is the build host) ------------------
-# The launcher source comes from the launcher repo (SM121_LAUNCHER_URL @
-# SM121_LAUNCHER_REV). The binary + a rev stamp + the cargo target cache
-# live in the add-on's data folder ($HOME — HA sets it to the add-on's data
-# dir; falls back to /root), so a reboot with an unchanged rev is fast and
-# a rev change recompiles once. First boot is slow (a full release build:
-# minutes on a Pi 5, longer on a Pi 4) — the add-on just shows "starting".
-HOME="${HOME:-/root}"
-SM121_BIN="$HOME/.sm121/bin/sm121"
-LAUNCHER_URL="${SM121_LAUNCHER_URL:-https://git.kilork.org/sm121/sm121-launcher.git}"
-LAUNCHER_REV="${SM121_LAUNCHER_REV:-main}"
-STAMP="$HOME/.sm121/rev"
-if [ ! -x "$SM121_BIN" ] || [ "$(cat "$STAMP" 2>/dev/null)" != "$LAUNCHER_REV" ]; then
-  echo "sm121: compiling from $LAUNCHER_URL @ $LAUNCHER_REV (first boot / rev changed)"
-  mkdir -p "$HOME/.sm121/bin"
-  SRC="$(mktemp -d)"
-  case "$LAUNCHER_REV" in
-    main|master) git clone -q --depth 1 --branch "$LAUNCHER_REV" "$LAUNCHER_URL" "$SRC" ;;
-    *) git clone -q "$LAUNCHER_URL" "$SRC" && git -C "$SRC" checkout -q "$LAUNCHER_REV" ;;
-  esac
-  CARGO_TARGET_DIR="$HOME/.sm121/target" \
-  CARGO_BUILD_JOBS="${SM121_BUILD_JOBS:-2}" \
-  cargo build --release --manifest-path "$SRC/Cargo.toml"
-  cp "$HOME/.sm121/target/release/sm121" "$SM121_BIN"
-  printf '%s' "$LAUNCHER_REV" > "$STAMP"
-  rm -rf "$SRC"
-  echo "sm121: built ($LAUNCHER_REV) → $SM121_BIN"
-fi
-
-# --- 3 + 4. sync + web, with the re-sync supervisor ----------------------------
-SM121="$SM121_BIN --config $CONFIG"
-$SM121 repos sync || echo "warning: initial repo sync failed (the supervisor retries)"
-
-INTERVAL="${SM121_REPO_RESYNC_MINUTES:-10}"
-(
-  while :; do
-    sleep "$((INTERVAL * 60))"
-    $SM121 repos sync || true
-  done
-) &
-
-# A web crash exits the container; the HA restart policy (always) re-runs
-# this entrypoint (re-sync + fresh dashboard). No state is lost (the run
-# ledger lives in the recipe repos, the cache in ~/.cache/sm121).
-exec $SM121 web \
-  --port "${SM121_WEB_PORT:-3100}" \
-  --bind "${SM121_WEB_BIND:-0.0.0.0}" \
-  ${SM121_WEB_TOKEN:+--token "$SM121_WEB_TOKEN"}

+ 7 - 0
scripts/smoke-addon.sh

@@ -15,6 +15,13 @@ cd "$(dirname "$0")/.."
 IMG="${1:-sm121-addon:latest}"
 TOKEN=smoke-test
 
+# The image source lives in the sibling add-on repo (the HA add-on store).
+# Build it locally if it isn't there yet (CI builds the registry one).
+if ! docker image inspect "$IMG" >/dev/null 2>&1; then
+  echo "building $IMG from ../sm121-ha-integration/sm121/"
+  docker build -q -t "$IMG" -f ../sm121-ha-integration/sm121/Dockerfile ../sm121-ha-integration/sm121/
+fi
+
 # A minimal `file://` recipe repo, mounted read-only at /repos. It lives
 # under target/ (gitignored) so it sits on a volume the docker VM shares
 # — colima only mounts /Volumes here; a $HOME tmpdir would mount empty.