feat: Hugo version pinning via harness.yaml with verified auto-download #22

Merged
dominik.polakovics merged 2 commits from afk/5 into main 2026-07-12 15:38:06 +02:00

What

Deterministic Hugo versions per site (issue #5): a harness.yaml at the site root pins the exact Hugo version; the harness downloads that extended-edition release on first use, sha256-verifies it against the official checksums file, caches it per-user, and runs it for the supervised preview. An explicit hugo.binary override serves air-gapped hosts. With a pin or override in effect there is no silent fallback to a system hugo — every failure refuses startup with an actionable error.

How

  • internal/siteconfig — parses harness.yaml. Missing file = no pin; unknown keys tolerated (the file grows in later issues); malformed YAML or a non-exact version (v-prefix, ranges, latest) is a clear startup error naming the file. The pin stays a plain scalar at hugo.version so CI can extract it with yq -r .hugo.version harness.yaml (or the sed one-liner in the README).
  • internal/hugobin — the resolver contract: (pin/override, OS/arch) → ready-to-run binary + provenance. Strict order: hugo.binary (used exactly, validated, never downloads) → hugo.version (cache at ~/.cache/deckle/hugo/<version>/<os>-<arch>/hugo, cold cache downloads + verifies + installs atomically via temp-file-then-rename) → hugo from PATH, logged as "no pin in effect". Reusable by pre-flight builds (#10) and init scaffolding (#17). Download/verify logic is fully injectable (base URL, HTTP client, cache dir, OS/arch) — tests run offline against fake archives, including checksum-mismatch, missing-asset, and cache-reuse (zero requests when warm) paths.
  • cmd/harness + internal/serverserve resolves once at startup and hands the path to the supervisor; GET /api/status gains additive hugo.version / hugo.source (pinned / binary / path) fields — all pre-existing status fields unchanged. The UI status pill shows the active version and provenance.

Verified

  • Full suites green: 17/17 frontend tests, all Go packages (go vet, gofmt clean, CGO_ENABLED=0 go test ./...).
  • End-to-end against real releases: cold start downloaded and verified 0.148.1, warm restart reused the cache with zero network, switching the pin to 0.147.9 downloaded and ran the new version (visible in /api/status), preview served on both.
  • Failure probes: missing override path, malformed harness.yaml, version: "latest", and a nonexistent pin (99.99.99 → checksums 404) all refuse startup with errors naming the file/pin and the hugo.binary escape hatch; no-pin sites run hugo from PATH exactly as before, with provenance path.

Closes #5

🤖 Generated with Claude Code

https://claude.ai/code/session_01AbjCbYy6BR3osKsdPqo32X

## What Deterministic Hugo versions per site (issue #5): a `harness.yaml` at the site root pins the exact Hugo version; the harness downloads that extended-edition release on first use, sha256-verifies it against the official checksums file, caches it per-user, and runs it for the supervised preview. An explicit `hugo.binary` override serves air-gapped hosts. With a pin or override in effect there is no silent fallback to a system hugo — every failure refuses startup with an actionable error. ## How - **`internal/siteconfig`** — parses `harness.yaml`. Missing file = no pin; unknown keys tolerated (the file grows in later issues); malformed YAML or a non-exact version (`v`-prefix, ranges, `latest`) is a clear startup error naming the file. The pin stays a plain scalar at `hugo.version` so CI can extract it with `yq -r .hugo.version harness.yaml` (or the `sed` one-liner in the README). - **`internal/hugobin`** — the resolver contract: (pin/override, OS/arch) → ready-to-run binary + provenance. Strict order: `hugo.binary` (used exactly, validated, never downloads) → `hugo.version` (cache at `~/.cache/deckle/hugo/<version>/<os>-<arch>/hugo`, cold cache downloads + verifies + installs atomically via temp-file-then-rename) → `hugo` from `PATH`, logged as "no pin in effect". Reusable by pre-flight builds (#10) and `init` scaffolding (#17). Download/verify logic is fully injectable (base URL, HTTP client, cache dir, OS/arch) — tests run offline against fake archives, including checksum-mismatch, missing-asset, and cache-reuse (zero requests when warm) paths. - **`cmd/harness` + `internal/server`** — `serve` resolves once at startup and hands the path to the supervisor; `GET /api/status` gains additive `hugo.version` / `hugo.source` (`pinned` / `binary` / `path`) fields — all pre-existing status fields unchanged. The UI status pill shows the active version and provenance. ## Verified - Full suites green: 17/17 frontend tests, all Go packages (`go vet`, `gofmt` clean, `CGO_ENABLED=0 go test ./...`). - End-to-end against real releases: cold start downloaded and verified 0.148.1, warm restart reused the cache with zero network, switching the pin to 0.147.9 downloaded and ran the new version (visible in `/api/status`), preview served on both. - Failure probes: missing override path, malformed `harness.yaml`, `version: "latest"`, and a nonexistent pin (`99.99.99` → checksums 404) all refuse startup with errors naming the file/pin and the `hugo.binary` escape hatch; no-pin sites run `hugo` from `PATH` exactly as before, with provenance `path`. Closes #5 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01AbjCbYy6BR3osKsdPqo32X
feat: pin Hugo versions per site via harness.yaml with verified auto-download
All checks were successful
ci / build-and-test (pull_request) Successful in 57s
128e26e468
harness.yaml at the site root pins the exact Hugo version the harness
runs (issue #5). Resolution is strict and never falls back silently:

- internal/siteconfig parses harness.yaml (lenient to unknown keys,
  exact x.y.z pins only, malformed files are clear startup errors)
- internal/hugobin resolves the binary: an explicit hugo.binary
  override wins (air-gapped escape hatch, never downloads), then a
  pinned version — downloaded extended-edition from official releases,
  sha256-verified against the release checksums file, and installed
  atomically into a per-user cache reused across runs and sites — and
  only with no pin at all does it fall back to hugo from PATH,
  logged as such
- any failure under a pin or override (download error, checksum
  mismatch, missing asset, bad override path) refuses startup with an
  actionable error naming the pin and the escape hatch
- GET /api/status gains additive hugo.version / hugo.source fields
  (frozen fields unchanged) and the UI status pill shows the active
  version and provenance; changing the pin and restarting switches
  versions visibly
- the pin is a plain scalar extractable by CI one-liners (yq/sed),
  documented in the README

Closes #5

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbjCbYy6BR3osKsdPqo32X
Merge remote-tracking branch 'origin/main' into afk/5
All checks were successful
ci / build-and-test (pull_request) Successful in 1m5s
51747d4290
# Conflicts:
#	internal/server/server.go
#	internal/server/server_test.go
#	web/src/test/App.test.tsx
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle!22
No description provided.