Skip to content

landing-page-production #25

landing-page-production

landing-page-production #25

name: landing-page-production
# Promotes the current landing page to PRODUCTION: the `open-design-landing`
# Cloudflare Pages project, served at open-design.ai. This is the ONLY
# workflow that names the production project, and it is manual-only
# (workflow_dispatch) β€” a merge to `main` can never reach production on its
# own; it only updates the staging project (staging.open-design.ai) via
# `landing-page-staging`. Gate this further by configuring required reviewers
# on the GitHub `production` environment (Settings β†’ Environments).
#
# The build is identical to staging/CI, so what you reviewed on
# staging.open-design.ai is what ships.
on:
workflow_dispatch:
inputs:
reason:
description: 'Why promote now? (recorded in the run log)'
required: false
permissions:
contents: read
deployments: write
# Never cancel an in-flight production deploy.
concurrency:
group: landing-page-production
cancel-in-progress: false
jobs:
deploy:
name: Deploy landing page to production
# Production ships `main` only. workflow_dispatch can be launched from any
# ref via the Actions "Use workflow from" dropdown; gate the whole job on
# the main ref so a dispatch from a feature branch/tag is skipped outright
# (no deploy) instead of recording a non-main production run β€” which would
# also dodge blog-indexing's `workflow_run` `branches: [main]` filter.
if: github.repository == 'nexu-io/open-design' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 20
environment:
name: production
url: https://open-design.ai
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
with:
# Production always ships `main`. workflow_dispatch can be launched
# from any ref via the Actions "Use workflow from" dropdown, so pin
# the checkout to main β€” the deployed artifact must equal reviewed
# main, never whatever branch/tag the operator happened to select.
ref: main
- name: Setup pnpm
uses: pnpm/action-setup@v5
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Resolve Playwright version
id: playwright-version
run: |
version=$(node -p "require('./apps/landing-page/package.json').devDependencies.playwright.replace(/[^0-9.]/g,'')")
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Cache generated previews
id: previews-cache
uses: actions/cache@v5.0.5
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 }}-
- name: Cache Playwright browsers
uses: actions/cache@v5.0.5
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright Chromium
run: 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 previews before build so they end up in `out/previews/`.
# Soft vs. hard failure is enforced inside the script itself:
# individual broken `example.html` entries are logged and skipped,
# but a systemic failure (chromium launch error, every job failing)
# exits non-zero so we don't silently ship a deploy with zero
# thumbnails to production.
- 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
- name: Build landing page
env:
PUBLIC_GA_MEASUREMENT_ID: ${{ vars.PUBLIC_GA_MEASUREMENT_ID }}
run: pnpm --filter @open-design/landing-page build:static
- 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 Cloudflare image resizing URLs
run: |
node <<'NODE'
const { readFileSync } = require('node:fs');
const html = readFileSync('apps/landing-page/out/index.html', 'utf8');
const resizedUrls = html.match(/https:\/\/static\.open-design\.ai\/cdn-cgi\/image\//g) ?? [];
if (resizedUrls.length < 16) {
console.error(`Expected at least 16 Cloudflare resized image URLs, found ${resizedUrls.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
# `--branch=main` IS the Cloudflare Pages production branch, so this
# publishes to the production domain (open-design.ai).
- name: Deploy to Cloudflare Pages (production)
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
--branch=main