From 10560c144649d97af915f04a39a229cdde190cb2 Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Sun, 16 Aug 2026 18:46:21 -0400 Subject: [PATCH] fix(version): resolve git metadata from env/SOURCE_COMMIT for Coolify builds Coolify strips .git from the build context, so git CLI calls in generate:version-info fail during Docker builds. The script now resolves metadata in priority order: GIT_* env overrides, then SOURCE_COMMIT (Coolify's injected build arg, shortened to 7 chars), then git CLI fallback for local builds. Dockerfile passes the ARGs through as ENV before the build; DEPLOYMENT.md documents the required 'Include Source Commit in Build' setting. --- DEPLOYMENT.md | 2 ++ Dockerfile | 29 +++++++++++++++++++++--- src/scripts/generateVersionInfo.ts | 36 ++++++++++++++++++++++++------ 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 99e3294..ed5f590 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -205,6 +205,8 @@ Recommended flow: 4. Coolify waits for `/api/health` to come back 200, then routes traffic. 5. If a new migration shipped with that deploy, run it via `docker exec` or a Scheduled Task (Sec. 7). +> **Version metadata in builds.** The image build embeds git info into `src/generated/versionInfo.json` (shown in the admin VersionOverlay and on `/api/version`). Coolify **deletes `.git`** from the build context, so the build reads the commit SHA from the `SOURCE_COMMIT` build arg instead of `git` (see the Dockerfile's `ARG SOURCE_COMMIT` block). For that to work, enable **"Include Source Commit in Build"** under the application's *Advanced* settings (and leave "Inject Build Args to Dockerfile" on, which is the default) — otherwise the version overlay falls back to `version` only. Tags, commit date, and commit subject have no Coolify equivalent and stay null on Coolify builds; local builds (`docker build`, `bun run dev`) still use real `git`. + No SSH, no `rsync`, no `systemctl`. The legacy `build/deploy.sh` (gitignored, the old SSH+systemd flow to `jmf-usrv-2404`) is **no longer used** — exclude it from future deploys. `bun run deploy` (the npm script) will still try to run it (it calls `postversion → bash ./build/deploy.sh`); don't run it from your local machine anymore, or remove that script entry from `package.json` once you've moved onto Coolify for good. --- diff --git a/Dockerfile b/Dockerfile index 4ee4748..95eb61b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,9 +62,11 @@ RUN for i in 1 2 3; do \ # Full install (incl. devDeps) + Next.js standalone build. FROM base AS builder # `git` is needed only at build time so `generate:version-info` can populate -# commitHash / gitDescribe / commitDate in src/generated/versionInfo.json -# (the script gracefully no-ops without it, but we want the metadata in the -# admin VersionOverlay). Not carried into the runner image. +# commitHash / gitDescribe / commitDate in src/generated/versionInfo.json. It +# is the LOCAL-build fallback: Coolify strips `.git` from the build context, +# so there the script reads the commit SHA from the SOURCE_COMMIT build arg +# instead (see the ARG/ENV block above `RUN bun run build`). Not carried into +# the runner image. RUN apk add --no-cache git WORKDIR /app COPY package.json bun.lock ./ @@ -101,6 +103,27 @@ ARG EMAIL_PORT_BUILD=587 ENV PAYLOAD_SECRET=$PAYLOAD_SECRET_BUILD \ EMAIL_HOST=$EMAIL_HOST_BUILD \ EMAIL_PORT=$EMAIL_PORT_BUILD +# Git metadata passthrough for `generate:version-info` (the script reads env +# vars first). Coolify strips `.git` from the build context, so builds there +# must get the commit SHA via build args: Coolify injects `ARG +# SOURCE_COMMIT=` into the Dockerfile after each FROM only when "Include +# Source Commit in Build" is enabled (application settings → Advanced) and +# "Inject Build Args to Dockerfile" is on (default). The bare `ARG` +# declarations below preserve that injected value (no `=default` reset), and +# the `ENV` forwards it to `bun run build`. The GIT_* args are optional +# overrides for other build environments; none of this ships to runtime. +ARG SOURCE_COMMIT +ARG GIT_TAG +ARG GIT_DESCRIBE +ARG GIT_COMMIT_HASH +ARG GIT_COMMIT_DATE +ARG GIT_COMMIT_SUBJECT +ENV SOURCE_COMMIT=$SOURCE_COMMIT \ + GIT_TAG=$GIT_TAG \ + GIT_DESCRIBE=$GIT_DESCRIBE \ + GIT_COMMIT_HASH=$GIT_COMMIT_HASH \ + GIT_COMMIT_DATE=$GIT_COMMIT_DATE \ + GIT_COMMIT_SUBJECT=$GIT_COMMIT_SUBJECT RUN bun run build # ─────────────────────────── runner ────────────────────────────── diff --git a/src/scripts/generateVersionInfo.ts b/src/scripts/generateVersionInfo.ts index 7fb47be..9a271b7 100644 --- a/src/scripts/generateVersionInfo.ts +++ b/src/scripts/generateVersionInfo.ts @@ -4,8 +4,20 @@ * VersionOverlay via GET /api/version (the file is read at runtime so a fresh * post-bump regeneration rsync'd to the server shows up without a rebuild). * - * Gracefully no-ops on missing git metadata (no .git, no tags) so the JSON - * always exists with whatever fields are resolvable. + * Git metadata resolution order (first non-empty wins): + * 1. Environment variables — explicit overrides for CI systems that inject + * metadata as env vars: + * GIT_TAG, GIT_DESCRIBE, GIT_COMMIT_HASH, GIT_COMMIT_DATE, GIT_COMMIT_SUBJECT + * 2. SOURCE_COMMIT — the commit SHA Coolify injects as a build arg when + * "Include Source Commit in Build" is enabled (application settings → + * Advanced). Coolify strips `.git` from the build context, so `git` is + * unavailable there. Only the SHA is provided — tags, date, and subject + * have no Coolify equivalent and stay null in that case. + * 3. `git` from the project root — the local-dev path (and any build context + * that still contains .git, e.g. a local `docker build`). + * + * Gracefully no-ops on missing git metadata (no .git, no tags, no env) so the + * JSON always exists with whatever fields are resolvable. */ import { execSync } from "node:child_process"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; @@ -25,6 +37,15 @@ function git(args: string): string | null { } } +function envVar(name: string): string | null { + const value = process.env[name]; + return typeof value === "string" && value.length > 0 ? value : null; +} + +function shortCommit(sha: string): string { + return sha.length > 7 ? sha.slice(0, 7) : sha; +} + function readPackageVersion(): string | null { try { const pkg = JSON.parse(readFileSync(path.join(projectRoot, "package.json"), "utf8")); @@ -36,11 +57,12 @@ function readPackageVersion(): string | null { function main(): void { const version = readPackageVersion(); - const tag = git("describe --tags --always --abbrev=0"); - const gitDescribe = git("describe --tags --always --abbrev=7"); - const commitHash = git("rev-parse --short HEAD"); - const commitDate = git("log -1 --format=%cI"); - const commitSubject = git("log -1 --format=%s"); + const tag = envVar("GIT_TAG") ?? git("describe --tags --always --abbrev=0"); + const gitDescribe = envVar("GIT_DESCRIBE") ?? git("describe --tags --always --abbrev=7"); + const rawCommit = envVar("GIT_COMMIT_HASH") ?? envVar("SOURCE_COMMIT"); + const commitHash = rawCommit ? shortCommit(rawCommit) : git("rev-parse --short HEAD"); + const commitDate = envVar("GIT_COMMIT_DATE") ?? git("log -1 --format=%cI"); + const commitSubject = envVar("GIT_COMMIT_SUBJECT") ?? git("log -1 --format=%s"); const buildTime = new Date().toISOString(); const info = {