Identify the build by a source fingerprint, not a commit SHA

The marker read 'dev' on the live site, so it was decoration rather than
evidence. A commit SHA is the wrong thing here twice over:

- Coolify does not pass a commit SHA into a Docker Compose build, so the
  ARG fell back to 'dev' and would have done so forever.
- A file naming the commit has to live inside that commit, so it can only
  ever name the parent. Amending the commit to fix that just moved the
  discrepancy.

Replaced with a hash of the .py/.css/.toml actually copied into the image,
computed during the Docker build. It is true by construction, cannot go
stale, and answers the real question: is this the code that was built.

Two build-order bugs found and fixed while proving it (both caught by
building, which is the point):
- Docker's ENV does not evaluate $(cat file)
- the chown referenced a user created later in the file

Verified: build ok, fingerprint d7d427a2e770 in the image, sidebar renders
it, Central Towers converts inside the image (192 written, 23 unplaced, gate
not blocked).
This commit is contained in:
MEGA INFRA 2026-10-02 21:59:35 +02:00
parent 7fd221288a
commit 772ba7fa33
5 changed files with 26 additions and 27 deletions

View file

@ -19,11 +19,6 @@ ENV PYTHONUNBUFFERED=1 \
WORKDIR /app
# Which build is this? Read from a file IN the repo rather than a build arg,
# because Coolify does not pass a commit SHA into a Docker Compose build - an
# ARG would silently fall back to "dev" forever and the marker would never
# tell you anything. Docker's ENV also does not evaluate $(cat ...), so the
# file is simply copied and the app reads it at startup.
# Dependencies first, so a code change does not reinstall the world.
COPY requirements.txt .
@ -33,12 +28,22 @@ COPY app.py run.py ./
COPY boq_engine/ ./boq_engine/
COPY static/ ./static/
COPY .streamlit/ ./.streamlit/
COPY build.txt ./build.txt
# Run as a non-root user. UID 1000 matches the usual Coolify/Docker convention.
RUN useradd --create-home --uid 1000 --shell /bin/bash megainfra \
&& mkdir -p /data/profiles /app/out \
&& chown -R megainfra:megainfra /app /data
# A fingerprint of the actual source, computed HERE at image build time.
#
# A commit SHA looked better but is self-referential and can never be right:
# the file naming the commit has to live in that commit, so it can only ever
# name the parent. A hash of the files actually in the image has no such
# problem - it is true by construction and cannot go stale.
RUN find /app -name '*.py' -o -name '*.css' -o -name '*.toml' \
| sort | xargs sha256sum | sha256sum | cut -c1-12 > /app/build.txt \
&& chown megainfra:megainfra /app/build.txt \
&& echo "build fingerprint: $(cat /app/build.txt)"
USER megainfra
ENV HOME=/home/megainfra \

View file

@ -8,12 +8,15 @@ disagree.
Anything you disagree with, you settle **in the app** and regenerate. The
consultant never has to leave the screen.
Runs entirely on your machine. Nothing is uploaded anywhere. The sidebar shows
the build SHA, so you can always see which version you are looking at. It is
read from **`build.txt` in the repo**, not a build argument, because Coolify
does not pass a commit SHA into a Docker Compose build — an `ARG` silently
falls back to `dev` forever and the marker then tells you nothing.
`./update_build_marker.sh` rewrites it; run it as part of committing.
Runs entirely on your machine. Nothing is uploaded anywhere. The sidebar shows a
**build fingerprint** — a hash of the `.py`/`.css`/`.toml` actually copied into
the image, computed during the Docker build. It reads e.g. `build d7d427a2e770`.
A commit SHA was the obvious first choice and was wrong twice over: Coolify
does not pass a commit SHA into a Docker Compose build, so an `ARG` silently
falls back to `dev` forever; and a file naming the commit cannot live in the
commit it names, so it could only ever name the parent. A hash of the shipped
files is true by construction and cannot go stale.
![the app](docs/app-screenshot.png)

13
app.py
View file

@ -111,13 +111,12 @@ def _fp(data: bytes) -> str:
def build_sha() -> str:
"""Which build is this? Shown in the sidebar so a deploy can be confirmed
without digging through Coolify's log.
"""Fingerprint of the source in this image, shown in the sidebar.
Falls back through several routes because Streamlit executes this file in
an exec context where __file__ is not guaranteed - a bare reference to it
would take the whole app down. Falls back to the env var, then to a
literal, and never raises.
Computed at image build time over the .py/.css/.toml actually copied in, so
it identifies the running code by construction. Falls back through several
paths because Streamlit execs this file where __file__ is not guaranteed -
a bare reference to it would take the whole app down - and never raises.
"""
candidates = []
try:
@ -135,7 +134,7 @@ def build_sha() -> str:
return val
except OSError:
continue
return os.environ.get("BOQ_BUILD_SHA") or "dev"
return "unknown"
def money(v) -> str:

View file

@ -1 +0,0 @@
bcba46a

View file

@ -1,7 +0,0 @@
#!/usr/bin/env bash
# Rewrite build.txt to the current commit. Run as part of committing, so the
# marker in the sidebar always names the build that is actually deployed.
# Wired up as a git post-commit hook when the repo is set up locally.
set -e
cd "$(dirname "$0")"
git rev-parse --short HEAD > build.txt