Skip to content

Fix: a docker source build reported "dev", not its version - #125

Merged
tbcsec merged 1 commit into
mainfrom
claude/fix-docker-version
Aug 1, 2026
Merged

Fix: a docker source build reported "dev", not its version#125
tbcsec merged 1 commit into
mainfrom
claude/fix-docker-version

Conversation

@tbcsec

@tbcsec tbcsec commented Aug 1, 2026

Copy link
Copy Markdown
Owner

What & why

A fresh git clone + docker compose up --build reported its version as dev, not 1.2.0-src — the README's headline install path, so most deployments.

backend/Dockerfile had:

ARG APP_VERSION=dev
ENV APP_VERSION=${APP_VERSION}

A Dockerfile has no conditional ENV, so that line always runs — and pydantic-settings ranks an environment variable above the Python default. ENV APP_VERSION=dev therefore shadowed config.py's "1.2.0-src" on every source build.

Two consequences, the second worse than the first:

  1. Adoption data collapsed into one opaque dev bucket with no version granularity.
  2. Source deployments could never be told a new release existed"dev" doesn't parse, so is_newer() always returned False.

#124 changed the Python default from "dev" to "1.2.0-src" but never touched the env var overriding it, so it fixed nothing for the path that matters.

The fix

ARG APP_VERSION=empty, meaning "not a release image" — with a validator in config.py resolving blank to SOURCE_BUILD_VERSION. Blank includes whitespace: a quoted compose value or a heredoc newline would otherwise reach the update endpoint and be bucketed as invalid.

Verified across every path:

APP_VERSION reports
unset (bare uvicorn) 1.2.0-src
"" (source docker build) 1.2.0-src
" " (whitespace) 1.2.0-src
" v1.2.0 \n" (stray newline) v1.2.0
v1.2.0 (release image) v1.2.0

The default also moves out of the settings field into a module constant, so there's one name to grep, bump and assert on rather than a literal buried in a class.

Guards on the mechanism, not the value

The value read 1.2.0-src correctly the whole time — only the interaction was wrong. So both new guards check the mechanism:

  • a test asserting ARG APP_VERSION carries no literal default;
  • a second tag-push CI step doing the same, so a regression blocks the release rather than shipping.

Confirmed both fire: restoring ARG APP_VERSION=dev blocks.

How this got through

Worth recording, because the gap was in the testing approach rather than the code. The config default was tested by importing Settings in isolation, where no env var exists, and the existing CI guard only read config.py. Both agreed on 1.2.0-src. Neither exercised a container. A single docker compose up --build would have caught it in seconds — and that's the check that was missing when the version-resolution mechanism changed.

Checklist

  • Backend tests pass — cd backend && .venv/bin/pytest (569 passed, 2 new)
  • Frontend checks pass — no frontend files touched; backend-only change
  • If UI-observable, I ran it in the browser and confirmed it works — the version string renders through existing code; verified at the settings layer
  • Follows the architectural rules in CONTRIBUTING.md
  • One migration for this PR if the schema changed — no migration, config and CI only
  • This is not a security fix (those follow SECURITY.md)

Like #124, this wants merging before v1.2.0 is tagged — otherwise the first release ships images whose source-build counterpart reports the wrong version.

After merging, the useful check is a real one: pull, docker compose up --build, and confirm Admin → Site settings → General reads "Running version 1.2.0-src". A running container keeps reporting dev until rebuilt, since the value is baked at image build time.

🤖 Generated with Claude Code

`backend/Dockerfile` had `ARG APP_VERSION=dev` feeding `ENV
APP_VERSION=${APP_VERSION}`. A Dockerfile has no conditional ENV, so that
line always ran — and pydantic-settings ranks an environment variable
above the Python default. `ENV APP_VERSION=dev` therefore shadowed
config.py's "1.2.0-src" on every `docker compose up --build`, which is the
README's headline install path. Every source deployment reported "dev",
which also meant none of them could ever be told a new release existed
("dev" doesn't parse, so is_newer always returned False).

The recent change from "dev" to "1.2.0-src" only moved the Python default;
it never touched the env var that was overriding it, so it fixed nothing
for the path that matters.

ARG now defaults to **empty** — empty means "not a release image" — and a
validator resolves blank to SOURCE_BUILD_VERSION. Blank includes
whitespace: a quoted compose value or a heredoc newline would otherwise
reach the update endpoint and be bucketed as unparseable.

The default also moves out of the field and into a module constant, so
there's one name to grep, bump and assert on rather than a literal buried
in a settings class.

Two guards, both on the *mechanism* rather than the value — the value read
correctly the whole time, only the interaction was wrong:

- a test asserting `ARG APP_VERSION` carries no literal default;
- a second tag-push CI step doing the same, so a regression blocks the
  release rather than shipping.

Why this got through: the config default was tested by importing Settings
in isolation, where no env var exists, and the existing CI guard only read
config.py. Both agreed on "1.2.0-src". Neither exercised a container. A
single `docker compose up --build` would have caught it.

Co-Authored-By: Claude <noreply@anthropic.com>
@tbcsec
tbcsec merged commit f19f3c2 into main Aug 1, 2026
4 checks passed
@tbcsec
tbcsec deleted the claude/fix-docker-version branch August 1, 2026 15:32
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