mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* feat(plugins): site-wide plugin detail pages, share-to-site links, landing deploy trigger Why: a merged plugin PR didn't redeploy the landing site (plugins/** was missing from the deploy paths), and the desktop Share menu copied a local/404 link instead of the public marketplace URL. The landing plugin routing left by the detail-page rework also 404'd: the locale listing's cards used a multi-segment href while detail pages were single-segment, and only 388 bundled _official plugins had pages. What changed: - Deploy: landing-page deploy/ci trigger on plugins/**, and skip the slow previews step on an exact cache hit (cache key aligned across both workflows so a PR-built cache is reused by main). - Share URL: packages/contracts/plugin-url.ts owns the single-segment plugin URL scheme; the web Share menu and the landing site both derive links from it. Web links now point at https://open-design.ai/plugins/<slug>/. - Full detail coverage: detail pages now cover all 403 local plugins (_official incl. atoms + community), each rendered from its local manifest. Fixes the locale-listing 404s and the community manifest-name/catalog-id (- vs /) mismatch. - Self-host: daemon exposes OD_SITE_ORIGIN via /api/app-config; web falls back to the canonical origin until the daemon answers. Validation: pnpm guard, pnpm typecheck (all packages), contracts + web tests green, and a full build E2E confirming all 403 catalog ids and locale-listing cards resolve to built detail pages (0 missing). * chore: retrigger CI * ci(landing): carry plugins/** trigger + previews cache-hit into #2994 split workflows Merged origin/main, which split landing deploy into staging + manual production (#2994). git auto-migrated my landing-page-deploy.yml changes into landing-page-staging.yml via rename detection (plugins/** path, fallback-preview-card.ts cache key, cache-hit skip all carried). The new manual landing-page-production.yml didn't have them, so add the previews cache-key alignment + cache-hit skip there too (plugins/** path is N/A — production is workflow_dispatch only). * fix(ci): wrangler-action uses pnpm so it tolerates landing's workspace dep This PR added @open-design/contracts (workspace:*) to apps/landing-page/package.json so the landing site can share the plugin-url slug rules. But the landing deploy/preview steps run cloudflare/wrangler-action with packageManager: npm in workingDirectory apps/landing-page, and 'npm i wrangler' chokes on the workspace: protocol (EUNSUPPORTEDPROTOCOL), failing 'Validate landing page'. Switch all three landing wrangler-action steps (staging / ci preview / production) to packageManager: pnpm, which is workspace-aware. * test(e2e): bundled plugins now offer the README badge After this branch, buildPluginShareUrl returns a public open-design.ai link for bundled plugins (not just official-marketplace ones), so the home-starter share menu now shows 'Copy README badge'. Update the assertion from toHaveCount(0) to toBeVisible(). * fix(landing): drop @open-design/contracts dep, use a landing-local slug helper Per review on #2999: the marketing site must not import @open-design/contracts (AGENTS.md boundary — it's the web/daemon product-runtime contract layer). Move the slug/path helpers into landing-local app/_lib/plugin-slug.ts; the web client keeps contracts' plugin-url. The two derive the same scheme and are verified in lockstep by the e2e route check (403 share URLs -> 403 detail pages, 0 missing). landing no longer has a workspace dep, so revert the wrangler-action packageManager back to npm. * fix(landing): include plugins/_official in previews cache key Per review on #2999: generate-previews.ts builds bundled-plugin preview jobs from plugins/_official/**/open-design.json and renders fallback cards from manifest fields (title/description/mode/scenario/tags). With plugins/** now triggering the workflow but the cache key not hashing plugin inputs, a plugin-only PR/merge could exact-hit an old cache and skip the preview regen, shipping with a stale or missing /previews/plugins/<manifest-id>.png. Add plugins/_official/** to the cache key in all three landing workflows (ci, staging, production). community is not currently covered by generate-previews so its glob is omitted. * fix(plugins): include community marketplace installs in share gate hasPublicPage now covers sourceMarketplaceId === 'community' so the README badge and public detail link surface for community installs. Community manifest names carry a community- prefix that diverges from the landing-page route slug, so URL derivation uses sourceMarketplaceEntryName (community/<folder>) instead — pluginDetailSlug takes the last segment, matching the /plugins/<folder>/ route the landing page emits. Adds component tests for buildPluginShareUrl, badge copy, and the Open-in-marketplace link for a community/registry-starter record. Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code) --------- Co-authored-by: mrcfps <mrc@powerformer.com>
260 lines
11 KiB
YAML
260 lines
11 KiB
YAML
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
|
|
- .github/workflows/blog-indexing-monitor.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
|
|
- .github/workflows/blog-indexing-monitor.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: 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 }}-
|
|
|
|
# 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: Lint changed blog SEO
|
|
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 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
|
|
|
|
# --- 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: 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,
|
|
});
|
|
}
|