Hugo version pinning: pin in harness.yaml, auto-download, CI honors it #5

Closed
opened 2026-07-11 12:59:19 +02:00 by dominik.polakovics · 1 comment

What to build

Deterministic Hugo versions per site (docs/DECISIONS.md D13, D25): harness.yaml at the repo root carries the pinned Hugo version; the tool downloads that exact binary (with checksum verification) into a cache on first run and uses it for the preview server and all builds. The scaffolded CI workflow (later issue) reads the same pin — structure the pin so a workflow can extract it.

Acceptance criteria

  • harness.yaml is parsed; hugo.version selects the binary used for hugo server and pre-flight builds
  • Missing binary is downloaded for the current OS/arch, checksum-verified, cached outside the repo, and reused across runs
  • Download failure or checksum mismatch produces a clear error and refuses to fall back silently to a system hugo
  • An explicit hugo.binary path override in harness.yaml bypasses download (air-gapped hosts)
  • Changing the pin and restarting switches versions; the active version is visible in the UI/status endpoint

Blocked by

## What to build Deterministic Hugo versions per site (docs/DECISIONS.md D13, D25): `harness.yaml` at the repo root carries the pinned Hugo version; the tool downloads that exact binary (with checksum verification) into a cache on first run and uses it for the preview server and all builds. The scaffolded CI workflow (later issue) reads the same pin — structure the pin so a workflow can extract it. ## Acceptance criteria - [ ] `harness.yaml` is parsed; `hugo.version` selects the binary used for `hugo server` and pre-flight builds - [ ] Missing binary is downloaded for the current OS/arch, checksum-verified, cached outside the repo, and reused across runs - [ ] Download failure or checksum mismatch produces a clear error and refuses to fall back silently to a system hugo - [ ] An explicit `hugo.binary` path override in harness.yaml bypasses download (air-gapped hosts) - [ ] Changing the pin and restarting switches versions; the active version is visible in the UI/status endpoint ## Blocked by - #1
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: enhancement
Summary: Deterministic Hugo versions per site: a harness.yaml at the site root pins the Hugo version; the harness downloads exactly that binary (checksum-verified) into a per-user cache on first run and uses it for the supervised preview server, with an explicit hugo.binary override for air-gapped hosts and no silent fallback to a system Hugo when a pin exists.

Dependency note: this issue is hard-blocked by #1 (walking skeleton), which is merged: it provides the supervisor whose configuration already accepts a Hugo binary path (defaulting to hugo on PATH). Beyond that this is fully independent Go-side work — no dependency on #2, #3, or #4, and it can run concurrently with them. The issue body's docs/DECISIONS.md D13/D25 references point at a file that is not committed — this brief is self-contained.

Current behavior:
The supervisor launches whatever binary its config names, defaulting to hugo on PATH; the CLI never sets it, so every run uses the machine's system Hugo. No harness.yaml exists and nothing parses one. Which Hugo version a site is edited and previewed with is whatever happens to be installed — unmanaged drift between machines and CI. The status endpoint says nothing about the Hugo version in use.

Desired behavior:

harness.yaml. Read from the root of the site directory the harness is pointed at (for serve, the --site directory — the root of the site's repo). Shape (exact key names below are the contract; the file will grow more keys in later issues, so unknown keys must be tolerated):

hugo:
  version: "0.148.1"          # exact version, no ranges
  # binary: /usr/local/bin/hugo  # optional: explicit path, bypasses download

The pin must be trivially extractable by a CI workflow with a one-liner (yq, or even grep) — keep it a plain scalar at a stable key path, and note the extraction path in the README. A malformed harness.yaml is a clear startup error naming the file and the problem, never silently ignored.

Resolution order. Exactly one binary is chosen at startup, and the harness logs which one and why:

  1. hugo.binary set — use exactly that path; verify it exists and is executable; never download. This is the air-gapped escape hatch.
  2. hugo.version set — use the cached binary for that version, downloading it first if missing (below).
  3. No harness.yaml, or no pin in it — today's behavior: hugo from PATH. This keeps existing sites working; the log states that no pin is in effect.

Download and cache. When a pinned version is not cached: download the official release artifact for the pinned version matching the current OS/arch, extended edition (the edition this project targets — if no extended artifact exists for the pinned version/platform, fail with a clear error). Verify the artifact against the checksums published for that release before trusting it; on mismatch, delete the artifact and fail loudly. Extract the binary into a cache outside the repo in the per-user cache location, keyed at least by version/OS/arch, so it is reused across runs and across sites pinning the same version. Writes into the cache are atomic (download/extract to a temp path, rename into place) so an interrupted run never leaves a corrupt entry that later runs would trust.

No silent fallback. When a pin or override exists, any failure — network error, missing release asset, checksum mismatch, override path missing or not executable — refuses to start the preview with a different Hugo. The error must be actionable (what was pinned, what failed, and that hugo.binary exists as an offline escape hatch). Falling back quietly to a system hugo is explicitly forbidden.

Visibility. The active Hugo version and how it was resolved (pinned download / explicit binary / PATH) are exposed on the status endpoint — as additive fields; the existing status fields are a frozen contract with the frontend and must not change shape — and surfaced in the shell UI (e.g. alongside the existing status pill). Changing the pin in harness.yaml and restarting the harness switches versions, visibly.

Key interfaces:

  • harness.yaml config shape as above. Put its parsing in a package reusable by later issues (init scaffolding in #17 writes this file; pre-flight builds in #10 must resolve the same binary) rather than inside the CLI entry point.
  • A resolver contract along the lines of: (site config, OS/arch) → absolute path to a ready-to-run Hugo binary, or an error. The supervisor stays ignorant of downloads — it already takes a binary path; the resolver supplies it.
  • Download/verify logic testable without network and without real 100MB artifacts: inject the fetch (e.g. an http.Client/URL base or a fetcher interface) so tests can serve fake archives and fake checksum files, including the mismatch path.
  • Status endpoint: additive Hugo-version/provenance fields on the existing Hugo status object; frontend types extended to match.

Acceptance criteria:

  • harness.yaml at the site root is parsed; hugo.version selects the binary used to launch the supervised hugo server
  • A missing pinned binary is downloaded for the current OS/arch (extended edition), checksum-verified, cached outside the repo, and reused: a second start with a warm cache performs no download
  • Download failure, missing release asset, or checksum mismatch produces a clear, actionable error and the harness refuses to fall back to a system hugo
  • hugo.binary in harness.yaml bypasses download entirely and uses exactly that path; a missing/non-executable override path is a clear error
  • With no harness.yaml or no pin, behavior is unchanged (hugo from PATH) and the log says so
  • Changing the pin and restarting switches versions; the active version and its provenance are visible via the status endpoint and in the shell UI
  • The pin is a plain scalar a CI workflow can extract with a one-liner; the README documents harness.yaml and the extraction
  • A malformed harness.yaml is a clear startup error naming the problem
  • Tests cover resolution order, cache reuse, checksum mismatch, and the no-fallback guarantee without hitting the network

Out of scope:

  • Scaffolding harness.yaml and the customer-site CI workflow that reads the pin (#17) — this issue only structures the pin so that workflow can read it
  • Pre-flight publish builds (#10) — but the resolver must be reusable by them
  • Any other harness.yaml keys or a general site-config system — only the hugo section
  • Version ranges, "latest", or auto-upgrade semantics — exact pins only
  • Managing any toolchain other than Hugo (Node, Go modules, Hugo modules)
  • Changing supervisor behavior (restart/backoff/error surfacing) beyond which binary it launches
> *This was generated by AI during triage.* ## Agent Brief **Category:** enhancement **Summary:** Deterministic Hugo versions per site: a `harness.yaml` at the site root pins the Hugo version; the harness downloads exactly that binary (checksum-verified) into a per-user cache on first run and uses it for the supervised preview server, with an explicit `hugo.binary` override for air-gapped hosts and no silent fallback to a system Hugo when a pin exists. **Dependency note:** this issue is **hard-blocked by #1** (walking skeleton), which is merged: it provides the supervisor whose configuration already accepts a Hugo binary path (defaulting to `hugo` on `PATH`). Beyond that this is fully independent Go-side work — no dependency on #2, #3, or #4, and it can run concurrently with them. The issue body's `docs/DECISIONS.md` D13/D25 references point at a file that is not committed — this brief is self-contained. **Current behavior:** The supervisor launches whatever binary its config names, defaulting to `hugo` on `PATH`; the CLI never sets it, so every run uses the machine's system Hugo. No `harness.yaml` exists and nothing parses one. Which Hugo version a site is edited and previewed with is whatever happens to be installed — unmanaged drift between machines and CI. The status endpoint says nothing about the Hugo version in use. **Desired behavior:** *`harness.yaml`.* Read from the root of the site directory the harness is pointed at (for `serve`, the `--site` directory — the root of the site's repo). Shape (exact key names below are the contract; the file will grow more keys in later issues, so unknown keys must be tolerated): ```yaml hugo: version: "0.148.1" # exact version, no ranges # binary: /usr/local/bin/hugo # optional: explicit path, bypasses download ``` The pin must be trivially extractable by a CI workflow with a one-liner (`yq`, or even grep) — keep it a plain scalar at a stable key path, and note the extraction path in the README. A malformed `harness.yaml` is a clear startup error naming the file and the problem, never silently ignored. *Resolution order.* Exactly one binary is chosen at startup, and the harness logs which one and why: 1. `hugo.binary` set — use exactly that path; verify it exists and is executable; never download. This is the air-gapped escape hatch. 2. `hugo.version` set — use the cached binary for that version, downloading it first if missing (below). 3. No `harness.yaml`, or no pin in it — today's behavior: `hugo` from `PATH`. This keeps existing sites working; the log states that no pin is in effect. *Download and cache.* When a pinned version is not cached: download the official release artifact for the pinned version matching the current OS/arch, extended edition (the edition this project targets — if no extended artifact exists for the pinned version/platform, fail with a clear error). Verify the artifact against the checksums published for that release before trusting it; on mismatch, delete the artifact and fail loudly. Extract the binary into a cache **outside the repo** in the per-user cache location, keyed at least by version/OS/arch, so it is reused across runs and across sites pinning the same version. Writes into the cache are atomic (download/extract to a temp path, rename into place) so an interrupted run never leaves a corrupt entry that later runs would trust. *No silent fallback.* When a pin or override exists, any failure — network error, missing release asset, checksum mismatch, override path missing or not executable — refuses to start the preview with a different Hugo. The error must be actionable (what was pinned, what failed, and that `hugo.binary` exists as an offline escape hatch). Falling back quietly to a system `hugo` is explicitly forbidden. *Visibility.* The active Hugo version and how it was resolved (pinned download / explicit binary / PATH) are exposed on the status endpoint — as additive fields; the existing status fields are a frozen contract with the frontend and must not change shape — and surfaced in the shell UI (e.g. alongside the existing status pill). Changing the pin in `harness.yaml` and restarting the harness switches versions, visibly. **Key interfaces:** - `harness.yaml` config shape as above. Put its parsing in a package reusable by later issues (`init` scaffolding in #17 writes this file; pre-flight builds in #10 must resolve the same binary) rather than inside the CLI entry point. - A resolver contract along the lines of: (site config, OS/arch) → absolute path to a ready-to-run Hugo binary, or an error. The supervisor stays ignorant of downloads — it already takes a binary path; the resolver supplies it. - Download/verify logic testable without network and without real 100MB artifacts: inject the fetch (e.g. an `http.Client`/URL base or a fetcher interface) so tests can serve fake archives and fake checksum files, including the mismatch path. - Status endpoint: additive Hugo-version/provenance fields on the existing Hugo status object; frontend types extended to match. **Acceptance criteria:** - [ ] `harness.yaml` at the site root is parsed; `hugo.version` selects the binary used to launch the supervised `hugo server` - [ ] A missing pinned binary is downloaded for the current OS/arch (extended edition), checksum-verified, cached outside the repo, and reused: a second start with a warm cache performs no download - [ ] Download failure, missing release asset, or checksum mismatch produces a clear, actionable error and the harness refuses to fall back to a system `hugo` - [ ] `hugo.binary` in `harness.yaml` bypasses download entirely and uses exactly that path; a missing/non-executable override path is a clear error - [ ] With no `harness.yaml` or no pin, behavior is unchanged (`hugo` from `PATH`) and the log says so - [ ] Changing the pin and restarting switches versions; the active version and its provenance are visible via the status endpoint and in the shell UI - [ ] The pin is a plain scalar a CI workflow can extract with a one-liner; the README documents `harness.yaml` and the extraction - [ ] A malformed `harness.yaml` is a clear startup error naming the problem - [ ] Tests cover resolution order, cache reuse, checksum mismatch, and the no-fallback guarantee without hitting the network **Out of scope:** - Scaffolding `harness.yaml` and the customer-site CI workflow that reads the pin (#17) — this issue only structures the pin so that workflow *can* read it - Pre-flight publish builds (#10) — but the resolver must be reusable by them - Any other `harness.yaml` keys or a general site-config system — only the `hugo` section - Version ranges, "latest", or auto-upgrade semantics — exact pins only - Managing any toolchain other than Hugo (Node, Go modules, Hugo modules) - Changing supervisor behavior (restart/backoff/error surfacing) beyond which binary it launches
Sign in to join this conversation.
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#5
No description provided.