diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3e37552ac..78bad0e36d 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 @@ -457,6 +471,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 0000000000..447c6e94d1 --- /dev/null +++ b/.github/workflows/playground_preview_cleanup.yml @@ -0,0 +1,28 @@ +name: Playground Preview Cleanup + +on: + pull_request_target: + 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/.github/workflows/playground_preview_upload.yml b/.github/workflows/playground_preview_upload.yml new file mode 100644 index 0000000000..3c38f7498a --- /dev/null +++ b/.github/workflows/playground_preview_upload.yml @@ -0,0 +1,60 @@ +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 + pull-requests: write + 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" + + - 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 }} diff --git a/packages/dev-playground/src/Bindings.res b/packages/dev-playground/src/Bindings.res index 694aad8080..1b7f3f497f 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 25a09c0249..7464ee94b0 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 f0b075bade..45c4c7c540 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 7bd1e608ac..91ffae6b5b 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 625c2a52d8..39a9fbeb2e 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} }