| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- 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
- # The marker sits on its own line: markdown renders no link on a
- # line that opens with a raw HTML comment.
- printf '%s\n\n%s\n' \
- "$marker" \
- "[Preview for #$PR](https://pr-$PR.${CF_PROJECT}.pages.dev) (requires Cloudflare Access sign-in)" \
- | gh pr comment "$PR" --body-file -
|