Skip to content

bug: /api/version still reports "unknown" without start.sh — the "unknown" sentinel is truthy, so #993's VERSION-file fallback is unreachable #1810

Description

@AndriiPasternak31

Summary

GET /api/version returns "version": "unknown" (and components.backend, components.agent_server, components.base_image with it) on any image built without the VERSION build arg — i.e. any docker compose -f docker-compose.prod.yml build that doesn't go through scripts/deploy/start.sh.

#993 anticipated exactly this operator and added two independent mitigations. Both are present in the tree, and neither works, because the "unknown" sentinel is a truthy string that short-circuits the fallback chain before the file is ever consulted.

Observed upgrading a live instance v0.8.0 → v0.8.5:

{ "version": "unknown", "components": { "backend": "unknown",
  "base_image": "trinity-agent-base:unknown" }, "git_commit": "unknown" }

The instance could not report which build it was running — during an upgrade, which is precisely when you need it.

Root cause

The intended 3-tier resolution (src/backend/main.py:1282-1299):

# Version resolution order (#993):
#   1. VERSION env var — build-stamped from git (e.g. "0.9.0+g4c640b6e"), …
#   2. VERSION file — curated semver, mounted in dev / copied in image.
#   3. "unknown" — neither present.
version = os.getenv("VERSION") or None
if not version:
    version_paths = [Path("/app/VERSION"), …]
    …
version = version or "unknown"

Tier 2 is unreachable. docker/backend/Dockerfile:19-25:

ARG VERSION=unknown
…
ENV VERSION=${VERSION} \

ENV is unconditional, so the env var is always set in the image — to the literal string "unknown" when no build arg was passed. os.getenv("VERSION") therefore returns "unknown", which is truthy, so or None does not fire, if not version: is False, and the /app/VERSION file is never read.

Both mitigations #993 added are consequently dead code in the only case they were written for:

  • docker/backend/Dockerfile:114COPY ../../VERSION /app/VERSION, whose own comment reads "prod compose doesn't [mount it], so without this COPY the version field falls back to unknown". The COPY lands; the reader never gets there.
  • docker/backend/Dockerfile:17-18"Defaults to unknown so local builds and operators who skip start.sh still get a well-typed response." Well-typed, but the response is unknown rather than the curated semver sitting in the image one path away.

This is a sentinel-vs-absence bug: ARG+ENV cannot express "unset", so the sentinel must be filtered on read.

Suggested fix

Treat the sentinel as absence at the single read site:

_version_env = os.getenv("VERSION")
version = _version_env if _version_env and _version_env != "unknown" else None

Tier 2 then resolves 0.8.5 from the baked file, and an operator who skips start.sh gets the curated semver without the git suffix — a correct, honest, partial answer instead of nothing.

Scope note: the same ARG=unknown → ENV shape applies to GIT_COMMIT, GIT_BRANCH, BUILD_DATE, GIT_COMMIT_SUBJECT, GIT_COMMIT_TIMESTAMP. Those have no file fallback (git metadata genuinely isn't in the image), so "unknown" is the right answer for them and they need no change — but it's worth a comment saying so, since the asymmetry is what makes this bug easy to reintroduce.

Acceptance criteria

  • docker compose -f docker-compose.prod.yml build (no build args) → /api/version reports the VERSION file contents, not "unknown"
  • Building with the build arg still wins and keeps the git suffix (0.8.5+g<sha>[.dirty])
  • components.backend / agent_server / base_image track the resolved value
  • Regression test covering VERSION=unknown in env plus a present /app/VERSION — the exact combination that is broken today

Environment

Trinity 0.8.5 (e9eb40da), prod compose, built via docker compose build without start.sh.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions