Reference in New Issue
Block a user
Delete Branch "26-hugo-deliberate-version"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Two issues, one branch, because they cannot safely be separated — see #18's
second sequencing comment. Under the apk hugo 0.139.0 that CI ran until now,
localeis an unknown key that 0.139.0 silently ignores, downgrading thegenerated RSS from
<language>en-us</language>to<language>en</language>with no warning and exit 0. #18 therefore had toland after the Hugo version moved, in the same branch.
Commits
4720c40916f978languageCodebecomeslocale2a95023f7d6149deploy.ymlis byte-identical tomainagain#26 — how Hugo is installed
script/bootstrapdidpkg_install hugo hugo hugo hugo, so the tool thatproduces the published artifact was whatever the base image's package repo
served: hugo 0.139.0, about two years behind, chosen by nobody, and liable
to change silently on any base image digest bump.
Option A from the issue,
go install, which verifies the module againstsum.golang.org. This keeps a single pinned base image; a digest-pinned Hugocontainer would have reintroduced the second base image #7 deliberately
removed.
Two commented constants carry the decision:
HUGO_VERSION="v0.164.0"— the current stable release (2026-07-06).HUGO_GOTOOLCHAIN="go1.26.5".The toolchain constant is not decoration. hugo v0.164.0's
go.moddeclares
go 1.26.0, and alpine 3.21'sgopackage is 1.23.9 built withGOTOOLCHAIN=local, so a barego installrefuses to run at all:Naming the toolchain makes Go fetch it through the module proxy and verify it
against
sum.golang.orglike any other module, so the chain stayshash-verified end to end — and the compiler that builds Hugo becomes
deliberate too rather than inherited from the base image.
CGO_ENABLED=0is deliberate: standard Hugo, not extended. Confirmed no.scss/.sass, noresources.ToCSS, no PostCSS, and no image processing(
.Resize/.Fill/.Fit/images.*all absent); the CSS is plain and inlinedby
readFileinbaseof.html. The+extendedon the apk build this replaceswas incidental. The script says so in a comment so a later change does not
assume extended is needed without rechecking.
Two implementation details worth a reviewer's attention:
/usr/local/bin, not a GOPATH bin dir, because it mustbe on the default
PATHof a fresh shell — theDockerfile'sRUN make checkanddeploy.yml'sscript/teststep each start their own.HUGO_BIN_DIRis overridable for unprivileged installs, andgo installruns as the invoking user so a workstation's module cache is not populated
as root.
missing hugo: an olderhugo on
PATHhas to be replaced, not accepted, or the pin means nothing.A same-version
+extendedbuild is accepted since it renders this siteidentically. After installing, the script re-checks what
hugoonPATHactually resolves to and fails loudly if something shadows it.
Rendered output — the diff
Built
public/three ways in one container carrying both binaries, onidentical sources:
languageCode(whatmainbuilds today)languageCode(isolates the version jump)locale(isolates the config change)Across a two-year version jump the only byte that differs anywhere in
public/is:which is the change describing itself, and is not something to accommodate.
Everything else — including
index.xml,sitemap.xmland the minified CSS —is byte-identical.
#18 — the RSS regression, explicitly
cis the merged state, on the Hugo the build actually uses:<language>en-us</language>. The regression #18's second commentfound does not occur, because the key swap only ever meets a Hugo that
understands it.
b -> cbeing identical is the direct evidence that the swapis a pure no-op at 0.164.0.
The deprecation warning is gone:
grep -c deprecatedover the final checklayer output is
0.Verification on the real deploy path
script/cibuildcannot cover this —docker buildrunsRUNunder/bin/shwith no Actions runtime — so
deploy.ymlgot a temporary branch trigger andthe
buildjob really executed underact_runnerat2a95023:The
deployjob never ran: itsif: github.ref_name == 'main'guard held, sonothing reached Cloudflare. That guard was not touched.
2a95023is deliberately kept in this branch's history rather thanrebased away, so the required confirmation is independently checkable:
deploy.ymlin the merge candidate is byte-identical tomain's.Build cost
Cold
script/cibuild: 2m36s — 52.6s bootstrap layer (apk go, toolchainfetch, Hugo compile), 100s image export, rest negligible. Comfortably inside
the five-minute budget; the runner did the equivalent
checkjob in 1m8s.The check image grows to 683 MB because the Go toolchain and module cache
stay in the bootstrap layer. That image is only ever built to run checks — it
is never published or deployed — so this is not touched here. I did not
measure
main's image for a baseline number.Not verified / out of scope
blocks it off
main. Thebuildjob — which is everything up to andincluding artifact upload — did run green.
clawbotgets 403 there. Job outcomescome from the commit-status API; the layer-by-layer evidence above is from
local
docker buildruns.script/cibuildexited 0 in 3.1s withRUN script/bootstrapCACHED.That run is still meaningful because
RUN make checkshowsDONE 1.5s, notCACHED, and its output carrieshugo v0.164.0— but a green exit codealone would have proved nothing.
.dockerignore, and howgit,make,nodejsandnpmare installed (#19 settled that).Hugo deprecated the project config key `languageCode` in v0.158.0 in favour of `locale`, and says it will be removed. The preceding commit moves the build onto hugo v0.164.0, which emits: WARN deprecated: project config key languageCode was deprecated in Hugo v0.158.0 and will be removed in a future release. Use locale instead. Left alone that would be a third routinely-ignored warning in the build output alongside #13's taxonomy warning, and a latent breakage once the key is dropped. Sequencing matters and is why this rides in the same branch, on top of the version move rather than before it. Under the apk hugo 0.139.0 that CI ran until the preceding commit, `locale` is simply an unknown key: 0.139.0 ignores it and falls back, which downgrades the generated RSS from <language>en-us</language> to <language>en</language>. No warning, no error, exit 0 - an output regression the gate would not have caught. Landing this first would have broken the published feed. Verified on hugo v0.164.0, the version the build now actually uses: - the RSS <language> element still reads en-us; - the html lang attribute is unchanged; - public/ is byte-identical to the preceding commit's output, so the key swap is a pure no-op on rendered content; - the deprecation warning is gone from the build output.What this builds, and what actually proved it
#26 —
script/bootstrapno longer doespkg_install hugo. It installsgithub.com/gohugoio/hugo@v0.164.0withgo install(verified againstsum.golang.org) plus an explicitly namedGOTOOLCHAIN=go1.26.5, withoutwhich the install cannot run at all: hugo v0.164.0 needs go >= 1.26.0 and
alpine 3.21's go is 1.23.9 with
GOTOOLCHAIN=local.CGO_ENABLED=0,non-extended, deliberately and with the reasoning recorded in the script.
#18 —
hugo.toml'slanguageCodebecomeslocale, committed on topof the version move so the key never meets a Hugo that would silently ignore
it.
Verification, in order of how much it proves
1. The real deploy path, under
act_runner— the only thing that couldhave caught the class of failure that took this repo down in #7.
deploy.ymlcarried a temporary branch trigger at2a95023:That
buildjob ranscript/bootstrap— i.e.apk add go, the toolchainfetch, and the Hugo compile — inside the pinned alpine container, then
script/test, then the artifact upload.deploynever ran: theif: github.ref_name == 'main'guard held and nothing reached Cloudflare.The guard was not modified.
2a95023is still in this branch's history on purpose, so the requiredconfirmation is checkable by anyone:
2. Rendered output, three-way. One container carrying both binaries,
identical sources:
a= apk 0.139.0 +languageCode,b= 0.164.0 +languageCode,c= 0.164.0 +locale.Across a two-year version jump the only differing byte anywhere in
public/is
-<meta name=generator content="Hugo 0.139.0">/+<meta name=generator content="Hugo 0.164.0">.index.xml,sitemap.xml,the taxonomy feeds and the minified CSS are byte-identical. The RSS
<language>element readsen-usin the merged state — the regressionthat dictated the ordering does not occur.
b -> cbeing identical is thedirect evidence the key swap is a pure no-op at 0.164.0. The
languageCodedeprecation warning is gone (
grep -c deprecatedover the check layer output:0).3. Cold
script/cibuild: 2m36s — 52.6s bootstrap layer, 100s imageexport. Inside the five-minute budget.
RUN make checkshowedDONE, notCACHED, and its output carriedhugo v0.164.0.Caveats, stated plainly
checkrun on this PR's head (f7d6149) is nearly worthless asevidence: green in 10s, because the bootstrap layer was cached — #23
exactly as described. The load-bearing runs are the two above. I am pointing
at this rather than quoting the green tick.
clawbot; job outcomes are from the commit-statusAPI, and the layer-level evidence is from local
docker buildruns.bootstrap layer). It is only ever built to run checks, never published, so I
left it alone rather than expand scope.
main's image was not measured fora baseline.
Review: PASS
Independent review of PR #27 at
f7d6149. No blocking findings.Central claims, independently reproduced
GOFLAGS,GONOSUMDB,GOPRIVATE,GOSUMDB,GOINSECUREorGONOSUMCHECK. In the pinned alpine digest:GOSUMDB=sum.golang.org,GOPROXY=proxy.golang.org,direct, all the disabling vars empty, nogo/envfile. Satisfies #26 DoD 1.GOTOOLCHAINaddition is verified too — confirmed directly, not taken on assertion. After an uncached build, the image's module cache containscache/download/sumdb/sum.golang.org/lookup/golang.org/toolchain@v0.0.1-go1.26.5.linux-amd64alongsidegolang.org/toolchain/@v/v0.0.1-go1.26.5.linux-amd64.ziphash. The toolchain was fetched as an ordinary module and checked against the checksum database. No supply-chain hole.<meta name=generator content="Hugo 0.164.0">; b→c byte-identical across the wholepublic/tree. RSS<language>en-us</language>in a, b and c — the #18 regression does not occur in the merged state;<html lang=en>unchanged. Zero deprecation lines; the only remaining warning is #13's taxonomy warning.deploy.ymlidentity holds.git diff main HEAD -- .gitea/workflows/deploy.ymlis empty;git diff 2a95023 HEADshows only the temporary trigger entry and its comment. Theif: github.ref_name == 'main'guard is intact at head. This is the check that mattered on PR #22.2a95023:buildsuccess 1m1s,deployskipped,checksuccess 1m8s. Headf7d6149:checksuccess in 10s — cache-served and worthless as evidence, as the author states.docker build --no-cacheon a throwaway tag: 2m26s (corroborates the 2m36s figure, inside the 5-minute budget), exactly oneCACHEDlayer and it isWORKDIR /src.RUN script/bootstrapDONE 49.9s,RUN make checkDONE 1.5s withhugo v0.164.0 linux/amd64in its output andAll matched files use Prettier code style!./usr/local/binis on a freshsh's default PATH ahead of/usr/bin. Re-runningscript/bootstrapin the built image is a 0.04s no-op (version-aware guard is genuinely idempotent). Planting a fakehugo v0.139.0in/usr/local/sbin(earlier in PATH) makes the post-install re-check fire: clear diagnostic to stderr, exit 1. It is not decorative.Also checked and clean: two commits, one per issue, each ending
(closes #N);TODO.mdupdated inside both; exactly 5 files touched, no debris;make fmtclean; no scope creep beyond the smalldetect_sudoextraction, which the new non-apt install path requires; naming and shell idiom consistent with the script; inclusive terminology; fast-forwardable ontomainwith no conflict; no Claude/Anthropic reference or attribution trailer anywhere in tree, diff, commit messages, or PR body.Non-blocking notes
docker image inspectgives 683 MB at head, matching the PR exactly; the baseline the author did not measure is 60 MB onmain. However thedocker imagessize column reports 2.25 GB head vs 84.4 MB main, anddocker historyattributes 1.56 GB to the bootstrap layer alone. Whichever metric the runner's disk actually pays, the check image grows 11x-27x. Not a defect — the image is never published or deployed and the policy budget is on build time — but "683 MB" is the friendliest of the available numbers and worth knowing before someone is surprised by runner disk.script/bootstrap(and commit4720c40's message) saysgo install"is the mechanism REPO_POLICIES.md already names for Go".REPO_POLICIES.mddoes not exist in this repo — adding it is literally the currentTODO.mdNext Step. The claim is true of the policy, but a reader following the pointer finds nothing.proxy.golang.orgwhere it previously depended on an alpine apk mirror. Equivalent in kind, not obviously worse, and it ran green on the real runner — but a proxy outage now breaks deploys, which was not previously true. Noting, not objecting.go1.23.9, which bootstraps the verifiedgo1.26.5. Repo-wide gap already tracked in #19 and acknowledged indeploy.yml's own comment; out of scope here, but a supply-chain PR should say so out loud.TODO.md's Workflow rotation (Next Step to Completed, pull from Future) was not performed — the Next Step entry is preserved unchanged. Correct given this work was issue-driven rather than TODO-driven; noted only so it is not mistaken for an omission.Nothing in the PR body is oversold. Every load-bearing figure I could check reproduced, and the two weak pieces of evidence — the 10s head
checkrun and the never-executed Cloudflare deploy — are disclosed by the author before a reviewer has to find them.Not verified: the Cloudflare Pages deploy itself, by design. Out of scope per instructions: #13, #16, #23, #25, #8's
.dockerignore.PASS accepted.
merge-ready, merging directly —mainis unprotected, so thisdoes not go to @sneak.
The check that mattered: the
GOTOOLCHAINaddition was scope this issue didnot anticipate, and a downloaded compiler that was not itself hash-verified
would have been a supply-chain hole introduced by a supply-chain fix. The
reviewer confirmed it from the module cache —
sumdb/sum.golang.org/lookup/ golang.org/toolchain@v0.0.1-go1.26.5.linux-amd64with matching.ziphash—rather than accepting the assertion. Chain holds.
Three follow-ups, none blocking this merge:
docker image inspectagainst a 60 MB baseline,but
docker imagesreports 2.25 GB vs 84.4 MB anddocker historyputs1.56 GB in the bootstrap layer. 11x–27x depending on metric. The image is
never published and the policy budget is on build time (2m26s cold, well
inside 5 minutes), so this does not block — but 683 MB was the friendliest
available number and should not be the one that gets quoted. Filing.
proxy.golang.orgwhere they previously depended on an alpine apk mirror.Equivalent in kind, but a proxy outage now breaks deploys, which was not
true before. Worth stating plainly since this is a supply-chain change.
script/bootstrapand commit4720c40citeREPO_POLICIES.md, which doesnot exist in this repo yet. Dangling pointer; #8 adds the file and resolves
it.
Also noted: the trust root still includes alpine's unpinned apk
go1.23.9bootstrapping the verified
go1.26.5. Out of scope per #19's resolution, butcorrect to have surfaced in a supply-chain PR.
TODO.md's Workflow rotation was deliberately not performed — correct forissue-driven work, recorded so it is not read as an omission.