Skip to content

Resolve the base image at build time instead of pinning a version - #4

Merged
eculver merged 5 commits into
masterfrom
eculver/resolve-base-image-at-build-time
Oct 21, 2026
Merged

eculver merged 5 commits into
masterfrom
eculver/resolve-base-image-at-build-time

Conversation

@eculver

@eculver eculver commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Context

#2 shipped build.pkr.hcl with a hardcoded release tag and a hardcoded image name. Both are versions somebody has to remember to bump: the tag goes stale after every release, and the name leaves latest.json whenever the matrix moves (it is derived as the two newest Ubuntu LTS by the two newest CUDA, so names drop out by construction). This removes both and adds CI.

What changed

The template resolves the base image at build time. Packer's http data source fetches latest.json, or <tag>/manifest.json when base_image_tag is set. The index lists each image's sha256 beside its URL, so iso_checksum comes from the same document as iso_url and the two cannot disagree. That also deletes the file:SHA256SUMS indirection and the base_image_checksum variable with its =none escape hatch, which existed only because a pinned tag could predate the checksum file.

Empty base_image_name means the newest CUDA for ubuntu_codename. Candidates are the release's base-variant images for that codename; the newest CUDA wins, ranked numerically so 13.10 outranks 13.9. ubuntu_codename therefore applies to both starting points. An explicit name still wins. An unknown name or codename fails before any download.

Pinning stays available and is opt-in. -var base_image_tag=<tag> fixes the release. The README says plainly that tracking latest means next month's rebuild starts from a different base, and that pinning does not make the build bit-for-bit reproducible since customize.sh still installs whatever the archive has that day. The index carries only the current tag and the release repo is private, so the only source of an earlier tag is a previous build's output/sfc_base/manifest.json, which records the tag, image name, URL and sha256 that were resolved.

The output file is a fixed sfc-base-custom.raw. With the image chosen at build time, a filename that followed it would make the README's upload command wrong for anyone who changed a variable.

CI (.github/workflows/ci.yml): packer fmt -check, packer validate -evaluate-datasources and shellcheck on pull requests, pushes to master, and daily. The daily run matters because the template resolves the index at build time, so the repo can stop building without a commit. GitHub disables schedules in a public repo after 60 days without activity; the workflow comment records that, and the release pipeline in sfc_base_image will build this example on every release, which is immune to it.

Rough edge, documented

Plain packer validate does not execute data sources, so it fails with invalid checksum: encoding/hex: invalid byte: U+003C '<', which does not sound like the real problem. The flag is packer validate -evaluate-datasources .; the README's quick start and troubleshooting table both say so, and CI uses it. packer build needs no flag. Fixing this at the source would mean a CDN redirect in front of the index, which was considered and declined.

Verification

Resolution checked against the live index by starting builds and reading the URL Packer chose:

Inputs Resolved
none v20261008.012133/ubuntu-24.04-cuda-13.4
ubuntu_codename=resolute v20261008.012133/ubuntu-26.04-cuda-13.4
base_image_name=ubuntu-26.04-cuda-13.3 that image
base_image_tag=v20260930.063326 v20260930.063326/ubuntu-24.04-cuda-13.2, the newest 24.04 in that release
unknown name or codename Call to function "max" failed: must pass at least one number, before any download

Ranking checked against a crafted local index: on a CUDA tie the base variant wins over a -minimal one, 13.10 outranks 13.9, and a bare 14 resolves.

Real build of the final revision on Linux with KVM (Packer 1.15.3, QEMU 8.2), no variables passed:

Trying https://tiny-llama.sfcc.xyz/v20261008.012133/ubuntu-24.04-cuda-13.4.qcow2
==> Removing build-time SSH access
==> Resetting per-machine identity
==> Image finalized
Build 'qemu.sfc_base' finished after 4 minutes 592 milliseconds.

qemu-img info: file format: raw, virtual size: 20 GiB, 7.02 GiB on disk. manifest.json records tag v20261008.012133, image ubuntu-24.04-cuda-13.4 and the matching URL and sha256.

Every error string in the troubleshooting table was reproduced verbatim. packer fmt -check, packer validate -evaluate-datasources, actionlint and shellcheck are clean, and CI passed on the final commit.

An adversarial review of the whole branch produced eight confirmed findings, all addressed: the false "tags are listed by the index" claim, variant ties on list order, a minor-less cuda_version, a CI OVMF step that tested nothing and was the likeliest to flake, missing permissions, the 60-day schedule rule, a README example that was itself a version in disguise, and what removing the base-image path involves.

Two remaining example tags

One in the base_image_tag description showing the tag format, and one in the sample of Packer's output above. Neither is copy-pasteable into a build; the one command a reader would copy uses a <tag from latest.json> placeholder.

🤖 Generated with Claude Code

The template hard-coded a release tag, so the repo carried a version
somebody had to remember to bump, and every day after a release it
pointed at something stale.

Packer has a built-in http data source, and sfc_base_image already
publishes a release index, so the template can just ask: latest.json
for the current release, <tag>/manifest.json for a specific one. No
version in the repo.

The index lists each image's sha256 beside its URL, which turns out to
be worth more than dropping the pin. iso_checksum now comes from the
same document as iso_url, so the two cannot disagree, and the
file:SHA256SUMS indirection is gone along with the whole
base_image_checksum variable and its "=none" escape hatch -- which
existed only because a pinned tag could predate the checksum file.

Pinning stays available and is the documented answer for anyone who
needs two builds to produce the same bytes: -var base_image_tag=<tag>.
Tracking latest means next month's rebuild starts from a different
base, so the README says that plainly rather than leaving it implied.

One rough edge, documented in the troubleshooting table: plain
`packer validate` does not execute data sources, so it now fails with
`invalid checksum: encoding/hex: invalid byte: U+003C '<'`. The flag
is `packer validate -evaluate-datasources`. The other two new failure
modes are in the table too -- a tag with no index 404s from the data
source, and an unknown base_image_name fails on an empty collection
rather than downloading the wrong thing.

Verified on Linux with KVM. No tag passed, resolved the current
release and enforced its digest:

  Trying .../v20260930.063326/ubuntu-24.04-cuda-13.2.qcow2
    ?checksum=sha256%3Aac30efbe2de11060802b12fa0f27e81a316f49d814ba...
  Build 'qemu.sfc_base' finished after 3 minutes 55 seconds.

That build is also the first to inherit the base image's sshd
hardening, which only became possible with the release cut today:

  ubuntu hardening inherited: PasswordAuthentication no
  build override: removed
  root: root:!:...  random-seed: GONE

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@semanticdiff-com

semanticdiff-com Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Review changes with  SemanticDiff

Changed Files
File Status
  .github/workflows/ci.yml  0% smaller
  README.md Unsupported file format
  build.pkr.hcl Unsupported file format
  cloud-init/user-data Unsupported file format

eculver and others added 4 commits October 2, 2026 10:13
An end-user review walked the README literally. Four things it says are
wrong, and in one case following it leaves a template that will not
load.

The -only warning was false. Packer exits 1 on a mistyped -only and
lists the valid build names; it does not, as this README claimed,
silently succeed having run nothing. That claim came from testing
`packer validate`, which does behave that way, and generalizing to
`packer build` without checking. Quick start spent a paragraph telling
people to audit their terminal for a non-event, and the troubleshooting
table taught a diagnosis for a symptom nobody will see.

"Making it yours" told readers to delete three things to drop the
stock-Ubuntu path, and omitted the fourth: the "source.qemu.ubuntu"
entry in the build block's sources list. Doing exactly what it said
produced `Error: Unknown source qemu.ubuntu`, with no row in the
troubleshooting table to explain it. The README broke the template and
then offered no way out. Now four bullets, re-tested by following it to
the letter.

"Pin the release if you need two builds to produce the same bytes" was
false in a way that mattered, because it was the stated reason to keep
pinning at all: customize.sh runs apt-get install, so pinning the base
image does not pin the Ubuntu archive. Now describes what pinning
actually buys -- a repeatable starting point, same driver, CUDA and
kernel -- and says plainly that it is not bit-for-bit.

The two network failures the new resolver introduces were undocumented
and are the ones a laptop hits: DNS/offline, and a captive portal whose
HTML lands in a jsondecode error. Both now have rows, as does the case
where a 404 means the index is missing rather than a bad pin -- the old
row told people to drop a flag they never passed.

Also: builds now write output/sfc_base/manifest.json recording the
release they resolved, which is what makes pinning usable (build once,
read the tag, pin it). The output filename tracks base_image_name, which
the upload command silently contradicted. The stock-Ubuntu path now says
where its artifact lands. A qemu-img check before pushing 20 GiB. The
quoted sample output was not what Packer prints. The CDN host is named
as ours once, rather than appearing as a bare domain beside a sha256 the
reader is asked to trust. "Image requirements" moved after the
troubleshooting table, since it opens by addressing readers who are not
following this README.

Not taken: a request_timeout on the data source -- not a supported
argument in Packer 1.15, verified. -var accelerator=hvf for Intel Macs
is probably a large win there but is untested, so it stays unwritten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
base_image_name defaulted to ubuntu-24.04-cuda-13.2, which was a
version in disguise: the release matrix is two newest LTS by two newest
CUDA, so the name drops out of latest.json the next time CUDA moves and
the default build fails on an empty lookup. A hardcoded tag was removed
for the same reason.

Empty base_image_name now means the image with the newest CUDA for
ubuntu_codename, ranked numerically so 13.10 would outrank 13.9.
ubuntu_codename therefore applies to both starting points. An explicit
name still wins, and the pinned-tag path is unchanged.

The output file is now a fixed sfc-base-custom.raw. With the image
chosen at build time, a filename that followed it would make the upload
command in the README wrong for anyone who changed a variable;
manifest.json already records what was picked.

Verified against the live index by starting builds and reading the URL
Packer chose: the default resolves ubuntu-24.04-cuda-13.4 from
v20261008.012133, ubuntu_codename=resolute resolves
ubuntu-26.04-cuda-13.4, an explicit name is honored, and
base_image_tag=v20260930.063326 resolves that release's
ubuntu-24.04-cuda-13.2. An unknown name or codename fails before any
download with the max() error the troubleshooting table now documents.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Runs on pull requests, pushes to master, and daily. The daily run is
the point: the template resolves the release index at build time, so
the repo can stop building without a commit, and validate with
-evaluate-datasources is exactly the resolution a reader's build does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…sions

The variable description and the 404 troubleshooting row said older
tags are "listed by the release index". They are not: latest.json
carries one tag, and the repository that cuts releases is private. The
only place a reader can get an earlier tag is a previous build's
manifest.json, which both now say.

The default candidate filter now keeps only the base variant, so a
non-base build published into the same release cannot tie with the base
image on CUDA and win on list order, and a cuda_version with no minor
ranks as .0 instead of failing on an index error. Both checked against
a crafted local index: on a 13.10 tie the base image wins over a
-minimal one, 13.10 outranks 13.9, and a bare "14" resolves.

CI drops the OVMF install: validate never checks firmware paths, so the
step tested nothing and was the one most likely to flake (apt-get
install with stale lists). Adds permissions: contents: read, and
records GitHub's 60-day inactivity rule for schedules in a public repo,
since this repo is meant to go quiet.

README: the base_image_name example uses a placeholder rather than a
name that will leave the index, and the customization section says what
removing the base-image path involves, since data sources are evaluated
for every build. Comments that still said jammy now say noble and
resolute.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@eculver
eculver merged commit 12d3ced into master Oct 21, 2026
1 check passed
@eculver
eculver deleted the eculver/resolve-base-image-at-build-time branch October 21, 2026 06:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant