#!/usr/bin/env node import { execFileSync } from 'node:child_process' function fail(message) { console.error(`release_components=FAIL ${message}`) process.exit(1) } const base = process.argv[2] || '' const head = process.argv[3] || 'HEAD' if (!/^[0-9a-f]{40}$/.test(base)) { fail('base must be a full lowercase Git SHA') } function git(args) { try { return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], }).trim() } catch (error) { const stderr = error.stderr?.toString().trim() fail(stderr ? `git_error=${JSON.stringify(stderr)}` : 'git command failed') } } const resolvedBase = git(['rev-parse', '--verify', `${base}^{commit}`]) const resolvedHead = git(['rev-parse', '--verify', `${head}^{commit}`]) if (resolvedBase !== base) fail('base did not resolve exactly') if (!/^[0-9a-f]{40}$/.test(resolvedHead)) fail('head did not resolve to a commit') try { execFileSync('git', ['merge-base', '--is-ancestor', base, resolvedHead], { stdio: 'ignore', }) } catch { fail('base is not an ancestor of head') } const changed = git([ 'diff', '--name-only', '--no-renames', `${base}..${resolvedHead}`, ]).split('\n').filter(Boolean) const apiPatterns = [ /^apps\/api\//, /^go\.work(?:\.sum)?$/, ] const webPatterns = [ /^apps\/web\//, /^packages\/contracts\//, /^pnpm-lock\.yaml$/, /^pnpm-workspace\.yaml$/, /^nx\.json$/, /^package\.json$/, /^docker\/nginx\.conf$/, ] const sharedRuntimePatterns = [ /^\.dockerignore$/, /^docker-compose(?:\.[^.]+)?\.ya?ml$/, ] function dockerfileSections(ref) { let contents try { contents = execFileSync('git', ['show', `${ref}:Dockerfile`], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], }) } catch { return null } const sections = new Map([['global', []]]) let section = 'global' for (const line of contents.split('\n')) { const match = line.match(/^FROM\s+.+?\s+AS\s+([A-Za-z0-9._-]+)(?:\s|$)/i) if (match) section = match[1].toLowerCase() if (!sections.has(section)) sections.set(section, []) sections.get(section).push(line) } return sections } function sectionChanged(before, after, name) { return (before.get(name) || []).join('\n') !== (after.get(name) || []).join('\n') } function classifyDockerfile() { const before = dockerfileSections(base) const after = dockerfileSections(resolvedHead) if (!before || !after) return { apiChanged: true, webChanged: true } const knownApi = new Set(['api-builder', 'api']) const knownWeb = new Set(['web-builder', 'web']) let apiChanged = false let webChanged = false const beforeGlobal = before.get('global') || [] const afterGlobal = after.get('global') || [] const globalLines = (lines, pattern) => lines.filter((line) => pattern.test(line)).join('\n') const apiGlobal = /\b(?:GO_VERSION|GO_BUILD_IMAGE|API_RUNTIME_IMAGE)\b/ const webGlobal = /\b(?:NODE_VERSION|NODE_BUILD_IMAGE|WEB_RUNTIME_IMAGE)\b/ if (globalLines(beforeGlobal, apiGlobal) !== globalLines(afterGlobal, apiGlobal)) apiChanged = true if (globalLines(beforeGlobal, webGlobal) !== globalLines(afterGlobal, webGlobal)) webChanged = true const sharedGlobal = (lines) => lines.filter((line) => !apiGlobal.test(line) && !webGlobal.test(line)).join('\n') if (sharedGlobal(beforeGlobal) !== sharedGlobal(afterGlobal)) { apiChanged = true webChanged = true } const sectionNames = new Set([...before.keys(), ...after.keys()]) sectionNames.delete('global') for (const name of sectionNames) { if (!sectionChanged(before, after, name)) continue if (knownApi.has(name)) apiChanged = true else if (knownWeb.has(name)) webChanged = true else { apiChanged = true webChanged = true } } return { apiChanged, webChanged } } let api = false let web = false let migrationsChanged = false if (changed.includes('Dockerfile')) { const dockerfile = classifyDockerfile() api = dockerfile.apiChanged web = dockerfile.webChanged } for (const file of changed) { if (file === 'Dockerfile') continue if (/^apps\/api\/migrations\//.test(file)) migrationsChanged = true if (sharedRuntimePatterns.some((pattern) => pattern.test(file))) { api = true web = true continue } if (apiPatterns.some((pattern) => pattern.test(file))) api = true if (webPatterns.some((pattern) => pattern.test(file))) web = true } const components = api && web ? 'all' : api ? 'api' : web ? 'web' : 'none' console.log(JSON.stringify({ base, head: resolvedHead, components, migrationsChanged, changedFiles: changed, }))