-
Notifications
You must be signed in to change notification settings - Fork 9.7k
306 lines (285 loc) Β· 13.8 KB
/
Copy pathlanding-page-ci.yml
File metadata and controls
306 lines (285 loc) Β· 13.8 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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
name: landing-page-ci
on:
pull_request:
paths:
# Workflow files
- .github/workflows/landing-page-ci.yml
- .github/workflows/landing-page-staging.yml
- .github/workflows/landing-page-production.yml
- .github/workflows/blog-indexing-on-deploy.yml
# Landing page sources
- apps/landing-page/**
# Design template source of truth for the homepage.
- design-templates/open-design-landing/**
# Content sources globbed by Astro content collections β without
# these the deploy can be silently skipped when only Markdown
# content is touched.
- skills/**
- design-systems/**
- craft/**
- templates/**
# Plugin manifests power the bundled-plugin catalog and the new
# `_lib/bundled-plugins.ts` reader; CI must rerun when their
# `title_i18n` / `description_i18n` maps or other fields change.
- plugins/**
# Workspace plumbing
- package.json
- pnpm-lock.yaml
- pnpm-workspace.yaml
# Merge queue trigger so PRs that touch the same paths can clear
# `Validate landing page` / `Strict PR visual tests` while queued.
# Without this branch ruleset blocks merges (the queue waits forever
# for a check name that never gets dispatched against the merge_group
# ref), which is the exact deadlock observed during the 5/26 release
# window.
merge_group:
types: [checks_requested]
push:
branches:
- main
paths:
- .github/workflows/landing-page-ci.yml
- .github/workflows/landing-page-staging.yml
- .github/workflows/landing-page-production.yml
- .github/workflows/blog-indexing-on-deploy.yml
- apps/landing-page/**
- design-templates/open-design-landing/**
- skills/**
- design-systems/**
- craft/**
- templates/**
- plugins/**
- package.json
- pnpm-lock.yaml
- pnpm-workspace.yaml
workflow_dispatch:
permissions:
contents: read
# Needed to post/update the preview-URL comment on the PR. Fork PRs run
# with a read-only token regardless, so the preview steps below are gated
# to same-repo branches.
pull-requests: write
concurrency:
group: landing-page-ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
validate:
name: Validate landing page
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
- name: Setup workspace
uses: ./.github/actions/setup-workspace
- name: Restore generated previews
id: previews-cache
uses: actions/cache/restore@v5
with:
path: apps/landing-page/public/previews
key: landing-page-previews-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'package.json', 'apps/landing-page/package.json', 'apps/landing-page/scripts/generate-previews.ts', 'apps/landing-page/scripts/fallback-preview-card.ts', 'skills/**', 'design-templates/**', 'templates/live-artifacts/**', 'plugins/_official/**') }}
restore-keys: |
landing-page-previews-${{ runner.os }}-
# Cache the Playwright browser binaries between runs. The cache key
# is pinned to the playwright version we depend on (kept in
# apps/landing-page/package.json) so a bump invalidates correctly.
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
with:
package-json-path: apps/landing-page/package.json
install-command: pnpm --filter @open-design/landing-page exec playwright install --with-deps chromium
- name: Typecheck landing page
run: pnpm --filter @open-design/landing-page typecheck
# Generate the per-skill / per-template thumbnail PNGs *before*
# the build so they ship in `out/previews/` automatically. The
# script itself decides what's a soft vs. hard failure: a single
# broken `example.html` is logged and skipped, but a chromium
# launch failure or a 100%-failure run exits non-zero so the
# build stops instead of silently shipping zero thumbnails.
- name: Generate skill + template previews
# Exact previews-cache hit β public/previews already holds the correct
# thumbnails, skip the slow Playwright render. A restore-keys partial
# hit keeps cache-hit false, so we still regenerate β no stale-thumbnail
# drift.
if: steps.previews-cache.outputs.cache-hit != 'true'
run: pnpm --filter @open-design/landing-page previews
# No PUBLIC_GA_MEASUREMENT_ID for PR/CI builds: the per-PR preview must
# not report into the production GA property. OD_LANDING_NOINDEX=1 keeps
# the PR preview (pr-<n>.open-design-landing-staging.pages.dev) out of
# search engines. Only `landing-page-production` builds without these.
- name: Build landing page
env:
OD_LANDING_NOINDEX: '1'
run: pnpm --filter @open-design/landing-page build:static
- name: Save generated previews
if: ${{ steps.previews-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
uses: actions/cache/save@v5
with:
path: apps/landing-page/public/previews
key: landing-page-previews-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'package.json', 'apps/landing-page/package.json', 'apps/landing-page/scripts/generate-previews.ts', 'apps/landing-page/scripts/fallback-preview-card.ts', 'skills/**', 'design-templates/**', 'templates/live-artifacts/**', 'plugins/_official/**') }}
- name: Lint changed blog SEO
# The build above sets OD_LANDING_NOINDEX=1, so every rendered page in
# `out/` carries `noindex`. Pass the same flag through so the SEO linter
# treats that staging-only noindex as expected instead of an
# indexability blocker (production builds leave the flag unset).
env:
OD_LANDING_NOINDEX: '1'
run: |
BASE="${{ github.event.pull_request.base.sha || github.event.before || '' }}"
if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ]; then
# merge_group (and first-push) events have no base SHA. Resolve a
# concrete commit instead of passing the literal "HEAD^", which the
# blog-indexing scripts' assertSafeGitRef rejects (no "^" allowed).
BASE="$(git rev-parse HEAD^)"
fi
pnpm --filter @open-design/landing-page exec tsx scripts/blog-indexing/lint-blog-seo.ts \
--base "$BASE" \
--head HEAD \
--rendered-out apps/landing-page/out
- name: Guard blog URL changes
run: |
BASE="${{ github.event.pull_request.base.sha || github.event.before || '' }}"
if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ]; then
# merge_group (and first-push) events have no base SHA. Resolve a
# concrete commit instead of passing the literal "HEAD^", which the
# blog-indexing scripts' assertSafeGitRef rejects (no "^" allowed).
BASE="$(git rev-parse HEAD^)"
fi
pnpm --filter @open-design/landing-page exec tsx scripts/blog-indexing/check-blog-url-changes.ts \
--base "$BASE" \
--head HEAD
- name: Verify zero external JavaScript
run: |
node <<'NODE'
const { readFileSync } = require('node:fs');
const html = readFileSync('apps/landing-page/out/index.html', 'utf8');
const forbidden = [
/<script\b[^>]*\bsrc=/i,
/type=["']module["']/i,
/\/_astro\/[^"'<>\s]+\.js/i,
];
for (const pattern of forbidden) {
if (pattern.test(html)) {
console.error(`Unexpected client JavaScript matched ${pattern}`);
process.exit(1);
}
}
NODE
- name: Verify homepage ships optimized WebP art
run: |
node <<'NODE'
const { readFileSync } = require('node:fs');
const html = readFileSync('apps/landing-page/out/index.html', 'utf8');
// The homepage serves its hero / gallery / method art as optimized,
// origin-hosted WebP (each kept <1MB by the changed-file blob guard)
// instead of Cloudflare Image Resizing variants β see PR #4158, the
// local-first homepage design refresh. Require the same floor of 16
// optimized references so a regression that reintroduces raw,
// unoptimized art (or drops the gallery) is still caught.
const webpRefs = html.match(/\/[A-Za-z0-9/_-]+\.webp/g) ?? [];
if (webpRefs.length < 16) {
console.error(`Expected at least 16 optimized WebP image references, found ${webpRefs.length}`);
process.exit(1);
}
if (/(?:src|content)=["']\/assets\/[A-Za-z0-9_.-]+\.png/.test(html)) {
console.error('Found local /assets/*.png image reference in generated landing HTML.');
process.exit(1);
}
NODE
- name: Verify deploy file count under Cloudflare Pages cap
run: |
# Cloudflare Pages rejects deployments over 20,000 files. Localized
# routes (especially per-locale catalog detail pages) can silently
# multiply the output, so fail well before the hard cap β a regression
# is caught here instead of at the wrangler deploy step. `out/`
# already includes the generated previews at this point.
count=$(find apps/landing-page/out -type f | wc -l | tr -d '[:space:]')
echo "landing-page deploy file count: $count"
if [ "$count" -ge 19000 ]; then
echo "::error::landing-page build emitted $count files; Cloudflare Pages caps deployments at 20,000. Reduce per-locale page fan-out (see PR #4158)."
exit 1
fi
# --- PR preview deploy -------------------------------------------------
# Publish this PR's built site to its own preview URL in the STAGING
# project (`--branch=pr-<number>`) so reviewers see the rendered result
# before merge. It lands in the staging project, never the production
# project. Gated to same-repo branches: fork PRs run without the
# Cloudflare secrets and with a read-only token, so they skip the
# deploy/comment and keep just the validation above.
- name: Prepare staging Pages configuration
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
run: cp apps/landing-page/wrangler.staging.toml apps/landing-page/wrangler.toml
- name: Apply attribution ledger migrations (staging)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: apps/landing-page
packageManager: npm
wranglerVersion: "4.110.0"
command: d1 migrations apply open-design-landing-staging-attribution --remote
- name: Deploy PR preview to Cloudflare Pages
id: preview
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: apps/landing-page
packageManager: npm
command: >
pages deploy out
--project-name=open-design-landing-staging
--branch=pr-${{ github.event.pull_request.number }}
- name: Comment preview URL on PR
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@v7
env:
DEPLOY_URL: ${{ steps.preview.outputs.deployment-url }}
ALIAS_URL: ${{ steps.preview.outputs.pages-deployment-alias-url }}
with:
script: |
const marker = '<!-- landing-preview -->';
const deploy = process.env.DEPLOY_URL || '';
const alias =
process.env.ALIAS_URL ||
`https://pr-${context.issue.number}.open-design-landing-staging.pages.dev`;
const sha = context.payload.pull_request.head.sha.slice(0, 7);
const body = [
marker,
'### π Landing page preview',
'',
'This PR is deployed to a Cloudflare Pages preview β **not** staging or production:',
'',
`- Stable alias: ${alias}`,
deploy ? `- This build: ${deploy}` : '',
'',
`Updated for commit \`${sha}\`.`,
]
.filter(Boolean)
.join('\n');
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 100,
});
const existing = comments.find((c) => c.body && c.body.includes(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: context.issue.number,
body,
});
}