From b3d1e121d64ddf2d5d169e50c326b4dbf5954b2a Mon Sep 17 00:00:00 2001 From: scanner Date: Tue, 6 Oct 2026 01:27:58 -0400 Subject: [PATCH 1/2] fix(build): fail prebuild when content sync fails unless DOCS_SYNC_OPTIONAL=1 Signed-off-by: scanner --- CONTRIBUTING.md | 5 ++++ changelog.d/fixed-prebuild-strict-sync.md | 1 + package.json | 2 +- scripts/run-sync-step.test.ts | 30 +++++++++++++++++++++ scripts/run-sync-step.ts | 33 +++++++++++++++++++++++ 5 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixed-prebuild-strict-sync.md create mode 100644 scripts/run-sync-step.test.ts create mode 100644 scripts/run-sync-step.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2bbb97a..9c20ae4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,6 +43,11 @@ Then open the local URL printed by Next.js, usually . For a production-style preview, run `npm run build` followed by `npm run start`. +`npm run build` fails if a doc-sync step in `prebuild` fails. When working +offline, set `DOCS_SYNC_OPTIONAL=1` (for example +`DOCS_SYNC_OPTIONAL=1 npm run build`) to keep the committed content instead. +CI and Netlify always use the strict behavior. + ## Code review requirements A PR merges once it passes the CI gates defined in `.github/workflows/` — Build, diff --git a/changelog.d/fixed-prebuild-strict-sync.md b/changelog.d/fixed-prebuild-strict-sync.md new file mode 100644 index 0000000..e068a97 --- /dev/null +++ b/changelog.d/fixed-prebuild-strict-sync.md @@ -0,0 +1 @@ +- Make `prebuild` fail the build when a content sync step fails instead of masking it with `|| echo`; set `DOCS_SYNC_OPTIONAL=1` to keep the offline fallback to committed content for local development. diff --git a/package.json b/package.json index 6dd5b66..c6555c4 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ }, "scripts": { "dev": "next dev --turbopack", - "prebuild": "(tsx scripts/update-meeting-recordings.ts || echo \"meeting recordings refresh unavailable - using committed data\") && (tsx scripts/sync-hive-docs.ts || echo \"hive docs sync unavailable - using committed content\") && (tsx scripts/sync-sibling-docs.ts || echo \"sibling docs sync unavailable - using committed content\") && tsx scripts/generate-shared-config.ts", + "prebuild": "tsx scripts/run-sync-step.ts scripts/update-meeting-recordings.ts && tsx scripts/run-sync-step.ts scripts/sync-hive-docs.ts && tsx scripts/run-sync-step.ts scripts/sync-sibling-docs.ts && tsx scripts/generate-shared-config.ts", "build": "next build", "start": "next start", "lint": "eslint src/ scripts/ netlify/", diff --git a/scripts/run-sync-step.test.ts b/scripts/run-sync-step.test.ts new file mode 100644 index 0000000..9dcfa94 --- /dev/null +++ b/scripts/run-sync-step.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it, vi } from 'vitest' +import type { spawnSync } from 'node:child_process' +import { isSyncOptional, runSyncStep } from './run-sync-step' + +const fake = (status: number | null) => + vi.fn(() => ({ status })) as unknown as typeof spawnSync + +describe('run-sync-step', () => { + it('is strict unless DOCS_SYNC_OPTIONAL=1', () => { + expect(isSyncOptional({})).toBe(false) + expect(isSyncOptional({ DOCS_SYNC_OPTIONAL: '0' })).toBe(false) + expect(isSyncOptional({ DOCS_SYNC_OPTIONAL: '1' })).toBe(true) + }) + + it('passes through success', () => { + expect(runSyncStep('a.ts', {}, fake(0))).toBe(0) + }) + + it('propagates failure by default', () => { + expect(runSyncStep('a.ts', {}, fake(1))).toBe(1) + expect(runSyncStep('a.ts', {}, fake(null))).toBe(1) + }) + + it('tolerates failure when opted in', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + expect(runSyncStep('a.ts', { DOCS_SYNC_OPTIONAL: '1' }, fake(1))).toBe(0) + expect(warn).toHaveBeenCalled() + warn.mockRestore() + }) +}) diff --git a/scripts/run-sync-step.ts b/scripts/run-sync-step.ts new file mode 100644 index 0000000..e6f3fe3 --- /dev/null +++ b/scripts/run-sync-step.ts @@ -0,0 +1,33 @@ +/** + * Runs one prebuild sync step (a tsx script) and propagates its exit code. + * Failures fail the build unless DOCS_SYNC_OPTIONAL=1 is set, in which case the + * committed content is kept and a warning is printed (local/offline use only). + */ +import { spawnSync } from 'node:child_process' + +export function isSyncOptional(env: NodeJS.ProcessEnv = process.env): boolean { + return env.DOCS_SYNC_OPTIONAL === '1' +} + +export function runSyncStep( + script: string, + env: NodeJS.ProcessEnv = process.env, + run: typeof spawnSync = spawnSync, +): number { + const result = run('tsx', [script], { stdio: 'inherit', env, shell: process.platform === 'win32' }) + const status = result.status ?? 1 + if (status !== 0 && isSyncOptional(env)) { + console.warn(`${script} failed (exit ${status}); DOCS_SYNC_OPTIONAL=1 so using committed content`) + return 0 + } + return status +} + +if (process.argv[1]?.endsWith('run-sync-step.ts')) { + const script = process.argv[2] + if (!script) { + console.error('usage: tsx scripts/run-sync-step.ts