Skip to content

feat(version): stamp build identity via ldflags and expose it at runtime - #39

Merged
thinkbig1979 merged 1 commit into
mainfrom
feat/build-identity-ldflags
Jul 31, 2026
Merged

feat(version): stamp build identity via ldflags and expose it at runtime#39
thinkbig1979 merged 1 commit into
mainfrom
feat/build-identity-ldflags

Conversation

@thinkbig1979

Copy link
Copy Markdown
Owner

Closes agent-os-r7e.

What

Build identity — version, commit, build date — stamped into the binary at link
time, exposed at GET /api/v1/version, on the startup log line, and in the UI.

Before this there was no way to answer "what is actually running on this host?"
Not from the API, not from the UI, not from the logs. docker-compose.prod.yaml
defaults to :latest with a watchtower label, so the common case was a mutable
tag behind an unidentifiable binary.

Decisions

/api/v1/version is public. Monitoring and support need the answer without
a session, and the exact version is already visible in the image tag the
operator pulled. It is registered on the unauthenticated /api/v1 group and
listed in middleware.PublicPaths, because that list is consulted by the CSRF
middleware as well as auth — so the two stay consistent if the route ever moves
under the protected group. A unit test pins that.

docker-compose.prod.yaml keeps :latest. The bead asked to consider
pinning. A pinned tag in a template file goes stale on every release and nothing
in the repo keeps it current. The comment now says the default is deliberate and
points at the new endpoint for identifying what is actually running.

The check that matters

A typo in an -X symbol path is accepted silently by the linker: no error, no
warning, the default survives, the build succeeds, and the image ships reporting
dev. Two things guard it.

backend/internal/version/ldflags_test.go pins the Dockerfile's -X paths to
the symbols actually declared in the package (import path via
reflect.TypeFor[Info]().PkgPath(), so a package move is caught too) and asserts
the ARG defaults exist. Proven by injecting a typo (versionverison):

--- FAIL: TestDockerfileLdflagsResolveToThisPackage
    -X .../internal/verison.Commit targets package ".../internal/verison",
      but the version package is ".../internal/version"
    Commit is never stamped: no `-X .../internal/version.Commit=` entry

And the real Dockerfile was built and run three ways:

build startup log GET /api/v1/version
docker build (no args) version=dev commit=unknown built=unknown {"version":"dev",...} HTTP 200
--build-arg VERSION=0.9.0 COMMIT=6fb9879 BUILD_DATE=2026-07-31T20:15:00Z version=0.9.0 commit=6fb9879 built=2026-07-31T20:15:00Z {"version":"0.9.0","commit":"6fb9879","buildDate":"2026-07-31T20:15:00Z"} HTTP 200
--build-arg VERSION= COMMIT= BUILD_DATE= (empty) version=dev commit=unknown built=unknown {"version":"dev",...} HTTP 200

The third row is why the RUN line carries ${VAR:-default} rewrites: an ARG
default only applies when the arg is absent, so a CI expression that resolved
to nothing would have stamped an empty string — the exact silent failure this
feature exists to prevent.

All three GETs ran with auth enabled and no token, which is the public-route
proof.

docker-publish.yml sources the values from the same docker/metadata-action
step that names the image tag — outputs.version is 1.2.3 for a v1.2.3 tag
and manual-<sha> for a dispatch run. BUILD_DATE uses
fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'],
which is metadata-action's own documented example for this, so the OCI label and
the API agree. This is the one piece not executed locally — it runs on the next
tag push.

UI

Settings → About shows version, commit and build date, links to the release
notes for a semver build, and says so plainly for a local dev build. unknown
becomes an em dash rather than being shown verbatim, since it reads as an error.
Five tests, including the two negative cases (no release link for dev or for a
manual-<sha> dispatch build) — both confirmed to fail when their guard is
removed.

Gates

backend:  go build ./... && go vet ./...       OK
          go test ./... -count=1               734 passed, 0 failed   (baseline 727)
          go vet -tags=integration ./internal/integrationtest/   OK
frontend: eslint .                             exit 0, no output
          npx tsc -b                           no errors
          vitest run                           833 passed / 82 files  (baseline 828 / 81)
          vite build                           built in 8.58s

gofmt -l flags internal/middleware/auth.go, which was already unformatted on
main (confirmed by stashing) — agent-os-far's territory, not touched here.

Test images removed and docker builder prune -f run afterwards (4.18GB).

🤖 Generated with Claude Code

"What is actually running on this host?" had no answer: no version constant in
the backend, no -ldflags in the Dockerfile, no /api/v1/version, and a startup
log line carrying nothing about the build. docker-compose.prod.yaml defaults to
:latest with a watchtower label, so the common case was a mutable tag behind an
unidentifiable binary.

- internal/version holds Version/Commit/BuildDate, defaulting to dev/unknown so
  an unstamped binary reads as a local build rather than as a bug.
- The Dockerfile's backend stage stamps them with -ldflags -X. ARG defaults keep
  a plain `docker build` at "dev"; ${VAR:-default} rewrites cover the case the
  ARG defaults do not, where a --build-arg carries an empty value.
- docker-publish.yml supplies them from the same metadata-action step that names
  the image tag, so a published image reports the tag it shipped under.
- GET /api/v1/version is deliberately public and listed in
  middleware.PublicPaths, which CSRF consults as well as auth. Monitoring and
  support can answer the question without a session; the trade is that the exact
  version leaks to an unauthenticated caller, which is already visible in the
  image tag.
- Startup log line carries version/commit/built, on the first line so it
  survives a death later in startup.
- Settings -> About shows it, linking to the release notes for a semver build.

A typo in an -X symbol path is accepted silently by the linker and leaves the
default in place, so version/ldflags_test.go pins the Dockerfile's -X paths to
the symbols actually declared in the package, and asserts the ARG defaults.
Verified by injecting a typo: the test names the wrong package and reports
Commit as never stamped.

Verified end to end through the real Dockerfile: a plain build reports
version=dev, a build with args reports them exactly, and the endpoint answers
HTTP 200 with auth enabled and no token.

Closes agent-os-r7e

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit 72cda95.

@thinkbig1979
thinkbig1979 merged commit ad493af into main Jul 31, 2026
9 checks passed
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