-
-
Notifications
You must be signed in to change notification settings - Fork 89
194 lines (178 loc) · 9.99 KB
/
Copy pathui-preview.yml
File metadata and controls
194 lines (178 loc) · 9.99 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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
name: UI Preview Build
# Per-PR preview pipeline, split by trust (reshaped 2026-07-24 — one fewer runner slot per same-repo
# UI push, which is nearly every UI push at this repo's volume):
#
# SAME-REPO PRs (`build-deploy`): a branch in this repo implies a collaborator, and same-repo
# pull_request runs get secrets — so ONE job builds and deploys inline via the shared
# .github/actions/deploy-ui-preview composite. No artifact hop, no second runner, and the
# workflow_run deploy job below skips runner-free.
#
# FORK PRs (`build`): unchanged two-stage boundary. The build runs fork code with NO secrets (fork
# PRs get a read-only token) and only produces the built `dist` artifact; the trusted
# `ui-preview-deploy.yml` (workflow_run) then deploys that artifact WITH secrets and records the
# GitHub Deployment that Reviewbot reads for the "after" screenshot. Building runs the PR's
# (possibly fork-authored) code — doing that with no secret access, and deploying the resulting
# bundle in a separate trusted step that never executes fork code, is the standard way to give fork
# PRs previews without exposing Cloudflare credentials to untrusted code. Do NOT collapse the fork
# path into build-deploy: that boundary is the entire security model.
on:
pull_request:
# Explicit list because the default (opened/synchronize/reopened) omits ready_for_review -- once the
# build jobs below skip draft PRs, marking a PR ready must itself trigger a real preview build (#6670),
# not wait for the next push. Mirrors ci.yml's pull_request.types comment/list exactly.
types: [opened, synchronize, reopened, ready_for_review]
# Scoped to exactly what apps/loopover-ui actually depends on (packages/loopover-ui-kit only) --
# NOT the whole "packages/**" workspace, which also matches loopover-miner/loopover-mcp/loopover-engine/
# discovery-index and previously built+deployed a pointless preview for every PR touching those (#ci-scope).
# This check isn't required (see branch protection), so a future package apps/loopover-ui starts
# depending on that isn't added here will silently stop getting previews rather than failing loud --
# keep this list in sync with what apps/loopover-ui/package.json actually imports.
paths:
- "apps/loopover-ui/**"
- "packages/loopover-ui-kit/**"
permissions:
contents: read
concurrency:
group: ui-preview-build-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
# ------------------------- SAME-REPO: build + deploy in one runner -------------------------
build-deploy:
name: Build UI preview artifact
# Same-repo only (fork PRs take the `build` job below). Skip draft PRs (#6670, anti-abuse): a full
# npm ci + UI build on every push, including a force-push, to a PR nobody has marked ready yet.
if: ${{ github.event.pull_request.draft != true && github.event.pull_request.head.repo.fork != true }}
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
deployments: write # record the preview Deployment Reviewbot reads
# Same environment binding as ui-preview-deploy.yml's job: the org can attach approval gating
# and/or environment-scoped secrets to every path that holds Cloudflare credentials.
environment:
name: preview
url: ${{ steps.deploy.outputs.preview_url }}
steps:
- name: Checkout PR head
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version-file: .nvmrc
cache: npm
- name: Install dependencies
run: npm ci
# Same steps as the root `ui:build` script, except the last one: that script also builds
# @loopover/ui-miner (`npm --workspace @loopover/ui-miner run build`), a full separate Vite app
# this workflow never uploads or deploys -- every UI PR was paying for that build and throwing
# away the result. `npx turbo run build --filter=@loopover/ui` replaces the last two `ui:build`
# steps (`extension:build && miner-extension:build && ui build` already covered by
# @loopover/ui#build's own turbo.json dependsOn) and skips ui-miner entirely.
# @loopover/contract joins the engine filter because `ui:openapi` imports src/openapi/schemas.ts,
# which re-exports the public-API response schemas from @loopover/contract/public-api (#9282/#9521).
# Without it this step failed with ERR_MODULE_NOT_FOUND on dist/public-api.js -- the contract was
# never built here at all, only in the CI workflow's own steps.
- name: Build UI
env:
VITE_LOOPOVER_API_ORIGIN: https://api.loopover.ai
# Preview-only: enables the synthetic demo session (useSession().signInPreview) so reviewbot can
# screenshot the authenticated /app/* dashboard via ?preview=1 instead of the sign-in wall. The
# production build (ui-deploy.yml) does NOT set this, so the escape hatch is dead-code-eliminated
# from prod. (#authed-route-preview)
VITE_PREVIEW: "1"
run: npm run ui:kit:build && npx turbo run build --filter=@loopover/engine --filter=@loopover/contract && npm run ui:openapi && npx turbo run build --filter=@loopover/ui
# Deploys stay best-effort when the Cloudflare secrets aren't configured (Reviewbot shows
# before-only) — mirrors ui-preview-deploy.yml's guard.
- 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
- name: Deploy preview
id: deploy
if: ${{ steps.cfg.outputs.ready == 'true' }}
uses: ./.github/actions/deploy-ui-preview
with:
pr-number: ${{ github.event.pull_request.number }}
# GitHub-set; the same value the checkout above pinned.
head-sha: ${{ github.event.pull_request.head.sha }}
dist-dir: apps/loopover-ui/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
# A deploy was attempted but never produced a preview (build-side failures skip this via the
# cfg gate ordering: cfg only runs after a successful build). Record a `failure`
# deployment_status so Reviewbot flips the "after" cell from an eternal spinner to a terminal
# "preview deploy failed" card. Gated on CF creds so a credential-less skip records nothing.
if: ${{ failure() && steps.cfg.outputs.ready == 'true' }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const sha = context.payload.pull_request.head.sha; // GitHub-set; never fork-supplied
const prNumber = context.payload.pull_request.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}.`);
# ------------------------- FORK: unprivileged build half only -------------------------
build:
name: Build UI preview artifact (fork)
# Fork PRs only — no secrets here, ever (see the file header). Draft guard as above.
if: ${{ github.event.pull_request.draft != true && github.event.pull_request.head.repo.fork == true }}
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout PR head
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version-file: .nvmrc
cache: npm
- name: Install dependencies
run: npm ci
# Same build as build-deploy above — see its comment for the turbo-filter rationale.
- name: Build UI
env:
VITE_LOOPOVER_API_ORIGIN: https://api.loopover.ai
VITE_PREVIEW: "1"
run: npm run ui:kit:build && npx turbo run build --filter=@loopover/engine --filter=@loopover/contract && npm run ui:openapi && npx turbo run build --filter=@loopover/ui
# The trusted deploy workflow downloads this by name + run-id. It contains only the built bundle
# (server/ + client/) — no secrets, no source needed downstream.
- name: Upload built UI artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ui-preview-dist
path: apps/loopover-ui/dist
if-no-files-found: error
retention-days: 1