Repository navigation
build(image): ship semibase as a container image - #6
Merged
Merged
Conversation
Consumers resolve the binary from PATH today, so whichever build happens to be installed is the one that provisions their bench. A published image gives them a version they can name: SemiPlot copies /semibase out of it onto its own postgres image and runs it as an init script over the unix socket. The image is FROM scratch and holds one file. Nothing runs it as a base, so a shell, a libc and CA certificates would be weight nobody executes; the module is pure pgx and builds CGO_ENABLED=0. The binary is copied from the linux release artifact rather than built again, so the bytes in the image and the bytes on the release page are the same. The release job pushes :latest and an immutable :vX.Y.Z after creating the GitHub release, never before - consumers still download the release assets. A prerelease tag publishes its version tag and leaves :latest where it is. It logs in with docker login rather than docker/login-action: no third-party action needs the write-capable token for one command. CI builds the image and runs it, so a broken Dockerfile fails on the pull request instead of at tag time. CI pushes nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prerelease policy reached the image and stopped there: the GitHub release was created with prerelease: false and make_latest: true hardcoded, so a v1.2.3-rc1 became the Latest release the deployment doc points commissioning engineers at, while the image :latest correctly stayed put. Worse, nothing held :latest to moving forward at all. Re-running an old tag's workflow from the Actions UI republished that old version as :latest, and two tags pushed together raced because the concurrency group carried github.ref. A consumer tracking :latest downgraded with no signal. So "latest" is now one decision, made once in the Read the tag step: the tag carries no - suffix AND it is the newest release tag in the repository. The release's prerelease and make_latest fields and the image's :latest push all read that answer, so the release page and the registry cannot disagree, and neither can walk backwards. The newest tag comes from git's version ordering over unsuffixed tags, which compares numeric fields as numbers - v0.10.0 outranks v0.9.0 - and keeps git's placement of v1.0.0-rc1 after v1.0.0 out of it. A skipped :latest push prints why. The concurrency group drops github.ref so two tag pushes serialise rather than race. The image build moves ahead of release creation: the build is the half that fails on a bad copy or a bad Dockerfile, and it now fails with nothing published to withdraw. Only login and push stay after it. The built image runs `version` before being pushed - CI's smoke check exercises a binary built without -trimpath and -s -w, so these are the only bytes proved. docker logout runs on a trap, covering the skip and failure paths. Both builds pass --platform linux/amd64, and the release job reads the manifest back. A FROM --platform= pin was tried and does not do this: with the pin set to arm64 and no build flag, an amd64 host still produced an amd64 image, since scratch carries no config for the output to inherit. The image also gains org.opencontainers.image.source, which is what links the GHCR package to this repository. /samples/ is ignored. It holds archive dumps from a customer installation, was untracked and unignored in a public repository, and the image/ rule added with the Dockerfile only kept it away from the Docker daemon, not from git add -A. Nothing inside it is touched. The Russian deployment doc gains the third artifact as what it is: an image for consumers' test benches, never part of commissioning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Consumers resolve the
semibasebinary fromSEMIBASE_EXEorPATH, so on a developer machine whichever build happens to be installed is the one that provisions. SemiPlot's fixture carries a pinned-version constant that nothing enforces.An image fixes that at the source. SemiPlot's Dockerfile does
COPY --from=ghcr.io/semiteq/semibase:latest /semibaseontopostgres:17-alpine, where PR #5 lets it run as an init script over the unix socket.The image
FROM scratch, one file, an ENTRYPOINT. Not alpine, not distroless: the only consumer copies the binary out and never uses the image as a base, and the module is purepgxbuiltCGO_ENABLED=0. It carries no init script — that is the consumer's — and no consumer knowledge.3.39 MB compressed, one layer, one entry. The binary in it is
cmp-identical to the release asset rather than a second build, so "the image contains the shipped binary" is provable.latestonly moves forwardSemiPlot tracks
:latestdeliberately: delivered installations update neither service, so the only pair ever newly deployed is newest-SemiBase with current-reader. That contract needs enforcing, and two paths would have broken it — re-running an old tag's workflow from the Actions UI, and two tags pushed together racing through agithub.ref-keyed concurrency group.One decision now gates both the GitHub release and the image push: no prerelease suffix and the tag is the newest release tag. Dry-run over a realistic tag set:
The same gate drives
prereleaseandmake_lateston the GitHub release, so the release page and the image tag cannot disagree —docs/deployment.mdsends commissioning engineers to/releases/latest, and an rc must not land there either.Measured, not assumed
FROM --platform=does not set the manifest architecture. Pinned tolinux/arm64on an amd64 host, the output was stilllinux/amd64—scratchcarries no config to inherit. Onlydocker build --platformworks, so that is what carries the claim, with a manifest read-back that fails the job.latestnever names a version whose assets are missing. Partial failure is safe in the recoverable direction: release created, push failed leaves:lateston the previous version, and a plain workflow re-run converges because the build is-trimpathreproducible.docker logoutruns on anEXITtrap, so it covers the skip and failure paths too.One operational item no workflow can do
The first push creates the GHCR package private, and SemiPlot's CI pulls anonymously. Someone flips it to public once in the package settings after the first tagged release. Documented rather than left to be found by a failing consumer build.
Also
/samples/is added to.gitignore. That directory holds PostgreSQL dumps from a customer installation and was untracked but not ignored, in a public repository — the newimage/build context stopped the Docker daemon from seeing it, and nothing stoppedgit add -A.🤖 Generated with Claude Code