ci(ui-preview): give fork PRs preview deploys (build-untrusted / deploy-trusted split) - #643
Conversation
|
Note Gittensory Gate skippedPR closed before full evaluation. No late first comment was created.
💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers. |
|
gittensory · advisory review Reviewed 2 changed file(s) — two independent AI reviewers. Suggested action: ✅ Safe to merge — both reviewers found no blocking issues. Reviewer A · Suggestions
Worth double-checking
Reviewer B · Suggestions
Worth double-checking
|
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
… deploy-trusted split Fork PRs never got a UI preview: ui-preview.yml was gated `if: head.repo.full_name == github.repository` (forks have no Cloudflare secrets), so it skipped entirely, no Deployment was recorded, and Reviewbot's before/after comment was stuck on a perpetual "Rendering preview…" placeholder. Workers Builds can't fill the gap either — it only builds branches that live in this repo, and a fork PR's commits live in the contributor's fork. Split the pipeline so forks get previews without exposing secrets to untrusted code: - ui-preview.yml (renamed "UI Preview Build", on: pull_request): builds EVERY PR incl forks, WITHOUT secrets (read-only token), and uploads only the built `dist`. - ui-preview-deploy.yml (new, on: workflow_run): runs from the DEFAULT branch (so the definition is trusted, never the fork's) WITH secrets. Downloads the built artifact (never checks out or runs fork source), `wrangler versions upload`s it to a transient workers.dev preview, and records the GitHub Deployment + status Reviewbot reads. Security boundary: the build runs fork code with no secret access; the deploy has secrets but runs no fork code (wrangler only uploads the bundle — fork code executes solely inside the isolated workers.dev preview when the URL is visited). The PR number is resolved from the GitHub-set head_sha (workflow_run.pull_requests is empty for forks), never from fork-supplied data, and the Wrangler config is written trustside so a fork can't control bindings/routes/vars. No Reviewbot change needed — it reads the same `preview/pr-N` Deployment as before. Note: the deploy half only takes effect once this is on the default branch (workflow_run always uses the default-branch definition), so it can't fully run on this PR itself.
Address the security findings + reviewer notes on the fork-preview deploy workflow: - Bind the deploy job (the only one with Cloudflare secrets) to a `preview` GitHub environment so approval gating / environment-scoped secrets can be attached. Unprotected by default so previews stay automatic. (Superagent P1) - Validate the untrusted fork-built artifact before handing it to wrangler: reject symlinks (path-traversal/exfil vector), require the expected SSR structure (server/index.mjs + client/), and allowlist file extensions so a malicious build can't smuggle scripts/binaries. (Superagent P1) - Pin the parsed preview URL to the gittensory-ui worker on workers.dev, so a changed wrangler output format or any other URL in the logs can't be recorded as the preview by mistake. (both reviewers) actionlint (incl shellcheck) clean.
ced305f to
82b12cc
Compare
… config (#645) The trusted preview config embedded the production custom domain (routes: gittensory.aethereal.dev, custom_domain: true). It is inert under `wrangler versions upload` (a 0%-traffic preview version applies no routes), but it was a latent footgun: if that command were ever changed to `wrangler deploy`, fork-built code would immediately seize the production domain at 100% traffic. A workers.dev preview neither needs nor uses the route, so removing it eliminates that path; a comment now warns against re-adding it. Defense-in-depth follow-up to the fork-safe preview pipeline (#643), flagged by an adversarial security audit of that pipeline.
Problem
Fork PRs (e.g. #635) never get a UI preview, so reviewbot's before/after comment is stuck forever on the "Rendering preview…" placeholder.
Root cause:
ui-preview.ymlis gatedif: head.repo.full_name == github.repository— forks have no Cloudflare secrets, so the job skips entirely → no Deployment recorded → reviewbot has no "after" URL. Workers Builds can't cover it either: it only builds branches in this repo, and a fork PR's commits live in the contributor's fork. So neither mechanism builds fork code.Fix — split build (untrusted) from deploy (trusted)
ui-preview.yml(renamed UI Preview Build,on: pull_request): builds every PR incl forks, without secrets (read-only token), and uploads only the builtdistartifact.ui-preview-deploy.yml(new,on: workflow_run): runs from the default branch (trusted definition, never the fork's) with secrets. Downloads the built artifact, validates it (no symlinks, expected SSR structure, allowlisted file types),wrangler versions uploads it to a transientworkers.devpreview, and records thepreview/pr-NGitHub Deployment + status reviewbot already reads.Security model
wrangleronly uploads the bundle; fork code executes solely inside the isolatedworkers.devpreview when the URL is visited.previewenvironment so approval gating / scoped secrets can be attached.head_sha(workflow_run.pull_requestsis empty for forks), never fork-supplied data; the Wrangler config is written trust-side so a fork can't control bindings/routes/vars.No linked issue: maintainer-lane CI chore
No linked issue: this is a maintainer-lane CI / infra chore on the PR-preview pipeline (safely enabling fork-PR previews). There is no separate tracking issue — it was surfaced directly while debugging reviewbot's stuck "Rendering preview…" on fork PR #635 (referenced for context).
Notes
preview/pr-NDeployment as before.workflow_runalways uses the default-branch workflow definition, so the deploy half can't fully run on this PR — it takes effect once merged. Validate afterward on a fork PR (e.g. update feat(api): maintainer quality-dashboard service + endpoint #635 with main so its build re-runs).actionlint(clean).🤖 Generated with Claude Code