#!/usr/bin/env node /** * ci-build-brand.mjs — assemble ONE brand's Hugo build directory in CI and * (optionally) build it. Portable (Linux runner), no hardcoded paths. * * It merges three sources into a clean build dir: * 1. the TEMPLATE repo (engine): layouts/, assets/, static/, i18n/en.yaml, * package.json(+lock), tailwind.config.js, postcss.config.js, and the * VERIFIED, human-maintained data/locales.yaml + data/payment_methods.yaml * (these are never LLM-generated and must come from the template). * 2. the brand overlay from the content repo: repo//{content,data,i18n,hugo.toml} * 3. the template's verified data/ files are copied AFTER the brand overlay so * they always win over anything the pipeline emitted. * * Usage: * node ci-build-brand.mjs \ * --template \ * --brand-dir > \ * --out \ * [--base-url https://.lilsus.fun/] * * Env fallbacks: TEMPLATE_DIR, BRAND_DIR, BUILD_DIR, BASE_URL. * Exits non-zero on any missing required input so CI fails loudly. */ import { existsSync, rmSync, mkdirSync, cpSync, copyFileSync, readdirSync } from 'node:fs'; import { join, basename } from 'node:path'; function arg(name, envName) { const i = process.argv.indexOf('--' + name); if (i !== -1 && process.argv[i + 1]) return process.argv[i + 1]; if (envName && process.env[envName]) return process.env[envName]; return undefined; } const TEMPLATE = arg('template', 'TEMPLATE_DIR'); const BRAND_DIR = arg('brand-dir', 'BRAND_DIR'); const OUT = arg('out', 'BUILD_DIR'); const BASE_URL = arg('base-url', 'BASE_URL'); // optional; only echoed as a hint function die(msg) { console.error('ci-build-brand: ' + msg); process.exit(1); } if (!TEMPLATE || !existsSync(TEMPLATE)) die('missing/invalid --template (TEMPLATE_DIR): ' + TEMPLATE); if (!BRAND_DIR || !existsSync(BRAND_DIR)) die('missing/invalid --brand-dir (BRAND_DIR): ' + BRAND_DIR); if (!OUT) die('missing --out (BUILD_DIR)'); if (!existsSync(join(BRAND_DIR, 'hugo.toml'))) die('brand dir has no hugo.toml: ' + BRAND_DIR); const brand = basename(BRAND_DIR); console.log(`ci-build-brand: brand=${brand}`); console.log(` template = ${TEMPLATE}`); console.log(` brandDir = ${BRAND_DIR}`); console.log(` out = ${OUT}`); // Fresh build dir. rmSync(OUT, { recursive: true, force: true }); mkdirSync(OUT, { recursive: true }); // 1. Template engine dirs + files. const TEMPLATE_DIRS = ['layouts', 'assets', 'static', 'i18n', 'scripts']; for (const d of TEMPLATE_DIRS) { const s = join(TEMPLATE, d); if (existsSync(s)) cpSync(s, join(OUT, d), { recursive: true }); } const TEMPLATE_FILES = ['package.json', 'package-lock.json', 'tailwind.config.js', 'postcss.config.js']; for (const f of TEMPLATE_FILES) { const s = join(TEMPLATE, f); if (existsSync(s)) copyFileSync(s, join(OUT, f)); } // 2. Brand overlay: content, data, i18n, hugo.toml. for (const d of ['content', 'data', 'i18n']) { const s = join(BRAND_DIR, d); if (existsSync(s)) cpSync(s, join(OUT, d), { recursive: true }); } copyFileSync(join(BRAND_DIR, 'hugo.toml'), join(OUT, 'hugo.toml')); // Drop the human-note marker if the pipeline emitted it (not valid Hugo data). rmSync(join(OUT, 'data', 'NOTE-locales-payments.md'), { force: true }); // 3. VERIFIED data that must come from the template and always win. mkdirSync(join(OUT, 'data'), { recursive: true }); for (const f of ['locales.yaml', 'payment_methods.yaml']) { const s = join(TEMPLATE, 'data', f); if (existsSync(s)) copyFileSync(s, join(OUT, 'data', f)); else console.warn(` WARN: template is missing data/${f} — RG/payment partials may fail`); } // Report. function countMd(dir) { if (!existsSync(dir)) return 0; return readdirSync(dir).filter((f) => f.endsWith('.md')).length; } console.log(` data files: ${existsSync(join(OUT, 'data')) ? readdirSync(join(OUT, 'data')).join(',') : 'none'}`); console.log(` content top-level .md files: ${countMd(join(OUT, 'content'))}`); console.log(`ci-build-brand: build dir ready at ${OUT}`); if (BASE_URL) console.log(` (build with: hugo --source "${OUT}" --gc --minify --baseURL "${BASE_URL}")`);