diff --git a/.gitignore b/.gitignore index de2f95f370..0a704f17c4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ node_modules dist out build +!website/build/ +!website/build/sites-vite-plugin.ts .open-next .env diff --git a/tests/baseline/ross-website-contract.test.mjs b/tests/baseline/ross-website-contract.test.mjs index 42c160cb93..18618b4b09 100644 --- a/tests/baseline/ross-website-contract.test.mjs +++ b/tests/baseline/ross-website-contract.test.mjs @@ -12,6 +12,7 @@ test("the public website is a separate build target", () => { const rootPackage = json("package.json"); const websitePackage = json("website/package.json"); assert.equal(existsSync(resolve(root, "website/.openai/hosting.json")), true); + assert.equal(existsSync(resolve(root, "website/build/sites-vite-plugin.ts")), true); assert.equal(websitePackage.name, "ross-ontario"); assert.match(rootPackage.scripts["install:all"], /--prefix website/); assert.match(rootPackage.scripts.build, /build:website/); @@ -19,6 +20,32 @@ test("the public website is a separate build target", () => { assert.match(rootPackage.scripts.check, /test:website/); }); +test("the website build plugin is deliberately tracked", () => { + const ignoreRules = read(".gitignore"); + assert.match(ignoreRules, /!website\/build\//); + assert.match(ignoreRules, /!website\/build\/sites-vite-plugin\.ts/); + assert.match(read("website/vite.config.ts"), /\.\/build\/sites-vite-plugin/); +}); + +test("website shell helpers survive browser uploads without executable modes", () => { + const wrappers = [ + "website/scripts/build-verified.sh", + "website/scripts/install-ci.sh", + "website/scripts/validate-artifact.sh", + ]; + for (const wrapper of wrappers) { + assert.match( + read(wrapper), + /exec bash "\$\{script_dir\}\/sites-env\.sh" -- bash "\$0" "\$@"/, + wrapper, + ); + } + assert.match( + read("website/scripts/build-verified.sh"), + /bash "\$\{script_dir\}\/validate-artifact\.sh"/, + ); +}); + test("the public website derives identity and policy from central configuration", () => { const central = json("config/ross-brand.json"); const siteConfig = read("website/app/site-config.ts"); diff --git a/website/build/sites-vite-plugin.ts b/website/build/sites-vite-plugin.ts new file mode 100644 index 0000000000..26563fdd77 --- /dev/null +++ b/website/build/sites-vite-plugin.ts @@ -0,0 +1,45 @@ +import { access, cp, mkdir, rm } from "node:fs/promises"; +import { resolve } from "node:path"; +import type { Plugin } from "vite"; + +async function exists(path: string): Promise { + try { + await access(path); + return true; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return false; + } + throw error; + } +} + +// Packages Sites metadata and migrations after Vite finishes compiling. +export function sites(): Plugin { + let root = process.cwd(); + + return { + name: "sites", + apply: "build", + configResolved(config) { + root = config.root; + }, + async closeBundle() { + const outputDirectory = resolve(root, "dist", ".openai"); + const hostingConfig = resolve(root, ".openai", "hosting.json"); + const drizzleSource = resolve(root, "drizzle"); + + await rm(outputDirectory, { recursive: true, force: true }); + await mkdir(outputDirectory, { recursive: true }); + + if (await exists(hostingConfig)) { + await cp(hostingConfig, resolve(outputDirectory, "hosting.json")); + } + if (await exists(drizzleSource)) { + await cp(drizzleSource, resolve(outputDirectory, "drizzle"), { + recursive: true, + }); + } + }, + }; +} diff --git a/website/scripts/build-verified.sh b/website/scripts/build-verified.sh index 2368b5a94d..1dea9ee58a 100644 --- a/website/scripts/build-verified.sh +++ b/website/scripts/build-verified.sh @@ -4,7 +4,8 @@ set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ "${SITES_ENV_READY:-}" != "1" ]]; then - exec "${script_dir}/sites-env.sh" -- "$0" "$@" + # Browser-based GitHub uploads do not preserve executable file modes. + exec bash "${script_dir}/sites-env.sh" -- bash "$0" "$@" fi command -v timeout >/dev/null || { @@ -25,4 +26,4 @@ timeout \ "${SITES_BUILD_TIMEOUT:-3m}" \ "${vinext}" build -"${script_dir}/validate-artifact.sh" +bash "${script_dir}/validate-artifact.sh" diff --git a/website/scripts/install-ci.sh b/website/scripts/install-ci.sh index 7a22fd4631..30ca9d41c3 100644 --- a/website/scripts/install-ci.sh +++ b/website/scripts/install-ci.sh @@ -4,7 +4,8 @@ set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ "${SITES_ENV_READY:-}" != "1" ]]; then - exec "${script_dir}/sites-env.sh" -- "$0" "$@" + # Browser-based GitHub uploads do not preserve executable file modes. + exec bash "${script_dir}/sites-env.sh" -- bash "$0" "$@" fi command -v flock >/dev/null || { diff --git a/website/scripts/validate-artifact.sh b/website/scripts/validate-artifact.sh index 4eb9041c48..dc2532f788 100644 --- a/website/scripts/validate-artifact.sh +++ b/website/scripts/validate-artifact.sh @@ -4,7 +4,8 @@ set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ "${SITES_ENV_READY:-}" != "1" ]]; then - exec "${script_dir}/sites-env.sh" -- "$0" "$@" + # Browser-based GitHub uploads do not preserve executable file modes. + exec bash "${script_dir}/sites-env.sh" -- bash "$0" "$@" fi worker="${SITES_PROJECT_ROOT}/dist/server/index.js"