From 873cbba605f562d6e3a96a4b0e068f30014da165 Mon Sep 17 00:00:00 2001 From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com> Date: Sat, 24 Oct 2026 10:54:12 -0500 Subject: [PATCH 1/3] web: a Debug testapp on the store test channel, and qa:needed PR previews web.yml builds fizzyed.it/testapp/debug/ from main on every push: Debug, -Dapp-registry-url the store test catalog, storage fizzy-testapp-debug. web-pr.yml builds fizzyed.it/testapp/pr-N/ only for same-repo PRs labelled qa:needed (on label, and on push while labelled), tells the PR's private thread, and removes the release when the PR closes or loses the label. Part of fizzyedit/work#188 Agent-Team: linux Worker: claude Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/web-pr.yml | 149 +++++++++++++++++++++++++++++++++++ .github/workflows/web.yml | 32 ++++++-- 2 files changed, 174 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/web-pr.yml diff --git a/.github/workflows/web-pr.yml b/.github/workflows/web-pr.yml new file mode 100644 index 000000000..3e9bb6d28 --- /dev/null +++ b/.github/workflows/web-pr.yml @@ -0,0 +1,149 @@ +# A preview of a pull request's head at fizzyed.it/testapp/pr-N/, for the PRs a person asked QA +# on: **only those labelled `qa:needed`**, so unlabelled PRs cost no CI. +# +# - Built when the label is applied, and on each push while the PR carries it. +# - A Debug build that loads plugins through the store's test channel, like +# fizzyed.it/testapp/debug/ (web.yml), with browser storage of its own per PR. +# - Removed when the PR closes or loses the label: its release in fizzyedit/website is deleted and +# the site redeployed, which lays out only the releases that exist (website `deploy-web.yml` +# puts each `web-pr-N` under /testapp/pr-N/). +# - The URL is commented on the PR's private thread (the fizzyedit/work issue titled +# `PR #N: ...`), never on the PR, whose watchers are public. +# +# Same-repo PRs only: a fork's code never gets a build here. The build job runs the PR's code +# with no secret; only `publish`, which checks out nothing, holds WEBSITE_DISPATCH_TOKEN. +# +# Needs the secrets WEBSITE_DISPATCH_TOKEN (as web.yml) and WORK_THREAD_TOKEN: Issues read/write +# on fizzyedit/work, for the thread comment (the step is skipped without it). + +name: Web PR preview + +on: + pull_request: + types: [labeled, unlabeled, synchronize, reopened, closed] + +env: + ZIG_VERSION: "0.17.0" + WEBSITE: fizzyedit/website + +# One preview per PR at a time; a push replaces the build in flight. +concurrency: + group: web-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + build: + # Labelled qa:needed, and (for the labeled event) by that very label. + if: >- + github.event.action != 'closed' + && github.event.pull_request.head.repo.full_name == github.repository + && contains(github.event.pull_request.labels.*.name, 'qa:needed') + && (github.event.action != 'labeled' || github.event.label.name == 'qa:needed') + && github.event.action != 'unlabeled' + permissions: + contents: read + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Zig cache key + id: zig-cache-key + run: | + prefix="zig-${{ env.ZIG_VERSION }}-web-pr-${{ runner.os }}-${{ runner.arch }}-" + echo "key=${prefix}${{ hashFiles('**/build.zig.zon') }}" >> "$GITHUB_OUTPUT" + - uses: mlugg/setup-zig@v2 + with: + version: ${{ env.ZIG_VERSION }} + use-cache: false + # Restore only: a PR's build never writes the cache main's builds share. + - name: Restore Zig cache + uses: actions/cache/restore@v4 + with: + path: .zig-cache + key: ${{ steps.zig-cache-key.outputs.key }} + restore-keys: zig-${{ env.ZIG_VERSION }}- + - name: Pre-create Zig cache tmp/ + run: mkdir -p "$ZIG_GLOBAL_CACHE_DIR/tmp" "$ZIG_LOCAL_CACHE_DIR/tmp" + - name: Fetch dependencies (with retries) + run: bash scripts/ci-retry.sh zig build --fetch + - name: Build web app + run: | + bash scripts/ci-retry.sh zig build web -Doptimize=Debug \ + -Dweb-storage="fizzy-testapp-pr-${{ github.event.pull_request.number }}" \ + -Dapp-registry-url=https://plugins.fizzyed.it/test/catalog + - name: Zip the build + env: + SHA: ${{ github.event.pull_request.head.sha }} + PR: ${{ github.event.pull_request.number }} + run: | + printf '{"channel":"pr-%s","sha":"%s","run":"%s/%s/actions/runs/%s"}\n' \ + "$PR" "$SHA" "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID" > zig-out/web/build.json + (cd zig-out/web && zip -qr "$GITHUB_WORKSPACE/web.zip" .) + - uses: actions/upload-artifact@v4 + with: + name: web-pr + path: web.zip + retention-days: 1 + + publish: + needs: build + permissions: + contents: read + runs-on: ubuntu-latest + env: + PR: ${{ github.event.pull_request.number }} + RELEASE: web-pr-${{ github.event.pull_request.number }} + steps: + - uses: actions/download-artifact@v4 + with: + name: web-pr + - name: Hand the build to the website + env: + GH_TOKEN: ${{ secrets.WEBSITE_DISPATCH_TOKEN }} + SHA: ${{ github.event.pull_request.head.sha }} + run: | + notes="Preview of $GITHUB_REPOSITORY#$PR at https://fizzyed.it/testapp/pr-$PR/, built from $SHA" + if ! gh release view "$RELEASE" --repo "$WEBSITE" > /dev/null 2>&1; then + gh release create "$RELEASE" --repo "$WEBSITE" --prerelease --title "$RELEASE" --notes "$notes" + fi + gh release upload "$RELEASE" web.zip --clobber --repo "$WEBSITE" + gh release edit "$RELEASE" --repo "$WEBSITE" --notes "$notes" + gh api "repos/$WEBSITE/dispatches" -f event_type=fizzy-updated + # Once per PR: a later push replaces the build under the same URL. + - name: Tell the PR's private thread + env: + GH_TOKEN: ${{ secrets.WORK_THREAD_TOKEN }} + SHA: ${{ github.event.pull_request.head.sha }} + run: | + if [ -z "$GH_TOKEN" ]; then echo "No WORK_THREAD_TOKEN: the thread is not told."; exit 0; fi + url="https://fizzyed.it/testapp/pr-$PR/" + thread=$(gh issue list -R fizzyedit/work --label pr-thread --state all --search "\"PR #$PR:\" in:title" \ + --json number,title --jq "[.[] | select(.title | startswith(\"PR #$PR:\"))][0].number // empty") + if [ -z "$thread" ]; then echo "No thread for PR #$PR: nothing to comment on."; exit 0; fi + if gh issue view "$thread" -R fizzyedit/work --json comments --jq '.comments[].body' | grep -qF "Preview: $url"; then + echo "Thread already has the URL."; exit 0 + fi + printf 'Preview: %s\n\nA Debug build of %s, loading plugins from the store test channel. It is rebuilt on each push while the PR is labelled `qa:needed`, and removed when it closes or loses the label.\n' \ + "$url" "$SHA" | gh issue comment "$thread" -R fizzyedit/work --body-file - + + # The release (and so /testapp/pr-N/) goes when the PR closes or loses the label. + remove: + if: >- + github.event.pull_request.head.repo.full_name == github.repository + && (github.event.action == 'closed' + || (github.event.action == 'unlabeled' && github.event.label.name == 'qa:needed')) + runs-on: ubuntu-latest + env: + RELEASE: web-pr-${{ github.event.pull_request.number }} + steps: + - name: Remove the preview + env: + GH_TOKEN: ${{ secrets.WEBSITE_DISPATCH_TOKEN }} + run: | + if gh release view "$RELEASE" --repo "$WEBSITE" > /dev/null 2>&1; then + gh release delete "$RELEASE" --repo "$WEBSITE" --cleanup-tag --yes + gh api "repos/$WEBSITE/dispatches" -f event_type=fizzy-updated + else + echo "No $RELEASE to remove." + fi diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index a74d33b3f..a8f5265f5 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -17,9 +17,16 @@ # a clean release. A record, not a tag: pointing a ref at a release commit whose workflows differ # from main's counts as changing workflows, which this workflow's token may never do. # +# - **fizzyed.it/testapp/debug/ is `main` as a Debug build that loads its plugins through the +# store's test channel** (`-Dapp-registry-url=`; design/store.md in fizzyedit/plans), +# so find, install, update and load are tried end to end without going live. Nothing is compiled +# in: the web cannot load plugin libraries, but it can fetch the store's wasm builds. Its +# storage name is its own too. +# # Each copy goes to a release in fizzyedit/website (`web-main` for the app, `web-preview` for the -# test copy), its zip replaced every time, and the website is asked to redeploy. Pull requests -# publish nothing: ci.yml compiles them for the web (`check-web`). +# test copy, `web-debug` for the debug copy), its zip replaced every time, and the website is asked +# to redeploy. Pull requests publish nothing here: ci.yml compiles them for the web +# (`check-web`), and web-pr.yml previews the ones labelled `qa:needed`. # # A release published here asks for a redeploy too, without a build: the site links releases. # @@ -107,14 +114,19 @@ jobs: live_tag=$(gh api "repos/$GITHUB_REPOSITORY/deployments?environment=web-live&per_page=1" --jq '.[0].description // ""') live="" if [ -n "$live_tag" ]; then live=$(git show "$live_tag:sdk/src/version.zig" 2>/dev/null | fingerprint || true); fi - copies='[{"channel":"testapp","release":"web-preview","storage":"fizzy-testapp","ref":"'"$GITHUB_SHA"'"}' + # `optimize` and `registry` per copy: the live store for the real ones, the test channel + # (plugins built against main's SDK) for the Debug copy. + live_reg=https://plugins.fizzyed.it/catalog + test_reg=https://plugins.fizzyed.it/test/catalog + copies='[{"channel":"testapp","release":"web-preview","storage":"fizzy-testapp","optimize":"ReleaseSmall","registry":"'"$live_reg"'","ref":"'"$GITHUB_SHA"'"}' + copies="$copies"',{"channel":"debug","release":"web-debug","storage":"fizzy-testapp-debug","optimize":"Debug","registry":"'"$test_reg"'","ref":"'"$GITHUB_SHA"'"}' if [ -n "$live" ] && [ "$here" = "$live" ]; then echo "main is on the live plugin SDK ($live): the app goes up from it." - copies="$copies"',{"channel":"app","release":"web-main","storage":"fizzy","ref":"'"$GITHUB_SHA"'"}' + copies="$copies"',{"channel":"app","release":"web-main","storage":"fizzy","optimize":"ReleaseSmall","registry":"'"$live_reg"'","ref":"'"$GITHUB_SHA"'"}' elif [ -n "${{ inputs.go_live }}" ]; then ref=$(git rev-parse "$live_tag^{commit}") echo "main has moved past the live SDK: the app goes up from its release commit ($ref)." - copies="$copies"',{"channel":"app","release":"web-main","storage":"fizzy","ref":"'"$ref"'"}' + copies="$copies"',{"channel":"app","release":"web-main","storage":"fizzy","optimize":"ReleaseSmall","registry":"'"$live_reg"'","ref":"'"$ref"'"}' else why="main's plugin SDK fingerprint ($here) is not the live one (${live_tag:-nothing live yet}: ${live:-none})" echo "::notice title=fizzyed.it/app held::$why. It goes up when that SDK goes live (go_live)." @@ -169,9 +181,13 @@ jobs: # class* into its plugin ABI fingerprint — Debug/ReleaseSafe are "safe", ReleaseFast and # ReleaseSmall are "fast" — and the plugin store publishes "fast" builds only. Built # ReleaseSafe, the app asks the registry for a shard nobody publishes under, so every - # plugin in a healthy store reads "No store build" and nothing can be installed. + # plugin in a healthy store reads "No store build" and nothing can be installed. The debug + # copy is the exception on purpose: a Debug host's class is "safe", and the test channel + # publishes ReleaseSafe builds against main's SDK for it. - name: Build web app - run: bash scripts/ci-retry.sh zig build web -Doptimize=ReleaseSmall -Dweb-storage="${{ matrix.storage }}" + run: | + bash scripts/ci-retry.sh zig build web -Doptimize="${{ matrix.optimize }}" \ + -Dweb-storage="${{ matrix.storage }}" -Dapp-registry-url="${{ matrix.registry }}" - name: Save Zig cache if: success() && steps.zig-cache.outputs.cache-matched-key == '' @@ -226,6 +242,8 @@ jobs: run: | if [ "$CHANNEL" = app ]; then notes="The app at https://fizzyed.it/app/, the live plugin SDK, built from $GITHUB_REPOSITORY@$SHA" + elif [ "$CHANNEL" = debug ]; then + notes="main as Debug with the store's test channel, at https://fizzyed.it/testapp/debug/, built from $GITHUB_REPOSITORY@$SHA" else notes="main, at https://fizzyed.it/testapp/, built from $GITHUB_REPOSITORY@$SHA" fi From 8264d0af4d7d8737cf0f4deecc8702a32aaac3da Mon Sep 17 00:00:00 2001 From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:13:07 -0500 Subject: [PATCH 2/3] ci: web-pr preview cancellation and failure path Move the concurrency group from the workflow onto the build, publish and remove jobs, so a label event that skips them cannot cancel a build in flight. publish runs only on success; remove also runs when the build fails, so a stale preview is not left under the PR's URL. Agent-Team: linux Worker: claude Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/web-pr.yml | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/.github/workflows/web-pr.yml b/.github/workflows/web-pr.yml index 3e9bb6d28..ec286178e 100644 --- a/.github/workflows/web-pr.yml +++ b/.github/workflows/web-pr.yml @@ -26,13 +26,14 @@ env: ZIG_VERSION: "0.17.0" WEBSITE: fizzyedit/website -# One preview per PR at a time; a push replaces the build in flight. -concurrency: - group: web-pr-${{ github.event.pull_request.number }} - cancel-in-progress: true - +# One preview per PR at a time: a push replaces the build in flight, and removal replaces a build +# that would put the preview back. The group is on the jobs, not the workflow: a skipped job never +# joins it, so a label event for anything but `qa:needed` cannot cancel a running build. jobs: build: + concurrency: + group: web-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true # Labelled qa:needed, and (for the labeled event) by that very label. if: >- github.event.action != 'closed' @@ -88,6 +89,10 @@ jobs: publish: needs: build + if: success() + concurrency: + group: web-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true permissions: contents: read runs-on: ubuntu-latest @@ -127,12 +132,20 @@ jobs: printf 'Preview: %s\n\nA Debug build of %s, loading plugins from the store test channel. It is rebuilt on each push while the PR is labelled `qa:needed`, and removed when it closes or loses the label.\n' \ "$url" "$SHA" | gh issue comment "$thread" -R fizzyedit/work --body-file - - # The release (and so /testapp/pr-N/) goes when the PR closes or loses the label. + # The release (and so /testapp/pr-N/) goes when the PR closes or loses the label, and when a + # build fails, so the URL never serves an older push's build as if it were this one. remove: + needs: build if: >- - github.event.pull_request.head.repo.full_name == github.repository + !cancelled() + && github.event.pull_request.head.repo.full_name == github.repository && (github.event.action == 'closed' - || (github.event.action == 'unlabeled' && github.event.label.name == 'qa:needed')) + || (github.event.action == 'unlabeled' && github.event.label.name == 'qa:needed') + || needs.build.result == 'failure') + concurrency: + group: web-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + permissions: {} runs-on: ubuntu-latest env: RELEASE: web-pr-${{ github.event.pull_request.number }} From 6a215a40843ae29fa10a700cd306cf2e0166f2f2 Mon Sep 17 00:00:00 2001 From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:20:05 -0500 Subject: [PATCH 3/3] ci: web-pr previews on their own origin, serialized deploys Agent-Team: linux Worker: claude Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/web-pr.yml | 143 ++++++++++++++++++++++++----------- .github/workflows/web.yml | 6 +- 2 files changed, 101 insertions(+), 48 deletions(-) diff --git a/.github/workflows/web-pr.yml b/.github/workflows/web-pr.yml index ec286178e..6ad2e764e 100644 --- a/.github/workflows/web-pr.yml +++ b/.github/workflows/web-pr.yml @@ -1,20 +1,23 @@ -# A preview of a pull request's head at fizzyed.it/testapp/pr-N/, for the PRs a person asked QA -# on: **only those labelled `qa:needed`**, so unlabelled PRs cost no CI. +# A preview of a pull request's head at https://pr-N-fizzy-previews.fizzyedit.workers.dev/, for the +# PRs a person asked QA on: **only those labelled `qa:needed`**, so unlabelled PRs cost no CI. # # - Built when the label is applied, and on each push while the PR carries it. # - A Debug build that loads plugins through the store's test channel, like # fizzyed.it/testapp/debug/ (web.yml), with browser storage of its own per PR. -# - Removed when the PR closes or loses the label: its release in fizzyedit/website is deleted and -# the site redeployed, which lays out only the releases that exist (website `deploy-web.yml` -# puts each `web-pr-N` under /testapp/pr-N/). +# - Served from a **separate origin** (a Cloudflare Workers preview of the Worker `fizzy-previews`, +# behind Cloudflare Access), never fizzyed.it: a preview runs a PR's code before it is reviewed, +# and must not share browser storage or OAuth state with the real site. +# - Removed when the PR closes or loses the label, or when its build fails. # - The URL is commented on the PR's private thread (the fizzyedit/work issue titled # `PR #N: ...`), never on the PR, whose watchers are public. # -# Same-repo PRs only: a fork's code never gets a build here. The build job runs the PR's code -# with no secret; only `publish`, which checks out nothing, holds WEBSITE_DISPATCH_TOKEN. +# Same-repo PRs only: a fork's code never gets a build here. Three kinds of job, three secrets: +# `build` runs the PR's code with no secret; `publish` and `remove` hold the Cloudflare secrets +# and run none of it (they unzip the build into a directory and hand it to wrangler); `comment` +# holds WORK_THREAD_TOKEN and checks out nothing. # -# Needs the secrets WEBSITE_DISPATCH_TOKEN (as web.yml) and WORK_THREAD_TOKEN: Issues read/write -# on fizzyedit/work, for the thread comment (the step is skipped without it). +# Needs the secrets CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID (Workers edit), and +# WORK_THREAD_TOKEN: Issues read/write on fizzyedit/work (the thread comment is skipped without it). name: Web PR preview @@ -24,15 +27,19 @@ on: env: ZIG_VERSION: "0.17.0" - WEBSITE: fizzyedit/website + WORKER: fizzy-previews + SUBDOMAIN: fizzyedit + WRANGLER: wrangler@4.135.0 -# One preview per PR at a time: a push replaces the build in flight, and removal replaces a build -# that would put the preview back. The group is on the jobs, not the workflow: a skipped job never -# joins it, so a label event for anything but `qa:needed` cannot cancel a running build. +# Two groups per PR. `web-pr-N-build` is cancel-in-progress: a push replaces the build in flight, +# and a skipped job never joins it, so a label event for anything but `qa:needed` cannot cancel a +# running build. `web-pr-N-deploy` (publish, remove) queues and never cancels, so a deploy is +# never cut off halfway and a build's cancellation cannot be caused by an older run's publish; each +# deploy job checks the PR's current head before it acts. jobs: build: concurrency: - group: web-pr-${{ github.event.pull_request.number }} + group: web-pr-${{ github.event.pull_request.number }}-build cancel-in-progress: true # Labelled qa:needed, and (for the labeled event) by that very label. if: >- @@ -91,72 +98,116 @@ jobs: needs: build if: success() concurrency: - group: web-pr-${{ github.event.pull_request.number }} - cancel-in-progress: true + group: web-pr-${{ github.event.pull_request.number }}-deploy + cancel-in-progress: false permissions: contents: read + pull-requests: read runs-on: ubuntu-latest env: PR: ${{ github.event.pull_request.number }} - RELEASE: web-pr-${{ github.event.pull_request.number }} + SHA: ${{ github.event.pull_request.head.sha }} + outputs: + url: ${{ steps.deploy.outputs.url }} steps: + # A newer push, a closed PR or a removed label since this run's build: publish nothing, so + # an older build never overwrites a newer one nor brings back a removed preview. + - name: Is this still the PR's head? + id: fresh + env: + GH_TOKEN: ${{ github.token }} + run: | + read -r state head labelled < <(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR" \ + --jq '[.state, .head.sha, ([.labels[].name] | index("qa:needed") != null)] | @tsv') + if [ "$state" = open ] && [ "$head" = "$SHA" ] && [ "$labelled" = true ]; then + echo "current=true" >> "$GITHUB_OUTPUT" + else + echo "Stale run: PR is $state at $head (labelled: $labelled), this build is $SHA. Nothing published." + echo "current=false" >> "$GITHUB_OUTPUT" + fi - uses: actions/download-artifact@v4 + if: steps.fresh.outputs.current == 'true' with: name: web-pr - - name: Hand the build to the website + - name: Deploy the preview + id: deploy + if: steps.fresh.outputs.current == 'true' env: - GH_TOKEN: ${{ secrets.WEBSITE_DISPATCH_TOKEN }} - SHA: ${{ github.event.pull_request.head.sha }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} run: | - notes="Preview of $GITHUB_REPOSITORY#$PR at https://fizzyed.it/testapp/pr-$PR/, built from $SHA" - if ! gh release view "$RELEASE" --repo "$WEBSITE" > /dev/null 2>&1; then - gh release create "$RELEASE" --repo "$WEBSITE" --prerelease --title "$RELEASE" --notes "$notes" + if [ -z "$CLOUDFLARE_API_TOKEN" ] || [ -z "$CLOUDFLARE_ACCOUNT_ID" ]; then + echo "::error title=No Cloudflare secrets::CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID must be set for previews." + exit 1 fi - gh release upload "$RELEASE" web.zip --clobber --repo "$WEBSITE" - gh release edit "$RELEASE" --repo "$WEBSITE" --notes "$notes" - gh api "repos/$WEBSITE/dispatches" -f event_type=fizzy-updated - # Once per PR: a later push replaces the build under the same URL. + # The build is data here: unzipped, never run. + mkdir site deploy + unzip -q web.zip -d site + printf '{"name":"%s","compatibility_date":"2026-10-01","assets":{"directory":"../site"}}\n' "$WORKER" > deploy/wrangler.jsonc + (cd deploy && npx --yes "$WRANGLER" preview --name "pr-$PR") + echo "url=https://pr-$PR-$WORKER.$SUBDOMAIN.workers.dev/" >> "$GITHUB_OUTPUT" + + # A job of its own: WORK_THREAD_TOKEN never sits beside the PR's code or the Cloudflare secrets. + # Once per PR: a later push replaces the build under the same URL. + comment: + needs: publish + if: needs.publish.outputs.url != '' + permissions: {} + runs-on: ubuntu-latest + env: + PR: ${{ github.event.pull_request.number }} + SHA: ${{ github.event.pull_request.head.sha }} + URL: ${{ needs.publish.outputs.url }} + steps: - name: Tell the PR's private thread env: GH_TOKEN: ${{ secrets.WORK_THREAD_TOKEN }} - SHA: ${{ github.event.pull_request.head.sha }} run: | if [ -z "$GH_TOKEN" ]; then echo "No WORK_THREAD_TOKEN: the thread is not told."; exit 0; fi - url="https://fizzyed.it/testapp/pr-$PR/" thread=$(gh issue list -R fizzyedit/work --label pr-thread --state all --search "\"PR #$PR:\" in:title" \ --json number,title --jq "[.[] | select(.title | startswith(\"PR #$PR:\"))][0].number // empty") if [ -z "$thread" ]; then echo "No thread for PR #$PR: nothing to comment on."; exit 0; fi - if gh issue view "$thread" -R fizzyedit/work --json comments --jq '.comments[].body' | grep -qF "Preview: $url"; then + if gh issue view "$thread" -R fizzyedit/work --json comments --jq '.comments[].body' | grep -qF "Preview: $URL"; then echo "Thread already has the URL."; exit 0 fi - printf 'Preview: %s\n\nA Debug build of %s, loading plugins from the store test channel. It is rebuilt on each push while the PR is labelled `qa:needed`, and removed when it closes or loses the label.\n' \ - "$url" "$SHA" | gh issue comment "$thread" -R fizzyedit/work --body-file - + printf 'Preview: %s\n\nA Debug build of %s on an origin of its own, loading plugins from the store test channel. It is rebuilt on each push while the PR is labelled `qa:needed`, and removed when it closes or loses the label.\n' \ + "$URL" "$SHA" | gh issue comment "$thread" -R fizzyedit/work --body-file - - # The release (and so /testapp/pr-N/) goes when the PR closes or loses the label, and when a - # build fails, so the URL never serves an older push's build as if it were this one. + # The preview goes when the PR closes or loses the label, and when this push's build or publish + # fails, so the URL never serves an older push's build as if it were this one. A failure only + # removes while this run's commit is still the PR's head: a newer push owns the preview. remove: - needs: build + needs: [build, publish] if: >- !cancelled() && github.event.pull_request.head.repo.full_name == github.repository && (github.event.action == 'closed' || (github.event.action == 'unlabeled' && github.event.label.name == 'qa:needed') - || needs.build.result == 'failure') + || needs.build.result == 'failure' + || needs.publish.result == 'failure') concurrency: - group: web-pr-${{ github.event.pull_request.number }} - cancel-in-progress: true - permissions: {} + group: web-pr-${{ github.event.pull_request.number }}-deploy + cancel-in-progress: false + permissions: + contents: read + pull-requests: read runs-on: ubuntu-latest env: - RELEASE: web-pr-${{ github.event.pull_request.number }} + PR: ${{ github.event.pull_request.number }} + SHA: ${{ github.event.pull_request.head.sha }} steps: - name: Remove the preview env: - GH_TOKEN: ${{ secrets.WEBSITE_DISPATCH_TOKEN }} + GH_TOKEN: ${{ github.token }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} run: | - if gh release view "$RELEASE" --repo "$WEBSITE" > /dev/null 2>&1; then - gh release delete "$RELEASE" --repo "$WEBSITE" --cleanup-tag --yes - gh api "repos/$WEBSITE/dispatches" -f event_type=fizzy-updated - else - echo "No $RELEASE to remove." + if [ -z "$CLOUDFLARE_API_TOKEN" ] || [ -z "$CLOUDFLARE_ACCOUNT_ID" ]; then + echo "::error title=No Cloudflare secrets::CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID must be set for previews." + exit 1 + fi + if [ "${{ github.event.action }}" = synchronize ] || [ "${{ github.event.action }}" = labeled ] || [ "${{ github.event.action }}" = reopened ]; then + head=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR" --jq .head.sha) + if [ "$head" != "$SHA" ]; then echo "A newer push ($head) owns the preview; leaving it."; exit 0; fi fi + npx --yes "$WRANGLER" preview delete --name "pr-$PR" --skip-confirmation || echo "No preview pr-$PR to remove." diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index a8f5265f5..e506499b4 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -182,8 +182,10 @@ jobs: # ReleaseSmall are "fast" — and the plugin store publishes "fast" builds only. Built # ReleaseSafe, the app asks the registry for a shard nobody publishes under, so every # plugin in a healthy store reads "No store build" and nothing can be installed. The debug - # copy is the exception on purpose: a Debug host's class is "safe", and the test channel - # publishes ReleaseSafe builds against main's SDK for it. + # copy is the exception on purpose: a Debug host's class is "safe", so it reads the test + # channel's "safe" shard (ReleaseSafe plugin builds against main's SDK). Until the app asks + # for the class it was built with (work#188 step d, `optimize_classes`), it still asks for + # "fast" and installs nothing from the test channel. - name: Build web app run: | bash scripts/ci-retry.sh zig build web -Doptimize="${{ matrix.optimize }}" \