because it rhymes 🤷🏻♀️
prebuilt docker images + ready-to-use spread yamls so you can drop Canonical spread into any project without writing a backend or maintaining test infra. images live on ghcr as multiarch manifests; per-version yamls live as github release assets.
Warning
This project has many footguns. The test containers are run with no authentication and in --privileged mode. Make sure you know what you are doing.
two image flavours, each for ubuntu 22.04, 24.04, 25.10, 26.04, 26.10 x amd64 / arm64 / s390x / ppc64le, published as multiarch tags at ghcr.io/lczyk/spread-bread/<flavour>:<ver>. heads-up: s390x + ppc64le images build under qemu and ship untested (no native runners); amd64 + arm64 are tested on every release:
bread-- base: ubuntu + sshd. general-purpose spread system; the test suite installs whatever else it needs.bread-chisel-releases-- bread +chisel+ the shell + container tooling typically needed by chisel-releases spread tests (curl, wget, git, jq, file, sudo, tree, docker, skopeo).chiselanddockerare built from source (canonical/chisel pinned by SHA, docker/cli pinned by version tag) so the bundled binaries are go 1.25+ and survive qemu emulation.
drop a ready-made spread yaml into your project. no clone, no build. yamls are attached to the rolling downloads release:
curl -fsSL https://github.com/lczyk/spread-bread/releases/latest/download/bread-chisel-releases-26.04.yaml -o spread.yaml
spread
allocate inside the yaml docker runs the matching multiarch ghcr image; --platform linux/<arch> picks the right arch from the manifest list. first run pulls the image; subsequent runs hit the local docker cache.
available yamls in the release:
bread-{22.04,24.04,25.10,26.04,26.10}.yamlbread-chisel-releases-{22.04,24.04,25.10,26.04,26.10}.yaml
the allocate script reaches the container's sshd in one of two ways, picked automatically from uname:
bridge(default on linux) -- connects to the container's docker bridge IP. relies on the host sharing the docker bridge network, so172.17.x.xis routable.publish(default on macOS) -- publishes sshd to127.0.0.1:<ephemeral port>and connects there. Docker Desktop on macOS runs the daemon in a VM, so bridge IPs are not host-routable; publishing a port is.
override the default with BREAD_NET:
BREAD_NET=publish spread # force port-publishing (e.g. to test the macOS path on linux)
BREAD_NET=bridge spread # force bridge IPs
ubuntu 26.04's patched GNU tar (1.35+dfsg-4ubuntu0.x) resolves extraction paths through a syscall Docker Desktop's Rosetta emulation does not implement, so in an amd64 container on apple silicon every archive entry below the top level fails with Function not implemented. spread hits this when it unpacks the project, reports cannot send project content, and after three tries gives up with Cannot allocate ... after too many retries. 26.10 and everything at or below 25.10 are unaffected, as are the qemu-emulated arches.
the images work around it: /bin/tar is a shim that probes GNU tar once per container and, if it is broken, routes extraction to bsdtar (see hack/tar-shim.sh). creation stays on GNU tar, which spread needs for --sort=name when it packs artifacts. on unaffected hosts the shim is inert.
prefer a precompiled spread CLI over go install? same release ships statically-linked binaries for linux amd64 / arm64 / s390x / ppc64le:
curl -fsSL https://github.com/lczyk/spread-bread/releases/latest/download/spread-linux-amd64 -o /usr/local/bin/spread
chmod +x /usr/local/bin/spread
verify the checksum:
curl -fsSL https://github.com/lczyk/spread-bread/releases/latest/download/spread-linux-amd64.sha256 | sha256sum -c -
verify the cosign signature (keyless OIDC, no account):
curl -fsSL https://github.com/lczyk/spread-bread/releases/latest/download/spread-linux-amd64.cosign.bundle -o spread.cosign.bundle
cosign verify-blob --bundle spread.cosign.bundle \
--certificate-identity-regexp '^https://github\.com/lczyk/spread-bread/' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
/usr/local/bin/spread
the cert-identity regex above is approximate; consult the actual issued cert on the first signed release if cosign verify-blob rejects it.
spread-bread/
makefile # build images + generate inlined yamls + run contract tests
hack/
build_binaries.sh # cross-compile chisel + spread + docker cli in one ubuntu/go builder
build_image.sh # per-image build w/ hash-stamp short-circuit
hash_inputs.sh # per-image input hash (drives stamp invalidation)
check_base.sh # detect upstream ubuntu base digest drift; rewrite @sha256 pins
inline_scripts.rb # splice scripts/*.sh into yaml templates
tar-shim.sh # image /bin/tar; routes extraction to bsdtar where gnu tar is broken
scripts/ # allocate / discard scripts, one pair per flavour
images/ # one Dockerfile per (flavour, ubuntu version)
templates/ # yaml templates with `source scripts/...` markers
inlined/ # generated self-contained spread yamls (release artefacts)
cache/binaries/ # gitignored; cross-compiled chisel / spread / docker per arch
demo/ # worked example with local builds + a small test suite
spread.yaml # hand-maintained inlined yaml, LTS-only (24.04 + 26.04, both arches)
makefile
tests/{unit,integration,lib}/
tests/ # spread-in-spread contract tests against the inlined yamls
spread.yaml # outer spread (uses the bread-test image as its system)
Dockerfile.bread-test-26.04 # test-host image: bread:26.04 + docker + spread (not published)
contract-{bread,bread-chisel-releases}/run/task.yaml
_inner-{bread,bread-chisel-releases}/contract/task.yaml
.github/workflows/ # ci (build + test) on PR / push to main; release on r* tag; daily base-refresh
default goal is help:
make # list targets
make all # build all images + generate inlined yamls
narrow the matrix via VER / ARCH:
make build-bread VER=24.04
make build-bread VER=24.04 ARCH=amd64
make build-bread-chisel-releases ARCH=arm64
regenerate distribution yamls (already committed under inlined/, but regenerate after script edits):
make inline
run the demo (LTS systems only -- 24.04 + 26.04 x amd64 / arm64). builds the required bread images first if missing:
make demo
run the spread-in-spread contract tests against the locally-built images (same as ci):
cd tests && spread
the outer spreads a bread-test container; that container runs an inner spread against each inlined yaml; the inner asserts the contract (ubuntu version + arch match the system name, plus chisel --version + tool presence for the chisel flavour).
release.yaml triggers on push of an r[0-9]+ tag. on success it:
- builds the binary cache + all images -- amd64 / arm64 on per-arch native runners, s390x / ppc64le under qemu.
- pushes 8 multiarch manifests to
ghcr.io/lczyk/spread-bread/{bread,bread-chisel-releases}:<ver>. - attaches
inlined/*.yamlto a rolling github release calledyamls. olderr*releases are deleted (release objects only; the underlying tags stay).
ci (ci.yaml) is build + test on every PR + push to main; no publish.
anyway, here is this project's pet bread:
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⣶⣿
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣴⣿⣿⣿⣿⡟
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠤⣤⣄⣉⠙⢻⡟⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠴⢿⣷⣦⣤⣈⣉⣀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠒⠶⣶⣦⣤⣈⠙⢻⡟⠁⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠐⠾⢿⣷⣦⣤⣤⣤⣤⡤⠊⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⣀⠐⠶⣶⣤⣄⡉⠻⣿⣿⠏⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠾⢿⣿⣦⣤⣬⣉⣉⣤⠞⠁⠀⠀⠀⠀⠀⠀⠀⠀
⠀⢠⣄⠑⠲⠤⣈⠙⢻⣿⡿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⢀⣿⣿⣿⣶⣦⣤⣤⠞⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠸⣿⣿⣿⠿⠟⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
