From a0df92bfd141d4019586dba2aa28d6dcfcce4184 Mon Sep 17 00:00:00 2001 From: Istvan Matejcsok <119620946+matejcsok-ee@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:50:49 +0200 Subject: [PATCH] deploy: backed editor at editor.pcbjam.com (remote mode, same CDN WASM) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New release.yml job `deploy-editor` (parallel to deploy-demo, same per-tag WASM manifest): builds the standalone in REMOTE mode via the new scripts/deploy/build-editor.mjs — projects/libs/auth from the closed API (VITE_API_BASE_URL=https://api.pcbjam.com, VITE_LIBS_SOURCE=synced), Yjs board rooms through the API host's path route (VITE_YJS_ENDPOINT=api origin, doc source ydoc) — and ships it to the pcbjam-editor Pages project. The demo deploy is unchanged. The closed stack itself (api./app.pcbjam.com) deploys from pcbjam-private; when both sides change, that repo tags first. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014dasZuqo6FStgT3rkC85im --- .github/workflows/release.yml | 49 ++++++++++ scripts/deploy/build-editor.mjs | 152 ++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 scripts/deploy/build-editor.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd0f1aa..d308a32 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,6 +35,11 @@ env: LIB_TAG: "10.0.3" PAGES_PROJECT: pcbjam-demo PAGES_PROD_BRANCH: production + # Backed editor deployment (remote mode against the closed API). The closed + # stack (api./app.pcbjam.com) deploys from the pcbjam-private repo; release + # order when both change: pcbjam-private first, then this repo. + EDITOR_PAGES_PROJECT: pcbjam-editor + EDITOR_API_BASE: https://api.pcbjam.com CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} WRANGLER_CMD: npx --yes wrangler@4 @@ -133,3 +138,47 @@ jobs: --project-name "$PAGES_PROJECT" --branch "$PAGES_PROD_BRANCH" --commit-dirty=true + + # 4) Backed editor (editor.pcbjam.com): the SAME standalone, built in remote + # mode against the closed API (pcbjam-private's api.pcbjam.com) instead of + # the static gallery. Reuses this tag's CDN WASM manifest — no extra WASM + # work. Runs in parallel with deploy-demo (separate runner, own dist/). + deploy-editor: + needs: [meta, publish-wasm] + runs-on: ubuntu-latest + env: + RELEASE_TAG: ${{ needs.meta.outputs.tag }} + steps: + - uses: actions/checkout@v4 + with: + submodules: false + - name: Init pcbjam-shared submodule + run: git submodule update --init --depth 1 web/pcbjam-shared + - uses: pnpm/action-setup@v4 + with: + version: 10.33.0 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + cache-dependency-path: web/pnpm-lock.yaml + - name: Install standalone workspace + run: pnpm --dir web install --frozen-lockfile + + - name: Build editor (remote mode) + run: > + node scripts/deploy/build-editor.mjs --tag "$RELEASE_TAG" + --cdn "$CDN" --api-base "$EDITOR_API_BASE" + + - name: Ensure Pages project exists + run: > + npx --yes wrangler@4 pages project create "$EDITOR_PAGES_PROJECT" + --production-branch "$PAGES_PROD_BRANCH" + || echo "pages project create skipped (already exists)" + + - name: Deploy to Cloudflare Pages + run: > + npx --yes wrangler@4 pages deploy web/standalone/dist + --project-name "$EDITOR_PAGES_PROJECT" + --branch "$PAGES_PROD_BRANCH" + --commit-dirty=true diff --git a/scripts/deploy/build-editor.mjs b/scripts/deploy/build-editor.mjs new file mode 100644 index 0000000..61406cd --- /dev/null +++ b/scripts/deploy/build-editor.mjs @@ -0,0 +1,152 @@ +#!/usr/bin/env node +// Build the GPL standalone for the BACKED editor deployment (editor.pcbjam.com): +// remote mode against the closed API (projects, libs, auth session cookie) with +// Yjs board rooms on the same API host (path-routed to the sync worker), pinned +// to the same CDN WASM root + per-tag manifest as the demo. The mirror of the +// closed repo's scripts/dev-all.mjs standalone env, with prod origins. +// +// node scripts/deploy/build-editor.mjs --tag v1.2.3 --api-base https://api.pcbjam.com +// +// Unlike build-demo.mjs there is NO static gallery, NO IDB project layer and NO +// CDN libs pin: projects and libraries come from the backend (VITE_LIBS_SOURCE= +// synced → the server's r2-idb-sync bridge). WASM still comes from the CDN. + +import { execFileSync } from "node:child_process"; +import { + copyFileSync, + existsSync, + lstatSync, + renameSync, + rmSync, +} from "node:fs"; +import { join, resolve } from "node:path"; + +function parseArgs(argv) { + const a = { + tag: null, + cdn: "https://cdn.pcbjam.com", + apiBase: null, + repo: "https://github.com/emergence-engineering/pcbjam", + // Yjs endpoint: defaults to the API origin — board rooms are path-routed + // (`/parties/board-room/*`) to the sync worker on the same hostname, so the + // same-site session cookie rides the WS handshake. + yjsEndpoint: null, + // kicad-packages3D snapshot (libs/kicad-models//); omitted ⇒ 3D models off. + modelsTag: null, + plausible: null, + }; + for (let i = 2; i < argv.length; i++) { + const next = () => argv[++i]; + switch (argv[i]) { + case "--tag": a.tag = next(); break; + case "--cdn": a.cdn = next(); break; + case "--api-base": a.apiBase = next(); break; + case "--repo": a.repo = next(); break; + case "--yjs-endpoint": a.yjsEndpoint = next(); break; + case "--models-tag": a.modelsTag = next(); break; + case "--plausible": a.plausible = next(); break; + default: throw new Error(`unknown arg: ${argv[i]}`); + } + } + if (!a.tag) throw new Error("--tag is required"); + if (!a.apiBase) throw new Error("--api-base is required"); + a.cdn = a.cdn.replace(/\/+$/, ""); + a.apiBase = a.apiBase.replace(/\/+$/, ""); + a.repo = a.repo.replace(/\/+$/, ""); + a.yjsEndpoint = (a.yjsEndpoint || a.apiBase).replace(/\/+$/, ""); + return a; +} + +// Best-effort source commit for the version badge's corresponding-source link +// (GPLv3): empty string if git isn't available. +function gitSha(cwd) { + try { + return execFileSync("git", ["rev-parse", "HEAD"], { cwd }) + .toString() + .trim(); + } catch { + return ""; + } +} + +function main() { + const a = parseArgs(process.argv); + const repoRoot = resolve(process.cwd()); + const standalone = join(repoRoot, "web/standalone"); + const dist = join(standalone, "dist"); + const publicWasm = join(standalone, "public/wasm"); + const stash = join(standalone, "public/.wasm.editor-stashed"); + + const env = { + ...process.env, + // Versioned CDN WASM — identical mechanism to the demo build. + VITE_WASM_ROOT: `${a.cdn}/wasm`, + VITE_WASM_MANIFEST: `manifest-${a.tag}.json`, + // Remote mode: projects, files and auth all come from the closed API. + // (No VITE_PROJECT_SOURCE ⇒ "remote"; no VITE_LOCAL_PROJECTS.) + VITE_API_BASE_URL: a.apiBase, + // Live collab: Y.Doc rooms on the sync worker, reached through the API + // host's path route; documents load from their ydoc. + VITE_YJS_PROVIDER: "partykit", + VITE_YJS_ENDPOINT: a.yjsEndpoint, + VITE_DOC_SOURCE: "ydoc", + // Libraries through the server's r2-idb-sync bridge (one /bundle per lib, + // IndexedDB-cached) — NOT the CDN static libs the demo uses. + VITE_LIBS_SOURCE: "synced", + // 3D models stay CDN-static when a snapshot tag is given (same as demo). + ...(a.modelsTag + ? { + VITE_MODELS_MANIFEST_URL: `${a.cdn}/libs/kicad-models/${a.modelsTag}/manifest.json`, + } + : {}), + // Build identity for the version badge (GPLv3 corresponding source). + VITE_APP_TAG: a.tag, + VITE_GIT_SHA: gitSha(repoRoot), + VITE_REPO_URL: a.repo, + ...(a.plausible ? { VITE_PLAUSIBLE_DOMAIN: a.plausible } : {}), + }; + + console.log(`build-editor: tag=${a.tag} api=${a.apiBase} cdn=${a.cdn}`); + console.log(` VITE_WASM_ROOT=${env.VITE_WASM_ROOT}`); + console.log(` VITE_WASM_MANIFEST=${env.VITE_WASM_MANIFEST}`); + console.log(` VITE_API_BASE_URL=${env.VITE_API_BASE_URL}`); + console.log(` VITE_YJS_ENDPOINT=${env.VITE_YJS_ENDPOINT} (provider=${env.VITE_YJS_PROVIDER}, doc=${env.VITE_DOC_SOURCE})`); + console.log(` VITE_LIBS_SOURCE=${env.VITE_LIBS_SOURCE}`); + console.log(` VITE_MODELS_MANIFEST_URL=${env.VITE_MODELS_MANIFEST_URL ?? "(unset — 3D models off)"}`); + console.log(` VITE_APP_TAG=${env.VITE_APP_TAG} VITE_GIT_SHA=${env.VITE_GIT_SHA || "(none)"}`); + console.log(` VITE_PLAUSIBLE_DOMAIN=${env.VITE_PLAUSIBLE_DOMAIN || "(off)"}`); + + // Keep the dev-only WASM symlink out of the bundle (CDN serves it). + const hadWasm = existsSync(publicWasm) || isSymlink(publicWasm); + if (hadWasm) renameSync(publicWasm, stash); + try { + execFileSync( + "pnpm", + ["--dir", "web", "--filter", "@pcbjam/standalone", "build"], + { cwd: repoRoot, env, stdio: "inherit" }, + ); + } finally { + if (hadWasm) renameSync(stash, publicWasm); + } + + // Belt-and-suspenders: never ship local wasm even if a copy slipped through. + rmSync(join(dist, "wasm"), { recursive: true, force: true }); + + // Same Pages headers as the demo: COOP/COEP for WASM threads (the API's CORS + // satisfies COEP for credentialed cross-origin fetches) + SPA fallback. + for (const f of ["_headers", "_redirects"]) { + copyFileSync(join(repoRoot, "deploy/demo", f), join(dist, f)); + } + + console.log(`done → ${dist} (ready for: wrangler pages deploy)`); +} + +function isSymlink(p) { + try { + return lstatSync(p).isSymbolicLink(); + } catch { + return false; + } +} + +main();