ci: deploy isolated GPL staging stack
This commit is contained in:
parent
4d75d44a78
commit
f90cf7d2da
6 changed files with 224 additions and 1 deletions
106
.github/workflows/deploy-staging.yml
vendored
Normal file
106
.github/workflows/deploy-staging.yml
vendored
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
name: deploy staging (GPL stack)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [staging]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-staging-gpl
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: "22"
|
||||||
|
CDN_ORIGIN: https://pcbjam-cdn-staging.pcbjam-staging.workers.dev
|
||||||
|
API_ORIGIN: https://pcbjam-server-staging.pcbjam-staging.workers.dev
|
||||||
|
APP_ORIGIN: https://pcbjam-app-staging.pcbjam-staging.workers.dev
|
||||||
|
EDITOR_ORIGIN: https://pcbjam-editor-staging.pcbjam-staging.workers.dev
|
||||||
|
BUCKET: pcbjam-cdn-staging
|
||||||
|
WRANGLER_CMD: npx --yes wrangler@4.99.0
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
uses: ./.github/workflows/wasm-build.yml
|
||||||
|
with:
|
||||||
|
opt_level: "-O1"
|
||||||
|
build_3d_viewer: "ON"
|
||||||
|
run_tests: true
|
||||||
|
upload_output: true
|
||||||
|
|
||||||
|
publish-and-deploy:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubicloud-standard-8
|
||||||
|
timeout-minutes: 90
|
||||||
|
environment: staging
|
||||||
|
env:
|
||||||
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: false
|
||||||
|
|
||||||
|
- name: Validate staging deployment credentials
|
||||||
|
run: |
|
||||||
|
test -n "$CLOUDFLARE_API_TOKEN" || { echo "staging CLOUDFLARE_API_TOKEN is missing"; exit 1; }
|
||||||
|
test -n "$CLOUDFLARE_ACCOUNT_ID" || { echo "staging CLOUDFLARE_ACCOUNT_ID variable is missing"; exit 1; }
|
||||||
|
test "$CLOUDFLARE_ACCOUNT_ID" = "5be62ba000cabfaad4d11a6ef28f8395" || { echo "refusing to deploy outside the staging Cloudflare account"; exit 1; }
|
||||||
|
|
||||||
|
- 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: ${{ env.NODE_VERSION }}
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: web/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install standalone workspace
|
||||||
|
run: pnpm --dir web install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Download WASM output
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: wasm-output
|
||||||
|
path: output
|
||||||
|
|
||||||
|
- name: Publish WASM to staging R2
|
||||||
|
env:
|
||||||
|
STAGING_TAG: staging-${{ github.sha }}
|
||||||
|
run: >
|
||||||
|
node scripts/deploy/publish-wasm.mjs --tag "$STAGING_TAG"
|
||||||
|
--src output --driver r2 --bucket "$BUCKET" --remote
|
||||||
|
--compress br --quality 11
|
||||||
|
|
||||||
|
- name: Deploy staging CDN Worker
|
||||||
|
run: npx --yes wrangler@4.99.0 deploy --config workers/cdn/wrangler.staging.jsonc
|
||||||
|
|
||||||
|
- name: Build staging editor
|
||||||
|
env:
|
||||||
|
STAGING_TAG: staging-${{ github.sha }}
|
||||||
|
run: >
|
||||||
|
node scripts/deploy/build-editor.mjs --tag "$STAGING_TAG"
|
||||||
|
--cdn "$CDN_ORIGIN" --api-base "$API_ORIGIN"
|
||||||
|
--app-base "$APP_ORIGIN" --errors-env staging
|
||||||
|
|
||||||
|
- name: Deploy staging editor Worker
|
||||||
|
run: npx --yes wrangler@4.99.0 deploy --config web/standalone/wrangler.staging.jsonc
|
||||||
|
|
||||||
|
- name: Smoke-check staging GPL surfaces
|
||||||
|
env:
|
||||||
|
STAGING_TAG: staging-${{ github.sha }}
|
||||||
|
run: |
|
||||||
|
curl --fail --retry 12 --retry-delay 5 "$CDN_ORIGIN/wasm/manifest-$STAGING_TAG.json" >/dev/null
|
||||||
|
curl --fail --retry 12 --retry-delay 5 "$EDITOR_ORIGIN/" >/dev/null
|
||||||
|
curl --fail --retry 12 --retry-delay 5 -I "$CDN_ORIGIN/wasm/manifest-$STAGING_TAG.json" | grep -qi '^access-control-allow-origin: \*'
|
||||||
|
headers=$(curl --fail --retry 12 --retry-delay 5 -I "$EDITOR_ORIGIN/")
|
||||||
|
echo "$headers" | grep -qi '^cross-origin-opener-policy: same-origin'
|
||||||
|
echo "$headers" | grep -qi '^cross-origin-embedder-policy: require-corp'
|
||||||
|
echo "$headers" | grep -qi '^x-robots-tag:.*noindex'
|
||||||
|
|
@ -5,3 +5,8 @@
|
||||||
/*
|
/*
|
||||||
Cross-Origin-Opener-Policy: same-origin
|
Cross-Origin-Opener-Policy: same-origin
|
||||||
Cross-Origin-Embedder-Policy: require-corp
|
Cross-Origin-Embedder-Policy: require-corp
|
||||||
|
|
||||||
|
# Never index the free staging hostname. This absolute rule is inert on the
|
||||||
|
# production Pages/custom-domain deployments that reuse this file.
|
||||||
|
https://pcbjam-editor-staging.pcbjam-staging.workers.dev/*
|
||||||
|
X-Robots-Tag: noindex, nofollow
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,10 @@ function main() {
|
||||||
const standalone = join(repoRoot, "web/standalone");
|
const standalone = join(repoRoot, "web/standalone");
|
||||||
const dist = join(standalone, "dist");
|
const dist = join(standalone, "dist");
|
||||||
const publicWasm = join(standalone, "public/wasm");
|
const publicWasm = join(standalone, "public/wasm");
|
||||||
const stash = join(standalone, "public/.wasm.editor-stashed");
|
// Keep the temporary symlink OUTSIDE public/. Vite copies every public entry,
|
||||||
|
// including dot-directories; stashing it under public previously smuggled the
|
||||||
|
// full local WASM tree into dist under `.wasm.editor-stashed/`.
|
||||||
|
const stash = join(standalone, ".wasm.editor-stashed");
|
||||||
|
|
||||||
const env = {
|
const env = {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
|
@ -152,6 +155,7 @@ function main() {
|
||||||
|
|
||||||
// Belt-and-suspenders: never ship local wasm even if a copy slipped through.
|
// Belt-and-suspenders: never ship local wasm even if a copy slipped through.
|
||||||
rmSync(join(dist, "wasm"), { recursive: true, force: true });
|
rmSync(join(dist, "wasm"), { recursive: true, force: true });
|
||||||
|
rmSync(join(dist, ".wasm.editor-stashed"), { recursive: true, force: true });
|
||||||
|
|
||||||
// Same Pages headers as the demo: COOP/COEP for WASM threads (the API's CORS
|
// Same Pages headers as the demo: COOP/COEP for WASM threads (the API's CORS
|
||||||
// satisfies COEP for credentialed cross-origin fetches) + SPA fallback.
|
// satisfies COEP for credentialed cross-origin fetches) + SPA fallback.
|
||||||
|
|
|
||||||
11
web/standalone/wrangler.staging.jsonc
Normal file
11
web/standalone/wrangler.staging.jsonc
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"$schema": "../../../node_modules/wrangler/config-schema.json",
|
||||||
|
"name": "pcbjam-editor-staging",
|
||||||
|
"compatibility_date": "2026-07-27",
|
||||||
|
"workers_dev": true,
|
||||||
|
"preview_urls": false,
|
||||||
|
"assets": {
|
||||||
|
"directory": "./dist",
|
||||||
|
"not_found_handling": "single-page-application"
|
||||||
|
}
|
||||||
|
}
|
||||||
85
workers/cdn/src/index.ts
Normal file
85
workers/cdn/src/index.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
interface Env {
|
||||||
|
BUCKET: R2Bucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseHeaders = (): Headers => {
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.set("Access-Control-Allow-Origin", "*");
|
||||||
|
headers.set("Cross-Origin-Resource-Policy", "cross-origin");
|
||||||
|
headers.set("X-Content-Type-Options", "nosniff");
|
||||||
|
headers.set("X-Robots-Tag", "noindex, nofollow");
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
|
if (request.method === "OPTIONS") {
|
||||||
|
const headers = baseHeaders();
|
||||||
|
headers.set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
|
||||||
|
headers.set("Access-Control-Allow-Headers", "Range, If-None-Match");
|
||||||
|
headers.set("Access-Control-Max-Age", "86400");
|
||||||
|
return new Response(null, { status: 204, headers });
|
||||||
|
}
|
||||||
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
||||||
|
return new Response("Method Not Allowed", {
|
||||||
|
status: 405,
|
||||||
|
headers: baseHeaders(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
let key: string;
|
||||||
|
try {
|
||||||
|
key = decodeURIComponent(url.pathname.replace(/^\/+/, ""));
|
||||||
|
} catch {
|
||||||
|
return new Response("Bad Request", { status: 400, headers: baseHeaders() });
|
||||||
|
}
|
||||||
|
if (!key) {
|
||||||
|
return Response.json(
|
||||||
|
{ ok: true, service: "pcbjam-cdn-staging" },
|
||||||
|
{ headers: baseHeaders() },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const object = await env.BUCKET.get(key, {
|
||||||
|
range: request.headers,
|
||||||
|
});
|
||||||
|
if (!object) {
|
||||||
|
return new Response("Not Found", { status: 404, headers: baseHeaders() });
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = baseHeaders();
|
||||||
|
object.writeHttpMetadata(headers);
|
||||||
|
headers.set("ETag", object.httpEtag);
|
||||||
|
headers.set("Accept-Ranges", "bytes");
|
||||||
|
|
||||||
|
if (request.headers.get("If-None-Match") === object.httpEtag) {
|
||||||
|
return new Response(null, { status: 304, headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = 200;
|
||||||
|
if (object.range) {
|
||||||
|
const suffix = "suffix" in object.range ? object.range.suffix : undefined;
|
||||||
|
const length = suffix
|
||||||
|
? Math.min(suffix, object.size)
|
||||||
|
: "length" in object.range && object.range.length !== undefined
|
||||||
|
? object.range.length
|
||||||
|
: object.size - ("offset" in object.range ? (object.range.offset ?? 0) : 0);
|
||||||
|
const offset = suffix
|
||||||
|
? object.size - length
|
||||||
|
: "offset" in object.range
|
||||||
|
? (object.range.offset ?? 0)
|
||||||
|
: 0;
|
||||||
|
headers.set("Content-Range", `bytes ${offset}-${offset + length - 1}/${object.size}`);
|
||||||
|
headers.set("Content-Length", String(length));
|
||||||
|
status = 206;
|
||||||
|
} else {
|
||||||
|
headers.set("Content-Length", String(object.size));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(request.method === "HEAD" ? null : object.body, {
|
||||||
|
status,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
12
workers/cdn/wrangler.staging.jsonc
Normal file
12
workers/cdn/wrangler.staging.jsonc
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"$schema": "../../../node_modules/wrangler/config-schema.json",
|
||||||
|
"name": "pcbjam-cdn-staging",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"compatibility_date": "2026-07-27",
|
||||||
|
"workers_dev": true,
|
||||||
|
"preview_urls": false,
|
||||||
|
"observability": { "enabled": true },
|
||||||
|
"r2_buckets": [
|
||||||
|
{ "binding": "BUCKET", "bucket_name": "pcbjam-cdn-staging" }
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue