-
-
Notifications
You must be signed in to change notification settings - Fork 89
179 lines (168 loc) · 9.42 KB
/
Copy pathui-preview-deploy.yml
File metadata and controls
179 lines (168 loc) · 9.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
name: UI Preview Deploy
# FORK-ONLY deploy half of the per-PR preview pipeline (since 2026-07-24 — same-repo PRs deploy
# inline in ui-preview.yml's build-deploy job, and this job skips runner-free for their builds).
# Triggered when "UI Preview Build" completes. Because it runs on `workflow_run`, GitHub always
# executes the workflow definition from the DEFAULT BRANCH (never the fork's), so it is trusted and
# may use secrets. It downloads the built `dist` artifact (the only checkout below is of the default
# branch, solely to invoke the shared local composite action — it never checks out or runs PR/fork
# source), deploys a transient preview version via .github/actions/deploy-ui-preview, and records the
# GitHub Deployment + status that Reviewbot reads to render the "after" screenshot.
#
# Security boundary: the BUILD ran fork code with NO secrets; this 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 later visited). This is what makes fork-PR previews safe.
#
# Required repo secrets:
# CLOUDFLARE_API_TOKEN — token with "Workers Scripts:Edit" on the account
# CLOUDFLARE_ACCOUNT_ID — the Cloudflare account id that owns loopover-ui
on:
workflow_run:
workflows: ["UI Preview Build"]
types: [completed]
permissions:
contents: read
actions: read # download the build artifact from the triggering run
deployments: write # record the preview Deployment Reviewbot reads
pull-requests: read # resolve the fork PR for this build by matching head SHA
concurrency:
group: ui-preview-deploy-${{ github.event.workflow_run.head_sha }}
cancel-in-progress: true
jobs:
deploy:
name: Deploy UI preview version
# Bind the ONE fork-facing job that holds Cloudflare credentials to a GitHub deployment environment,
# so the org can attach approval gating and/or environment-scoped secrets to it. Unprotected by
# default (so previews stay automatic); add required reviewers in Settings → Environments → preview
# to require a manual approval before any fork preview deploys.
environment:
name: preview
url: ${{ steps.deploy.outputs.preview_url }}
# Only successful build runs that originated from a FORK pull_request: same-repo PRs deploy inline
# in ui-preview.yml (their build workflow still completes and fires this trigger — the repo
# comparison below is what lets those runs skip without consuming a runner). A path-skipped or
# failed build still fires workflow_run with a non-success conclusion — ignore those too.
if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check Cloudflare secrets
id: cfg
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
if [ -n "$CLOUDFLARE_API_TOKEN" ] && [ -n "$CLOUDFLARE_ACCOUNT_ID" ]; then
echo "ready=true" >> "$GITHUB_OUTPUT"
else
echo "ready=false" >> "$GITHUB_OUTPUT"
echo "::notice::CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID not set — skipping preview deploy (Reviewbot shows before-only)."
fi
# TRUSTED checkout of the default branch (github.sha for a workflow_run event is the default
# branch head) — required only so the local `uses: ./.github/actions/deploy-ui-preview` reference
# below can find its action.yml. Never the fork's code.
- name: Checkout default branch
if: steps.cfg.outputs.ready == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Setup Node
if: steps.cfg.outputs.ready == 'true'
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
# The trusted checkout above restored .nvmrc, so the old hand-synced hardcoded version
# (the no-checkout exception this job used to carry) is no longer needed.
node-version-file: .nvmrc
# Resolve the fork PR this build belongs to BEFORE deploying, so the composite can record the
# deployment against it. Both easy signals are EMPTY for fork PRs (the common case here):
# workflow_run.pull_requests is empty for cross-repo runs, and the commit→PR association API
# doesn't index a fork-head commit from the base repo. So we ALSO match the open PR whose head
# points at this exact build SHA — head.sha is GitHub-set (the base repo tracks the fork head as
# refs/pull/N/head), so it's safe to trust even for forks.
- name: Resolve PR for this build
id: pr
if: steps.cfg.outputs.ready == 'true'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const sha = context.payload.workflow_run.head_sha; // GitHub-set; never fork-supplied
const slug = `${context.repo.owner}/${context.repo.repo}`;
let prNumber = context.payload.workflow_run.pull_requests?.[0]?.number;
if (!prNumber) {
const assoc = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: sha,
});
prNumber = assoc.data.find((p) => p.state === "open" && p.base.repo.full_name === slug)?.number;
}
if (!prNumber) {
// Fork-safe fallback: scan open PRs for the one whose head is this build's commit.
const openPrs = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: "open",
per_page: 100,
});
prNumber = openPrs.find((p) => p.head.sha === sha)?.number;
}
if (!prNumber) {
core.setFailed(`Could not resolve an open PR for ${sha} — skipping deploy.`);
return;
}
core.setOutput("number", String(prNumber));
# Cross-run download: the artifact lives on the triggering build run, not this one.
- name: Download built UI artifact
if: steps.cfg.outputs.ready == 'true'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ui-preview-dist
path: preview-dist
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
# Validation, trusted config, wrangler upload, and the success Deployment record all live in the
# shared composite (kept byte-identical with ui-preview.yml's same-repo call site by construction).
- name: Deploy preview
id: deploy
if: steps.cfg.outputs.ready == 'true'
uses: ./.github/actions/deploy-ui-preview
with:
pr-number: ${{ steps.pr.outputs.number }}
head-sha: ${{ github.event.workflow_run.head_sha }}
dist-dir: preview-dist
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Record FAILED deployment for Reviewbot
# Runs when an earlier step in this job failed (artifact validation rejected the bundle, the
# wrangler upload errored, etc.) — i.e. a deploy was attempted but never produced a preview.
# Record a `failure` deployment_status so Reviewbot flips the "after" cell from an eternal
# spinner to a terminal "preview deploy failed" card, instead of waiting forever for a success
# event that will never come. Gated on CF creds so a credential-less skip records nothing;
# reuses the resolution step's PR (if THAT failed, there is nothing to record against — the
# resolution step already setFailed with the reason).
if: ${{ failure() && steps.cfg.outputs.ready == 'true' && steps.pr.outputs.number != '' }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const sha = context.payload.workflow_run.head_sha; // GitHub-set; never fork-supplied
const prNumber = Number(${{ toJSON(steps.pr.outputs.number) }});
const deployment = await github.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref: sha,
environment: `preview/pr-${prNumber}`,
auto_merge: false,
required_contexts: [],
transient_environment: true,
description: "LoopOver UI preview (failed)",
payload: JSON.stringify({ pr: prNumber, head_sha: sha }),
});
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: deployment.data.id,
state: "failure",
environment: `preview/pr-${prNumber}`,
description: "Preview deploy failed",
});
core.notice(`Recorded FAILED preview deployment for PR #${prNumber}.`);