ci: add build-preview workflow

This commit is contained in:
imccyu
2026-08-21 20:35:33 +08:00
parent 3cc90952cc
commit 3a47674798
7 changed files with 254 additions and 3 deletions
@@ -0,0 +1,166 @@
name: Build PR preview
# Every push to a pull request publishes that pull request's preview to
# Cloudflare Pages under its own branch alias, behind Cloudflare Access. The
# upload carries build products only: the workflow never grants the deployment
# platform access to this repository's sources.
on:
pull_request:
types: [opened, synchronize, reopened]
# Within one pull request the newest build wins. Across pull requests there is
# nothing to serialize: each uploads to its own branch alias, so two deployments
# never contend for the same URL.
concurrency:
group: build-preview-cloudflare-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
env:
PRIMARY_NODE_VERSION: '24'
# Cloudflare Pages project receiving the upload. Its preview deployments are
# the surface the Access application protects; the project's production branch
# is deliberately a name no deployment uses, so no unprotected URL exists.
CF_PROJECT: dsh-build-preview
# CI runs must never report to the production telemetry endpoint baked into
# apps/cli/cordis.yml (AppCLIEntry disables the row when set).
DSH_TELEMETRY_DISABLED: '1'
jobs:
preview:
runs-on: dsh-ubuntu-24-04-16core
name: cloudflare pages preview
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: pnpm/action-setup@v4
with:
dest: ${{ runner.temp }}/setup-pnpm
- uses: actions/setup-node@v6
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
- name: Configure pnpm store path
id: pnpm-store
run: |
store_root="$HOME/.local/share/pnpm/store"
echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
echo "path=$store_path" >> "$GITHUB_OUTPUT"
# Read-only: the preview lane consumes the default-branch cache without
# putting cache upload on its own path.
- uses: actions/cache/restore@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
- name: Install (immutable)
run: pnpm install --frozen-lockfile
# apps/web consumes workspace packages as built lib products, and
# build:preview packs the image through the packer's installed bin
# (lib/bin.js), so neither half exists before the full build runs.
- name: Build workspace
run: pnpm run build
- name: Build the preview page and pack the VFS image
env:
DSH_CLIENT_TITLE: DSH preview pr-${{ github.event.pull_request.number }}
run: pnpm --filter @deepseek-ai/dsh-web-frontend run build:preview
# Sourcemaps carry complete sources and stay off the deployment platform.
# index.html is the served page, which cannot boot without a host
# injecting window.__DSH_BOOT__; replacing it with the worker page makes
# the deployment root the usable entry instead of a page that never boots.
- name: Shape the upload
run: |
find apps/web/dist -name '*.map' -delete
cp apps/web/dist/preview.html apps/web/dist/index.html
- name: Upload to Cloudflare Pages
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
npx --yes wrangler@4 pages deploy apps/web/dist \
--project-name "$CF_PROJECT" \
--branch "pr-${{ github.event.pull_request.number }}" \
--commit-dirty=true
# The image is what a worker boot fails on first and least visibly, so the
# run only passes once the protected URL serves it as gzip bytes. Three
# facts are asserted, each with its own failure meaning:
# 200 Access admitted the request; a 302 means the
# Access policy is missing its Service Auth rule
# for this token
# no content-encoding the platform did not claim transport
# compression, which would make the browser
# decode the body and leave the worker's
# DecompressionStream inflating a plain tar
# gzip magic 1f 8b the bytes really are the gzip member the
# packer wrote
# Accept-Encoding is sent because a browser sends it; the assertion is
# about what the platform does with a body that is already compressed.
- name: Verify the protected deployment serves the image
env:
CF_ACCESS_CLIENT_ID: ${{ secrets.CF_ACCESS_CLIENT_ID }}
CF_ACCESS_CLIENT_SECRET: ${{ secrets.CF_ACCESS_CLIENT_SECRET }}
run: |
url="https://pr-${{ github.event.pull_request.number }}.${CF_PROJECT}.pages.dev"
image="$url/preview/vfs-image.tar.gz"
code=000
for attempt in 1 2 3 4 5; do
code=$(curl -sS -o image.bin -D headers.txt -w '%{http_code}' \
-H 'Accept-Encoding: gzip' \
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
"$image" || echo 000)
echo "attempt $attempt: HTTP $code"
if [ "$code" = "200" ]; then break; fi
sleep 10
done
if [ "$code" != "200" ]; then
echo "the protected image URL answered $code, not 200"
head -20 headers.txt
exit 1
fi
if grep -qi '^content-encoding:' headers.txt; then
echo "the platform declared transport compression on an already-compressed image:"
grep -i '^content-encoding:' headers.txt
exit 1
fi
magic=$(head -c 2 image.bin | od -An -tx1 | tr -d ' \n')
if [ "$magic" != "1f8b" ]; then
echo "image does not start with the gzip magic number: $magic"
exit 1
fi
echo "image served as $(wc -c < image.bin) gzip bytes"
# The alias URL follows from the pull request number, so it is stable
# across redeploys and worth stating once. The marker makes the comment
# idempotent: a pull request opened before this workflow existed never
# sees an `opened` event, and every later push must not restate the URL.
- name: Comment the preview URL
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ github.event.pull_request.number }}
run: |
marker='<!-- dsh-preview-url -->'
existing=$(gh pr view "$PR" --json comments \
--jq "[.comments[] | select(.body | contains(\"$marker\")) | .url] | first // empty")
if [ -n "$existing" ]; then
echo "preview URL already commented: $existing"
exit 0
fi
gh pr comment "$PR" --body \
"$marker \n [Preview for #$PR](https://pr-$PR.${CF_PROJECT}.pages.dev) (requires Cloudflare Access sign-in)"