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(), };