From 38cc6696a4bd5ae306f2ebf18716d4ad2f796890 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Tue, 14 Jul 2026 22:32:35 -0400 Subject: [PATCH] Fix commit hash not showing in Docker builds (#43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix commit hash not showing in Docker builds build_info.js computed buildHash via `git rev-parse --short HEAD` at runtime, but the final image intentionally has no git binary and no .git directory (kept lean, per .dockerignore) — so this always failed silently and the footer's version line showed "unknown" for every Docker deployment. Working correctly only for bare-metal/dev, where git + .git are actually present. Added a throwaway gitinfo build stage that reuses the main base image (no extra pull) with git installed just for this stage, reads .git from the build context (now no longer excluded — see .dockerignore), and bakes the resolved short hash into a small file that IS copied into the final image. build_info.js reads that file first, falling back to the old git-rev-parse behavior (still needed for bare-metal). Verified against a real build: `docker exec sso-manager cat /app/.build_commit` matches `git rev-parse --short HEAD` on the host, and the footer now shows the real hash instead of "unknown". Same fix already applied to proxy (theta42/proxy#133). * Support GIT_COMMIT build-arg override for submodule builds The gitinfo stage from the previous commit works for a standalone clone (.git is a real directory) but not when this repo is built as a git submodule (e.g. from theta-env): a submodule's .git is a pointer FILE, not a directory — the real object database lives in the superproject's .git/modules/, outside this repo's own directory and therefore outside Docker's build context entirely. `git rev-parse` can never resolve it from in here no matter what, so builds via theta-env still baked in "unknown" despite the earlier fix. Add an optional GIT_COMMIT build-arg that, when set, wins over the in-context git resolution. theta-env's setup.sh now computes it on the host (where the submodule DOES resolve correctly) and passes it via docker-compose.yml's build.args. Same fix in proxy: theta42/proxy#133. Verified via theta-env's actual setup.sh end to end: rebuilding with this change, `docker exec sso-manager cat /app/.build_commit` now matches `git -C sso-manager-node rev-parse --short HEAD` on the host (previously: "unknown"). --- .dockerignore | 5 ++++- Dockerfile.openldap | 33 +++++++++++++++++++++++++++++++++ nodejs/utils/build_info.js | 24 +++++++++++++++++++----- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/.dockerignore b/.dockerignore index fd11775..e689124 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,8 @@ # Git -.git +# NOTE: .git is intentionally NOT excluded — the gitinfo build stage in +# Dockerfile.openldap reads it to bake the commit hash into the image (see +# nodejs/utils/build_info.js), then it's discarded before the final stage. +# It never ends up in the final image. .gitignore # Development diff --git a/Dockerfile.openldap b/Dockerfile.openldap index 181ba98..2eb780f 100644 --- a/Dockerfile.openldap +++ b/Dockerfile.openldap @@ -3,6 +3,36 @@ # self-contained single-node deployment. For production, run a dedicated # LDAP server and configure the app via app_* env vars / mounted secrets.js. +# ── Git commit hash (build-time only) ──────────────────────────────────────── +# The final image intentionally has no git binary and no .git directory (kept +# lean, per .dockerignore), so `git rev-parse` always fails at runtime and +# build_info.js silently fell back to "unknown". Resolve it here instead, +# where .git IS available (build context), and bake just the short hash into +# a file — this stage itself is discarded, only /commit.txt survives via the +# COPY --from below. Reuses the main base image (already pulled for the real +# build below) rather than a separate one, so this adds no extra image pull. +# +# GIT_COMMIT lets a caller override the resolved hash instead of computing it +# from .git in this build context. Needed when this repo is built as a git +# submodule (e.g. from theta-env): a submodule's .git is a pointer FILE, not +# a directory — the real object database lives in the superproject's +# .git/modules/, outside this repo's own directory and therefore outside +# Docker's build context entirely, so `git rev-parse` can never resolve it +# from in here no matter what. theta-env's setup.sh passes --build-arg +# GIT_COMMIT=$(git -C sso-manager-node rev-parse --short HEAD), computed on +# the host where the submodule resolves correctly. +ARG GIT_COMMIT="" +FROM node:20-alpine AS gitinfo +ARG GIT_COMMIT +WORKDIR /repo +COPY .git ./.git +RUN if [ -n "$GIT_COMMIT" ]; then \ + echo "$GIT_COMMIT" > /commit.txt; \ + else \ + { apk add --no-cache git \ + && git rev-parse --short HEAD > /commit.txt; } 2>/dev/null || echo unknown > /commit.txt; \ + fi + FROM node:20-alpine # Install OpenLDAP and required packages. @@ -64,6 +94,9 @@ COPY nodejs/public ./public # level above the nodejs/ app dir). Without this the app crashes on startup. COPY tos.md /tos.md +# Baked commit hash from the gitinfo stage (see build_info.js). +COPY --from=gitinfo /commit.txt ./.build_commit + # Copy startup script COPY docker-entrypoint.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/docker-entrypoint.sh diff --git a/nodejs/utils/build_info.js b/nodejs/utils/build_info.js index 933679b..4cc3f83 100644 --- a/nodejs/utils/build_info.js +++ b/nodejs/utils/build_info.js @@ -1,15 +1,29 @@ 'use strict'; +const fs = require('fs'); +const path = require('path'); const { execSync } = require('child_process'); const { version: buildVersion } = require('../package.json'); -let buildHash = 'unknown'; -try { - buildHash = execSync('git rev-parse --short HEAD', { cwd: __dirname }).toString().trim(); -} catch (_) {} +// Docker builds bake the commit hash into ../.build_commit (see the gitinfo +// stage in Dockerfile.openldap) -- the final image has no git binary and no +// .git directory, so `git rev-parse` below always fails there. Bare-metal/dev +// runs have no baked file, so they fall back to asking git directly. +function readBuildHash() { + try { + const baked = fs.readFileSync(path.join(__dirname, '../.build_commit'), 'utf8').trim(); + if (baked) return baked; + } catch (_) {} + + try { + return execSync('git rev-parse --short HEAD', { cwd: __dirname }).toString().trim(); + } catch (_) { + return 'unknown'; + } +} module.exports = { buildVersion, - buildHash, + buildHash: readBuildHash(), buildYear: new Date().getFullYear(), };