Skip to content

Published container images report their version as dev, and their tree state as dirty #1133

Description

@gsavage

The published container images report themselves as dev, not as the release they are tagged with,
and their tree state as dirty. So kosli version inside a container cannot tell you which CLI
you are running.

From ghcr.io/kosli-dev/cli:v2.38.0 (digest
sha256:28b5291ed007d5c8afacb96ba393b50e70dcd5715d26bb46f27126d3cee4c10b, linux/arm64):

$ docker run --rm ghcr.io/kosli-dev/cli:v2.38.0 version
version.BuildInfo{Version:"dev+e5b4118", GitCommit:"e5b41181dd39f6f0e561f4c8359be2a68c94ab0a", GitTreeState:"dirty", GoVersion:"go1.26.6"}

The commit is right — v2.38.0 does tag e5b4118 — so the image holds the correct code. It is only
the version and tree-state strings that are wrong. The goreleaser release archive for the same
version is correct:

version.BuildInfo{Version:"v2.38.0", ..., GitTreeState:"clean", ...}

Why

Two independent causes, both in the Docker build path rather than in the Go code.

The version string. Dockerfile runs a bare make build, and .github/workflows/docker.yml
passes no build-args. The Makefile derives the version from the git tag:

GIT_TAG        = $(shell git describe --tags --abbrev=0 --exact-match 2>/dev/null)
BINARY_VERSION ?= ${GIT_TAG}
VERSION_METADATA = $(GIT_SHA)

docker.yml checks out with fetch-depth: 3 and no fetch-tags, so the clone in the build context
has no tags at all. git describe finds nothing, BINARY_VERSION is empty, the version ldflag is
never set, and internal/version/version.go's version = "dev" default survives. VERSION_METADATA
stays at GIT_SHA, which is where the +e5b4118 suffix comes from.

The workflow already knows the answer — it takes a tag input, and the release workflow passes the
release tag to it — so it is only a matter of getting it to make build.

The tree state. .dockerignore is deny-all with an allow-list that keeps .git/:

*
!cmd/
!internal/
!Makefile
!go.*
!.git/

So the builder has the real git history but only a subset of the working tree. git status --porcelain therefore reports every tracked file outside that allow-list as deleted, and the
Makefile's GIT_DIRTY check turns that into dirty. The tree is not actually dirty; the build
context is just partial.

Suggested fix

Pass the version in rather than deriving it inside a partial checkout. In Dockerfile:

ARG VERSION
ARG GIT_COMMIT
RUN make build VERSION=${VERSION} GIT_COMMIT=${GIT_COMMIT} GIT_DIRTY=clean

and in the build step in docker.yml:

build-args: |
  VERSION=${{ inputs.tag }}
  GIT_COMMIT=${{ github.sha }}

That also removes the build's dependence on .git/ being in the context, so !.git/ can come out
of .dockerignore — which shrinks the context and stops history changes busting the build cache.
Hard-coding GIT_DIRTY=clean is honest here because CI builds from a clean checkout; if that feels
too strong, the alternative is to compute it outside the container and pass it in the same way.

Worth asserting afterwards, so this cannot come back silently: a step in docker.yml that runs the
built image and checks kosli version --short equals the tag it is about to be published under.

Why we noticed

Kosli Capture ships the CLI inside its own image. We wanted to copy the binary out of
ghcr.io/kosli-dev/cli — the pattern Dockerfile.alpine in this repo uses — but a container whose
kosli version cannot name its release is a bad place to start debugging a failed snapshot, so we
fetch the checksum-verified release archive instead. That works, but it is more machinery than the
COPY --from would have been, and we would rather go back to the image once this is fixed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions