Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions azure-pipelines-npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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/<ver>/),
# 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
77 changes: 76 additions & 1 deletion azure-pipelines.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# 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)
# 4. Bundle Size — ceiling checks
# 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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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/<N>/, 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/<N>/).
# 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)
113 changes: 113 additions & 0 deletions config/frontdoor-playground-spa.md
Original file line number Diff line number Diff line change
@@ -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/<N>/` | `azure-pipelines.yml` |
| Per-version | `https://liteplayground.babylonjs.com/v/<ver>/` | `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/<ver>/**`, and `/pr/<N>/**` 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.
56 changes: 40 additions & 16 deletions config/templates/upload-static-site.yml
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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: |
Expand All @@ -36,25 +59,26 @@ 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
condition: succeeded()
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 }})

<sub>Build $(Build.BuildNumber) - $(Build.SourceBranchName) @ $(Build.SourceVersion)</sub>
<sub>Build $(Build.BuildNumber) - $(Build.SourceBranchName) @ $(Build.SourceVersion)</sub>
2 changes: 1 addition & 1 deletion playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,6 @@ <h2>Save snippet</h2>
</form>
</dialog>
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
<script type="module" src="/src/main.ts"></script>
<script type="module" src="/src/entry.ts"></script>
</body>
</html>
5 changes: 4 additions & 1 deletion playground/public/runner.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
-->
<script>
(function () {
var engine = new URLSearchParams(location.search).get("engine") || "/engine/dev/index.js";
// Default engine path is relative to this runner.html so it resolves
// under the app's deploy base (root or a /pr|/v sub-path). The parent
// normally passes an explicit, base-resolved URL via `?engine=`.
var engine = new URLSearchParams(location.search).get("engine") || "./engine/dev/index.js";
var map = document.createElement("script");
map.type = "importmap";
map.textContent = JSON.stringify({ imports: { "@babylonjs/lite": engine } });
Expand Down
Loading