REPO_POLICIES.md: "ALL external references must be pinned by cryptographic
hash. This includes Docker base images, Go modules, npm packages, GitHub
Actions, and anything else fetched from a remote source… There are zero
exceptions to this rule."
The Dockerfile's base image is correctly pinned:
# alpine 3.21, 2026-02-28
FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
But the very next thing it does is RUN script/bootstrap, which fetches
packages over the network at build time:
apk) apk add --no-cache "$4";;
for git, make, hugo, nodejs, and npm. Those resolve against alpine's
package repository whenever the build runs. Pinning the base image layer does
not pin what gets installed into it — the digest fixes the starting filesystem,
not the packages fetched afterward. So the actual toolchain that builds and
publishes lora.vegas is unpinned, which is the exact class of exposure the
policy exists to close.
The same applies to the other three package managers script/bootstrap
supports (nix-env -iA, apt-get install -y, brew install) and to script/fmt/script/fmt-check, which fetch prettier at run time via npx --yes prettier@3.4.2 — that one at least pins an exact version, though not a
hash.
How this surfaced
Found while reviewing #7 / PR #17. It predates that PR and was explicitly kept
out of its scope. PR #17 is about to add nodejs and git to the deploy
workflow's build container via apk add, which widens the same unpinned
surface — another reason to settle the policy here.
A second, related consequence
Alpine 3.21's package repository serves Hugo 0.139.0. Current Hugo is
0.164.0. So CI is green on a Hugo roughly two years behind, and #18 documents a
deprecation warning that current Hugo emits but CI cannot see. Whatever is
decided here should also decide how the Hugo version gets chosen deliberately
rather than inherited from whichever alpine release the base digest happens to
be.
This needs a decision before implementation
There is no clean, low-cost way to hash-pin apk packages, and the options
differ a lot in cost and durability. I do not think an implementer should pick
one unilaterally.
Option A — pin apk package versions (not hashes).apk add hugo=0.139.0-r0 etc. Cheap, one line per package. Weaknesses: alpine garbage
collects old package versions from the repo, so builds break when a pinned
version is dropped; and a version is not a hash, so this is compliance in
spirit, not to the letter.
Option B — install Hugo from a hash-verified upstream release archive. This
is the pattern REPO_POLICIES.md already prescribes for exactly this problem:
"download a specific release archive from GitHub, verify its hash (hardcoded in
the Dockerfile or script), and only then install." Fully compliant, pins the
Hugo version deliberately, and fixes the stale-Hugo problem in the same stroke.
Costs: more code in script/bootstrap, and the hash must be updated by hand on
every Hugo bump. Would apply to Hugo specifically; git/make/nodejs would
still come from apk.
Option C — build a base image once, pin it by digest, and use it
everywhere. Move the whole toolchain install into a separately built and
published image, then reference it by digest from both the Dockerfile and the
workflows. Strongest guarantee and the fastest CI, but it introduces a second
artifact to build, publish, and keep current — real infrastructure for a
five-page static site.
Option D — accept the gap and record it. Document in REPO_POLICIES.md (or
a repo-local note) that distro packages atop a pinned base image are considered
acceptable, and close this. Legitimate if the judgement is that a pinned alpine
digest plus a trusted distro repo is enough. It should be an explicit recorded
decision rather than a silent gap.
My recommendation: Option B for Hugo, Option D for the rest. Hugo is the
tool that actually produces the published artifact and the one whose version
drift is already causing problems (#18), so it earns a real pin. git, make,
and nodejs are build-time conveniences whose behaviour does not affect
output, and chasing hashes for them is poor value. That combination is
defensible, cheap, and fixes the stale-Hugo issue.
Definition of done (once a direction is chosen)
The chosen approach is implemented in script/bootstrap, covering the apk
path at minimum; state explicitly whether the nix/apt/brew paths are also
covered or deliberately left out.
If a hash is hardcoded, it is accompanied by a # <name> <version>, YYYY-MM-DD comment in the canonical format, and the download fails closed
on hash mismatch.
The Hugo version used by CI is stated explicitly somewhere in the repo
rather than being an emergent property of the base image.
If Option D, the decision and its rationale are written down; this issue is
closed referencing that text.
make check passes and script/cibuild succeeds.
TODO.md updated.
Explicitly out of scope
Bumping the Hugo version itself as a behavioural change. If moving off
0.139.0 changes rendered output, that belongs in its own issue with its own
before/after verification.
#18 (the languageCode deprecation) — related, tracked separately.
## Problem
`REPO_POLICIES.md`: "**ALL external references must be pinned by cryptographic
hash.** This includes Docker base images, Go modules, npm packages, GitHub
Actions, and anything else fetched from a remote source… There are zero
exceptions to this rule."
The `Dockerfile`'s base image is correctly pinned:
```
# alpine 3.21, 2026-02-28
FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
```
But the very next thing it does is `RUN script/bootstrap`, which fetches
packages over the network at build time:
```sh
apk) apk add --no-cache "$4" ;;
```
for `git`, `make`, `hugo`, `nodejs`, and `npm`. Those resolve against alpine's
package repository whenever the build runs. Pinning the base image layer does
not pin what gets installed into it — the digest fixes the starting filesystem,
not the packages fetched afterward. So the actual toolchain that builds and
publishes lora.vegas is unpinned, which is the exact class of exposure the
policy exists to close.
The same applies to the other three package managers `script/bootstrap`
supports (`nix-env -iA`, `apt-get install -y`, `brew install`) and to
`script/fmt`/`script/fmt-check`, which fetch prettier at run time via `npx
--yes prettier@3.4.2` — that one at least pins an exact version, though not a
hash.
## How this surfaced
Found while reviewing #7 / PR #17. It predates that PR and was explicitly kept
out of its scope. PR #17 is about to add `nodejs` and `git` to the deploy
workflow's build container via `apk add`, which widens the same unpinned
surface — another reason to settle the policy here.
## A second, related consequence
Alpine 3.21's package repository serves **Hugo 0.139.0**. Current Hugo is
0.164.0. So CI is green on a Hugo roughly two years behind, and #18 documents a
deprecation warning that current Hugo emits but CI cannot see. Whatever is
decided here should also decide how the Hugo version gets chosen deliberately
rather than inherited from whichever alpine release the base digest happens to
be.
## This needs a decision before implementation
There is no clean, low-cost way to hash-pin apk packages, and the options
differ a lot in cost and durability. I do not think an implementer should pick
one unilaterally.
**Option A — pin apk package versions (not hashes).** `apk add
hugo=0.139.0-r0` etc. Cheap, one line per package. Weaknesses: alpine garbage
collects old package versions from the repo, so builds break when a pinned
version is dropped; and a version is not a hash, so this is compliance in
spirit, not to the letter.
**Option B — install Hugo from a hash-verified upstream release archive.** This
is the pattern `REPO_POLICIES.md` already prescribes for exactly this problem:
"download a specific release archive from GitHub, verify its hash (hardcoded in
the Dockerfile or script), and only then install." Fully compliant, pins the
Hugo version deliberately, and fixes the stale-Hugo problem in the same stroke.
Costs: more code in `script/bootstrap`, and the hash must be updated by hand on
every Hugo bump. Would apply to Hugo specifically; `git`/`make`/`nodejs` would
still come from apk.
**Option C — build a base image once, pin it by digest, and use it
everywhere.** Move the whole toolchain install into a separately built and
published image, then reference it by digest from both the `Dockerfile` and the
workflows. Strongest guarantee and the fastest CI, but it introduces a second
artifact to build, publish, and keep current — real infrastructure for a
five-page static site.
**Option D — accept the gap and record it.** Document in `REPO_POLICIES.md` (or
a repo-local note) that distro packages atop a pinned base image are considered
acceptable, and close this. Legitimate if the judgement is that a pinned alpine
digest plus a trusted distro repo is enough. It should be an explicit recorded
decision rather than a silent gap.
**My recommendation: Option B for Hugo, Option D for the rest.** Hugo is the
tool that actually produces the published artifact and the one whose version
drift is already causing problems (#18), so it earns a real pin. `git`, `make`,
and `nodejs` are build-time conveniences whose behaviour does not affect
output, and chasing hashes for them is poor value. That combination is
defensible, cheap, and fixes the stale-Hugo issue.
## Definition of done (once a direction is chosen)
1. The chosen approach is implemented in `script/bootstrap`, covering the apk
path at minimum; state explicitly whether the nix/apt/brew paths are also
covered or deliberately left out.
2. If a hash is hardcoded, it is accompanied by a `# <name> <version>,
YYYY-MM-DD` comment in the canonical format, and the download fails closed
on hash mismatch.
3. The Hugo version used by CI is stated explicitly somewhere in the repo
rather than being an emergent property of the base image.
4. If Option D, the decision and its rationale are written down; this issue is
closed referencing that text.
5. `make check` passes and `script/cibuild` succeeds.
6. `TODO.md` updated.
## Explicitly out of scope
- Bumping the Hugo version itself as a behavioural change. If moving off
0.139.0 changes rendered output, that belongs in its own issue with its own
before/after verification.
- #18 (the `languageCode` deprecation) — related, tracked separately.
Assigning to @sneak for the direction.
sneak
was assigned by clawbot2026-08-09 04:02:08 +02:00
Decision from @sneak: apk does not need pinning. Closing as Option D —
accepted and recorded.
This overrides my recommendation, which was Option B (hash-verified upstream
Hugo release archive) for Hugo with Option D for the rest. Recording the
decision as given rather than the one I proposed.
What is now settled policy for this repo
Distro packages installed on top of a digest-pinned base image are acceptable
and are not treated as a violation of the "ALL external references must be
pinned by cryptographic hash" rule. Concretely, script/bootstrap may continue
to install git, make, hugo, nodejs, and npm via apk add (and via
the equivalent nix/apt/brew paths) without version or hash pinning.
The reasoning that supports this, for anyone reading later: the base image is
pinned by digest, the package repository is the distribution's own signed
index, and apk verifies package signatures against keys baked into that pinned
image. That is a materially different trust position from curl | bash or an
unpinned third-party container tag, which is what the policy rule is actually
aimed at. The policy's own worked example — "download a specific release
archive from GitHub, verify its hash, and only then install" — is about
untrusted third-party artifacts, not distribution packages.
No code change is required. Nothing to implement.
One consequence that remains open, deliberately not reopened here
Accepting unpinned apk means the Hugo version is an inherited variable, not a
chosen one. Alpine 3.21 currently serves Hugo 0.139.0; upstream current is
0.164.0. CI therefore builds the site on a Hugo roughly two years behind
whatever a developer runs locally.
That is not a pinning problem and is out of scope for this decision, but it has
one live effect already tracked: #18 exists because a current Hugo emits a languageCode deprecation warning that CI's older Hugo cannot see. So the repo
can be green in CI while a developer sees warnings locally, and the reverse is
also possible — a future Hugo could change rendering in a way CI would not
catch until the base image moves.
I am not filing a follow-up for this unless you want one. If you would like
the Hugo version to be a deliberate choice rather than a side effect of which
alpine digest is pinned, say so and I will file it; it would be a small issue
(pin a Hugo version explicitly, or bump the base image on a schedule). Left
alone otherwise — this is exactly the kind of thing that should not be
manufactured into work without being asked for.
Closing.
**Decision from @sneak: apk does not need pinning. Closing as Option D —
accepted and recorded.**
This overrides my recommendation, which was Option B (hash-verified upstream
Hugo release archive) for Hugo with Option D for the rest. Recording the
decision as given rather than the one I proposed.
## What is now settled policy for this repo
Distro packages installed on top of a digest-pinned base image are acceptable
and are **not** treated as a violation of the "ALL external references must be
pinned by cryptographic hash" rule. Concretely, `script/bootstrap` may continue
to install `git`, `make`, `hugo`, `nodejs`, and `npm` via `apk add` (and via
the equivalent nix/apt/brew paths) without version or hash pinning.
The reasoning that supports this, for anyone reading later: the base image is
pinned by digest, the package repository is the distribution's own signed
index, and apk verifies package signatures against keys baked into that pinned
image. That is a materially different trust position from `curl | bash` or an
unpinned third-party container tag, which is what the policy rule is actually
aimed at. The policy's own worked example — "download a specific release
archive from GitHub, verify its hash, and only then install" — is about
untrusted third-party artifacts, not distribution packages.
No code change is required. Nothing to implement.
## One consequence that remains open, deliberately not reopened here
Accepting unpinned apk means **the Hugo version is an inherited variable, not a
chosen one.** Alpine 3.21 currently serves Hugo 0.139.0; upstream current is
0.164.0. CI therefore builds the site on a Hugo roughly two years behind
whatever a developer runs locally.
That is not a pinning problem and is out of scope for this decision, but it has
one live effect already tracked: **#18** exists because a current Hugo emits a
`languageCode` deprecation warning that CI's older Hugo cannot see. So the repo
can be green in CI while a developer sees warnings locally, and the reverse is
also possible — a future Hugo could change rendering in a way CI would not
catch until the base image moves.
I am **not** filing a follow-up for this unless you want one. If you would like
the Hugo version to be a deliberate choice rather than a side effect of which
alpine digest is pinned, say so and I will file it; it would be a small issue
(pin a Hugo version explicitly, or bump the base image on a schedule). Left
alone otherwise — this is exactly the kind of thing that should not be
manufactured into work without being asked for.
Closing.
Follow-up decision from @sneak, narrowing this issue's resolution: the Hugo
version must be deliberate — container or go install by hash, not distro
Hugo.
This issue's outcome stands for git, make, nodejs, and npm: apk on a
digest-pinned base is fine for build-time conveniences. Hugo is carved out,
because it produces the published artifact, so its version is a property of the
output rather than of the build environment.
Filed as #26, which also records that standard (non-extended) Hugo is
sufficient here — verified: no SCSS, no asset pipeline, no image processing;
CSS is inlined via readFile. That matters because it makes go install
viable at all, since Hugo extended requires CGO.
Follow-up decision from @sneak, narrowing this issue's resolution: **the Hugo
version must be deliberate — container or `go install` by hash, not distro
Hugo.**
This issue's outcome stands for `git`, `make`, `nodejs`, and `npm`: apk on a
digest-pinned base is fine for build-time conveniences. Hugo is carved out,
because it produces the published artifact, so its version is a property of the
output rather than of the build environment.
Filed as **#26**, which also records that standard (non-extended) Hugo is
sufficient here — verified: no SCSS, no asset pipeline, no image processing;
CSS is inlined via `readFile`. That matters because it makes `go install`
viable at all, since Hugo extended requires CGO.
This issue stays closed; #26 carries the work.
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.
Problem
REPO_POLICIES.md: "ALL external references must be pinned by cryptographichash. This includes Docker base images, Go modules, npm packages, GitHub
Actions, and anything else fetched from a remote source… There are zero
exceptions to this rule."
The
Dockerfile's base image is correctly pinned:But the very next thing it does is
RUN script/bootstrap, which fetchespackages over the network at build time:
for
git,make,hugo,nodejs, andnpm. Those resolve against alpine'spackage repository whenever the build runs. Pinning the base image layer does
not pin what gets installed into it — the digest fixes the starting filesystem,
not the packages fetched afterward. So the actual toolchain that builds and
publishes lora.vegas is unpinned, which is the exact class of exposure the
policy exists to close.
The same applies to the other three package managers
script/bootstrapsupports (
nix-env -iA,apt-get install -y,brew install) and toscript/fmt/script/fmt-check, which fetch prettier at run time vianpx --yes prettier@3.4.2— that one at least pins an exact version, though not ahash.
How this surfaced
Found while reviewing #7 / PR #17. It predates that PR and was explicitly kept
out of its scope. PR #17 is about to add
nodejsandgitto the deployworkflow's build container via
apk add, which widens the same unpinnedsurface — another reason to settle the policy here.
A second, related consequence
Alpine 3.21's package repository serves Hugo 0.139.0. Current Hugo is
0.164.0. So CI is green on a Hugo roughly two years behind, and #18 documents a
deprecation warning that current Hugo emits but CI cannot see. Whatever is
decided here should also decide how the Hugo version gets chosen deliberately
rather than inherited from whichever alpine release the base digest happens to
be.
This needs a decision before implementation
There is no clean, low-cost way to hash-pin apk packages, and the options
differ a lot in cost and durability. I do not think an implementer should pick
one unilaterally.
Option A — pin apk package versions (not hashes).
apk add hugo=0.139.0-r0etc. Cheap, one line per package. Weaknesses: alpine garbagecollects old package versions from the repo, so builds break when a pinned
version is dropped; and a version is not a hash, so this is compliance in
spirit, not to the letter.
Option B — install Hugo from a hash-verified upstream release archive. This
is the pattern
REPO_POLICIES.mdalready prescribes for exactly this problem:"download a specific release archive from GitHub, verify its hash (hardcoded in
the Dockerfile or script), and only then install." Fully compliant, pins the
Hugo version deliberately, and fixes the stale-Hugo problem in the same stroke.
Costs: more code in
script/bootstrap, and the hash must be updated by hand onevery Hugo bump. Would apply to Hugo specifically;
git/make/nodejswouldstill come from apk.
Option C — build a base image once, pin it by digest, and use it
everywhere. Move the whole toolchain install into a separately built and
published image, then reference it by digest from both the
Dockerfileand theworkflows. Strongest guarantee and the fastest CI, but it introduces a second
artifact to build, publish, and keep current — real infrastructure for a
five-page static site.
Option D — accept the gap and record it. Document in
REPO_POLICIES.md(ora repo-local note) that distro packages atop a pinned base image are considered
acceptable, and close this. Legitimate if the judgement is that a pinned alpine
digest plus a trusted distro repo is enough. It should be an explicit recorded
decision rather than a silent gap.
My recommendation: Option B for Hugo, Option D for the rest. Hugo is the
tool that actually produces the published artifact and the one whose version
drift is already causing problems (#18), so it earns a real pin.
git,make,and
nodejsare build-time conveniences whose behaviour does not affectoutput, and chasing hashes for them is poor value. That combination is
defensible, cheap, and fixes the stale-Hugo issue.
Definition of done (once a direction is chosen)
script/bootstrap, covering the apkpath at minimum; state explicitly whether the nix/apt/brew paths are also
covered or deliberately left out.
# <name> <version>, YYYY-MM-DDcomment in the canonical format, and the download fails closedon hash mismatch.
rather than being an emergent property of the base image.
closed referencing that text.
make checkpasses andscript/cibuildsucceeds.TODO.mdupdated.Explicitly out of scope
0.139.0 changes rendered output, that belongs in its own issue with its own
before/after verification.
languageCodedeprecation) — related, tracked separately.Assigning to @sneak for the direction.
Decision from @sneak: apk does not need pinning. Closing as Option D —
accepted and recorded.
This overrides my recommendation, which was Option B (hash-verified upstream
Hugo release archive) for Hugo with Option D for the rest. Recording the
decision as given rather than the one I proposed.
What is now settled policy for this repo
Distro packages installed on top of a digest-pinned base image are acceptable
and are not treated as a violation of the "ALL external references must be
pinned by cryptographic hash" rule. Concretely,
script/bootstrapmay continueto install
git,make,hugo,nodejs, andnpmviaapk add(and viathe equivalent nix/apt/brew paths) without version or hash pinning.
The reasoning that supports this, for anyone reading later: the base image is
pinned by digest, the package repository is the distribution's own signed
index, and apk verifies package signatures against keys baked into that pinned
image. That is a materially different trust position from
curl | bashor anunpinned third-party container tag, which is what the policy rule is actually
aimed at. The policy's own worked example — "download a specific release
archive from GitHub, verify its hash, and only then install" — is about
untrusted third-party artifacts, not distribution packages.
No code change is required. Nothing to implement.
One consequence that remains open, deliberately not reopened here
Accepting unpinned apk means the Hugo version is an inherited variable, not a
chosen one. Alpine 3.21 currently serves Hugo 0.139.0; upstream current is
0.164.0. CI therefore builds the site on a Hugo roughly two years behind
whatever a developer runs locally.
That is not a pinning problem and is out of scope for this decision, but it has
one live effect already tracked: #18 exists because a current Hugo emits a
languageCodedeprecation warning that CI's older Hugo cannot see. So the repocan be green in CI while a developer sees warnings locally, and the reverse is
also possible — a future Hugo could change rendering in a way CI would not
catch until the base image moves.
I am not filing a follow-up for this unless you want one. If you would like
the Hugo version to be a deliberate choice rather than a side effect of which
alpine digest is pinned, say so and I will file it; it would be a small issue
(pin a Hugo version explicitly, or bump the base image on a schedule). Left
alone otherwise — this is exactly the kind of thing that should not be
manufactured into work without being asked for.
Closing.
Follow-up decision from @sneak, narrowing this issue's resolution: the Hugo
version must be deliberate — container or
go installby hash, not distroHugo.
This issue's outcome stands for
git,make,nodejs, andnpm: apk on adigest-pinned base is fine for build-time conveniences. Hugo is carved out,
because it produces the published artifact, so its version is a property of the
output rather than of the build environment.
Filed as #26, which also records that standard (non-extended) Hugo is
sufficient here — verified: no SCSS, no asset pipeline, no image processing;
CSS is inlined via
readFile. That matters because it makesgo installviable at all, since Hugo extended requires CGO.
This issue stays closed; #26 carries the work.