name: Preview on: pull_request: # Also trigger on push to PR branches (for bots that push via API) push: branches: - "opencode/**" - "changeset-release/main" permissions: contents: write pull-requests: write concurrency: group: preview-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: # Resolve PR number for push events (bots pushing via API) resolve-pr: name: Resolve PR if: ${{ github.event_name == 'push' }} runs-on: ubuntu-latest outputs: pr_number: ${{ steps.find-pr.outputs.pr_number }} is_fork: ${{ steps.find-pr.outputs.is_fork }} steps: - name: Find PR for branch id: find-pr uses: actions/github-script@v7 with: script: | const branch = context.ref.replace('refs/heads/', ''); const { data: prs } = await github.rest.pulls.list({ owner: context.repo.owner, repo: context.repo.repo, head: `${context.repo.owner}:${branch}`, state: 'open', }); if (prs.length === 0) { core.warning(`No open PR found for branch ${branch}; skipping PR-scoped preview steps.`); core.setOutput('pr_number', ''); core.setOutput('is_fork', ''); return; } const pr = prs[0]; core.setOutput('pr_number', pr.number.toString()); core.setOutput('is_fork', (pr.head.repo.full_name !== pr.base.repo.full_name).toString()); pkg-preview: name: Package Preview if: ${{ github.repository_owner == 'cloudflare' }} runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 1 - name: Install Dependencies uses: ./.github/actions/install-dependencies with: filter: "@cloudflare/kumo..." - name: Build @cloudflare/kumo run: pnpm --filter @cloudflare/kumo build - name: Publish preview package run: pnpm dlx pkg-pr-new publish --pnpm --compact --no-template ./packages/kumo # Stage 1: Build docs (runs for ALL PRs including forks). # Produces an artifact consumed by either docs-deploy (internal) or # the preview-deploy.yml workflow_run workflow (forks). docs-build: name: Docs Build runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 1 - name: Install Dependencies uses: ./.github/actions/install-dependencies with: filter: "@cloudflare/kumo-docs-astro..." - name: Build @cloudflare/kumo run: pnpm --filter @cloudflare/kumo build - name: Build docs run: pnpm --filter @cloudflare/kumo-docs-astro build - name: Test docs run: pnpm --filter @cloudflare/kumo-docs-astro test - name: Upload docs artifact uses: actions/upload-artifact@v4 with: name: docs-preview-dist path: packages/kumo-docs-astro/dist/ include-hidden-files: true retention-days: 1 # Stage 2 (internal path): Deploy docs preview for non-fork PRs. # Runs in the same workflow with secrets available. # Fork PRs are handled by preview-deploy.yml (workflow_run). docs-deploy: name: Docs Deploy needs: [docs-build, resolve-pr] # Run for pull_request (non-fork) OR push events with resolved PR (non-fork) if: | always() && needs.docs-build.result == 'success' && ( (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || (github.event_name == 'push' && needs.resolve-pr.result == 'success' && needs.resolve-pr.outputs.is_fork == 'false') ) runs-on: ubuntu-latest timeout-minutes: 10 outputs: preview_url: ${{ steps.deploy.outputs.preview_url }} pr_number: ${{ steps.set-pr.outputs.pr_number }} steps: - name: Set PR number id: set-pr run: | if [ "${{ github.event_name }}" == "pull_request" ]; then echo "pr_number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" else echo "pr_number=${{ needs.resolve-pr.outputs.pr_number }}" >> "$GITHUB_OUTPUT" fi - name: Checkout uses: actions/checkout@v4 with: sparse-checkout: packages/kumo-docs-astro/wrangler.jsonc - name: Download docs artifact uses: actions/download-artifact@v4 with: name: docs-preview-dist path: packages/kumo-docs-astro/dist/ - name: Deploy docs preview id: deploy working-directory: packages/kumo-docs-astro env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} PR_NUMBER: ${{ steps.set-pr.outputs.pr_number }} run: | COMMIT_SHORT="${GITHUB_SHA:0:7}" # "wrangler versions upload" creates a preview version without # routing production traffic. Safe to run on every PR. VERSION_OUTPUT=$(npx wrangler versions upload --env="" --message "PR #${PR_NUMBER} (${COMMIT_SHORT})" 2>&1) || true echo "$VERSION_OUTPUT" # Verify upload succeeded if ! echo "$VERSION_OUTPUT" | grep -q "Worker Version ID:"; then echo "::error::wrangler versions upload failed -- no Worker Version ID in output" exit 1 fi # Extract preview URL from wrangler output PREVIEW_URL=$(echo "$VERSION_OUTPUT" | grep -oE 'Version Preview URL: https://[^ ]+' | sed 's/Version Preview URL: //') # Fallback: construct URL from version ID if [ -z "$PREVIEW_URL" ]; then VERSION_ID=$(echo "$VERSION_OUTPUT" | grep -oE 'Worker Version ID: [a-f0-9-]+' | sed 's/Worker Version ID: //') if [ -n "$VERSION_ID" ]; then VERSION_PREFIX=$(echo "$VERSION_ID" | cut -c1-8) PREVIEW_URL="https://${VERSION_PREFIX}-kumo-docs.design-engineering.workers.dev" fi fi if [ -z "$PREVIEW_URL" ]; then echo "::error::Failed to determine docs preview URL" exit 1 fi echo "preview_url=$PREVIEW_URL" >> "$GITHUB_OUTPUT" - name: Comment on PR uses: actions/github-script@v7 env: PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }} PR_NUMBER: ${{ steps.set-pr.outputs.pr_number }} with: script: | const marker = ''; const previewUrl = process.env.PREVIEW_URL; const prNumber = parseInt(process.env.PR_NUMBER, 10); const commitSha = context.sha.substring(0, 7); const body = [ marker, '### Docs Preview', '', `[View docs preview](${previewUrl})`, '', `Commit: \`${commitSha}\``, ].join('\n'); const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, }); const existing = comments.find(c => c.body?.startsWith(marker)); if (existing) { await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body, }); } else { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body, }); } visual-regression: name: Visual Regression needs: docs-deploy if: always() && needs.docs-deploy.result == 'success' && needs.docs-deploy.outputs.preview_url != '' runs-on: ubuntu-latest timeout-minutes: 15 steps: # Pin to main so that a PR modifying run-visual-regression.ts cannot # execute its own version of the script with secrets in env. - name: Checkout VR script from main uses: actions/checkout@v4 with: ref: main sparse-checkout: | ci/visual-regression .github/actions/install-dependencies # Fetch the PR's HEAD so git diff can detect changed files. # (HEAD points to main after checkout, so we need the explicit SHA) - name: Fetch PR head for git diff run: git fetch --depth=1 origin ${{ github.event.pull_request.head.sha }} - name: Install Dependencies uses: ./.github/actions/install-dependencies with: filter: "@cloudflare/kumo..." - name: Run Visual Regression env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_PR_NUMBER: ${{ needs.docs-deploy.outputs.pr_number }} GITHUB_REPOSITORY: ${{ github.repository }} PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} BEFORE_URL: "https://kumo-ui.com" AFTER_URL: ${{ needs.docs-deploy.outputs.preview_url }} SCREENSHOT_API_KEY: ${{ secrets.SCREENSHOT_API_KEY }} run: pnpm tsx ci/visual-regression/run-visual-regression.ts - name: Upload Screenshots uses: actions/upload-artifact@v4 if: always() with: name: visual-regression-screenshots path: ci/visual-regression/screenshots/ retention-days: 7