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..d321f57 --- /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..3cb40ba --- /dev/null +++ b/scripts/run-sync-step.ts @@ -0,0 +1,43 @@ +/** + * 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"; + +// Plain record rather than NodeJS.ProcessEnv: Next.js augments ProcessEnv with +// a required NODE_ENV, which makes object literals in tests unassignable. +export type SyncEnv = Record; + +export function isSyncOptional(env: SyncEnv = process.env): boolean { + return env.DOCS_SYNC_OPTIONAL === "1"; +} + +export function runSyncStep( + script: string, + env: SyncEnv = process.env, + run: typeof spawnSync = spawnSync +): number { + const result = run("tsx", [script], { + stdio: "inherit", + env: env as NodeJS.ProcessEnv, + 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