From @sneak: the Hugo version must be deliberate. Use a container or go install by hash. Do not use distro Hugo.
This narrows the resolution of #19. That issue settled that apk packages on a
digest-pinned base are acceptable for build-time conveniences, and that stands
for git, make, nodejs, and npm. Hugo is different: it is the tool that
produces the published artifact, so its version is a property of the output,
not of the build environment.
Current state
script/bootstrap line 67:
if missing hugo;then pkg_install hugo hugo hugo hugo;fi
Whatever the distro serves. On the pinned alpine 3.21 that is Hugo
0.139.0+extended; upstream current is 0.164.0. So the site is built by a
Hugo roughly two years behind, chosen by nobody, and it changes silently
whenever the base image digest is bumped.
It already has a visible effect: #18 exists because a current Hugo emits a languageCode deprecation warning that CI's 0.139.0 cannot see. CI is green on
a Hugo old enough not to know about the deprecation while a developer with a
current Hugo sees it locally.
Extended is not required — verified
Checked before specifying options, because it determines whether go install
is viable at all (Hugo extended needs CGO):
No SCSS/Sass anywhere: no .scss/.sass files, no resources.ToCSS, no
PostCSS.
CSS is delivered by {{ readFile "themes/loravega/static/css/style.css" | safeCSS }} in baseof.html — plain CSS inlined, no asset pipeline.
No image processing (.Resize/.Fill/.Fit/images.* all absent).
So standard Hugo is sufficient. The current image happening to carry +extended is incidental, not a requirement. Note this explicitly in the PR so
nobody later assumes extended is needed.
Options
Option A — go install at a pinned version (recommended). In script/bootstrap:
go install github.com/gohugoio/hugo@vX.Y.Z
Go verifies the module against sum.golang.org, so this is genuinely
hash-verified rather than version-pinned — it is the mechanism REPO_POLICIES.md already names for Go dependencies, and it satisfies "by
hash" without hand-maintaining a sha256. Costs: script/bootstrap gains a Go
toolchain (itself from apk, which #19 permits), and the build compiles Hugo
rather than fetching a binary, which will add time to a cold build. Set CGO_ENABLED=0 explicitly since extended is not needed.
Option B — a Hugo container pinned by digest. Reference an official/known
Hugo image by @sha256:... with the canonical # name version, YYYY-MM-DD
comment, and use it as a build stage or as the workflow container. Strongest
supply-chain position and no compile cost. Costs: it reintroduces a second
pinned base image alongside the alpine one — which is exactly what #7 removed
when it dropped klakegg/hugo:ext-alpine — and the deploy build job and the Dockerfile would need to agree on it.
My recommendation: Option A. It keeps one pinned base image, uses the
checksum-database mechanism the policy already prefers, and makes the Hugo
version a single visible constant in script/bootstrap. Option B is defensible
if cold-build time turns out to matter, but reintroducing a second image is a
real cost and #7 was specifically about consolidating onto one.
Either way, the chosen Hugo version must appear as an explicit, commented
constant, not be implied.
Definition of done
script/bootstrap installs a specific, explicitly-stated Hugo version by a
hash-verified mechanism. No pkg_install hugo.
The version is stated as a constant with a # hugo vX.Y.Z, YYYY-MM-DD
comment. hugo version in the built image reports exactly it.
Non-extended is used deliberately, with a comment recording that no SCSS or
image processing exists in this site.
Rendered output is byte-identical. Build public/ before and after and
diff it. Any difference must be explained and justified in the PR, not
waved through — this is a two-year version jump.
make check and script/cibuild pass, with the check layer confirmed
genuinely executed and not CACHED (see #23).
Cold-build time is reported. If Option A pushes the Docker build near the
5-minute policy budget, say so.
TODO.md updated in the same commit.
Critical — this touches the live deploy path
.gitea/workflows/deploy.yml's build job runs script/bootstrap inside the
pinned alpine container. Changing how Hugo is installed changes the
production deploy path. This repo has already taken one outage from a deploy.yml change that passed two adversarial reviews (#7), and the only
thing that actually caught it was making the job executable pre-merge.
So this work must use the same approach:
Add a temporary branch trigger to deploy.yml's on.push.branches so the build job really executes under act_runner.
The if: github.ref_name == 'main' guard on the deploy job is already in
place and must stay, so branch runs cannot reach Cloudflare.
Read the result from the commit-status API
(/api/v1/repos/sneak/lora.vegas/commits/<sha>/status) — readable by clawbot even though the Actions jobs/logs API 403s.
Iterate until the build job is green on the branch.
Remove the temporary trigger in the final commit.
Confirm no functional change to deploy.yml between the runner-verified
commit and the merge candidate. "It passed on the branch" and "it will pass
on main" are different claims.
A green script/cibuild does not cover this: docker build runs RUN
under /bin/sh with no Actions runtime.
Sequencing
Moving to a current Hugo will surface #18's languageCode deprecation
warning in CI, where it is currently invisible. Either fix#18 in the same
branch or land #18 first; do not merge this and leave CI newly noisy.
#13 (taxonomy warning) is unaffected but is the other outstanding warning.
#25 (--panicOnWarning) must come after this, #18, and #13, or the gate
goes red immediately.
Interacts with #23: verifying anything here needs a genuinely-executed check
layer.
Out of scope
Any rendering or content change. If the version jump changes output, that is
a finding to report, not something to accommodate by editing content.
Changing how git, make, nodejs, or npm are installed — #19 settled
that apk is fine for those.
## Decision
From @sneak: **the Hugo version must be deliberate. Use a container or `go
install` by hash. Do not use distro Hugo.**
This narrows the resolution of #19. That issue settled that `apk` packages on a
digest-pinned base are acceptable for build-time conveniences, and that stands
for `git`, `make`, `nodejs`, and `npm`. Hugo is different: it is the tool that
produces the published artifact, so its version is a property of the output,
not of the build environment.
## Current state
`script/bootstrap` line 67:
```sh
if missing hugo; then pkg_install hugo hugo hugo hugo; fi
```
Whatever the distro serves. On the pinned alpine 3.21 that is **Hugo
0.139.0+extended**; upstream current is **0.164.0**. So the site is built by a
Hugo roughly two years behind, chosen by nobody, and it changes silently
whenever the base image digest is bumped.
It already has a visible effect: **#18** exists because a current Hugo emits a
`languageCode` deprecation warning that CI's 0.139.0 cannot see. CI is green on
a Hugo old enough not to know about the deprecation while a developer with a
current Hugo sees it locally.
## Extended is not required — verified
Checked before specifying options, because it determines whether `go install`
is viable at all (Hugo extended needs CGO):
- No SCSS/Sass anywhere: no `.scss`/`.sass` files, no `resources.ToCSS`, no
PostCSS.
- CSS is delivered by `{{ readFile "themes/loravega/static/css/style.css" |
safeCSS }}` in `baseof.html` — plain CSS inlined, no asset pipeline.
- No image processing (`.Resize`/`.Fill`/`.Fit`/`images.*` all absent).
So **standard Hugo is sufficient**. The current image happening to carry
`+extended` is incidental, not a requirement. Note this explicitly in the PR so
nobody later assumes extended is needed.
## Options
**Option A — `go install` at a pinned version (recommended).** In
`script/bootstrap`:
```sh
go install github.com/gohugoio/hugo@vX.Y.Z
```
Go verifies the module against `sum.golang.org`, so this is genuinely
hash-verified rather than version-pinned — it is the mechanism
`REPO_POLICIES.md` already names for Go dependencies, and it satisfies "by
hash" without hand-maintaining a sha256. Costs: `script/bootstrap` gains a Go
toolchain (itself from apk, which #19 permits), and the build compiles Hugo
rather than fetching a binary, which will add time to a cold build. Set
`CGO_ENABLED=0` explicitly since extended is not needed.
**Option B — a Hugo container pinned by digest.** Reference an official/known
Hugo image by `@sha256:...` with the canonical `# name version, YYYY-MM-DD`
comment, and use it as a build stage or as the workflow container. Strongest
supply-chain position and no compile cost. Costs: it reintroduces a second
pinned base image alongside the alpine one — which is exactly what #7 removed
when it dropped `klakegg/hugo:ext-alpine` — and the deploy build job and the
`Dockerfile` would need to agree on it.
**My recommendation: Option A.** It keeps one pinned base image, uses the
checksum-database mechanism the policy already prefers, and makes the Hugo
version a single visible constant in `script/bootstrap`. Option B is defensible
if cold-build time turns out to matter, but reintroducing a second image is a
real cost and #7 was specifically about consolidating onto one.
Either way, **the chosen Hugo version must appear as an explicit, commented
constant**, not be implied.
## Definition of done
1. `script/bootstrap` installs a specific, explicitly-stated Hugo version by a
hash-verified mechanism. No `pkg_install hugo`.
2. The version is stated as a constant with a `# hugo vX.Y.Z, YYYY-MM-DD`
comment. `hugo version` in the built image reports exactly it.
3. Non-extended is used deliberately, with a comment recording that no SCSS or
image processing exists in this site.
4. **Rendered output is byte-identical.** Build `public/` before and after and
diff it. Any difference must be explained and justified in the PR, not
waved through — this is a two-year version jump.
5. `make check` and `script/cibuild` pass, with the check layer confirmed
genuinely executed and not `CACHED` (see #23).
6. Cold-build time is reported. If Option A pushes the Docker build near the
5-minute policy budget, say so.
7. `TODO.md` updated in the same commit.
## Critical — this touches the live deploy path
`.gitea/workflows/deploy.yml`'s `build` job runs `script/bootstrap` inside the
pinned alpine container. **Changing how Hugo is installed changes the
production deploy path.** This repo has already taken one outage from a
`deploy.yml` change that passed two adversarial reviews (#7), and the only
thing that actually caught it was making the job executable pre-merge.
So this work **must** use the same approach:
- Add a temporary branch trigger to `deploy.yml`'s `on.push.branches` so the
`build` job really executes under `act_runner`.
- The `if: github.ref_name == 'main'` guard on the `deploy` job is already in
place and must stay, so branch runs cannot reach Cloudflare.
- Read the result from the commit-status API
(`/api/v1/repos/sneak/lora.vegas/commits/<sha>/status`) — readable by
`clawbot` even though the Actions jobs/logs API 403s.
- Iterate until the `build` job is green on the branch.
- Remove the temporary trigger in the final commit.
- Confirm no functional change to `deploy.yml` between the runner-verified
commit and the merge candidate. "It passed on the branch" and "it will pass
on main" are different claims.
A green `script/cibuild` does **not** cover this: `docker build` runs `RUN`
under `/bin/sh` with no Actions runtime.
## Sequencing
- Moving to a current Hugo will **surface #18's `languageCode` deprecation
warning in CI**, where it is currently invisible. Either fix #18 in the same
branch or land #18 first; do not merge this and leave CI newly noisy.
- #13 (taxonomy warning) is unaffected but is the other outstanding warning.
- #25 (`--panicOnWarning`) must come **after** this, #18, and #13, or the gate
goes red immediately.
- Interacts with #23: verifying anything here needs a genuinely-executed check
layer.
## Out of scope
- Any rendering or content change. If the version jump changes output, that is
a finding to report, not something to accommodate by editing content.
- Changing how `git`, `make`, `nodejs`, or `npm` are installed — #19 settled
that apk is fine for those.
Taking Option A (go install), as recommended. #18 rides in the same
branch, committed on top, per its second sequencing comment.
Version: hugo v0.164.0 (2026-07-06) — the current stable release, per proxy.golang.org/github.com/gohugoio/hugo/@latest.
One thing the issue body did not anticipate
hugo v0.164.0's go.mod declares go 1.26.0. Alpine 3.21 ships go
1.23.9, and the alpine go package builds with GOTOOLCHAIN=local, so a
bare go install fails outright:
github.com/gohugoio/hugo@v0.164.0 requires go >= 1.26.0
(running go 1.23.9; GOTOOLCHAIN=local)
Fix: name the toolchain explicitly as a second commented constant
(GOTOOLCHAIN=go1.26.5). Go then fetches golang.org/toolchain through the
module proxy and verifies it against sum.golang.org exactly like any other
module, so this stays hash-verified end to end and the Go version becomes
deliberate too rather than inherited from the base image. Measured cold: apk add go 4s + go install 41s.
script/bootstrap changes
Two commented constants: HUGO_VERSION and HUGO_GOTOOLCHAIN, each with
the canonical # name version, YYYY-MM-DD form.
pkg_install hugo hugo hugo hugo is replaced by: install a Go toolchain
from the package manager (#19 permits apk for build-time conveniences), then CGO_ENABLED=0 GOTOOLCHAIN=... go install github.com/gohugoio/hugo@$HUGO_VERSION
into a temp GOBIN, then install -m 0755 the binary into /usr/local/bin.
Why /usr/local/bin and not the default GOPATH/bin: the Dockerfile's RUN make check and deploy.yml's script/test step each start a fresh
shell, so an install location that is not already on the default PATH
would not be found. HUGO_BIN_DIR stays overridable for unprivileged
installs.
CGO_ENABLED=0 explicitly, with a comment recording the verified finding
that standard (non-extended) Hugo is sufficient for this site: no .scss/.sass, no resources.ToCSS, no PostCSS, no image processing; CSS
is inlined via readFile in baseof.html. The +extended on the current
apk build is incidental.
The idempotency guard becomes version-aware rather than missing hugo — an old hugo already on PATH must be replaced, not
accepted.
Verification
Build public/ three ways and diff: (a) main + apk hugo 0.139.0, (b) new
hugo 0.164.0 + unchanged hugo.toml, (c) 0.164.0 + locale. (b) isolates
the version jump from the config change. Any difference gets reported and
justified, not accommodated.
RSS <language> asserted to still read en-us on the Hugo the build
actually uses.
deploy.yml gets a temporary branch trigger so the build job really runs
under act_runner; results read from the commit-status API; the trigger is
removed in the final commit and the deploy job's if: github.ref_name == 'main' guard stays untouched throughout.
Cold script/cibuild time reported, with the check layer confirmed
genuinely executed rather than CACHED (#23).
Two commits, one per issue.
## Implementation plan
Taking **Option A** (`go install`), as recommended. #18 rides in the same
branch, committed on top, per its second sequencing comment.
**Version: hugo v0.164.0 (2026-07-06)** — the current stable release, per
`proxy.golang.org/github.com/gohugoio/hugo/@latest`.
### One thing the issue body did not anticipate
`hugo v0.164.0`'s `go.mod` declares `go 1.26.0`. Alpine 3.21 ships **go
1.23.9**, and the alpine `go` package builds with `GOTOOLCHAIN=local`, so a
bare `go install` fails outright:
github.com/gohugoio/hugo@v0.164.0 requires go >= 1.26.0
(running go 1.23.9; GOTOOLCHAIN=local)
Fix: name the toolchain explicitly as a second commented constant
(`GOTOOLCHAIN=go1.26.5`). Go then fetches `golang.org/toolchain` through the
module proxy and verifies it against `sum.golang.org` exactly like any other
module, so this stays hash-verified end to end and the Go version becomes
deliberate too rather than inherited from the base image. Measured cold:
`apk add go` 4s + `go install` 41s.
### `script/bootstrap` changes
- Two commented constants: `HUGO_VERSION` and `HUGO_GOTOOLCHAIN`, each with
the canonical `# name version, YYYY-MM-DD` form.
- `pkg_install hugo hugo hugo hugo` is replaced by: install a Go toolchain
from the package manager (#19 permits apk for build-time conveniences), then
`CGO_ENABLED=0 GOTOOLCHAIN=... go install github.com/gohugoio/hugo@$HUGO_VERSION`
into a temp `GOBIN`, then `install -m 0755` the binary into
`/usr/local/bin`.
- Why `/usr/local/bin` and not the default `GOPATH/bin`: the `Dockerfile`'s
`RUN make check` and `deploy.yml`'s `script/test` step each start a **fresh
shell**, so an install location that is not already on the default `PATH`
would not be found. `HUGO_BIN_DIR` stays overridable for unprivileged
installs.
- `CGO_ENABLED=0` explicitly, with a comment recording the verified finding
that **standard (non-extended) Hugo is sufficient** for this site: no
`.scss`/`.sass`, no `resources.ToCSS`, no PostCSS, no image processing; CSS
is inlined via `readFile` in `baseof.html`. The `+extended` on the current
apk build is incidental.
- The idempotency guard becomes version-aware rather than
`missing hugo` — an old hugo already on `PATH` must be replaced, not
accepted.
### Verification
- Build `public/` three ways and diff: (a) `main` + apk hugo 0.139.0, (b) new
hugo 0.164.0 + unchanged `hugo.toml`, (c) 0.164.0 + `locale`. (b) isolates
the version jump from the config change. Any difference gets reported and
justified, not accommodated.
- RSS `<language>` asserted to still read `en-us` on the Hugo the build
actually uses.
- `deploy.yml` gets a temporary branch trigger so the `build` job really runs
under `act_runner`; results read from the commit-status API; the trigger is
removed in the final commit and the `deploy` job's
`if: github.ref_name == 'main'` guard stays untouched throughout.
- Cold `script/cibuild` time reported, with the check layer confirmed
genuinely executed rather than `CACHED` (#23).
Two commits, one per issue.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Decision
From @sneak: the Hugo version must be deliberate. Use a container or
go installby hash. Do not use distro Hugo.This narrows the resolution of #19. That issue settled that
apkpackages on adigest-pinned base are acceptable for build-time conveniences, and that stands
for
git,make,nodejs, andnpm. Hugo is different: it is the tool thatproduces the published artifact, so its version is a property of the output,
not of the build environment.
Current state
script/bootstrapline 67:Whatever the distro serves. On the pinned alpine 3.21 that is Hugo
0.139.0+extended; upstream current is 0.164.0. So the site is built by a
Hugo roughly two years behind, chosen by nobody, and it changes silently
whenever the base image digest is bumped.
It already has a visible effect: #18 exists because a current Hugo emits a
languageCodedeprecation warning that CI's 0.139.0 cannot see. CI is green ona Hugo old enough not to know about the deprecation while a developer with a
current Hugo sees it locally.
Extended is not required — verified
Checked before specifying options, because it determines whether
go installis viable at all (Hugo extended needs CGO):
.scss/.sassfiles, noresources.ToCSS, noPostCSS.
{{ readFile "themes/loravega/static/css/style.css" | safeCSS }}inbaseof.html— plain CSS inlined, no asset pipeline..Resize/.Fill/.Fit/images.*all absent).So standard Hugo is sufficient. The current image happening to carry
+extendedis incidental, not a requirement. Note this explicitly in the PR sonobody later assumes extended is needed.
Options
Option A —
go installat a pinned version (recommended). Inscript/bootstrap:Go verifies the module against
sum.golang.org, so this is genuinelyhash-verified rather than version-pinned — it is the mechanism
REPO_POLICIES.mdalready names for Go dependencies, and it satisfies "byhash" without hand-maintaining a sha256. Costs:
script/bootstrapgains a Gotoolchain (itself from apk, which #19 permits), and the build compiles Hugo
rather than fetching a binary, which will add time to a cold build. Set
CGO_ENABLED=0explicitly since extended is not needed.Option B — a Hugo container pinned by digest. Reference an official/known
Hugo image by
@sha256:...with the canonical# name version, YYYY-MM-DDcomment, and use it as a build stage or as the workflow container. Strongest
supply-chain position and no compile cost. Costs: it reintroduces a second
pinned base image alongside the alpine one — which is exactly what #7 removed
when it dropped
klakegg/hugo:ext-alpine— and the deploy build job and theDockerfilewould need to agree on it.My recommendation: Option A. It keeps one pinned base image, uses the
checksum-database mechanism the policy already prefers, and makes the Hugo
version a single visible constant in
script/bootstrap. Option B is defensibleif cold-build time turns out to matter, but reintroducing a second image is a
real cost and #7 was specifically about consolidating onto one.
Either way, the chosen Hugo version must appear as an explicit, commented
constant, not be implied.
Definition of done
script/bootstrapinstalls a specific, explicitly-stated Hugo version by ahash-verified mechanism. No
pkg_install hugo.# hugo vX.Y.Z, YYYY-MM-DDcomment.
hugo versionin the built image reports exactly it.image processing exists in this site.
public/before and after anddiff it. Any difference must be explained and justified in the PR, not
waved through — this is a two-year version jump.
make checkandscript/cibuildpass, with the check layer confirmedgenuinely executed and not
CACHED(see #23).5-minute policy budget, say so.
TODO.mdupdated in the same commit.Critical — this touches the live deploy path
.gitea/workflows/deploy.yml'sbuildjob runsscript/bootstrapinside thepinned alpine container. Changing how Hugo is installed changes the
production deploy path. This repo has already taken one outage from a
deploy.ymlchange that passed two adversarial reviews (#7), and the onlything that actually caught it was making the job executable pre-merge.
So this work must use the same approach:
deploy.yml'son.push.branchesso thebuildjob really executes underact_runner.if: github.ref_name == 'main'guard on thedeployjob is already inplace and must stay, so branch runs cannot reach Cloudflare.
(
/api/v1/repos/sneak/lora.vegas/commits/<sha>/status) — readable byclawboteven though the Actions jobs/logs API 403s.buildjob is green on the branch.deploy.ymlbetween the runner-verifiedcommit and the merge candidate. "It passed on the branch" and "it will pass
on main" are different claims.
A green
script/cibuilddoes not cover this:docker buildrunsRUNunder
/bin/shwith no Actions runtime.Sequencing
languageCodedeprecationwarning in CI, where it is currently invisible. Either fix #18 in the same
branch or land #18 first; do not merge this and leave CI newly noisy.
--panicOnWarning) must come after this, #18, and #13, or the gategoes red immediately.
layer.
Out of scope
a finding to report, not something to accommodate by editing content.
git,make,nodejs, ornpmare installed — #19 settledthat apk is fine for those.
Implementation plan
Taking Option A (
go install), as recommended. #18 rides in the samebranch, committed on top, per its second sequencing comment.
Version: hugo v0.164.0 (2026-07-06) — the current stable release, per
proxy.golang.org/github.com/gohugoio/hugo/@latest.One thing the issue body did not anticipate
hugo v0.164.0'sgo.moddeclaresgo 1.26.0. Alpine 3.21 ships go1.23.9, and the alpine
gopackage builds withGOTOOLCHAIN=local, so abare
go installfails outright:Fix: name the toolchain explicitly as a second commented constant
(
GOTOOLCHAIN=go1.26.5). Go then fetchesgolang.org/toolchainthrough themodule proxy and verifies it against
sum.golang.orgexactly like any othermodule, so this stays hash-verified end to end and the Go version becomes
deliberate too rather than inherited from the base image. Measured cold:
apk add go4s +go install41s.script/bootstrapchangesHUGO_VERSIONandHUGO_GOTOOLCHAIN, each withthe canonical
# name version, YYYY-MM-DDform.pkg_install hugo hugo hugo hugois replaced by: install a Go toolchainfrom the package manager (#19 permits apk for build-time conveniences), then
CGO_ENABLED=0 GOTOOLCHAIN=... go install github.com/gohugoio/hugo@$HUGO_VERSIONinto a temp
GOBIN, theninstall -m 0755the binary into/usr/local/bin./usr/local/binand not the defaultGOPATH/bin: theDockerfile'sRUN make checkanddeploy.yml'sscript/teststep each start a freshshell, so an install location that is not already on the default
PATHwould not be found.
HUGO_BIN_DIRstays overridable for unprivilegedinstalls.
CGO_ENABLED=0explicitly, with a comment recording the verified findingthat standard (non-extended) Hugo is sufficient for this site: no
.scss/.sass, noresources.ToCSS, no PostCSS, no image processing; CSSis inlined via
readFileinbaseof.html. The+extendedon the currentapk build is incidental.
missing hugo— an old hugo already onPATHmust be replaced, notaccepted.
Verification
public/three ways and diff: (a)main+ apk hugo 0.139.0, (b) newhugo 0.164.0 + unchanged
hugo.toml, (c) 0.164.0 +locale. (b) isolatesthe version jump from the config change. Any difference gets reported and
justified, not accommodated.
<language>asserted to still readen-uson the Hugo the buildactually uses.
deploy.ymlgets a temporary branch trigger so thebuildjob really runsunder
act_runner; results read from the commit-status API; the trigger isremoved in the final commit and the
deployjob'sif: github.ref_name == 'main'guard stays untouched throughout.script/cibuildtime reported, with the check layer confirmedgenuinely executed rather than
CACHED(#23).Two commits, one per issue.