Bumping upstream: the engine and the docker CLI
On this page
Skrog pins every upstream byte it ships. Nothing is ever fetched as “latest” — that is the determinism promise in PLAN §04, and breaking it once costs the wedge market. So a version bump is a reviewed commit, and this is what one involves.
There are two independent streams. They are often confused because both carry a Docker version number, and they can legitimately differ:
| stream | what it is | pinned in |
|---|---|---|
| engine | dockerd and friends, built from source into the rootfs | guest/rootfs/versions.env + internal/release/manifest.json |
| docker CLI | the docker.exe skrog cli install puts on PATH, plus compose, buildx, the credential helper | internal/dockercli/manifest.json |
A newer CLI talking to an older daemon is supported — the API negotiates — but a large gap is worth closing, because the acceptance suite only validates the combination that is pinned.
The engine
1. Find the values
The tag and its commit are both pinned (#88): a git tag can be moved and a
source repo compromised, and the result runs as root in every user’s engine VM.
build-engine.sh fails the build on mismatch.
gh api repos/moby/moby/releases --jq '[.[] | select(.prerelease==false)][0].tag_name'
gh api repos/moby/moby/commits/docker-v29.8.0 --jq .sha
Verify the method before trusting it. Resolve the tag that is already
pinned and confirm it reproduces the MOBY_SHA in the file. If it does not,
the query is wrong — git/ref/tags/<tag> returns the annotated tag object’s
SHA, which is not the commit, and pinning it would fail every build.
2. Check the Go floor, do not assume it
gh api "repos/moby/moby/contents/go.mod?ref=docker-v29.8.0" --jq .content | base64 -d | grep '^go '
GO_VERSION in versions.env must be at or above that. It is pinned because
the toolchain changes the output bytes, so raise it deliberately, not reflexively.
The floor is not the target. go.mod says what moby needs; its
Dockerfile (ARG GO_VERSION) says what moby ships with, and the release
notes list the stdlib CVEs that Go version fixes in dockerd:
gh api "repos/moby/moby/contents/Dockerfile?ref=docker-v29.9.0" --jq .content | base64 -d | grep '^ARG GO_VERSION'
29.9.0 was bumped to the floor (1.26.8) while moby built with 1.26.9, which
shipped rootfs-v29.9.0-1 without five Go security fixes. Match moby’s Go
version unless there is a stated reason not to, and after building, confirm it
rather than trusting the pin: go version on the extracted dockerd prints
the toolchain that actually built it.
3. Decide about containerd, runc and buildkit — deliberately
moby’s own Dockerfile carries ARG CONTAINERD_VERSION / ARG RUNC_VERSION.
These are informational, not authoritative. They describe moby’s dev
container and CI, and Skrog has never simply mirrored them: at 29.7.2, moby’s
Dockerfile said containerd v2.3.3 while Skrog shipped v2.1.4 — on purpose.
The rule in versions.env is the one that governs: the pinned set is a tested
combination, and skrog engine upgrade refuses combinations outside the
tested matrix. So:
- Bumping moby alone is normal and is usually the right change.
- Moving containerd/runc/buildkit is a separate decision with its own justification (a CVE, a bug you need fixed, a moby release that genuinely requires it) — never “GitHub lists a newer tag”.
- Any component you do move needs its
*_SHAupdated in the same commit.
4. Edit versions.env
ENGINE_VERSION=29.8.0
MOBY_TAG=docker-v29.8.0
MOBY_SHA=<the commit from step 1>
ROOTFS_REVISION=1 # reset to 1 on an engine bump; incremented for a
# rootfs-only content change
5. Edit internal/release/manifest.json in the same commit
TestManifestAgreesWithVersionsEnv asserts that the default engine entry
matches versions.env — engine version and every component. A versions.env
bump on its own does not compile past the tests, by design.
Add the new engine as "default": true, with the URL the release will have,
and "sha256": "". Keep the previous engine listed with its checksum
intact: that is what skrog engine list offers and what skrog engine rollback returns to. Dropping it would strand anyone who needs to go back.
The empty checksum is deliberate and temporary. TestTheDefaultEngineShipsBothArchitectures
and TestTheDefaultEngineResolvesOnThisHost require the default engine to be
installable on both architectures, and nothing with an empty checksum is. On a
pull request they skip, with a ::warning:: annotation saying the rootfs is
pending, rather than fail: a red the author expects is a red reviewers learn to
scroll past. They skip only when the sole problem is an empty checksum – a
missing architecture or a bad URL still fails – and only when
GITHUB_EVENT_NAME is pull_request, so a push to main and a local
go test still fail. The release is cut from this PR’s commit (step 7) and its
checksums land in the same PR, which then merges with the tests running for
real – main never carries a manifest it cannot install. Do not merge
while those two tests show as skipped.
6. Open the PR and let CI build it
rootfs.yml runs on any pull request touching guest/rootfs/**. It builds the
engine from source and runs smoke-test.sh, boot-test.sh and
reference-diff.sh. Those last two exist because a rootfs whose engine could
not start reached a published release twice — a green build alone has
already proven insufficient here, so treat the boot test as the real gate.
It does all of that twice, once per architecture, on native amd64 and
arm64 runners (#388). So a bump has to build, boot and match Docker’s
reference bundle on both. Read both jobs: an upstream tag that compiles on
x86-64 and not on aarch64 is a real possibility, and fail-fast: false is set
so the passing one still tells you which it is.
7. Release from the PR, then fill in the checksum in the same PR
Once rootfs.yml is green on both architectures, tag
rootfs-v<engine>-<revision> (here rootfs-v29.8.0-1) on the PR’s head
commit and let the workflow publish (see RELEASING.md).
Download both tarballs, check them against the published .sha256 assets,
put the digests into manifest.json in a second commit on the same PR, and
merge it once that is green.
Finally, exercise skrog engine upgrade from the previous version and
skrog engine rollback back to it, confirming data survives both.
The docker CLI
internal/dockercli/manifest.json pins the CLI, compose, buildx and the
credential helper — each by version, per-arch URL and sha256.
Where the checksums come from differs per project, and the file’s own comment
block is the reference:
- compose —
checksums.txton the release - buildx —
checksums-signed.txt(the one that covers Windows) - wincred —
checksums.txt - docker CLI — the exception:
download.docker.compublishes no checksum at all, so the amd64 digest is computed from the official download and pinned here. Every install still verifies exact bytes.
Windows arm64 has no upstream static docker.exe. Its entry stays a
placeholder with an empty sha256; Published() reads that as “not available on
this platform” and the installer skips it with an honest message. Do not invent
a URL to fill the hole.
To compute the CLI digest:
curl -fsSLO https://download.docker.com/win/static/stable/x86_64/docker-29.8.0.zip
sha256sum docker-29.8.0.zip
The CLI can be bumped on its own — it is a separate stream — but if it moves well ahead of the engine, open an issue to bring the engine along rather than letting the two drift silently.
Checklist
- Tag and commit SHA resolved, and the method verified against the existing pin
- Go floor checked against moby’s
go.mod, andGO_VERSIONmatched to moby’sDockerfile(the floor is not the target) - containerd / runc / buildkit: moved with a stated reason, or deliberately left alone
-
versions.envupdated,ROOTFS_REVISIONreset to 1 -
manifest.json: new default entry with empty sha256, previous engine retained -
go test ./internal/release/fails ONLY on the two default-engine checksum tests (expected until step 7; they skip on a PR, so check that they are NOT skipped before merging) -
rootfs.ymlgreen on both architectures, including the boot test and reference diff - Rootfs release tagged on the PR head and published
- Checksums verified against the downloaded bytes, committed to the same PR, PR green and merged
-
engine upgradeandrollbackverified with real data