diff --git a/azure-pipelines-npm-publish.yml b/azure-pipelines-npm-publish.yml index 40cc906c6d..68e00361ba 100644 --- a/azure-pipelines-npm-publish.yml +++ b/azure-pipelines-npm-publish.yml @@ -51,6 +51,11 @@ parameters: default: false variables: + - group: BabylonJS-Deployment + # Provides TOOLS_STORAGE_ACCOUNT — the tools storage account backing + # liteplayground.babylonjs.com, so versioned playground snapshots serve + # same-origin as nightly (required by the client-side deploy coordinator). + - group: BabylonJS-CI-Infrastructure - name: NODE_VERSION value: "22.x" - name: PNPM_VERSION @@ -240,3 +245,28 @@ stages: displayName: "Publish compat package to npm" env: NPM_TOKEN: $(NPM_TOKEN) + + # --- Versioned playground snapshot ------------------------------ + # After a successful publish, build the Lite Playground from this + # tagged commit and deploy it to an immutable per-version sub-path + # on the tools account (served at liteplayground.babylonjs.com/v//), + # so every release gets a permanent, self-consistent playground URL + # served same-origin as nightly (its self-hosted engine is built + # from — and stamped with — the published version via PACKAGE_VERSION). + # PLAYGROUND_BASE matches the public sub-path so assets, the runner, + # the engine, and SPA snippet routes resolve under it. Compile-time + # gated on dryRun so dry runs never upload. + - ${{ if eq(parameters.dryRun, false) }}: + - script: pnpm build:playground + displayName: "Build versioned playground" + env: + PACKAGE_VERSION: $(PACKAGE_VERSION) + PLAYGROUND_BASE: /v/$(PACKAGE_VERSION)/ + + - template: config/templates/upload-static-site.yml + parameters: + siteDir: $(System.DefaultWorkingDirectory)/playground/dist + deployPath: litePlayground/v/$(PACKAGE_VERSION) + storageAccount: $(TOOLS_STORAGE_ACCOUNT) + siteName: "Lite Playground v$(PACKAGE_VERSION)" + postComment: false diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 39d86090bf..c6fd047617 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,6 +1,6 @@ # Azure Pipelines — Babylon Lite CI # -# Runs on every PR targeting master. Seven parallel jobs: +# Runs on every PR targeting master. Eight parallel jobs: # 1. Release Markers — breaking-change marker validation # 2. API Report — public API diff comment + breaking-change validation # 3. Unit Tests — vitest + plumbing (Playwright) @@ -8,6 +8,7 @@ # 5. Perf Regression — Lite vs Stable on BrowserStack # 6. Parity (Cloud) — pixel-diff via cloud browser with real WebGPU # 7. Lint — ESLint + TypeScript type-check +# 8. Playground Snapshot — per-PR playground deploy + PR comment # trigger: none @@ -20,6 +21,10 @@ pr: variables: - group: BabylonJS-BrowserStack - group: BabylonJS-Deployment + # Provides TOOLS_STORAGE_ACCOUNT — the tools storage account backing + # liteplayground.babylonjs.com, so PR playground snapshots serve same-origin + # as nightly (required by the client-side deploy coordinator). + - group: BabylonJS-CI-Infrastructure - name: NODE_VERSION value: "22.x" - name: PNPM_VERSION @@ -298,6 +303,7 @@ stages: siteDir: $(System.DefaultWorkingDirectory)/lab/dist deployPath: lite/$(Build.BuildNumber)/lab siteName: "Lab" + linkUrl: $(SERVE_DOMAIN)/lite/$(Build.BuildNumber)/lab/index.html # ────────────────────────────────────────────────────────────── # Job 4: Performance Regression @@ -603,3 +609,72 @@ stages: mergeTestResults: true testRunTitle: "Compat Layer" continueOnError: true + + # ────────────────────────────────────────────────────────────── + # Job 8: PR Playground Snapshot + # ────────────────────────────────────────────────────────────── + # Builds the Lite Playground from THIS PR's source — the self-hosted + # engine is bundled from the same commit, so the runner executes the PR's + # engine code — and deploys it to a per-PR sub-path on the tools account + # (served at liteplayground.babylonjs.com/pr//, same origin as nightly), + # then comments the URL on the PR. PLAYGROUND_BASE is set to match the + # public sub-path so every asset, the runner iframe, the self-hosted + # engine, and the SPA snippet routes resolve under it. + # + # Fork PRs: Azure withholds secret variables (including the deploy token) + # from fork-triggered PRs by default, so on a fork the upload step simply + # no-ops (it is continueOnError) and the PR still passes. To publish + # snapshots for fork PRs, enable "Make secrets available to builds of + # forks" in the pipeline's trigger settings — note this exposes the + # deploy token to fork-authored code, a security trade-off to weigh. + - job: PlaygroundSnapshot + displayName: "PR Playground Snapshot" + pool: + vmImage: "ubuntu-latest" + variables: + # Immutable per-PR snapshot, served same-origin as nightly on the + # tools account so the deploy coordinator can hand off to it. + # deployPath — upload path inside the tools storage account + # (the `litePlayground` container maps to the + # subdomain root, so this becomes /pr//). + # playgroundBase — public base path baked into the build. + # playgroundUrl — public URL posted in the PR comment. + - name: deployPath + value: litePlayground/pr/$(System.PullRequest.PullRequestNumber) + - name: playgroundBase + value: /pr/$(System.PullRequest.PullRequestNumber)/ + - name: playgroundUrl + value: https://liteplayground.babylonjs.com/pr/$(System.PullRequest.PullRequestNumber)/ + steps: + # fetchDepth: 0 + tags so the engine build can resolve the latest + # npm-lite-v* tag for its version stamp (see vite.engine.config.ts). + - checkout: self + fetchDepth: 0 + fetchTags: true + + - task: UseNode@1 + inputs: + version: $(NODE_VERSION) + + - script: | + corepack enable + corepack prepare pnpm@$(PNPM_VERSION) --activate + displayName: "Enable pnpm via corepack" + + - script: pnpm install --frozen-lockfile + displayName: "Install dependencies" + + # PLAYGROUND_BASE must be leading- and trailing-slashed and match + # the public sub-path so the built app resolves assets under it. + - script: pnpm build:playground + displayName: "Build the playground (engine + app)" + env: + PLAYGROUND_BASE: $(playgroundBase) + + - template: config/templates/upload-static-site.yml + parameters: + siteDir: $(System.DefaultWorkingDirectory)/playground/dist + deployPath: $(deployPath) + storageAccount: $(TOOLS_STORAGE_ACCOUNT) + siteName: "Lite Playground" + linkUrl: $(playgroundUrl) diff --git a/config/frontdoor-playground-spa.md b/config/frontdoor-playground-spa.md new file mode 100644 index 0000000000..75d8d70622 --- /dev/null +++ b/config/frontdoor-playground-spa.md @@ -0,0 +1,113 @@ +# Front Door rules for the Lite Playground snapshots + +The playground is served as a single origin with three shapes: + +| Shape | URL | Built by | +| ---------------- | ----------------------------------------------- | ------------------------------- | +| Nightly (master) | `https://liteplayground.babylonjs.com/` | `azure-pipelines-playground.yml`| +| Per-PR snapshot | `https://liteplayground.babylonjs.com/pr//` | `azure-pipelines.yml` | +| Per-version | `https://liteplayground.babylonjs.com/v//` | `azure-pipelines-npm-publish.yml`| + +Each is an immutable, independently built static site. The app is a History-API +SPA, so a deep link or hard refresh (e.g. `/pr/387/snippet/XKIIYQ/v/3`) requests a +path that has **no file** at the origin. Front Door must serve the app shell for +those paths instead of a 404. + +## Why one static rewrite is enough + +Azure Front Door Standard/Premium **URL Rewrite** only rewrites a prefix to a +single **static** destination — it has no regex capture groups or server +variables ([docs](https://learn.microsoft.com/azure/frontdoor/front-door-url-rewrite)). +So it cannot compute a *per-base* fallback like `/pr/387/index.html`. It can only +ever fall back to one path: the root `/index.html`. + +That is exactly enough, because every deploy ships the same stable coordinator at +`assets/boot.js` (loaded by every `index.html`). When the root `index.html` is +served for a snapshot deep link, its coordinator reads `location.pathname`, +derives the base (`/pr/387/`), and hands off to `/pr/387/assets/boot.js`, which +boots the PR build. See `playground/src/entry.ts`. + +## The rule set (one rule) + +Attach a rule set to the playground route with a single rule. + +**Conditions (AND):** + +1. **Request file extension** — `Operator: Equal`, `Negate: true`, values = the + asset allow-list below. (Requests that already point at a real file — with an + extension — pass through to blob storage untouched.) +2. **URL path** — `Operator: EndsWith`, `Negate: true`, value = `/`. (Directory + roots like `/`, `/pr/387/`, `/v/1.4.0/` are served natively by blob static + website's index document, so they skip the rewrite.) + +**Action:** **URL rewrite** — Source pattern `/`, Destination `/index.html`, +Preserve unmatched path `No`. + +### ARM / Bicep shape + +```json +{ + "name": "SpaFallback", + "order": 1, + "conditions": [ + { + "name": "RequestFileExtension", + "parameters": { + "typeName": "DeliveryRuleRequestFileExtensionConditionParameters", + "operator": "Equal", + "negateCondition": true, + "matchValues": [ + "js", "mjs", "css", "map", "json", "html", "wasm", "ts", "wgsl", + "txt", "xml", "csv", "ico", "svg", "png", "jpg", "jpeg", "gif", + "webp", "avif", "bmp", "woff", "woff2", "ttf", "otf", "eot", + "env", "dds", "ktx", "ktx2", "basis", "hdr", "exr", + "glb", "gltf", "obj", "stl", "bin", "mp3", "wav", "ogg", "m4a", + "mp4", "webm" + ], + "transforms": ["Lowercase"] + } + }, + { + "name": "UrlPath", + "parameters": { + "typeName": "DeliveryRuleUrlPathMatchConditionParameters", + "operator": "EndsWith", + "negateCondition": true, + "matchValues": ["/"], + "transforms": [] + } + } + ], + "actions": [ + { + "name": "UrlRewrite", + "parameters": { + "typeName": "DeliveryRuleUrlRewriteActionParameters", + "sourcePattern": "/", + "destination": "/index.html", + "preserveUnmatchedPath": false + } + } + ] +} +``` + +## Caching + +- `index.html` and `assets/boot.js` — **no-cache / must-revalidate**. They are the + stable coordinator surface; they must always reflect the latest deploy. The + nightly pipeline purges the CDN endpoint on every master deploy, which flushes + the root copies. +- Everything under `assets/*-[hash].*`, `/v//**`, and `/pr//**` other than + `boot.js`/`index.html` — content-hashed or immutable, safe to cache long-term. + +## Edge cases + +- **Bare snapshot root without a trailing slash** (`/pr/387`): condition 2 is not + met (doesn't end with `/`) and it has no extension, so it is rewritten to + `/index.html`; the coordinator still derives `/pr/387/` and hands off correctly. +- **The `runner.html` iframe and engine files** (`/pr/387/runner.html`, + `/pr/387/engine/dev/index.js`, `index.d.ts`) all carry extensions in the + allow-list, so they pass through to blob storage and are never rewritten. +- **Legacy hash links** (`/#XKIIYQ`) never reach the server and keep working; the + app also still parses the path form for backward compatibility. diff --git a/config/templates/upload-static-site.yml b/config/templates/upload-static-site.yml index 2c63ea84b2..ce1048ed77 100644 --- a/config/templates/upload-static-site.yml +++ b/config/templates/upload-static-site.yml @@ -1,13 +1,17 @@ # Reusable step template: upload a static site directory and post its link to the PR # # Parameters: -# siteDir - path to the static site folder to zip and upload -# deployPath - storage path where the site should be served -# siteName - label used in display names and the PR comment +# siteDir - path to the static site folder to zip and upload +# deployPath - storage path where the site should be served +# siteName - label used in display names and the PR comment +# postComment - post a PR comment with the link (default true) +# storageAccount - storage account to upload to (default $(STORAGE_ACCOUNT)) +# linkUrl - public URL to link in the PR comment (when postComment) # # Required pipeline variables: # BabylonJS-Deployment variable group: DEPLOYMENT_SERVER, DEPLOY_TOKEN # Report upload settings: DEPLOY_ENDPOINT_UPLOAD, STORAGE_ACCOUNT, SERVE_DOMAIN +# Playground snapshots additionally pass TOOLS_STORAGE_ACCOUNT via storageAccount. # Required service connection: # BabylonBotPAT - GitHub PAT service connection for PR comments @@ -18,6 +22,25 @@ parameters: type: string - name: siteName type: string + # When false, upload the site but skip the PR comment. Used by non-PR + # pipelines (e.g. the npm-publish versioned playground) where there is no + # System.PullRequest context to comment on. + - name: postComment + type: boolean + default: true + # Storage account to upload to. Defaults to the report storage account; the + # playground snapshot jobs override this with the tools storage account so + # their sites serve from the playground subdomain. + - name: storageAccount + type: string + default: $(STORAGE_ACCOUNT) + # Public URL to link in the PR comment. Callers pass this explicitly because + # the served URL can't always be derived from the upload path (e.g. the tools + # account strips its container prefix from the public domain). Only used when + # postComment is true. + - name: linkUrl + type: string + default: "" steps: - script: | @@ -36,7 +59,7 @@ steps: -H "Content-Type: multipart/form-data" \ -H "Authorization: Bearer $(DEPLOY_TOKEN)" \ -F "path=${{ parameters.deployPath }}" \ - -F "storageAccount=$(STORAGE_ACCOUNT)" \ + -F "storageAccount=${{ parameters.storageAccount }}" \ -F "zip=@_static_site.zip" rm -f _static_site.zip @@ -44,17 +67,18 @@ steps: continueOnError: true displayName: "Upload ${{ parameters.siteName }} static site" - - task: GitHubComment@0 - condition: succeeded() - continueOnError: true - displayName: "Post ${{ parameters.siteName }} site link to PR" - inputs: - gitHubConnection: "BabylonBotPAT" - repositoryName: "$(Build.Repository.Name)" - id: "$(System.PullRequest.PullRequestNumber)" - comment: | - ### ${{ parameters.siteName }} - Static Site + - ${{ if eq(parameters.postComment, true) }}: + - task: GitHubComment@0 + condition: succeeded() + continueOnError: true + displayName: "Post ${{ parameters.siteName }} site link to PR" + inputs: + gitHubConnection: "BabylonBotPAT" + repositoryName: "$(Build.Repository.Name)" + id: "$(System.PullRequest.PullRequestNumber)" + comment: | + ### ${{ parameters.siteName }} - Static Site - [Open deployed site]($(SERVE_DOMAIN)/${{ parameters.deployPath }}/index.html) + [Open deployed site](${{ parameters.linkUrl }}) - Build $(Build.BuildNumber) - $(Build.SourceBranchName) @ $(Build.SourceVersion) + Build $(Build.BuildNumber) - $(Build.SourceBranchName) @ $(Build.SourceVersion) diff --git a/playground/index.html b/playground/index.html index 50ff77219b..1194d2c4e0 100644 --- a/playground/index.html +++ b/playground/index.html @@ -99,6 +99,6 @@

Save snippet

- + diff --git a/playground/public/runner.html b/playground/public/runner.html index d7147d3d73..75ef3799cf 100644 --- a/playground/public/runner.html +++ b/playground/public/runner.html @@ -12,7 +12,10 @@ -->