cloudflare/kumo

Public

mirrored from https://github.com/cloudflare/kumoAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
main

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

.github/workflows/preview-deploy.yml

236lines · modecode

1# Deploys docs preview for fork PRs via workflow_run.
2#
3# Security model: fork code runs in the untrusted pull_request context
4# (preview.yml docs-build job) and produces a static HTML artifact.
5# This workflow runs in the base repo context with secrets, but NEVER
6# checks out or executes fork code — it only deploys the built artifact.
7#
8# IMPORTANT: ALL checkouts in this workflow use ref: main (or no ref, which
9# also defaults to main via the workflow_run base context). The wrangler.jsonc
10# was previously checked out from the fork's SHA, but wrangler supports a
11# build.command field that executes arbitrary shell commands — meaning a
12# malicious wrangler.jsonc from a fork could exfiltrate CLOUDFLARE_API_TOKEN.
13# We always deploy using the wrangler config from main.
14#
15# The visual-regression job always checks out ci/visual-regression from main
16# (never the fork's SHA) before running it with secrets. The fork's component
17# changes are captured via the deployed preview URL (AFTER_URL), not via
18# executing the fork's copy of the script.
19#
20# For internal PRs, deployment happens directly in preview.yml (docs-deploy job).
21
22name: Preview Deploy (Forks)
23
24on:
25 workflow_run:
26 workflows: ["Preview"]
27 types: [completed]
28
29permissions:
30 contents: write
31 pull-requests: write
32
33concurrency:
34 group: preview-deploy-${{ github.event.workflow_run.id }}
35 cancel-in-progress: true
36
37jobs:
38 deploy:
39 name: Docs Deploy (Fork)
40 # Only run for:
41 # 1. Successful workflow runs
42 # 2. Pull request events (not push)
43 # 3. Fork PRs only (internal PRs deploy in preview.yml directly)
44 if: >-
45 github.event.workflow_run.conclusion == 'success' &&
46 github.event.workflow_run.event == 'pull_request' &&
47 github.event.workflow_run.head_repository.full_name != github.repository
48 runs-on: ubuntu-latest
49 timeout-minutes: 10
50 outputs:
51 preview_url: ${{ steps.deploy.outputs.preview_url }}
52 pr_number: ${{ steps.metadata.outputs.pr_number }}
53 steps:
54 # Always check out wrangler config from main — never from the fork's SHA.
55 # wrangler.jsonc supports a build.command field that executes arbitrary
56 # shell commands; checking out the fork's version with CLOUDFLARE_API_TOKEN
57 # in env would allow secret exfiltration via a malicious config.
58 - name: Checkout wrangler config from main
59 uses: actions/checkout@v4
60 with:
61 sparse-checkout: packages/kumo-docs-astro/wrangler.jsonc
62 ref: main
63
64 - name: Resolve PR metadata
65 id: metadata
66 uses: actions/github-script@v7
67 with:
68 script: |
69 // workflow_run.pull_requests can be empty for fork PRs.
70 // Fall back to the Pulls API when that happens.
71 const prs = context.payload.workflow_run.pull_requests;
72 if (prs && prs.length > 0) {
73 core.setOutput('pr_number', prs[0].number.toString());
74 core.setOutput('head_sha', prs[0].head.sha);
75 return;
76 }
77
78 // Fallback: find the PR matching the head SHA via API
79 const headSha = context.payload.workflow_run.head_sha;
80 const headBranch = context.payload.workflow_run.head_branch;
81 const { data: pulls } = await github.rest.pulls.list({
82 owner: context.repo.owner,
83 repo: context.repo.repo,
84 state: 'open',
85 head: `${context.payload.workflow_run.head_repository.full_name.split('/')[0]}:${headBranch}`,
86 });
87
88 const match = pulls.find(pr => pr.head.sha === headSha);
89 if (!match) {
90 core.setFailed(`No open PR found for SHA ${headSha} on branch ${headBranch}`);
91 return;
92 }
93
94 core.setOutput('pr_number', match.number.toString());
95 core.setOutput('head_sha', headSha);
96
97 - name: Download docs artifact
98 uses: actions/download-artifact@v4
99 with:
100 name: docs-preview-dist
101 path: packages/kumo-docs-astro/dist/
102 run-id: ${{ github.event.workflow_run.id }}
103 github-token: ${{ secrets.GITHUB_TOKEN }}
104
105 - name: Install wrangler
106 run: npm install -g wrangler
107
108 - name: Deploy docs preview
109 id: deploy
110 working-directory: packages/kumo-docs-astro
111 env:
112 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
113 CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
114 PR_NUMBER: ${{ steps.metadata.outputs.pr_number }}
115 HEAD_SHA: ${{ steps.metadata.outputs.head_sha }}
116 run: |
117 COMMIT_SHORT="${HEAD_SHA:0:7}"
118
119 VERSION_OUTPUT=$(wrangler versions upload --env="" --message "PR #${PR_NUMBER} (${COMMIT_SHORT})" 2>&1) || true
120 echo "$VERSION_OUTPUT"
121
122 if ! echo "$VERSION_OUTPUT" | grep -q "Worker Version ID:"; then
123 echo "::error::wrangler versions upload failed -- no Worker Version ID in output"
124 exit 1
125 fi
126
127 PREVIEW_URL=$(echo "$VERSION_OUTPUT" | grep -oE 'Version Preview URL: https://[^ ]+' | sed 's/Version Preview URL: //')
128
129 if [ -z "$PREVIEW_URL" ]; then
130 VERSION_ID=$(echo "$VERSION_OUTPUT" | grep -oE 'Worker Version ID: [a-f0-9-]+' | sed 's/Worker Version ID: //')
131 if [ -n "$VERSION_ID" ]; then
132 VERSION_PREFIX=$(echo "$VERSION_ID" | cut -c1-8)
133 PREVIEW_URL="https://${VERSION_PREFIX}-kumo-docs.design-engineering.workers.dev"
134 fi
135 fi
136
137 if [ -z "$PREVIEW_URL" ]; then
138 echo "::error::Failed to determine docs preview URL"
139 exit 1
140 fi
141
142 echo "preview_url=$PREVIEW_URL" >> "$GITHUB_OUTPUT"
143
144 - name: Comment on PR
145 uses: actions/github-script@v7
146 env:
147 PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }}
148 HEAD_SHA: ${{ steps.metadata.outputs.head_sha }}
149 PR_NUMBER: ${{ steps.metadata.outputs.pr_number }}
150 with:
151 script: |
152 const marker = '<!-- kumo-docs-preview -->';
153 const previewUrl = process.env.PREVIEW_URL;
154 const headSha = process.env.HEAD_SHA;
155 const prNumber = parseInt(process.env.PR_NUMBER, 10);
156 const body = [
157 marker,
158 '### Docs Preview',
159 '',
160 `[View docs preview](${previewUrl})`,
161 '',
162 `Commit: \`${headSha.substring(0, 7)}\``,
163 ].join('\n');
164
165 const { data: comments } = await github.rest.issues.listComments({
166 owner: context.repo.owner,
167 repo: context.repo.repo,
168 issue_number: prNumber,
169 });
170
171 const existing = comments.find(c => c.body?.startsWith(marker));
172
173 if (existing) {
174 await github.rest.issues.updateComment({
175 owner: context.repo.owner,
176 repo: context.repo.repo,
177 comment_id: existing.id,
178 body,
179 });
180 } else {
181 await github.rest.issues.createComment({
182 owner: context.repo.owner,
183 repo: context.repo.repo,
184 issue_number: prNumber,
185 body,
186 });
187 }
188
189 visual-regression:
190 name: Visual Regression (Fork)
191 needs: deploy
192 if: ${{ needs.deploy.outputs.preview_url }}
193 runs-on: ubuntu-latest
194 timeout-minutes: 15
195 steps:
196 # Always check out the VR script from main — never from the fork's SHA.
197 # The fork's component changes are captured via AFTER_URL (the deployed
198 # preview), not by running the fork's copy of run-visual-regression.ts.
199 # Running untrusted code from a fork with secrets in env is the exact
200 # attack vector demonstrated in PR #279.
201 - name: Checkout VR script from main
202 uses: actions/checkout@v4
203 with:
204 ref: main
205 sparse-checkout: |
206 ci/visual-regression
207 .github/actions/install-dependencies
208
209 # Fetch the fork's HEAD so git diff works for changed-file detection.
210 - name: Fetch fork HEAD for git diff
211 run: git fetch --depth=1 origin ${{ github.event.workflow_run.head_sha }}
212
213 - name: Install Dependencies
214 uses: ./.github/actions/install-dependencies
215 with:
216 filter: "@cloudflare/kumo..."
217
218 - name: Run Visual Regression
219 env:
220 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
221 GITHUB_PR_NUMBER: ${{ needs.deploy.outputs.pr_number }}
222 GITHUB_REPOSITORY: ${{ github.repository }}
223 GITHUB_BASE_REF: main
224 PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
225 BEFORE_URL: "https://kumo-ui.com"
226 AFTER_URL: ${{ needs.deploy.outputs.preview_url }}
227 SCREENSHOT_API_KEY: ${{ secrets.SCREENSHOT_API_KEY }}
228 run: pnpm tsx ci/visual-regression/run-visual-regression.ts
229
230 - name: Upload Screenshots
231 uses: actions/upload-artifact@v4
232 if: always()
233 with:
234 name: visual-regression-screenshots-fork
235 path: ci/visual-regression/screenshots/
236 retention-days: 7
237