From 49427f2587b962ed3e20e3f7232b555751c51d63 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Wed, 27 May 2026 22:45:08 +0200 Subject: [PATCH 1/3] Upload PR dev-playground bundles --- .github/workflows/ci.yml | 11 ++- .../workflows/playground_preview_cleanup.yml | 28 ++++++++ packages/dev-playground/src/Bindings.res | 2 + packages/dev-playground/src/CompilerApi.res | 70 +++++++++++++++++-- packages/dev-playground/src/CompilerApi.resi | 4 +- packages/dev-playground/src/Main.res | 6 +- packages/dev-playground/src/UrlState.res | 12 +--- packages/playground/package.json | 1 + .../scripts/upload_preview_bundle.mjs | 56 +++++++++++++++ 9 files changed, 170 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/playground_preview_cleanup.yml create mode 100644 packages/playground/scripts/upload_preview_bundle.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3e37552acc..7a6bbfdfba7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -395,11 +395,11 @@ jobs: if-no-files-found: error - name: Setup Rclone - if: ${{ matrix.build_playground && startsWith(github.ref, 'refs/tags/v') }} + if: ${{ matrix.build_playground && (startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'master' && github.event.pull_request.head.repo.full_name == github.repository)) }} uses: cometkim/rclone-actions/setup-rclone@main - name: Configure Rclone remote - if: ${{ matrix.build_playground && startsWith(github.ref, 'refs/tags/v') }} + if: ${{ matrix.build_playground && (startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'master' && github.event.pull_request.head.repo.full_name == github.repository)) }} uses: cometkim/rclone-actions/configure-remote/s3-provider@main with: name: rescript @@ -413,6 +413,12 @@ jobs: if: ${{ matrix.build_playground && startsWith(github.ref, 'refs/tags/v') }} run: yarn workspace playground upload-bundle + - name: Upload PR playground compiler to CDN + if: ${{ matrix.build_playground && github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'master' && github.event.pull_request.head.repo.full_name == github.repository }} + env: + PLAYGROUND_PREVIEW_ID: pr-${{ github.event.pull_request.number }} + run: yarn workspace playground upload-preview-bundle + - name: "Upload artifacts: binaries" if: matrix.upload_binaries uses: actions/upload-artifact@v7 @@ -457,6 +463,7 @@ jobs: env: VITE_DEFAULT_COMPILER_VERSION: master VITE_COMPILER_VERSIONS: '[{"id":"master","label":"master"}]' + VITE_COMPILER_PREVIEW_ROOT: https://cdn.rescript-lang.org/dev-playground-bundles GITHUB_PAGES_PATH: dev-playground PLAYGROUND_BUNDLE_ID: master steps: diff --git a/.github/workflows/playground_preview_cleanup.yml b/.github/workflows/playground_preview_cleanup.yml new file mode 100644 index 00000000000..675b8d8f987 --- /dev/null +++ b/.github/workflows/playground_preview_cleanup.yml @@ -0,0 +1,28 @@ +name: Playground Preview Cleanup + +on: + pull_request: + branches: [master] + types: [closed] + +jobs: + cleanup: + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + runs-on: ubuntu-24.04 + steps: + - name: Setup Rclone + uses: cometkim/rclone-actions/setup-rclone@main + + - name: Configure Rclone remote + uses: cometkim/rclone-actions/configure-remote/s3-provider@main + with: + name: rescript + provider: Cloudflare + endpoint: https://${{ vars.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com + access-key-id: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }} + secret-access-key: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }} + acl: private + + - name: Delete preview bundle + continue-on-error: true + run: rclone purge "rescript:cdn-assets/dev-playground-bundles/pr-${{ github.event.pull_request.number }}" diff --git a/packages/dev-playground/src/Bindings.res b/packages/dev-playground/src/Bindings.res index 694aad80805..1b7f3f497f6 100644 --- a/packages/dev-playground/src/Bindings.res +++ b/packages/dev-playground/src/Bindings.res @@ -10,6 +10,8 @@ module Env = { external viteDefaultCompilerVersion: option = "import.meta.env.VITE_DEFAULT_COMPILER_VERSION" @val external viteCompilerVersions: option = "import.meta.env.VITE_COMPILER_VERSIONS" + @val + external viteCompilerPreviewRoot: option = "import.meta.env.VITE_COMPILER_PREVIEW_ROOT" @val external viteBaseUrl: option = "import.meta.env.BASE_URL" } diff --git a/packages/dev-playground/src/CompilerApi.res b/packages/dev-playground/src/CompilerApi.res index 25a09c0249f..7464ee94b04 100644 --- a/packages/dev-playground/src/CompilerApi.res +++ b/packages/dev-playground/src/CompilerApi.res @@ -1,3 +1,9 @@ +type compilerVersion = { + versionId: string, + versionLabel: string, + versionRoot: option, +} + module Version = { type t = { id: string, @@ -10,12 +16,28 @@ module Version = { | _ => None } + let normalizeRoot = root => + if root->String.endsWith("/") { + root->String.slice(~start=0, ~end=root->String.length - 1) + } else { + root + } + + let toPublic = (version: compilerVersion): t => { + id: version.versionId, + label: version.versionLabel, + } + let fromJson = json => switch json { | JSON.Object(item) => let? Some(id) = item->jsonStringField("id") let? Some(label) = item->jsonStringField("label") - Some({id, label}) + Some({ + versionId: id, + versionLabel: label, + versionRoot: item->jsonStringField("root")->Option.map(normalizeRoot), + }) | _ => None } } @@ -86,7 +108,7 @@ let pathFromBase = relativePath => { } let parseCompilerVersions = defaultVersion => { - let fallback = [{Version.id: defaultVersion, label: defaultVersion}] + let fallback = [{versionId: defaultVersion, versionLabel: defaultVersion, versionRoot: None}] switch Env.viteCompilerVersions { | None | Some("") => fallback | Some(versionJson) => @@ -100,8 +122,13 @@ let parseCompilerVersions = defaultVersion => { } } -let availableCompilerVersions = parseCompilerVersions(defaultConfig.compilerVersion) +let compilerVersions = parseCompilerVersions(defaultConfig.compilerVersion) +let availableCompilerVersions = compilerVersions->Array.map(Version.toPublic) let compilerRoot = pathFromBase("playground-bundles") +let compilerPreviewRoot = switch Env.viteCompilerPreviewRoot { +| Some(root) => root === "" ? None : Some(root->Version.normalizeRoot) +| None => None +} let loadedScripts: Map.t> = Map.make() let compilerApis: Map.t = Map.make() let compilers: Map.t = Map.make() @@ -116,6 +143,41 @@ let hasFunction = (value, name) => let versionOrDefault = version => version === "" ? defaultConfig.compilerVersion : version +let isPreviewVersion = version => version->String.search(/^pr-[0-9]+$/) === 0 + +let previewVersionRoot = version => + switch compilerPreviewRoot { + | Some(root) if version->isPreviewVersion => Some(`${root}/${version}/bundle`) + | _ => None + } + +let versionRoot = version => { + let selectedVersion = versionOrDefault(version) + switch compilerVersions->Array.findMap(version => + version.versionId === selectedVersion ? version.versionRoot : None + ) { + | Some(root) => root + | None => + switch selectedVersion->previewVersionRoot { + | Some(root) => root + | None => `${compilerRoot}/${selectedVersion}` + } + } +} + +let isConfiguredVersion = version => + compilerVersions->Array.some(compilerVersion => compilerVersion.versionId === version) + +let isLoadableVersion = version => + version->isConfiguredVersion || version->previewVersionRoot->Option.isSome + +let selectableCompilerVersions = activeVersion => + if activeVersion->isConfiguredVersion || !(activeVersion->previewVersionRoot->Option.isSome) { + availableCompilerVersions + } else { + Array.concat(availableCompilerVersions, [{Version.id: activeVersion, label: activeVersion}]) + } + let createScriptLoadPromise = src => Promise.make((resolve, reject) => { let document = Document.current @@ -140,8 +202,6 @@ let loadScript = (src, ~cache=true) => createScriptLoadPromise(src) } -let versionRoot = version => `${compilerRoot}/${versionOrDefault(version)}` - let applyConfig = ( instance, ~moduleSystem: PlaygroundConfig.moduleSystem, diff --git a/packages/dev-playground/src/CompilerApi.resi b/packages/dev-playground/src/CompilerApi.resi index f0b075bade9..45c4c7c540b 100644 --- a/packages/dev-playground/src/CompilerApi.resi +++ b/packages/dev-playground/src/CompilerApi.resi @@ -38,7 +38,9 @@ type formatResult = result let defaultConfig: PlaygroundConfig.t -let availableCompilerVersions: array +let selectableCompilerVersions: string => array + +let isLoadableVersion: string => bool let init: string => promise diff --git a/packages/dev-playground/src/Main.res b/packages/dev-playground/src/Main.res index 7bd1e608ac1..91ffae6b5bd 100644 --- a/packages/dev-playground/src/Main.res +++ b/packages/dev-playground/src/Main.res @@ -231,9 +231,9 @@ module SettingsPanel = { }} > {Node.fragment( - CompilerApi.availableCompilerVersions->Array.map(version => - - ), + CompilerApi.selectableCompilerVersions( + Signal.get(config).compilerVersion, + )->Array.map(version => ), )} diff --git a/packages/dev-playground/src/UrlState.res b/packages/dev-playground/src/UrlState.res index 625c2a52d88..39a9fbeb2eb 100644 --- a/packages/dev-playground/src/UrlState.res +++ b/packages/dev-playground/src/UrlState.res @@ -85,13 +85,10 @@ let queryExperimentalFeatures = defaultExperimentalFeatures => | _ => defaultExperimentalFeatures } -let queryConfig = ( - ~defaultConfig: PlaygroundConfig.t, - ~availableCompilerVersions: array, -) => { +let queryConfig = (~defaultConfig: PlaygroundConfig.t) => { let requestedCompilerVersion = queryCompilerVersion(defaultConfig.compilerVersion) let compilerVersion = - availableCompilerVersions->Array.some(version => version.id === requestedCompilerVersion) + requestedCompilerVersion->CompilerApi.isLoadableVersion ? requestedCompilerVersion : defaultConfig.compilerVersion @@ -106,10 +103,7 @@ let queryConfig = ( let init = async (~defaultSource): state => { let source = await initialSource(defaultSource) - let config = queryConfig( - ~defaultConfig=CompilerApi.defaultConfig, - ~availableCompilerVersions=CompilerApi.availableCompilerVersions, - ) + let config = queryConfig(~defaultConfig=CompilerApi.defaultConfig) {source, config} } diff --git a/packages/playground/package.json b/packages/playground/package.json index 3f72c73adc1..ca37dc2b818 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -7,6 +7,7 @@ "test": "node ./playground_test.cjs", "build": "rescript clean && rescript build && node scripts/generate_cmijs.mjs && rollup -c && touch .buildstamp", "upload-bundle": "node scripts/upload_bundle.mjs", + "upload-preview-bundle": "node scripts/upload_preview_bundle.mjs", "serve-bundle": "node serve-bundle.mjs" }, "dependencies": { diff --git a/packages/playground/scripts/upload_preview_bundle.mjs b/packages/playground/scripts/upload_preview_bundle.mjs new file mode 100644 index 00000000000..458e9ddaeb1 --- /dev/null +++ b/packages/playground/scripts/upload_preview_bundle.mjs @@ -0,0 +1,56 @@ +#!/usr/bin/env node + +// @ts-check + +// Publishes a mutable PR playground compiler bundle to Cloudflare R2. +// The bundle is addressed by PR number, so each PR keeps one latest preview. + +import * as fs from "node:fs"; +import * as path from "node:path"; + +import { + exec, + playgroundDir, + playgroundPackagesDir, +} from "./common.mjs"; + +const previewId = process.env.PLAYGROUND_PREVIEW_ID; +if (!previewId || !/^pr-[0-9]+$/.test(previewId)) { + throw new Error("PLAYGROUND_PREVIEW_ID must look like pr-123"); +} + +const rcloneOpts = [ + "--stats 5", + "--checkers 5000", + "--transfers 8", + "--buffer-size 128M", + "--s3-no-check-bucket", + "--s3-chunk-size 128M", + "--s3-upload-concurrency 8", +].join(" "); + +const remote = process.env.RCLONE_REMOTE || "rescript"; +const bucket = "cdn-assets"; +const tmpDir = path.join(playgroundDir, ".tmp", "preview"); +const artifactsDir = path.join(tmpDir, previewId); +const target = `${remote}:${bucket}/dev-playground-bundles/${previewId}/bundle`; + +fs.rmSync(tmpDir, { recursive: true, force: true }); +fs.mkdirSync(artifactsDir, { recursive: true }); + +console.log("Copying compiler.js"); +fs.copyFileSync( + path.join(playgroundDir, "compiler.js"), + path.join(artifactsDir, "compiler.js"), +); + +console.log("Copying packages"); +fs.cpSync(playgroundPackagesDir, artifactsDir, { recursive: true }); + +console.log(`Uploading playground preview ${previewId}`); +exec(`rclone sync ${rcloneOpts} --fast-list \\ + "${artifactsDir}" \\ + "${target}" +`); + +console.log(`Uploaded playground preview to ${target}`); From a3e04a8586f8656df9c84d83ed7867cf20a8d1c7 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Wed, 27 May 2026 23:17:32 +0200 Subject: [PATCH 2/3] Add safe dev playground PR bundle previews --- .github/workflows/ci.yml | 24 +++++--- .../workflows/playground_preview_cleanup.yml | 2 +- .../workflows/playground_preview_upload.yml | 51 +++++++++++++++++ packages/playground/package.json | 1 - .../scripts/upload_preview_bundle.mjs | 56 ------------------- 5 files changed, 68 insertions(+), 66 deletions(-) create mode 100644 .github/workflows/playground_preview_upload.yml delete mode 100644 packages/playground/scripts/upload_preview_bundle.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a6bbfdfba7..78bad0e36dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -382,6 +382,20 @@ jobs: if: matrix.build_playground run: yarn workspace playground test + - name: Stage PR dev playground compiler bundle + if: ${{ matrix.build_playground && github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'master' && github.event.pull_request.head.repo.full_name == github.repository }} + env: + PLAYGROUND_PREVIEW_ID: pr-${{ github.event.pull_request.number }} + run: yarn workspace dev-playground stage-local-bundle "$PLAYGROUND_PREVIEW_ID" + + - name: "Upload artifacts: PR dev playground compiler bundle" + if: ${{ matrix.build_playground && github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'master' && github.event.pull_request.head.repo.full_name == github.repository }} + uses: actions/upload-artifact@v7 + with: + name: dev-playground-pr-${{ github.event.pull_request.number }}-bundle + path: packages/dev-playground/public/playground-bundles/pr-${{ github.event.pull_request.number }} + if-no-files-found: error + - name: Stage dev playground compiler bundle if: ${{ matrix.build_playground && github.event_name == 'push' && github.ref == 'refs/heads/master' }} run: yarn workspace dev-playground stage-master-bundle @@ -395,11 +409,11 @@ jobs: if-no-files-found: error - name: Setup Rclone - if: ${{ matrix.build_playground && (startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'master' && github.event.pull_request.head.repo.full_name == github.repository)) }} + if: ${{ matrix.build_playground && startsWith(github.ref, 'refs/tags/v') }} uses: cometkim/rclone-actions/setup-rclone@main - name: Configure Rclone remote - if: ${{ matrix.build_playground && (startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'master' && github.event.pull_request.head.repo.full_name == github.repository)) }} + if: ${{ matrix.build_playground && startsWith(github.ref, 'refs/tags/v') }} uses: cometkim/rclone-actions/configure-remote/s3-provider@main with: name: rescript @@ -413,12 +427,6 @@ jobs: if: ${{ matrix.build_playground && startsWith(github.ref, 'refs/tags/v') }} run: yarn workspace playground upload-bundle - - name: Upload PR playground compiler to CDN - if: ${{ matrix.build_playground && github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'master' && github.event.pull_request.head.repo.full_name == github.repository }} - env: - PLAYGROUND_PREVIEW_ID: pr-${{ github.event.pull_request.number }} - run: yarn workspace playground upload-preview-bundle - - name: "Upload artifacts: binaries" if: matrix.upload_binaries uses: actions/upload-artifact@v7 diff --git a/.github/workflows/playground_preview_cleanup.yml b/.github/workflows/playground_preview_cleanup.yml index 675b8d8f987..447c6e94d1f 100644 --- a/.github/workflows/playground_preview_cleanup.yml +++ b/.github/workflows/playground_preview_cleanup.yml @@ -1,7 +1,7 @@ name: Playground Preview Cleanup on: - pull_request: + pull_request_target: branches: [master] types: [closed] diff --git a/.github/workflows/playground_preview_upload.yml b/.github/workflows/playground_preview_upload.yml new file mode 100644 index 00000000000..767274d98f4 --- /dev/null +++ b/.github/workflows/playground_preview_upload.yml @@ -0,0 +1,51 @@ +name: Playground Preview Upload + +on: + workflow_run: + workflows: [CI] + types: [completed] + +jobs: + upload: + if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.pull_requests[0].base.ref == 'master' }} + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: read + env: + PLAYGROUND_PREVIEW_ID: pr-${{ github.event.workflow_run.pull_requests[0].number }} + steps: + - name: Download preview bundle artifact + uses: actions/download-artifact@v8 + with: + name: dev-playground-pr-${{ github.event.workflow_run.pull_requests[0].number }}-bundle + path: dev-playground-preview-bundle + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Rclone + uses: cometkim/rclone-actions/setup-rclone@main + + - name: Configure Rclone remote + uses: cometkim/rclone-actions/configure-remote/s3-provider@main + with: + name: rescript + provider: Cloudflare + endpoint: https://${{ vars.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com + access-key-id: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }} + secret-access-key: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }} + acl: private + + - name: Upload preview bundle + run: | + rclone sync \ + --stats 5 \ + --checkers 5000 \ + --transfers 8 \ + --buffer-size 128M \ + --s3-no-check-bucket \ + --s3-chunk-size 128M \ + --s3-upload-concurrency 8 \ + --fast-list \ + "dev-playground-preview-bundle" \ + "rescript:cdn-assets/dev-playground-bundles/${PLAYGROUND_PREVIEW_ID}/bundle" diff --git a/packages/playground/package.json b/packages/playground/package.json index ca37dc2b818..3f72c73adc1 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -7,7 +7,6 @@ "test": "node ./playground_test.cjs", "build": "rescript clean && rescript build && node scripts/generate_cmijs.mjs && rollup -c && touch .buildstamp", "upload-bundle": "node scripts/upload_bundle.mjs", - "upload-preview-bundle": "node scripts/upload_preview_bundle.mjs", "serve-bundle": "node serve-bundle.mjs" }, "dependencies": { diff --git a/packages/playground/scripts/upload_preview_bundle.mjs b/packages/playground/scripts/upload_preview_bundle.mjs deleted file mode 100644 index 458e9ddaeb1..00000000000 --- a/packages/playground/scripts/upload_preview_bundle.mjs +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env node - -// @ts-check - -// Publishes a mutable PR playground compiler bundle to Cloudflare R2. -// The bundle is addressed by PR number, so each PR keeps one latest preview. - -import * as fs from "node:fs"; -import * as path from "node:path"; - -import { - exec, - playgroundDir, - playgroundPackagesDir, -} from "./common.mjs"; - -const previewId = process.env.PLAYGROUND_PREVIEW_ID; -if (!previewId || !/^pr-[0-9]+$/.test(previewId)) { - throw new Error("PLAYGROUND_PREVIEW_ID must look like pr-123"); -} - -const rcloneOpts = [ - "--stats 5", - "--checkers 5000", - "--transfers 8", - "--buffer-size 128M", - "--s3-no-check-bucket", - "--s3-chunk-size 128M", - "--s3-upload-concurrency 8", -].join(" "); - -const remote = process.env.RCLONE_REMOTE || "rescript"; -const bucket = "cdn-assets"; -const tmpDir = path.join(playgroundDir, ".tmp", "preview"); -const artifactsDir = path.join(tmpDir, previewId); -const target = `${remote}:${bucket}/dev-playground-bundles/${previewId}/bundle`; - -fs.rmSync(tmpDir, { recursive: true, force: true }); -fs.mkdirSync(artifactsDir, { recursive: true }); - -console.log("Copying compiler.js"); -fs.copyFileSync( - path.join(playgroundDir, "compiler.js"), - path.join(artifactsDir, "compiler.js"), -); - -console.log("Copying packages"); -fs.cpSync(playgroundPackagesDir, artifactsDir, { recursive: true }); - -console.log(`Uploading playground preview ${previewId}`); -exec(`rclone sync ${rcloneOpts} --fast-list \\ - "${artifactsDir}" \\ - "${target}" -`); - -console.log(`Uploaded playground preview to ${target}`); From db758188b896241da421bd291d4dcf3699fa6c3f Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Thu, 28 May 2026 18:18:28 +0200 Subject: [PATCH 3/3] Post a comment with the playground URL --- .github/workflows/playground_preview_upload.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/playground_preview_upload.yml b/.github/workflows/playground_preview_upload.yml index 767274d98f4..3c38f7498ad 100644 --- a/.github/workflows/playground_preview_upload.yml +++ b/.github/workflows/playground_preview_upload.yml @@ -12,6 +12,7 @@ jobs: permissions: actions: read contents: read + pull-requests: write env: PLAYGROUND_PREVIEW_ID: pr-${{ github.event.workflow_run.pull_requests[0].number }} steps: @@ -49,3 +50,11 @@ jobs: --fast-list \ "dev-playground-preview-bundle" \ "rescript:cdn-assets/dev-playground-bundles/${PLAYGROUND_PREVIEW_ID}/bundle" + + - name: Comment playground preview URL + uses: thollander/actions-comment-pull-request@v3 + with: + pr-number: ${{ github.event.workflow_run.pull_requests[0].number }} + comment-tag: dev-playground-preview + message: | + Developer playground preview: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/dev-playground/?version=pr-${{ github.event.workflow_run.pull_requests[0].number }}