build-identity.ts94 lines · main
| 1 | import { readFileSync, statSync } from 'node:fs'; |
| 2 | import { resolve } from 'node:path'; |
| 3 | |
| 4 | /** |
| 5 | * Build identity helpers — shared across apps/api, apps/realtime, |
| 6 | * apps/runtime so every service answers /info identically. The resolver |
| 7 | * chain is: |
| 8 | * |
| 9 | * 1. BRIVEN_BUILD_SHA / BRIVEN_BUILD_AT — passed as Dockerfile ARGs by |
| 10 | * scripts/deploy-kvm4.sh. Preferred source for the deploy script |
| 11 | * path. |
| 12 | * 2. .git/HEAD inside the image — fallback for Dokploy auto-deploys |
| 13 | * which run `docker compose build` without --build-arg. Requires |
| 14 | * the .git dir to be in the build context (we removed it from |
| 15 | * .dockerignore for this reason). |
| 16 | * 3. The string "dev" — final fallback for fresh local checkouts / |
| 17 | * tests / anything where neither source resolves. |
| 18 | * |
| 19 | * "dev" is also treated as "unset" when seen in the env var because |
| 20 | * that's the ARG default in the Dockerfile — when Dokploy builds |
| 21 | * without passing the build-arg, the runtime env resolves to the |
| 22 | * literal string "dev", and we want the .git branch to fire then too. |
| 23 | */ |
| 24 | |
| 25 | const DEV_SENTINEL = 'dev'; |
| 26 | |
| 27 | function envValue(name: string): string | null { |
| 28 | const v = process.env[name]?.trim(); |
| 29 | return !v || v === DEV_SENTINEL ? null : v; |
| 30 | } |
| 31 | |
| 32 | /** |
| 33 | * Read the commit sha from .git/HEAD without shelling out to `git`. |
| 34 | * Handles three layouts: |
| 35 | * 1. detached HEAD — HEAD contains the sha directly |
| 36 | * 2. ref pointing at a loose — .git/refs/heads/<name> exists |
| 37 | * 3. ref pointing at a packed — entry lives in .git/packed-refs |
| 38 | * |
| 39 | * Returns null on any I/O failure so the caller can fall back to "dev". |
| 40 | */ |
| 41 | export function resolveShaFromGit(gitDir: string): string | null { |
| 42 | try { |
| 43 | const head = readFileSync(resolve(gitDir, 'HEAD'), 'utf8').trim(); |
| 44 | if (!head.startsWith('ref:')) return /^[0-9a-f]{40}$/.test(head) ? head : null; |
| 45 | const ref = head.slice(4).trim(); |
| 46 | try { |
| 47 | const sha = readFileSync(resolve(gitDir, ref), 'utf8').trim(); |
| 48 | if (/^[0-9a-f]{40}$/.test(sha)) return sha; |
| 49 | } catch { |
| 50 | // loose ref missing — try packed-refs |
| 51 | } |
| 52 | const packed = readFileSync(resolve(gitDir, 'packed-refs'), 'utf8'); |
| 53 | for (const line of packed.split('\n')) { |
| 54 | if (line.startsWith('#') || line.startsWith('^')) continue; |
| 55 | const [sha, name] = line.split(' '); |
| 56 | if (name === ref && sha && /^[0-9a-f]{40}$/.test(sha)) return sha; |
| 57 | } |
| 58 | return null; |
| 59 | } catch { |
| 60 | return null; |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | /** |
| 65 | * Build timestamp fallback — mtime of .git/HEAD inside the image. The |
| 66 | * mtime is updated whenever HEAD moves (checkout / commit / fetch+reset), |
| 67 | * so inside a freshly-built image it reflects when the docker build |
| 68 | * copied the .git tree. |
| 69 | */ |
| 70 | export function resolveBuildAtFromGit(gitDir: string): string | null { |
| 71 | try { |
| 72 | const stat = statSync(resolve(gitDir, 'HEAD')); |
| 73 | return new Date(stat.mtimeMs).toISOString(); |
| 74 | } catch { |
| 75 | return null; |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | export interface BuildIdentity { |
| 80 | buildSha: string; |
| 81 | buildAt: string; |
| 82 | } |
| 83 | |
| 84 | /** |
| 85 | * Resolve build identity for a service. Each app passes the path to |
| 86 | * its repo root's .git directory (typically two levels up from the |
| 87 | * runtime working dir — `apps/<name>` → `../../.git`). |
| 88 | */ |
| 89 | export function resolveBuildIdentity(repoRootGitDir: string): BuildIdentity { |
| 90 | const buildSha = envValue('BRIVEN_BUILD_SHA') ?? resolveShaFromGit(repoRootGitDir) ?? DEV_SENTINEL; |
| 91 | const buildAt = |
| 92 | envValue('BRIVEN_BUILD_AT') ?? resolveBuildAtFromGit(repoRootGitDir) ?? DEV_SENTINEL; |
| 93 | return { buildSha, buildAt }; |
| 94 | } |