feat(docker): bake git provenance into image via build-args + export from docker-run#866
Conversation
…from docker-run Closes the gap left by PR #864/#865: src/utilities/nodeVersion.ts already reads process.env.GIT_COMMIT/GIT_BRANCH/GIT_DIRTY when present, but nothing on the docker path was supplying them. The result was that getNetworkInfo.nodeVersion came back with commit: null on every Docker- booted node, defeating the original "is this host running the binary I think it is?" diagnostic value. Three pieces wire it end-to-end: 1) Dockerfile (runtime stage): ARG GIT_COMMIT/GIT_BRANCH/GIT_DIRTY/ BUILT_AT, then re-export each via ENV so the running node sees them in process.env. Defaults are empty strings — a build without the args (manual `docker build .`) still produces a runnable image, nodeVersion just surfaces null for the missing fields. 2) docker-compose.yml (node service build block): args passes the four values straight through with `:-` defaults, so any caller that exported them (the wrapper below, CI, an operator's shell) gets them baked in; anyone who didn't gets an empty-string ARG and the same null-field fallback. 3) scripts/docker-run: detects the working tree's git status with defensive guards (no-repo, no-git-binary, dirty-vs-clean) and exports the four variables before any `docker compose build`/`up`. `git diff-index --quiet HEAD` exit code → "true"/"false" string the nodeVersion module already parses. BUILT_AT is the UTC ISO-8601 timestamp at invocation. Operator-visible effect: after `./scripts/docker-run --rebuild -d`, curl getNetworkInfo and the response carries the real commit SHA, branch name, dirty flag, and build timestamp the image was made from. That answers "did this host actually pick up the fix I merged?" with a one-liner. Manual repro paths still work: docker compose build --build-arg GIT_COMMIT=$(git rev-parse HEAD) GIT_COMMIT=abc123 docker compose build
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
Warning Review limit reached
More reviews will be available in 28 minutes and 57 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR closes the gap where Docker-booted nodes always returned
Confidence Score: 4/5Safe to merge; the changes are additive and all fallback paths produce a runnable image with null provenance fields rather than a broken build. The Dockerfile and Compose changes are straightforward and well-guarded. The docker-run script has a small redundancy (recomputing the repo root that already exists as REPO_ROOT) and a behaviour quirk where CI checkouts in detached HEAD state will export HEAD as the branch name rather than null, which surfaces verbatim in the getNetworkInfo API response and could confuse operators trying to identify the source branch of a running node. scripts/docker-run — the detached HEAD branch-name behaviour and the GIT_REPO_DIR redundancy are both in the new provenance block. Important Files Changed
Sequence DiagramsequenceDiagram
participant Host as Host Shell
participant Script as scripts/docker-run
participant Git as git CLI
participant Compose as docker compose
participant Dockerfile as Dockerfile (runtime stage)
participant Node as Node Process
Host->>Script: ./scripts/docker-run --rebuild -d
Script->>Git: rev-parse HEAD
Git-->>Script: GIT_COMMIT (40-char SHA)
Script->>Git: rev-parse --abbrev-ref HEAD
Git-->>Script: GIT_BRANCH (or HEAD if detached)
Script->>Git: diff-index --quiet HEAD
Git-->>Script: exit 0 clean / 1 dirty
Script->>Script: export GIT_COMMIT GIT_BRANCH GIT_DIRTY BUILT_AT
Script->>Compose: docker compose build --no-cache node
Compose->>Dockerfile: --build-arg GIT_COMMIT GIT_BRANCH GIT_DIRTY BUILT_AT
Dockerfile->>Dockerfile: ARG to ENV baked into image layer
Compose->>Node: docker compose up
Node->>Node: nodeVersion.ts reads env vars
Reviews (1): Last reviewed commit: "feat(docker): bake git provenance into i..." | Re-trigger Greptile |
| if git -C "$(dirname "${BASH_SOURCE[0]}")/.." rev-parse --git-dir >/dev/null 2>&1; then | ||
| GIT_REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | ||
| export GIT_COMMIT="$(git -C "$GIT_REPO_DIR" rev-parse HEAD 2>/dev/null || true)" | ||
| export GIT_BRANCH="$(git -C "$GIT_REPO_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" | ||
| if git -C "$GIT_REPO_DIR" diff-index --quiet HEAD 2>/dev/null; then | ||
| export GIT_DIRTY="false" | ||
| else | ||
| export GIT_DIRTY="true" | ||
| fi | ||
| fi |
There was a problem hiding this comment.
The block recomputes the repository root using
${BASH_SOURCE[0]} when REPO_ROOT has already been resolved to the exact same path (via $0) at line 52 of the script. The two expansions resolve identically for a directly-executed script. Using REPO_ROOT directly removes the redundant subshell and keeps one canonical root variable throughout the script.
| if git -C "$(dirname "${BASH_SOURCE[0]}")/.." rev-parse --git-dir >/dev/null 2>&1; then | |
| GIT_REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | |
| export GIT_COMMIT="$(git -C "$GIT_REPO_DIR" rev-parse HEAD 2>/dev/null || true)" | |
| export GIT_BRANCH="$(git -C "$GIT_REPO_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" | |
| if git -C "$GIT_REPO_DIR" diff-index --quiet HEAD 2>/dev/null; then | |
| export GIT_DIRTY="false" | |
| else | |
| export GIT_DIRTY="true" | |
| fi | |
| fi | |
| if git -C "$REPO_ROOT" rev-parse --git-dir >/dev/null 2>&1; then | |
| export GIT_COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || true)" | |
| export GIT_BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" | |
| if git -C "$REPO_ROOT" diff-index --quiet HEAD 2>/dev/null; then | |
| export GIT_DIRTY="false" | |
| else | |
| export GIT_DIRTY="true" | |
| fi | |
| fi |
| if git -C "$(dirname "${BASH_SOURCE[0]}")/.." rev-parse --git-dir >/dev/null 2>&1; then | ||
| GIT_REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | ||
| export GIT_COMMIT="$(git -C "$GIT_REPO_DIR" rev-parse HEAD 2>/dev/null || true)" | ||
| export GIT_BRANCH="$(git -C "$GIT_REPO_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" |
There was a problem hiding this comment.
Detached HEAD produces
"HEAD" as branch name in API output
git rev-parse --abbrev-ref HEAD returns the literal string "HEAD" when the repo is in a detached HEAD state — which is the norm for GitHub Actions, GitLab CI, and most CI/CD checkouts for PRs or tag builds. That string passes the || null guard in nodeVersion.ts ("HEAD" is truthy), so the getNetworkInfo response surfaces "branch": "HEAD" instead of null, which is indistinguishable from an actual branch named HEAD and offers no useful diagnostic info. Consider filtering the value before export: [[ "$GIT_BRANCH" == "HEAD" ]] && unset GIT_BRANCH (or export empty string) to let the module fall through to null.
Summary
Closes the gap left by #864 + #865.
src/utilities/nodeVersion.tsalready readsprocess.env.GIT_COMMIT/GIT_BRANCH/GIT_DIRTYwhen present, but nothing on the Docker path was supplying them — sogetNetworkInfo.nodeVersioncame back withcommit: nullon every Docker-booted node, defeating the diagnostic value of the whole endpoint.Three pieces
ARG GIT_COMMIT/GIT_BRANCH/GIT_DIRTY/BUILT_AT, re-exported viaENVso the running node sees them inprocess.env. Empty-string defaults — a manualdocker build .without args still produces a runnable image;nodeVersionsurfacesnullfor the missing fields.build.args): passes the four values through with:-defaults. Any caller that exported them gets them baked in; anyone who didn't gets the null fallback.docker compose build/up.git diff-index --quiet HEADexit →"true"/"false"string the node module parses.BUILT_ATis the UTC ISO-8601 timestamp at invocation.Operator-visible effect
After
./scripts/docker-run --rebuild -d:{ "name": "demos-node-software", "version": "0.9.8", "commit": "<actual SHA>", "commitShort": "abc1234", "branch": "stabilisation", "dirty": false, "builtAt": "2026-05-26T14:55:00Z" }Answers "did this host actually pick up the fix I merged?" in one line.
Manual override paths still work
docker compose build --build-arg GIT_COMMIT=$(git rev-parse HEAD) GIT_COMMIT=abc123 docker compose buildTest plan
docker-runscript: hand-traced env export logic; defensive fallbacks for no-repo / no-git/binary./scripts/docker-run --rebuild -don dev.node2 + curl'inggetNetworkInfoagain