diff --git a/.claude/skills/add-model-page/SKILL.md b/.claude/skills/add-model-page/SKILL.md new file mode 100644 index 0000000000..0b7fe4b81e --- /dev/null +++ b/.claude/skills/add-model-page/SKILL.md @@ -0,0 +1,173 @@ +--- +name: add-model-page +description: 'add, update, or remove a model page entry on the comfy org website. creates a PR to Comfy-Org/ComfyUI_frontend apps/website folder with the change and posts a Vercel preview link back to Slack.' +--- + +# add-model-page + +add, update, or remove model pages in the ComfyUI website. + +## Trigger phrases + +- `Add a model page for ` +- `Update the model page for ` +- `Remove from model pages` + +## Phase 1 — Parse the request + +Extract: + +- **action**: `add` | `update` | `remove` +- **model-name**: raw string (e.g. `flux1-schnell`, `flux1_dev.safetensors`) + +Normalize to a slug: lowercase, replace `_` and `.` with `-`, strip file extensions. +Example: `flux1_dev.safetensors` → `flux1-dev` + +## Architecture overview + +Models come from two sources merged at build time: + +| File | Purpose | +| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `apps/website/src/config/generated-models.json` | Auto-generated from workflow_templates (slug, name, directory, huggingFaceUrl, workflowCount, displayName, thumbnailUrl, docsUrl) | +| `apps/website/src/config/model-metadata.ts` | Hand-curated overrides (docsUrl, blogUrl, featured) — only add entries that need overrides | +| `apps/website/src/config/models.ts` | Merges the two above; exports typed `Model[]` | + +To regenerate the JSON from workflow_templates: + +```bash +pnpm tsx apps/website/scripts/generate-models.ts +``` + +This writes `apps/website/src/config/generated-models.json` directly. +Thumbnails are populated from local `.webp` files in `workflow_templates/templates/` — no network access needed. + +--- + +## Phase 2 — Gather model data (ADD / UPDATE) + +Run the generator to get fresh data, then find the model: + +```bash +pnpm tsx apps/website/scripts/generate-models.ts +jq '.[] | select(.slug | contains("MODEL_SLUG"))' \ + apps/website/src/config/generated-models.json +``` + +The JSON fields are: + +- `slug` — URL slug +- `name` — exact filename or display name for partner nodes +- `huggingFaceUrl` — download URL (empty for partner nodes) +- `directory` — `diffusion_models` | `loras` | … | `partner_nodes` +- `workflowCount` — integer +- `displayName` — human-readable name + +If no match and it is a known API/partner model, add it to `API_PROVIDER_MAP` in +`generate-models.ts` and re-run. Otherwise tell the user. + +--- + +## Phase 3 — Check for existing entry + +```bash +jq --arg slug "${SLUG}" '.[] | select(.slug == $slug)' \ + apps/website/src/config/generated-models.json +``` + +- Match found + action is `add` → switch to UPDATE flow automatically +- No match + action is `update` → stop and tell the user + +--- + +## Phase 4A — ADD: new partner/API model not in workflow_templates + +For partner nodes (no local file), add an entry to `API_PROVIDER_MAP` in +`apps/website/scripts/generate-models.ts`: + +```typescript +mymodel: { name: 'My Model', slug: 'my-model' }, +``` + +Then re-run `pnpm tsx apps/website/scripts/generate-models.ts` — it will appear +in `generated-models.json` automatically. + +If you also want a `docsUrl`, `blogUrl`, or a link to the hub model page, add an entry to `model-metadata.ts`: + +```typescript +'my-model': { + docsUrl: 'https://docs.comfy.org/tutorials/...', + blogUrl: 'https://blog.comfy.org/...', + hubSlug: 'my-model', // slug at comfy.org/workflows/model/{hubSlug} — only set if the page exists (returns 200) + featured: true +} +``` + +No changes to `models.ts` or `translations.ts` are needed. + +--- + +## Phase 4B — UPDATE: edit existing entry + +Only `model-metadata.ts` needs editing for most updates (docsUrl, blogUrl, +featured). For `displayName` or `directory` changes, edit the entry directly in +`generated-models.json` (until the next generator run would overwrite it — then +fix the source in `generate-models.ts`). + +--- + +## Phase 4C — REMOVE: delete entry + +Remove the entry from `generated-models.json` (or mark it with `canonicalSlug` +pointing to the replacement). No translation file changes needed. + +--- + +## Phase 5 — Verify TypeScript + +```bash +pnpm typecheck 2>&1 | grep -E "error|warning" | head -20 +``` + +Fix any type errors before proceeding. Common issues: + +- `ModelDirectory` type not matching a new `directory` value — add it to the union +- JSON import shape mismatch — `generated-models.json` must match `OutputModel` + +--- + +## Phase 6 — Create PR + +```bash +BRANCH="add-model-page-MODEL-SLUG" # or update- / remove- +git checkout -b $BRANCH +git add apps/website/src/config/generated-models.json \ + apps/website/scripts/generate-models.ts \ + apps/website/src/config/model-metadata.ts +git commit -m "feat(models): add model page for MODEL-SLUG" +git push -u origin $BRANCH +gh pr create \ + --title "Add model page: MODEL-SLUG" \ + --body "$(cat <<'EOF' +Adds a new model page entry for MODEL-SLUG. + +## Changes +- `generated-models.json`: regenerated with new entry (workflowCount N, directory DIRECTORY) +- `model-metadata.ts`: editorial overrides (docsUrl, featured) if needed +EOF +)" +``` + +For UPDATE use branch `update-model-page-MODEL-SLUG`. +For REMOVE use `remove-model-page-MODEL-SLUG`. + +--- + +## Error states + +| Situation | Response | +| ------------------------------- | ---------------------------------------------------------------- | +| Model not in workflow templates | Ask user to verify spelling or add it manually as a partner node | +| Slug already exists (add) | Switch to update flow automatically | +| Slug not found (update/remove) | Stop and ask user to confirm | +| Typecheck fails | Fix the error before pushing | diff --git a/.claude/skills/backport-management/SKILL.md b/.claude/skills/backport-management/SKILL.md index 138eb98a84..de77272667 100644 --- a/.claude/skills/backport-management/SKILL.md +++ b/.claude/skills/backport-management/SKILL.md @@ -9,13 +9,18 @@ Cherry-pick backport management for Comfy-Org/ComfyUI_frontend stable release br ## Quick Start -1. **Discover** — Collect candidates from Slack bot + git log gap (`reference/discovery.md`) -2. **Analyze** — Categorize MUST/SHOULD/SKIP, check deps (`reference/analysis.md`) -3. **Human Review** — Present candidates in batches for interactive approval (see Interactive Approval Flow) -4. **Plan** — Order by dependency (leaf fixes first), group into waves per branch -5. **Execute** — Label-driven automation → worktree fallback for conflicts (`reference/execution.md`) -6. **Verify** — After each wave, verify branch integrity before proceeding -7. **Log & Report** — Generate session report (`reference/logging.md`) +1. **Discover** — Collect candidates from Slack bot + git log gap, then **reconcile both lists** (`reference/discovery.md`) +2. **Pre-filter by path** — Auto-skip PRs whose changed files are entirely under `apps/website/`, `browser_tests/`, `.github/`, `packages/design-system/`, `packages/{cloud,registry}-types/`, `.claude/`, `docs/`. Don't read PR bodies for these — they don't ship to core ComfyUI users (`reference/analysis.md`) +3. **Verify target file existence** — For each surviving candidate, run `git cat-file -e origin/$TARGET:$path` for primary changed files. If they don't exist on the target, auto-mark SKIP with reason `feature-not-on-branch` +4. **Tiered triage** — Bucket into **Tier 1 (core editor must-haves)**, **Tier 2 (cloud-distribution only)**, **Tier 3 (skip)** before reviewing individually (`reference/analysis.md`) +5. **Analyze** — Categorize remaining MUST/SHOULD, check deps (`reference/analysis.md`) +6. **Human Review** — Present candidates in batches for interactive approval, with tier context attached (see Interactive Approval Flow) +7. **Plan** — Order by dependency (leaf fixes first), group into waves per branch +8. **Test-then-resolve dry-run** — Classify clean vs conflict before committing time (`reference/execution.md`) +9. **Execute** — Label-driven automation for clean PRs → worktree fallback for conflicts (`reference/execution.md`) +10. **Public-API conflict review** — If conflict resolution touches a public LiteGraph callback, extension API, or `node.*` method, consult oracle for compat-regression review BEFORE pushing (`reference/execution.md`) +11. **Verify** — Per-PR validation (typecheck + targeted tests + lint on changed files) AND per-wave verification (full typecheck + test:unit on branch HEAD) +12. **Log & Report** — Generate session report + author accountability report + Slack status update (`reference/logging.md`) ## System Context @@ -107,6 +112,35 @@ Husky hooks fail in worktrees (can't find lint-staged config). Always use `git p In the 2026-04-06 session: core/1.42 got 18/26 auto-PRs, cloud/1.42 got only 1/25. The cloud branch has more divergence. **Always plan for manual fallback** — don't assume automation will handle most PRs. +### Cherry-Picked Tests Can Reference Files Added By Earlier Unbackported PRs + +A common conflict: PR A on main modifies a test file that was _added_ on main by an earlier PR B (not backported to the target). The cherry-pick of A reports "modify/delete" on B's test file because the file doesn't exist on the target. Adding the new file would smuggle in B's test scaffolding without B's runtime changes. + +**Detection:** Conflict says `deleted in HEAD and modified in `. Verify with: + +```bash +git log --diff-filter=A --oneline origin/main -- path/to/test.ts +``` + +If the introducing commit is **not** on the target branch, the test file isn't a real prerequisite for the runtime fix. + +**Fix:** `git rm` the test file (drop it from the backport). Document in the commit body which PR introduced it on main and why dropping it is safe. The runtime fix itself usually doesn't depend on these tests — coverage exists at the integration layer. + +### Backport-Only Compatibility Shims + +When a PR's _mechanism_ relies on changes upstream that aren't on the older branch, a literal cherry-pick can recreate the original bug for any consumer still using the old contract. This is most dangerous for **public LiteGraph callbacks, extension APIs, and `node.*` methods** that custom-node packages depend on. + +**Real example (#11541, core/1.43 backport):** The PR removed `LGraphNode.vue`'s legacy `handled === true` sync-return check from `handleDrop`, replacing it with `await node.onDragDrop(event, true)`. Safe on `main` because all in-repo `onDragDrop` handlers had migrated to participate in the new `claimEvent` flag. On `core/1.43`, `onDragDrop` is a public callback — custom-node packages with synchronous `onDragDrop` returning `true` would no longer have their event claimed, recreating the duplicate-node-creation bug the PR was fixing. + +**Detection:** The PR's diff modifies a file that is part of a public extension API surface. Look for: + +- `node.onXxx` callback assignments +- Methods on `LGraphNode`, `LGraphCanvas`, `LGraph`, `Subgraph` +- Public exports from `src/lib/litegraph/` +- Type changes affecting `litegraph-augmentation.d.ts` + +**Fix:** Add a backport-only compatibility shim that preserves the old contract while keeping the new fix. Document it explicitly in the commit body under a `## Backport-only compatibility fix` heading. Consult oracle for review before pushing — a bad shim is worse than no fix. + ## Conflict Triage **Always categorize before deciding to skip. High conflict count ≠ hard conflicts.** @@ -147,6 +181,26 @@ Skip these without discussion: - **Features not on target branch** — e.g., Painter, GLSLShader, appModeStore on core/1.40 - **Cloud-only PRs on core/\* branches** — Team workspaces, cloud queue, cloud-only login. (Note: app mode and Firebase auth are NOT cloud-only — see Branch Scope Rules) +### Path Pre-Filter (run BEFORE reading PR bodies) + +For 50+ candidate PRs, classify by changed paths first to skip the unproductive ones without spending time on triage. Run `git show --stat $SHA` (or `gh pr view --json files`) and bucket: + +| Path prefix | Bucket | Reason | +| ---------------------------------------------- | ---------------------- | ------------------------------------------------ | +| `apps/website/` | SKIP | Marketing/platform site, not core ComfyUI bundle | +| `apps/desktop-ui/` | SKIP for `core/*` | Desktop app, separate release cadence | +| `browser_tests/` only (no `src/`) | SKIP | Test-only | +| `.github/workflows/` only | SKIP | CI/release infra | +| `packages/design-system/` only | SKIP | Design tokens, not core | +| `packages/{cloud,registry,ingest}-types/` only | SKIP | Generated types | +| `.claude/`, `.agents/`, `docs/` | SKIP | Agent / documentation | +| `*.stories.ts` only | SKIP | Storybook only | +| `src/` (core editor) | KEEP — analyze further | Runtime/editor code that requires full triage | + +A PR touches multiple paths? Keep it if **any** changed file is under `src/` (or other core paths) and run normal analysis. Auto-skip is conservative — only skip when _all_ paths match the SKIP buckets. + +This filter alone removes ~30-50% of candidates in a typical session, leaving only the PRs that need real triage. + ## Wave Verification After merging each wave of PRs to a target branch, verify branch integrity before proceeding: diff --git a/.claude/skills/backport-management/reference/analysis.md b/.claude/skills/backport-management/reference/analysis.md index dd6a41b394..705448b51f 100644 --- a/.claude/skills/backport-management/reference/analysis.md +++ b/.claude/skills/backport-management/reference/analysis.md @@ -39,6 +39,89 @@ Check before backporting — these don't exist on older branches: - **App builder** — check per branch - **appModeStore.ts** — not on core/1.40 +### Verify Target File Existence (Run Before Cherry-Pick) + +Before cherry-picking any PR, confirm the files it modifies actually exist on the target branch. If they don't, the PR's runtime fix is for a feature that hasn't been added yet — skip cleanly without attempting cherry-pick: + +```bash +# For each file the PR changes +for f in $(gh pr view $PR --json files --jq '.files[].path' | grep -v "^browser_tests/\|\.test\." ); do + if ! git cat-file -e origin/$TARGET:$f 2>/dev/null; then + echo "MISSING on $TARGET: $f" + fi +done +``` + +If the _primary_ changed files (the runtime ones, not tests) are missing, mark the PR `SKIP / feature-not-on-branch`. This is faster than letting cherry-pick fail with modify/delete conflicts and gives a clean signal. + +This check is the first thing that runs after the path pre-filter and BEFORE you spend time reading PR descriptions. + +## Tiered Triage (Recommended for 30+ Candidates) + +Before the interactive Y/N approval flow, bucket all surviving candidates into three tiers. This surfaces release-engineering decisions that a flat MUST/SHOULD list obscures: + +### Tier 1 — Core Editor Must-Haves + +User-facing bugs, crashes, data corruption, or security issues in code paths that exist on the target branch. These are the strongest backport candidates. + +Indicators: + +- `fix:` prefix and the bug is reproducible on the target branch +- Crash guards, runtime null checks, race-condition fixes +- Data-loss bugs (state not persisted, duplicates, drops) +- Security hardening (CSRF, XSS, auth) +- Vue Nodes 2.0 regression cluster (if the target ships Vue Nodes 2.0) +- Subgraph correctness fixes +- Public-API extension callback fixes + +Recommend `Y` to user. + +### Tier 2 — Cloud-Distribution Only + +Bugs that only manifest on cloud-hosted distributions (Secrets panel, subscription flows, cloud signup, workspace tracking, etc.). Whether to backport depends on whether cloud ships from the target `core/*` branch in your release matrix. + +Indicators: + +- Files under `src/platform/secrets/`, `src/platform/subscription/`, signup flows +- PR description mentions cloud staging issues +- Fix gated behind cloud feature flags + +Default: ask the cloud release rotation owner. If unsure, defer. + +### Tier 3 — Skip + +Path pre-filter caught most of these. The rest are PRs where the diff _touches_ `src/` but the practical impact is non-user-facing or scoped to features the target doesn't ship. + +Indicators: + +- All changes in test files even if the PR touched `src/` test files +- Storybook stories only +- Lint config / lint rule additions +- Documentation comments +- Internal refactors with no behavior change + +### Presentation Format + +When showing tier results to the user, format as: + +```text +Tier 1 (N PRs) — strong backport candidates +- #11541 fix: stop duplicate node creation when dropping image on Vue nodes + Why: Vue Nodes 2.0 regression — async onDragDrop bypassed handled-check, drops bubble to document, spawns extra LoadImage nodes +- #10849 fix: store promoted widget values per SubgraphNode instance + Why: Multiple instances overwriting each other's promoted widget values — data loss + +Tier 2 (N PRs) — cloud-distribution release rotation should decide +- #11636 fix: enable Chrome password autofill on signup form +- ... + +Tier 3 (N PRs) — skip recommended +- #11586 fix: website polish (apps/website/ only) +- ... +``` + +Then run interactive Y/N over Tier 1 and Tier 2; Tier 3 gets confirmed-skip without per-PR review. + ## Dep Refresh PRs Always SKIP on stable branches. Risk of transitive dependency regressions outweighs audit cleanup benefit. If a specific CVE fix is needed, cherry-pick that individual fix instead. diff --git a/.claude/skills/backport-management/reference/discovery.md b/.claude/skills/backport-management/reference/discovery.md index ac826ce764..ab28e0815f 100644 --- a/.claude/skills/backport-management/reference/discovery.md +++ b/.claude/skills/backport-management/reference/discovery.md @@ -1,5 +1,11 @@ # Discovery — Candidate Collection +**Run all sources, then reconcile.** No single source is authoritative: + +- Slack bot may flag PRs that have already been backported (false positive) +- Git gap may include PRs that don't need backport (test-only, design-system, website) +- Bot can also miss PRs that landed without the right labels + ## Source 1: Slack Backport-Checker Bot Use `slackdump` skill to export `#frontend-releases` channel (C09K9TPU2G7): @@ -36,7 +42,43 @@ gh pr view $PR --json mergeCommit,title --jq '"Title: \(.title)\nMerge: \(.merge gh pr view $PR --json files --jq '.files[].path' ``` +## Source 4: Already-Backported PRs (cross-reference) + +When the target branch already has some cherry-picks on it (e.g., partway through a release window), extract the originals to avoid re-backporting: + +```bash +# Get all original PR numbers already backported to TARGET since the last release tag +git log --format="%H%n%B" $LAST_TAG..origin/$TARGET \ + | grep -oiE "(backport of|cherry.picked) #?[0-9]+" \ + | grep -oE "[0-9]+" \ + | sort -un > /tmp/already-backported.txt +``` + +Subtract this list from your candidates. + +## Reconciliation Workflow + +```bash +# 1. Slack bot list (parse from export) +# /tmp/bot-flagged.txt — one PR# per line, sorted + +# 2. Git gap fix/perf only +MB=$(git merge-base origin/main origin/$TARGET) +git log --format="%h|%s" $MB..origin/main \ + | grep -iE "^[a-f0-9]+\|(fix|perf)" \ + | grep -oE "#[0-9]+\)" | grep -oE "[0-9]+" \ + | sort -un > /tmp/gap-fixes.txt + +# 3. Already backported (Source 4 above) + +# 4. Candidates = (gap-fixes ∪ bot-flagged) − already-backported +sort -u /tmp/gap-fixes.txt /tmp/bot-flagged.txt > /tmp/union.txt +comm -23 /tmp/union.txt /tmp/already-backported.txt > /tmp/candidates.txt +``` + +The result is the input to the path pre-filter (`SKILL.md` Quick Start step 2). + ## Output: candidate_list.md Table per target branch: -| PR# | Title | Category | Flagged by Bot? | Decision | +| PR# | Title | Source (bot/gap/both) | Path bucket | Tier | Decision | diff --git a/.claude/skills/backport-management/reference/execution.md b/.claude/skills/backport-management/reference/execution.md index 2bc6f9f78a..0195091e71 100644 --- a/.claude/skills/backport-management/reference/execution.md +++ b/.claude/skills/backport-management/reference/execution.md @@ -6,6 +6,43 @@ 2. Medium gap next (quick win) 3. Largest gap last (main effort) +## Step 0: Test-Then-Resolve Pre-Pass (Recommended) + +Before triggering label-driven automation, run a dry-run cherry-pick loop to classify candidates. This is much faster than discovering conflicts after-the-fact across automation, manual cherry-picks, and CI failures. + +```bash +git fetch origin TARGET_BRANCH +git worktree add /tmp/dryrun-TARGET origin/TARGET_BRANCH +cd /tmp/dryrun-TARGET + +CLEAN=() +CONFLICT=() +for pr in "${CANDIDATES[@]}"; do + SHA=$(gh pr view $pr --json mergeCommit --jq '.mergeCommit.oid') + git checkout -b dryrun-$pr origin/TARGET_BRANCH 2>/dev/null + if git cherry-pick -m 1 $SHA 2>/dev/null; then + CLEAN+=($pr) + else + CONFLICT+=($pr) + git cherry-pick --abort + fi + git checkout --detach HEAD 2>/dev/null + git branch -D dryrun-$pr 2>/dev/null +done + +echo "CLEAN (${#CLEAN[@]}): ${CLEAN[*]}" +echo "CONFLICT (${#CONFLICT[@]}): ${CONFLICT[*]}" + +cd - +git worktree remove /tmp/dryrun-TARGET --force +``` + +Use the result to: + +- Send CLEAN PRs through label-driven automation (Step 1) — they'll typically self-merge +- Reserve manual worktree time (Step 3) for CONFLICT PRs only +- Surface PRs likely to need backport-only compat shims (CONFLICT files in `src/lib/litegraph/` or `src/scripts/app.ts`) + ## Step 1: Label-Driven Automation (Batch) ```bash @@ -88,6 +125,39 @@ for PR in ${CONFLICT_PRS[@]}; do git add . GIT_EDITOR=true git cherry-pick --continue + # ── Public-API conflict review (REQUIRED for extension-API surfaces) ── + # If the conflict resolution touched any of these surfaces, consult oracle + # BEFORE pushing. A bad shim is worse than no fix: + # - node.onXxx callback assignments (onDragDrop, onConnectionsChange, onRemoved, onConfigure, etc.) + # - Methods on LGraphNode, LGraphCanvas, LGraph, Subgraph + # - Public exports from src/lib/litegraph/ + # - Type changes in litegraph-augmentation.d.ts + # If a public callback's signature/contract changed: add a backport-only + # compatibility shim that preserves the OLD contract while keeping the + # new fix. Document it in the commit body under + # "## Backport-only compatibility fix". See SKILL.md gotcha section. + # ─────────────────────────────────────────────────────────────────────── + + # Per-PR validation BEFORE push (catches issues earlier than wave verification). + # Guard each targeted command against empty file lists — running `pnpm test:unit -- run` + # with no arg matchers would run the full suite, and `pnpm exec eslint` with no args errors. + pnpm typecheck + + mapfile -t TEST_FILES < <(git diff --name-only HEAD~1 | grep -E '\.test\.ts$' || true) + if [ ${#TEST_FILES[@]} -gt 0 ]; then + pnpm test:unit -- run "${TEST_FILES[@]}" + else + echo "No changed test files — skipping targeted unit tests" + fi + + mapfile -t CODE_FILES < <(git diff --name-only HEAD~1 | grep -E '\.(ts|vue)$' || true) + if [ ${#CODE_FILES[@]} -gt 0 ]; then + pnpm exec eslint "${CODE_FILES[@]}" + pnpm exec oxfmt --check "${CODE_FILES[@]}" + else + echo "No changed ts/vue files — skipping targeted lint/format" + fi + git push origin backport-$PR-to-TARGET --no-verify NEW_PR=$(gh pr create --base TARGET_BRANCH --head backport-$PR-to-TARGET \ --title "[backport TARGET] TITLE (#$PR)" \ @@ -243,6 +313,9 @@ gh pr checks $PR --watch --fail-fast && gh pr merge $PR --squash --admin 16. **Use `--no-verify` in worktrees** — husky hooks fail in `/tmp/` worktrees. Always push/commit with `--no-verify`. 17. **Automation success varies by branch** — core/1.42 got 18/26 auto-PRs (69%), cloud/1.42 got 1/25 (4%). Cloud branches diverge more. Plan for manual fallback. 18. **Test-then-resolve pattern** — for branches with low automation success, run a dry-run loop to classify clean vs conflict PRs before processing. This is much faster than resolving conflicts serially. +19. **Public-API conflict resolutions need oracle review** — when a conflict touches `node.onXxx` callbacks, `LGraphNode`/`LGraphCanvas`/`LGraph`/`Subgraph` methods, or types in `litegraph-augmentation.d.ts`, consult oracle BEFORE pushing. Custom-node packages depend on these contracts. A literal cherry-pick of a refactor-style fix can silently break extensions still using the old contract — sometimes recreating the very bug the PR was fixing. Document any backport-only compatibility shim explicitly in the commit body. +20. **Cherry-picked tests can require unbackported test scaffolding** — when a PR modifies a test file that was _added_ on main by an earlier unbackported PR, the cherry-pick reports modify/delete on that file. Drop it from the backport (`git rm`) and document which PR introduced it. Don't smuggle in test infrastructure without its runtime prerequisites. +21. **Per-PR validation catches issues earlier than wave verification** — for high-stakes branches, run `pnpm typecheck && pnpm exec eslint && pnpm exec oxfmt --check` per PR before pushing. Wave verification still matters (it catches cross-PR interactions), but per-PR makes attribution trivial when something fails. ## CI Failure Triage @@ -268,3 +341,40 @@ Common failure categories: | Type error | Interface changed on main but not branch | May need manual adaptation | **Never assume a failure is safe to skip.** Present all failures to the user with analysis. + +## PR Body Template (Manual Cherry-Picks) + +Manual cherry-pick PRs need detail beyond the automation's terse default. Use this template — reviewers will look here before re-deriving conflict-resolution logic from the diff. + +```markdown +Manual backport of #ORIG to `TARGET` for inclusion in `vX.Y.Z`. + +Cherry-picked from upstream merge commit `SHORT_SHA`. + +## Why + +[1-2 sentences from the original PR's "Summary" — what bug, what fix mechanism] + +## Conflict resolution + +- **`path/to/file`** — [what conflicted on this branch] → [resolution chosen + why] +- **`path/to/dropped-test.test.ts`** — added on main by unrelated PR #XXXX (not backported). Dropped from this backport; runtime fix intact. +- [...] + +## Backport-only compatibility fix (if applicable) + +[If you added a shim that wasn't in the upstream PR, document it here — what extension surface, what contract, what the shim preserves, why the upstream version would have regressed it] + +## Validation + +- `pnpm typecheck` ✅ +- `pnpm test:unit -- run ` ✅ (N/N passing) +- `pnpm exec eslint ` ✅ (0 errors) +- `pnpm exec oxfmt --check` ✅ (clean) + +[If manual e2e was skipped, explain why — e.g., requires live backend, headless not feasible. State that source is byte-identical to upstream + how long it's been baking on main.] + +Original PR: #ORIG / Original commit: `FULL_SHA` +``` + +The conflict-resolution section is non-negotiable — every conflict you resolved by hand needs a one-liner. This makes archaeology trivial six months later when someone asks "why does this look slightly different from main?" diff --git a/.claude/skills/perf-fix-with-proof/SKILL.md b/.claude/skills/perf-fix-with-proof/SKILL.md index 56ba55d2e5..6ffba72aa5 100644 --- a/.claude/skills/perf-fix-with-proof/SKILL.md +++ b/.claude/skills/perf-fix-with-proof/SKILL.md @@ -171,7 +171,7 @@ test('canvas text rendering with many nodes', async ({ comfyPage }) => { | ----------------- | ----------------------------------------------------- | | Perf test file | `browser_tests/tests/performance.spec.ts` | | PerformanceHelper | `browser_tests/fixtures/helpers/PerformanceHelper.ts` | -| Perf reporter | `browser_tests/helpers/perfReporter.ts` | +| Perf reporter | `browser_tests/fixtures/utils/perfReporter.ts` | | CI workflow | `.github/workflows/ci-perf-report.yaml` | | Report generator | `scripts/perf-report.ts` | | Stats utilities | `scripts/perf-stats.ts` | diff --git a/.claude/skills/reviewing-unit-tests/SKILL.md b/.claude/skills/reviewing-unit-tests/SKILL.md new file mode 100644 index 0000000000..dcf8a1499d --- /dev/null +++ b/.claude/skills/reviewing-unit-tests/SKILL.md @@ -0,0 +1,156 @@ +--- +name: reviewing-unit-tests +description: Use when reviewing Vitest unit-test diffs in ComfyUI_frontend, especially new mocks, store tests, component tests, or bugfix regression tests. +--- + +# Reviewing Unit Tests for ComfyUI_frontend + +## Overview + +Review for behavior and current repo rules, not motion. Compare to authoritative rules, not prior diffs or legacy snippets. + +## Review Workflow + +1. Identify the test type: component, store, composable, util, or bugfix regression. +2. Name the behavior the test proves. If you cannot say it in one sentence, request changes. +3. Open the authoritative doc section before judging structure. +4. Scan the red flags below. +5. State the verdict first. Name the failure mode. Cite the doc or rule. + +## Source of Truth / Precedence + +When docs and examples conflict, use this order: + +1. Explicit repo rules, lint rules, and note blocks. +2. [`docs/testing/vitest-patterns.md`](../../../docs/testing/vitest-patterns.md) +3. Rule sections in [`docs/testing/unit-testing.md`](../../../docs/testing/unit-testing.md), [`docs/testing/store-testing.md`](../../../docs/testing/store-testing.md), and [`docs/testing/component-testing.md`](../../../docs/testing/component-testing.md) +4. Example snippets +5. Prior diffs + +Apply these repo-specific clarifications: + +- [`docs/testing/component-testing.md`](../../../docs/testing/component-testing.md) starts with the authoritative rule: new component tests use `@testing-library/vue` with `@testing-library/user-event`. The `@vue/test-utils` snippets below it are legacy examples. +- [`docs/testing/store-testing.md`](../../../docs/testing/store-testing.md) still contains `as any` examples. Treat them as legacy snippets, not approval for new or edited test code. +- If docs conflict, prefer the stricter newer rule and call out the doc ambiguity. Do not approve through it. +- Motion != fix. + +## 30-Second Red Flags + +| If you see... | Failure mode | Default action | +| ----------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------- | +| New `@vue/test-utils` import in a new component test | legacy test API | Request changes | +| `vi.mock('vue-i18n', ...)` | mocked i18n | Request changes | +| `as any`, `@ts-expect-error`, `as Mock`, `as ReturnType`, `as unknown as X` | unnecessary cast or type escape | Request changes unless the author proves no safer type exists | +| `getXMock()`, renamed wrapper, or helper that only returns a mocked value | alias-by-renaming | Request changes | +| `beforeEach` recreates the return object for a module-mocked composable or service | shared mock setup drift | Request changes | +| Assertions only check defaults, mock plumbing, or CSS hooks | non-behavioral test | Request changes | +| Bugfix test has no proof it fails on pre-fix code | unproven regression | Request changes | + +## Rationalization Table + +| Excuse | Reality | +| ----------------------------------- | ------------------------------------------------------------------------------------------------------ | +| "I restructured the mocks" | If the indirection stayed, nothing improved. Flag `alias-by-renaming`. | +| "The docs do it" | Rule, note, and lint beat legacy snippet. Compare to the current rule, not the nearest example. | +| "TypeScript required the cast" | `vi.mocked()` usually narrows mock methods. Assertion-only references need no cast. | +| "Putting it in `beforeEach` is DRY" | Recreating module mock state in hooks hides singleton behavior and drifts from the documented pattern. | +| "It is only a nit" | Explicit repo-rule violations are never nits. | +| "No behavior changed, just cleanup" | Motion != fix. Ask what behavior got stronger. | +| "Mental revert is enough" | For bugfix tests, establish red on pre-fix code or ask the author to show it. | + +## Mocking Rules + +- Fail helpers that do not remove repeated setup, encode domain meaning, or simplify assertions. Barely earning the abstraction is not enough. +- For composables with reactive or singleton state, define stable mock state inside the `vi.mock()` factory. Access it per test via the composable itself. See [`docs/testing/unit-testing.md`](../../../docs/testing/unit-testing.md) "Mocking Composables with Reactive State". +- This does not ban local test data builders or per-test `vi.spyOn(...)`. +- Mock seams, not the project-owned module you are trying to exercise. For store tests, prefer real Pinia plus `createTestingPinia({ stubActions: false })` per [`docs/testing/vitest-patterns.md`](../../../docs/testing/vitest-patterns.md) and [`docs/testing/store-testing.md`](../../../docs/testing/store-testing.md). + +### Alias-by-Renaming + +```ts +// Before +const mockAdd = vi.hoisted(() => vi.fn()) + +// After: same indirection, new name +function getToastAddMock() { + return useToast().add +} +``` + +If the wrapper only renames or relays a mocked value, fail it. Inline the lookup at the call site or fetch the singleton mock via the documented pattern. + +### `vi.mocked()` Scope + +| Use case | `vi.mocked()` required? | +| --------------------------------------------------------------- | ----------------------- | +| `.mockReturnValue`, `.mockResolvedValue`, `.mockImplementation` | Yes | +| `.mock.calls`, `.mock.results` | Yes | +| `expect(fn).toHaveBeenCalled()` | No | +| `expect(fn).toHaveBeenCalledWith(...)` | No | + +- Flag casts whenever `vi.mocked()` would narrow correctly. +- Do not add `vi.mocked()` around assertion-only references just for style. + +### Reset Hygiene + +- Flag per-mock `mockClear()` or `mockReset()` when `vi.clearAllMocks()` or `vi.resetAllMocks()` already runs in the relevant hook chain. +- Review for redundancy or broken state management. Do not bikeshed `clearAllMocks` vs `resetAllMocks` unless behavior depends on it. + +### Third-Party Seams + +- Distinguish trivial hooks from behavior-rich APIs. +- Mocking single-method third-party hooks like `primevue/usetoast` is usually acceptable. +- That exception does not justify mocking behavior-rich third-party modules. + +### `vue-i18n` + +- Never mock `vue-i18n` in component tests. +- Use real `createI18n` per [`docs/testing/vitest-patterns.md`](../../../docs/testing/vitest-patterns.md) and the shared [`testI18n`](../../../src/components/searchbox/v2/__test__/testUtils.ts) setup. + +## Test-Body Rules + +| Smell | Review bar | +| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| Change-detector test | Reject. Default values alone prove nothing. | +| Mock-only assertion | Accept collaborator-call assertions only when the call is the meaningful external effect and the test also exercises the triggering behavior. | +| Non-behavioral assertion | Reject tests that only check classes, utility hooks, or styling internals. | +| New component test using `@vue/test-utils` | Request changes. Use `@testing-library/vue` plus `@testing-library/user-event`. | +| `any`, `as any`, or `@ts-expect-error` in new or edited test code | Request changes unless the author proves no safer type exists. Legacy doc snippets do not authorize it. | + +## Bugfix Regression Proof + +For `fix:` PRs or bugfix diffs: + +1. Identify the production change that fixes the bug. +2. Verify the new test fails on pre-fix code, or ask the author to show it. +3. If the test passes on broken code, request changes. + +A regression test that never proves red does not pin the bug. + +## Review Output Rules + +- State verdict before procedural questions. +- Do not lead with approval language like `LGTM, just one nit` or `approve and move on?`. +- Name the failure mode directly: `alias-by-renaming`, `unnecessary cast`, `mocked i18n`, `mock-only assertion`, `unproven regression`. +- Link the authoritative doc section in the review comment. +- If an explicit repo rule, lint rule, or authoritative doc note is violated, do not downgrade it to "minor deviation" or "nit". + +## Quick Reference + +| When you see... | Read this | +| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| New `vi.mock(...)` for a composable | [`docs/testing/unit-testing.md`](../../../docs/testing/unit-testing.md) -> "Mocking Composables with Reactive State" | +| New store test or store mock | [`docs/testing/vitest-patterns.md`](../../../docs/testing/vitest-patterns.md) setup + [`docs/testing/store-testing.md`](../../../docs/testing/store-testing.md) | +| New component test | Top note in [`docs/testing/component-testing.md`](../../../docs/testing/component-testing.md) | +| `vue-i18n` in a component test | [`docs/testing/vitest-patterns.md`](../../../docs/testing/vitest-patterns.md) + [`src/components/searchbox/v2/__test__/testUtils.ts`](../../../src/components/searchbox/v2/__test__/testUtils.ts) | +| Cast around a mock | [`docs/guidance/typescript.md`](../../../docs/guidance/typescript.md) -> "Type Assertion Hierarchy" | + +## Key Files to Read + +| Purpose | Path | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | +| Composable mocking patterns | [`docs/testing/unit-testing.md`](../../../docs/testing/unit-testing.md) | +| Store testing patterns | [`docs/testing/store-testing.md`](../../../docs/testing/store-testing.md) | +| Repo-wide Vitest setup defaults | [`docs/testing/vitest-patterns.md`](../../../docs/testing/vitest-patterns.md) | +| Component testing rule for new tests | [`docs/testing/component-testing.md`](../../../docs/testing/component-testing.md) | +| Real i18n setup | [`src/components/searchbox/v2/__test__/testUtils.ts`](../../../src/components/searchbox/v2/__test__/testUtils.ts) | diff --git a/.claude/skills/writing-playwright-tests/SKILL.md b/.claude/skills/writing-playwright-tests/SKILL.md index 84a39210a0..efc20dff85 100644 --- a/.claude/skills/writing-playwright-tests/SKILL.md +++ b/.claude/skills/writing-playwright-tests/SKILL.md @@ -114,7 +114,7 @@ await expect(async () => { ## CI Debugging 1. Download artifacts from failed CI run -2. Extract and view trace: `npx playwright show-trace trace.zip` +2. Extract and view trace: `pnpm dlx playwright show-trace trace.zip` 3. CI deploys HTML report to Cloudflare Pages (link in PR comment) 4. Reproduce CI: `CI=true pnpm test:browser` 5. Local runs: `pnpm test:browser:local` diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 20af82bec6..35c5d31183 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -19,15 +19,26 @@ reviews: - name: End-to-end regression coverage for fixes mode: error instructions: | - Use only PR metadata already available in the review context: the PR title, commit subjects in this PR, the files changed in this PR relative to the PR base (equivalent to `base...head`), and the PR description. - Do not rely on shell commands. Do not inspect reverse diffs, files changed only on the base branch, or files outside this PR. If the changed-file list or commit subjects are unavailable, mark the check inconclusive instead of guessing. + Use only PR metadata already available in the review context: + - the PR title + - commit subjects in this PR + - The files changed in this PR relative to the PR base (equivalent to `base...head`) + - the PR description. + Do not rely on shell commands. + Do not inspect reverse diffs, files changed only on the base branch, or files outside this PR. + If the changed-file list or commit subjects are unavailable, mark the check inconclusive instead of guessing. - Pass if at least one of the following is true: - 1. Neither the PR title nor any commit subject in the PR uses bug-fix language such as `fix`, `fixed`, `fixes`, `fixing`, `bugfix`, or `hotfix`. - 2. The PR changes at least one file under `browser_tests/`. - 3. The PR description includes a concrete, non-placeholder explanation of why an end-to-end regression test was not added. + Fail if all of the following are true: + 1. The PR title and/or any commit subject in the PR uses bug-fix language such as `fix`, `fixed`, `fixes`, `fixing`, `bugfix`, or `hotfix`. + 2. The PR changes files under `src/` or `packages/` related to the main frontend application but the PR does not change at least one file under `browser_tests/`. + 3. The PR description lacks a concrete explanation of why an end-to-end regression test was not added. + + Do not fail if the changes are exclusively in `apps/website`, just documentation changes, or changes related to CI processes. + The goal is to make sure that fixes include End-to-End regression tests. Do not insist on tests when the PR is not fixing a bug. + + Pass otherwise. + When failing, mention which bug-fix signal you found and ask the author to either add or update a Playwright regression test under `browser_tests/` or add a concrete explanation in the PR description of why an end-to-end regression test is not practical. - Fail otherwise. When failing, mention which bug-fix signal you found and ask the author to either add or update a Playwright regression test under `browser_tests/` or add a concrete explanation in the PR description of why an end-to-end regression test is not practical. - name: ADR compliance for entity/litegraph changes mode: warning instructions: | diff --git a/.env_example b/.env_example index 8500b8b750..81caf27585 100644 --- a/.env_example +++ b/.env_example @@ -46,3 +46,9 @@ ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579 # SENTRY_ORG=comfy-org # SENTRY_PROJECT=cloud-frontend-staging # SENTRY_PROJECT_PROD= # prod project slug for sourcemap uploads + +# Ashby (apps/website careers page build). +# Server-only; read inside the Astro build context. Do NOT prefix with PUBLIC_. +# When unset, the committed snapshot at apps/website/src/data/ashby-roles.snapshot.json is used. +# WEBSITE_ASHBY_API_KEY= +# WEBSITE_ASHBY_JOB_BOARD_NAME=comfy-org diff --git a/.github/actions/ashby-pull/action.yaml b/.github/actions/ashby-pull/action.yaml new file mode 100644 index 0000000000..d1b27f6c67 --- /dev/null +++ b/.github/actions/ashby-pull/action.yaml @@ -0,0 +1,23 @@ +name: Ashby Pull +description: 'Refresh the apps/website Ashby roles snapshot from the Ashby job board API' +inputs: + api_key: + description: 'Ashby API key (WEBSITE_ASHBY_API_KEY).' + required: true + job_board_name: + description: 'Ashby job board name (WEBSITE_ASHBY_JOB_BOARD_NAME).' + required: true +runs: + using: 'composite' + steps: + # Note: this action assumes the frontend repo is checked out at the workspace root. + + - name: Setup frontend + uses: ./.github/actions/setup-frontend + + - name: Refresh Ashby snapshot + shell: bash + env: + WEBSITE_ASHBY_API_KEY: ${{ inputs.api_key }} + WEBSITE_ASHBY_JOB_BOARD_NAME: ${{ inputs.job_board_name }} + run: pnpm --filter @comfyorg/website ashby:refresh-snapshot diff --git a/.github/actions/changes-filter/action.yaml b/.github/actions/changes-filter/action.yaml new file mode 100644 index 0000000000..3a37ca4a32 --- /dev/null +++ b/.github/actions/changes-filter/action.yaml @@ -0,0 +1,87 @@ +# Outputs default to 'true' for non-pull_request events (push, merge_group): +# granular path filtering is a PR-only optimization. This avoids the silent +# skip footgun where a job gated on e.g. `app-website-changes == 'true'` +# would never run on push. +# +# Shared dependency files (root package.json, pnpm-lock.yaml, +# pnpm-workspace.yaml) are folded into every app-* and packages-changes +# output so a lockfile bump correctly invalidates each granular gate. They +# are NOT folded into docs-changes. +# +# Two paths-filter steps are needed because predicate-quantifier=every is +# required for the negated globs in `should-run` but breaks multi-pattern +# OR filters like `docs:` and `deps:`. +# +# Requires the caller to have checked out the repository. + +name: 'Detect Path Changes' +description: > + Computes typed *-changes outputs and a back-compat should-run for + path-gated CI jobs. + +outputs: + should-run: + description: 'Any file outside `apps/`, `docs/`, `.storybook/`, or `**/*.md` changed.' + value: ${{ github.event_name != 'pull_request' || steps.relevant.outputs.relevant == 'true' }} + app-website-changes: + description: 'Shared deps or `apps/website/**` changed.' + value: ${{ github.event_name != 'pull_request' || steps.filter.outputs.deps == 'true' || steps.filter.outputs.app_website == 'true' }} + app-desktop-changes: + description: 'Shared deps or `apps/desktop-ui/**` changed.' + value: ${{ github.event_name != 'pull_request' || steps.filter.outputs.deps == 'true' || steps.filter.outputs.app_desktop == 'true' }} + app-frontend-changes: + description: 'Shared deps or `src/**` changed.' + value: ${{ github.event_name != 'pull_request' || steps.filter.outputs.deps == 'true' || steps.filter.outputs.app_frontend == 'true' }} + packages-changes: + description: 'Shared deps or `packages/**` changed.' + value: ${{ github.event_name != 'pull_request' || steps.filter.outputs.deps == 'true' || steps.filter.outputs.packages == 'true' }} + storybook-changes: + description: 'Shared deps or `.storybook/**` changed.' + value: ${{ github.event_name != 'pull_request' || steps.filter.outputs.deps == 'true' || steps.filter.outputs.storybook == 'true' }} + docs-changes: + description: '`docs/**` or any `**/*.md` changed (deps NOT folded in).' + value: ${{ github.event_name != 'pull_request' || steps.filter.outputs.docs == 'true' }} + dependency-changes: + description: 'Root `package.json`, `pnpm-lock.yaml`, or `pnpm-workspace.yaml` changed.' + value: ${{ github.event_name != 'pull_request' || steps.filter.outputs.deps == 'true' }} + +runs: + using: composite + steps: + - name: Filter typed changes + if: ${{ github.event_name == 'pull_request' }} + id: filter + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + with: + filters: | + app_website: + - 'apps/website/**' + app_desktop: + - 'apps/desktop-ui/**' + app_frontend: + - 'src/**' + packages: + - 'packages/**' + storybook: + - '.storybook/**' + docs: + - 'docs/**' + - '**/*.md' + deps: + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + + - name: Filter relevant changes + if: ${{ github.event_name == 'pull_request' }} + id: relevant + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + with: + predicate-quantifier: 'every' + filters: | + relevant: + - '**' + - '!apps/**' + - '!docs/**' + - '!.storybook/**' + - '!**/*.md' diff --git a/.github/actions/cloud-nodes-pull/action.yaml b/.github/actions/cloud-nodes-pull/action.yaml new file mode 100644 index 0000000000..8c4dfd2702 --- /dev/null +++ b/.github/actions/cloud-nodes-pull/action.yaml @@ -0,0 +1,19 @@ +name: Cloud Nodes Pull +description: 'Refresh the apps/website cloud nodes snapshot from the Comfy Cloud /api/object_info endpoint' +inputs: + api_key: + description: 'Comfy Cloud API key (WEBSITE_CLOUD_API_KEY).' + required: true +runs: + using: 'composite' + steps: + # Note: this action assumes the frontend repo is checked out at the workspace root. + + - name: Setup frontend + uses: ./.github/actions/setup-frontend + + - name: Refresh cloud nodes snapshot + shell: bash + env: + WEBSITE_CLOUD_API_KEY: ${{ inputs.api_key }} + run: pnpm --filter @comfyorg/website cloud-nodes:refresh-snapshot diff --git a/.github/actions/resolve-pr-from-workflow-run/action.yaml b/.github/actions/resolve-pr-from-workflow-run/action.yaml new file mode 100644 index 0000000000..6dea5c319e --- /dev/null +++ b/.github/actions/resolve-pr-from-workflow-run/action.yaml @@ -0,0 +1,88 @@ +name: Resolve PR from workflow_run +description: > + Resolves the PR number from a workflow_run event using pull_requests[0] + with a listPullRequestsAssociatedWithCommit fallback. + Skips closed/merged PRs and stale runs (head SHA mismatch). + +inputs: + token: + description: GitHub token for API calls + required: false + default: ${{ github.token }} + +outputs: + skip: + description: "'true' when no open PR was found or the run is stale" + value: ${{ steps.resolve.outputs.skip }} + number: + description: The PR number (empty when skip is true) + value: ${{ steps.resolve.outputs.number }} + base: + description: The PR base branch (empty when skip is true) + value: ${{ steps.resolve.outputs.base }} + head-sha: + description: The PR head SHA (empty when skip is true) + value: ${{ steps.resolve.outputs.head-sha }} + +runs: + using: composite + steps: + - name: Resolve PR + id: resolve + uses: actions/github-script@v8 + with: + github-token: ${{ inputs.token }} + script: | + let pr = context.payload.workflow_run.pull_requests?.[0]; + if (!pr) { + const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: context.payload.workflow_run.head_sha, + }); + pr = prs.find(p => p.state === 'open'); + } + + // Fork PRs: pull_requests is empty and commit SHA may not be in + // the base repo graph. Fall back to pulls.list with head filter. + if (!pr && context.payload.workflow_run.head_repository?.owner?.login) { + const { data: prs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${context.payload.workflow_run.head_repository.owner.login}:${context.payload.workflow_run.head_branch}`, + per_page: 1, + }); + pr = prs.find(p => p.head.sha === context.payload.workflow_run.head_sha); + } + + if (!pr) { + core.info('No open PR found for this workflow run — skipping.'); + core.setOutput('skip', 'true'); + return; + } + + const { data: livePr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + }); + + if (livePr.state !== 'open') { + core.info(`PR #${pr.number} is ${livePr.state} — skipping.`); + core.setOutput('skip', 'true'); + return; + } + + if (livePr.head.sha !== context.payload.workflow_run.head_sha) { + core.info( + `Stale run: workflow SHA ${context.payload.workflow_run.head_sha} != PR head ${livePr.head.sha}` + ); + core.setOutput('skip', 'true'); + return; + } + + core.setOutput('base', livePr.base.ref); + core.setOutput('head-sha', livePr.head.sha); + core.setOutput('skip', 'false'); + core.setOutput('number', String(pr.number)); diff --git a/.github/actions/upsert-comment-section/action.yaml b/.github/actions/upsert-comment-section/action.yaml new file mode 100644 index 0000000000..853af0aeb3 --- /dev/null +++ b/.github/actions/upsert-comment-section/action.yaml @@ -0,0 +1,79 @@ +name: Upsert Comment Section +description: > + Manage a consolidated PR comment with independently-updatable sections. + All website CI workflows share the marker . + Valid section names: "e2e", "preview", "screenshot-update". + +inputs: + pr-number: + description: PR number to comment on + required: true + section-name: + description: 'Section identifier: "e2e", "preview", or "screenshot-update"' + required: true + section-content: + description: Markdown content for this section + required: true + comment-marker: + description: Top-level HTML comment marker (must be for all callers) + required: true + token: + description: GitHub token with pull-requests write permission + required: true + +runs: + using: composite + steps: + - uses: actions/github-script@v8 + env: + INPUT_PR_NUMBER: ${{ inputs.pr-number }} + INPUT_SECTION_NAME: ${{ inputs.section-name }} + INPUT_SECTION_CONTENT: ${{ inputs.section-content }} + INPUT_COMMENT_MARKER: ${{ inputs.comment-marker }} + with: + github-token: ${{ inputs.token }} + script: | + const prNumber = Number(process.env.INPUT_PR_NUMBER) + const sectionName = process.env.INPUT_SECTION_NAME + const sectionContent = process.env.INPUT_SECTION_CONTENT + const commentMarker = process.env.INPUT_COMMENT_MARKER + + const sectionStart = `` + const sectionEnd = `` + const sectionBlock = `${sectionStart}\n${sectionContent}\n${sectionEnd}` + + // Escape special regex characters in delimiter strings + const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + + const comments = await github.paginate( + github.rest.issues.listComments, + { ...context.repo, issue_number: prNumber } + ) + + const existing = comments.find( + (c) => + c.user?.login === 'github-actions[bot]' && + c.body?.includes(commentMarker) + ) + + if (!existing) { + return github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body: `${commentMarker}\n${sectionBlock}` + }) + } + + const body = existing.body ?? '' + const sectionRegex = new RegExp( + `${escapeRegex(sectionStart)}[\\s\\S]*?${escapeRegex(sectionEnd)}` + ) + const updated = sectionRegex.test(body) + ? body.replace(sectionRegex, sectionBlock) + : body.trimEnd() + '\n\n' + sectionBlock + + return github.rest.issues.updateComment({ + ...context.repo, + comment_id: existing.id, + body: updated + }) diff --git a/.github/pr-images/fe-237-before-after.png b/.github/pr-images/fe-237-before-after.png new file mode 100644 index 0000000000..a104802cdc Binary files /dev/null and b/.github/pr-images/fe-237-before-after.png differ diff --git a/.github/workflows/ci-dist-telemetry-scan.yaml b/.github/workflows/ci-dist-telemetry-scan.yaml index 1821efd95d..c5e53d13fd 100644 --- a/.github/workflows/ci-dist-telemetry-scan.yaml +++ b/.github/workflows/ci-dist-telemetry-scan.yaml @@ -12,17 +12,30 @@ permissions: contents: read jobs: + changes: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + should-run: ${{ steps.changes.outputs.should-run }} + steps: + - uses: actions/checkout@v6 + - id: changes + uses: ./.github/actions/changes-filter + scan: + needs: changes + if: ${{ needs.changes.outputs.should-run == 'true' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@v6 - name: Install pnpm uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 - name: Use Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' cache: 'pnpm' diff --git a/.github/workflows/ci-oss-assets-validation.yaml b/.github/workflows/ci-oss-assets-validation.yaml index a145ca04f1..c05399d4dc 100644 --- a/.github/workflows/ci-oss-assets-validation.yaml +++ b/.github/workflows/ci-oss-assets-validation.yaml @@ -14,16 +14,29 @@ permissions: contents: read jobs: + changes: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + should-run: ${{ steps.changes.outputs.should-run }} + steps: + - uses: actions/checkout@v6 + - id: changes + uses: ./.github/actions/changes-filter + validate-fonts: + needs: changes + if: ${{ needs.changes.outputs.should-run == 'true' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@v6 - name: Install pnpm uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 - name: Use Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' cache: 'pnpm' @@ -68,15 +81,17 @@ jobs: echo '✅ No proprietary fonts found in dist' validate-licenses: + needs: changes + if: ${{ needs.changes.outputs.should-run == 'true' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@v6 - name: Install pnpm uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 - name: Use Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' cache: 'pnpm' diff --git a/.github/workflows/ci-perf-report.yaml b/.github/workflows/ci-perf-report.yaml index 3666ecc1f1..94dad8b916 100644 --- a/.github/workflows/ci-perf-report.yaml +++ b/.github/workflows/ci-perf-report.yaml @@ -3,10 +3,8 @@ name: 'CI: Performance Report' on: push: branches: [main, core/*] - paths-ignore: ['**/*.md'] pull_request: branches-ignore: [wip/*, draft/*, temp/*] - paths-ignore: ['**/*.md'] concurrency: group: perf-${{ github.ref }} @@ -16,12 +14,24 @@ permissions: contents: read jobs: + changes: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + should-run: ${{ steps.changes.outputs.should-run }} + steps: + - uses: actions/checkout@v6 + - id: changes + uses: ./.github/actions/changes-filter + perf-tests: - if: github.repository == 'Comfy-Org/ComfyUI_frontend' + needs: changes + if: ${{ needs.changes.outputs.should-run == 'true' && github.repository == 'Comfy-Org/ComfyUI_frontend' }} runs-on: ubuntu-latest timeout-minutes: 30 container: - image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12 + image: ghcr.io/comfy-org/comfyui-ci-container:0.0.17 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -44,10 +54,14 @@ jobs: - name: Start ComfyUI server uses: ./.github/actions/start-comfyui-server + # PRs run each test once to keep wall time bounded; main runs 3× so the + # baseline saved to perf-data has enough samples to median over noise. - name: Run performance tests id: perf continue-on-error: true - run: pnpm exec playwright test --project=performance --workers=1 --repeat-each=3 + env: + PERF_REPEAT: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && '3' || '2' }} + run: pnpm exec playwright test --project=performance --workers=1 --repeat-each=$PERF_REPEAT - name: Upload perf metrics if: always() @@ -58,21 +72,6 @@ jobs: retention-days: 30 if-no-files-found: warn - - name: Save PR metadata - if: github.event_name == 'pull_request' - run: | - mkdir -p temp/perf-meta - echo "${{ github.event.number }}" > temp/perf-meta/number.txt - echo "${{ github.event.pull_request.base.ref }}" > temp/perf-meta/base.txt - echo "${{ github.event.pull_request.head.sha }}" > temp/perf-meta/head-sha.txt - - - name: Upload PR metadata - if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v6 - with: - name: perf-meta - path: temp/perf-meta/ - - name: Save perf baseline to perf-data branch if: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.perf.outcome == 'success' continue-on-error: true diff --git a/.github/workflows/ci-size-data.yaml b/.github/workflows/ci-size-data.yaml index 1d28327349..ad8e7266c7 100644 --- a/.github/workflows/ci-size-data.yaml +++ b/.github/workflows/ci-size-data.yaml @@ -16,9 +16,22 @@ permissions: contents: read jobs: - collect: + changes: if: github.repository == 'Comfy-Org/ComfyUI_frontend' runs-on: ubuntu-latest + permissions: + contents: read + outputs: + should-run: ${{ steps.changes.outputs.should-run }} + steps: + - uses: actions/checkout@v6 + - id: changes + uses: ./.github/actions/changes-filter + + collect: + needs: changes + if: ${{ needs.changes.outputs.should-run == 'true' }} + runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -32,13 +45,6 @@ jobs: - name: Collect size data run: node scripts/size-collect.js - - name: Save PR metadata - if: ${{ github.event_name == 'pull_request' }} - run: | - echo ${{ github.event.number }} > ./temp/size/number.txt - echo ${{ github.base_ref }} > ./temp/size/base.txt - echo ${{ github.event.pull_request.head.sha }} > ./temp/size/head-sha.txt - - name: Upload size data uses: actions/upload-artifact@v6 with: diff --git a/.github/workflows/ci-tests-e2e-coverage.yaml b/.github/workflows/ci-tests-e2e-coverage.yaml index 8fdfe04279..246b551917 100644 --- a/.github/workflows/ci-tests-e2e-coverage.yaml +++ b/.github/workflows/ci-tests-e2e-coverage.yaml @@ -20,6 +20,8 @@ jobs: github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest timeout-minutes: 10 + outputs: + has-coverage: ${{ steps.coverage-shards.outputs.has-coverage }} steps: - name: Checkout repository @@ -37,31 +39,33 @@ jobs: path: temp/coverage-shards if_no_artifact_found: warn + - name: Detect shard coverage data + id: coverage-shards + run: | + if [ -d temp/coverage-shards ] && find temp/coverage-shards -name 'coverage.lcov' -type f | grep -q .; then + echo "has-coverage=true" >> "$GITHUB_OUTPUT" + else + echo "has-coverage=false" >> "$GITHUB_OUTPUT" + echo "No E2E coverage shard artifacts found; treating this run as skipped." >> "$GITHUB_STEP_SUMMARY" + fi + - name: Install lcov + if: steps.coverage-shards.outputs.has-coverage == 'true' run: sudo apt-get install -y -qq lcov - name: Merge shard coverage into single LCOV + if: steps.coverage-shards.outputs.has-coverage == 'true' run: | mkdir -p coverage/playwright LCOV_FILES=$(find temp/coverage-shards -name 'coverage.lcov' -type f) - if [ -z "$LCOV_FILES" ]; then - echo "No coverage.lcov files found" - touch coverage/playwright/coverage.lcov - exit 0 - fi ADD_ARGS="" for f in $LCOV_FILES; do ADD_ARGS="$ADD_ARGS -a $f"; done lcov $ADD_ARGS -o coverage/playwright/coverage.lcov wc -l coverage/playwright/coverage.lcov - name: Validate merged coverage + if: steps.coverage-shards.outputs.has-coverage == 'true' run: | - SHARD_COUNT=$(find temp/coverage-shards -name 'coverage.lcov' -type f | wc -l | tr -d ' ') - if [ "$SHARD_COUNT" -eq 0 ]; then - echo "::error::No shard coverage.lcov files found under temp/coverage-shards" - exit 1 - fi - MERGED_SF=$(grep -c '^SF:' coverage/playwright/coverage.lcov || echo 0) MERGED_LH=$(awk -F: '/^LH:/{s+=$2}END{print s+0}' coverage/playwright/coverage.lcov) MERGED_LF=$(awk -F: '/^LF:/{s+=$2}END{print s+0}' coverage/playwright/coverage.lcov) @@ -82,7 +86,7 @@ jobs: done - name: Upload merged coverage data - if: always() + if: steps.coverage-shards.outputs.has-coverage == 'true' uses: actions/upload-artifact@v6 with: name: e2e-coverage @@ -91,10 +95,57 @@ jobs: if-no-files-found: warn - name: Upload E2E coverage to Codecov - if: always() + if: steps.coverage-shards.outputs.has-coverage == 'true' uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 with: files: coverage/playwright/coverage.lcov flags: e2e token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false + + - name: Generate HTML coverage report + if: steps.coverage-shards.outputs.has-coverage == 'true' + run: | + genhtml coverage/playwright/coverage.lcov \ + -o coverage/html \ + --title "ComfyUI E2E Coverage" \ + --no-function-coverage \ + --precision 1 \ + --ignore-errors source,unmapped + + - name: Upload HTML report artifact + if: steps.coverage-shards.outputs.has-coverage == 'true' + uses: actions/upload-artifact@v6 + with: + name: e2e-coverage-html + path: coverage/html/ + retention-days: 30 + + deploy: + needs: merge + if: > + github.event.workflow_run.head_branch == 'main' && + needs.merge.outputs.has-coverage == 'true' && + github.event.workflow_run.event == 'push' + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Download HTML report + uses: actions/download-artifact@v7 + with: + name: e2e-coverage-html + path: coverage/html + + - name: Upload to GitHub Pages + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 + with: + path: coverage/html + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 diff --git a/.github/workflows/ci-tests-e2e-forks.yaml b/.github/workflows/ci-tests-e2e-forks.yaml index 3e34c846d0..eadc474f79 100644 --- a/.github/workflows/ci-tests-e2e-forks.yaml +++ b/.github/workflows/ci-tests-e2e-forks.yaml @@ -6,6 +6,10 @@ on: workflows: ['CI: Tests E2E'] types: [requested, completed] +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: true + jobs: deploy-and-comment-forked-pr: runs-on: ubuntu-latest @@ -30,49 +34,33 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - - name: Get PR Number + - name: Resolve PR from workflow_run context id: pr - uses: actions/github-script@v8 - with: - script: | - const { data: prs } = await github.rest.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - }); - - const pr = prs.find(p => p.head.sha === context.payload.workflow_run.head_sha); - - if (!pr) { - console.log('No PR found for SHA:', context.payload.workflow_run.head_sha); - return null; - } - - console.log(`Found PR #${pr.number} from fork: ${context.payload.workflow_run.head_repository.full_name}`); - return pr.number; + uses: ./.github/actions/resolve-pr-from-workflow-run - name: Handle Test Start - if: steps.pr.outputs.result != 'null' && github.event.action == 'requested' + if: steps.pr.outputs.skip != 'true' && github.event.action == 'requested' env: GITHUB_TOKEN: ${{ github.token }} run: | chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh ./scripts/cicd/pr-playwright-deploy-and-comment.sh \ - "${{ steps.pr.outputs.result }}" \ + "${{ steps.pr.outputs.number }}" \ "${{ github.event.workflow_run.head_branch }}" \ "starting" - name: Download and Deploy Reports - if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' - uses: actions/download-artifact@v7 + if: steps.pr.outputs.skip != 'true' && github.event.action == 'completed' + uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - pattern: playwright-report-* + run_id: ${{ github.event.workflow_run.id }} + name: playwright-report-.* + name_is_regexp: true path: reports + if_no_artifact_found: warn - name: Handle Test Completion - if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' + if: steps.pr.outputs.skip != 'true' && github.event.action == 'completed' && hashFiles('reports/**') != '' env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} @@ -85,6 +73,6 @@ jobs: chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh ./scripts/cicd/pr-playwright-deploy-and-comment.sh \ - "${{ steps.pr.outputs.result }}" \ + "${{ steps.pr.outputs.number }}" \ "${{ github.event.workflow_run.head_branch }}" \ "completed" diff --git a/.github/workflows/ci-tests-e2e.yaml b/.github/workflows/ci-tests-e2e.yaml index 59217f742c..76ff79d873 100644 --- a/.github/workflows/ci-tests-e2e.yaml +++ b/.github/workflows/ci-tests-e2e.yaml @@ -4,10 +4,8 @@ name: 'CI: Tests E2E' on: push: branches: [main, master, core/*, desktop/*] - paths-ignore: ['**/*.md'] pull_request: branches-ignore: [wip/*, draft/*, temp/*] - paths-ignore: ['**/*.md'] merge_group: workflow_dispatch: @@ -16,7 +14,20 @@ concurrency: cancel-in-progress: true jobs: + changes: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + should-run: ${{ steps.changes.outputs.should-run }} + steps: + - uses: actions/checkout@v6 + - id: changes + uses: ./.github/actions/changes-filter + setup: + needs: changes + if: ${{ needs.changes.outputs.should-run == 'true' }} runs-on: ubuntu-latest steps: - name: Checkout repository @@ -54,7 +65,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 container: - image: ghcr.io/comfy-org/comfyui-ci-container:0.0.16 + image: ghcr.io/comfy-org/comfyui-ci-container:0.0.17 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -112,7 +123,7 @@ jobs: needs: setup runs-on: ubuntu-latest container: - image: ghcr.io/comfy-org/comfyui-ci-container:0.0.16 + image: ghcr.io/comfy-org/comfyui-ci-container:0.0.17 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -164,9 +175,9 @@ jobs: # Merge sharded test reports (no container needed - only runs CLI) merge-reports: - needs: [playwright-tests-chromium-sharded] + needs: [changes, playwright-tests-chromium-sharded] runs-on: ubuntu-latest - if: ${{ !cancelled() }} + if: ${{ !cancelled() && needs.changes.outputs.should-run == 'true' }} steps: - name: Install pnpm uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 @@ -195,14 +206,38 @@ jobs: path: ./playwright-report/ retention-days: 30 + # Gate job — single required check that passes whether the matrix ran or was + # skipped. Branch rulesets require this instead of the individual matrix- + # expanded check names so PRs with no e2e-relevant changes aren't stuck. + e2e-status: + if: ${{ always() }} + needs: [changes, playwright-tests-chromium-sharded, playwright-tests] + runs-on: ubuntu-latest + steps: + - name: Check E2E results + env: + SHOULD_RUN: ${{ needs.changes.outputs.should-run }} + SHARDED: ${{ needs.playwright-tests-chromium-sharded.result }} + BROWSERS: ${{ needs.playwright-tests.result }} + run: | + [[ "$SHOULD_RUN" != "true" ]] && echo "E2E skipped" && exit 0 + [[ "$SHARDED" != "success" || "$BROWSERS" != "success" ]] && echo "E2E failed" && exit 1 + echo "E2E passed" + #### BEGIN Deployment and commenting (non-forked PRs only) # when using pull_request event, we have permission to comment directly # if its a forked repo, we need to use workflow_run event in a separate workflow (pr-playwright-deploy.yaml) # Post starting comment for non-forked PRs comment-on-pr-start: + needs: changes runs-on: ubuntu-latest - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false + if: >- + ${{ + needs.changes.outputs.should-run == 'true' && + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.fork == false + }} permissions: pull-requests: write steps: @@ -221,9 +256,15 @@ jobs: # Deploy and comment for non-forked PRs only deploy-and-comment: - needs: [playwright-tests, merge-reports] + needs: [changes, playwright-tests, merge-reports] runs-on: ubuntu-latest - if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false + if: >- + ${{ + always() && + needs.changes.outputs.should-run == 'true' && + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.fork == false + }} permissions: pull-requests: write contents: read diff --git a/.github/workflows/ci-tests-storybook-forks.yaml b/.github/workflows/ci-tests-storybook-forks.yaml index 0d4ab96f11..da8b5bf397 100644 --- a/.github/workflows/ci-tests-storybook-forks.yaml +++ b/.github/workflows/ci-tests-storybook-forks.yaml @@ -6,6 +6,10 @@ on: workflows: ['CI: Tests Storybook'] types: [requested, completed] +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: true + jobs: deploy-and-comment-forked-pr: runs-on: ubuntu-latest @@ -30,40 +34,23 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - - name: Get PR Number + - name: Resolve PR from workflow_run context id: pr - uses: actions/github-script@v8 - with: - script: | - const { data: prs } = await github.rest.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - }); - - const pr = prs.find(p => p.head.sha === context.payload.workflow_run.head_sha); - - if (!pr) { - console.log('No PR found for SHA:', context.payload.workflow_run.head_sha); - return null; - } - - console.log(`Found PR #${pr.number} from fork: ${context.payload.workflow_run.head_repository.full_name}`); - return pr.number; + uses: ./.github/actions/resolve-pr-from-workflow-run - name: Handle Storybook Start - if: steps.pr.outputs.result != 'null' && github.event.action == 'requested' + if: steps.pr.outputs.skip != 'true' && github.event.action == 'requested' env: GITHUB_TOKEN: ${{ github.token }} run: | chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh ./scripts/cicd/pr-storybook-deploy-and-comment.sh \ - "${{ steps.pr.outputs.result }}" \ + "${{ steps.pr.outputs.number }}" \ "${{ github.event.workflow_run.head_branch }}" \ "starting" - name: Download and Deploy Storybook - if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' && github.event.workflow_run.conclusion == 'success' + if: steps.pr.outputs.skip != 'true' && github.event.action == 'completed' && github.event.workflow_run.conclusion == 'success' uses: actions/download-artifact@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -72,7 +59,7 @@ jobs: path: storybook-static - name: Handle Storybook Completion - if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' + if: steps.pr.outputs.skip != 'true' && github.event.action == 'completed' env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} @@ -82,6 +69,6 @@ jobs: run: | chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh ./scripts/cicd/pr-storybook-deploy-and-comment.sh \ - "${{ steps.pr.outputs.result }}" \ + "${{ steps.pr.outputs.number }}" \ "${{ github.event.workflow_run.head_branch }}" \ "completed" diff --git a/.github/workflows/ci-tests-storybook.yaml b/.github/workflows/ci-tests-storybook.yaml index d2fa26826c..f53a254cf9 100644 --- a/.github/workflows/ci-tests-storybook.yaml +++ b/.github/workflows/ci-tests-storybook.yaml @@ -8,10 +8,29 @@ on: branches: [main] jobs: + changes: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + storybook-changes: ${{ steps.changes.outputs.storybook-changes }} + app-frontend-changes: ${{ steps.changes.outputs.app-frontend-changes }} + packages-changes: ${{ steps.changes.outputs.packages-changes }} + steps: + - uses: actions/checkout@v6 + - id: changes + uses: ./.github/actions/changes-filter + # Post starting comment for non-forked PRs comment-on-pr-start: + needs: changes runs-on: ubuntu-latest - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false + if: | + github.event_name == 'pull_request' + && github.event.pull_request.head.repo.fork == false + && (needs.changes.outputs.storybook-changes == 'true' + || needs.changes.outputs.app-frontend-changes == 'true' + || needs.changes.outputs.packages-changes == 'true') permissions: pull-requests: write steps: @@ -30,8 +49,13 @@ jobs: # Build Storybook for all PRs (free Cloudflare deployment) storybook-build: + needs: changes runs-on: ubuntu-latest - if: github.event_name == 'pull_request' + if: | + github.event_name == 'pull_request' + && (needs.changes.outputs.storybook-changes == 'true' + || needs.changes.outputs.app-frontend-changes == 'true' + || needs.changes.outputs.packages-changes == 'true') outputs: conclusion: ${{ steps.job-status.outputs.conclusion }} workflow-url: ${{ steps.workflow-url.outputs.url }} @@ -67,8 +91,15 @@ jobs: # Chromatic deployment only for version-bump-* branches or manual triggers chromatic-deployment: + needs: changes runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && startsWith(github.head_ref, 'version-bump-')) + if: | + github.event_name == 'workflow_dispatch' + || (github.event_name == 'pull_request' + && startsWith(github.head_ref, 'version-bump-') + && (needs.changes.outputs.storybook-changes == 'true' + || needs.changes.outputs.app-frontend-changes == 'true' + || needs.changes.outputs.packages-changes == 'true')) outputs: conclusion: ${{ steps.job-status.outputs.conclusion }} workflow-url: ${{ steps.workflow-url.outputs.url }} @@ -107,9 +138,15 @@ jobs: # Deploy and comment for non-forked PRs only deploy-and-comment: - needs: [storybook-build] + needs: [changes, storybook-build] runs-on: ubuntu-latest - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && always() + if: | + always() + && github.event_name == 'pull_request' + && github.event.pull_request.head.repo.fork == false + && (needs.changes.outputs.storybook-changes == 'true' + || needs.changes.outputs.app-frontend-changes == 'true' + || needs.changes.outputs.packages-changes == 'true') permissions: pull-requests: write contents: read diff --git a/.github/workflows/ci-tests-unit.yaml b/.github/workflows/ci-tests-unit.yaml index 3fc3095bd6..352eb8a49d 100644 --- a/.github/workflows/ci-tests-unit.yaml +++ b/.github/workflows/ci-tests-unit.yaml @@ -4,10 +4,8 @@ name: 'CI: Tests Unit' on: push: branches: [main, master, dev*, core/*, desktop/*] - paths-ignore: ['**/*.md'] pull_request: branches-ignore: [wip/*, draft/*, temp/*] - paths-ignore: ['**/*.md'] merge_group: concurrency: @@ -15,7 +13,20 @@ concurrency: cancel-in-progress: true jobs: + changes: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + should-run: ${{ steps.changes.outputs.should-run }} + steps: + - uses: actions/checkout@v6 + - id: changes + uses: ./.github/actions/changes-filter + test: + needs: changes + if: ${{ needs.changes.outputs.should-run == 'true' }} runs-on: ubuntu-latest steps: diff --git a/.github/workflows/ci-vercel-website-preview.yaml b/.github/workflows/ci-vercel-website-preview.yaml index 91784a5087..7e298abb81 100644 --- a/.github/workflows/ci-vercel-website-preview.yaml +++ b/.github/workflows/ci-vercel-website-preview.yaml @@ -4,6 +4,9 @@ name: 'CI: Vercel Website Preview' on: pull_request: types: [opened, synchronize, reopened] + branches-ignore: + - 'core/**' + - 'cloud/**' paths: - 'apps/website/**' - 'packages/design-system/**' @@ -18,6 +21,12 @@ on: env: VERCEL_ORG_ID: ${{ secrets.VERCEL_WEBSITE_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_WEBSITE_PROJECT_ID }} + VERCEL_TOKEN: ${{ secrets.VERCEL_WEBSITE_TOKEN }} + VERCEL_SCOPE: comfyui + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: deploy-preview: @@ -25,6 +34,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + env: + ALIAS_HOST: comfy-website-preview-pr-${{ github.event.pull_request.number }}.vercel.app steps: - name: Checkout repository uses: actions/checkout@v6 @@ -32,28 +43,87 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version-file: '.nvmrc' + - name: Install Vercel CLI run: npm install --global vercel@latest - name: Pull Vercel environment information - run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_WEBSITE_TOKEN }} + run: vercel pull --yes --environment=preview - name: Build project artifacts - run: vercel build --token=${{ secrets.VERCEL_WEBSITE_TOKEN }} + env: + WEBSITE_ASHBY_API_KEY: ${{ secrets.WEBSITE_ASHBY_API_KEY }} + WEBSITE_ASHBY_JOB_BOARD_NAME: ${{ secrets.WEBSITE_ASHBY_JOB_BOARD_NAME }} + WEBSITE_CLOUD_API_KEY: ${{ secrets.WEBSITE_CLOUD_API_KEY }} + run: vercel build + + - name: Fetch head commit metadata + id: head-commit + uses: actions/github-script@v8 + with: + script: | + const { data } = await github.rest.repos.getCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: context.payload.pull_request.head.sha, + }) + const author = data.author?.login || data.commit.author?.name || '' + const message = (data.commit.message || '').split('\n', 1)[0] + core.setOutput('author', author) + core.setOutput('message', message) - name: Deploy project artifacts to Vercel id: deploy + env: + GIT_COMMIT_REF: ${{ github.event.pull_request.head.ref }} + GIT_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} + GIT_AUTHOR_LOGIN: ${{ steps.head-commit.outputs.author }} + GIT_COMMIT_MESSAGE: ${{ steps.head-commit.outputs.message }} + GIT_PR_ID: ${{ github.event.pull_request.number }} + GIT_REPO: ${{ github.repository }} run: | - URL=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_WEBSITE_TOKEN }}) + URL=$(vercel deploy --prebuilt \ + --meta githubCommitRef="$GIT_COMMIT_REF" \ + --meta githubCommitSha="$GIT_COMMIT_SHA" \ + --meta githubCommitAuthorLogin="$GIT_AUTHOR_LOGIN" \ + --meta githubCommitMessage="$GIT_COMMIT_MESSAGE" \ + --meta githubPrId="$GIT_PR_ID" \ + --meta githubRepo="$GIT_REPO") echo "url=$URL" >> "$GITHUB_OUTPUT" - - name: Add deployment URL to summary - run: echo "**Preview:** ${{ steps.deploy.outputs.url }}" >> "$GITHUB_STEP_SUMMARY" - - - name: Save PR metadata + - name: Alias deployment to stable PR hostname + id: alias-set + continue-on-error: true + env: + DEPLOY_URL: ${{ steps.deploy.outputs.url }} run: | + vercel alias set "$DEPLOY_URL" "$ALIAS_HOST" --scope="$VERCEL_SCOPE" + + - name: Publish preview outputs + env: + DEPLOY_URL: ${{ steps.deploy.outputs.url }} + ALIAS_OK: ${{ steps.alias-set.outcome == 'success' }} + run: | + if [[ "$ALIAS_OK" == "true" ]]; then + STABLE_URL="https://$ALIAS_HOST" + else + STABLE_URL="$DEPLOY_URL" + fi mkdir -p temp/vercel-preview - echo "${{ steps.deploy.outputs.url }}" > temp/vercel-preview/url.txt + echo "$DEPLOY_URL" > temp/vercel-preview/url.txt + echo "$STABLE_URL" > temp/vercel-preview/stable-url.txt + { + echo "**Preview:** $STABLE_URL" + if [[ "$ALIAS_OK" == "true" ]]; then + echo "**This commit:** $DEPLOY_URL" + else + echo "_Stable alias update failed — URL reflects this commit only._" + fi + } >> "$GITHUB_STEP_SUMMARY" - name: Upload preview metadata uses: actions/upload-artifact@v6 @@ -71,19 +141,37 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version-file: '.nvmrc' + - name: Install Vercel CLI run: npm install --global vercel@latest - name: Pull Vercel environment information - run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_WEBSITE_TOKEN }} + run: vercel pull --yes --environment=production + + - name: Verify WEBSITE_CLOUD_API_KEY is present for production build + env: + WEBSITE_CLOUD_API_KEY: ${{ secrets.WEBSITE_CLOUD_API_KEY }} + run: | + if [ -z "${WEBSITE_CLOUD_API_KEY:-}" ]; then + echo "::error title=Missing WEBSITE_CLOUD_API_KEY::Production builds require WEBSITE_CLOUD_API_KEY so /cloud/supported-nodes is generated from fresh Cloud API data. Add it as a GitHub Actions repo secret and to the Vercel project environment. See apps/website/README.md." + exit 1 + fi - name: Build project artifacts - run: vercel build --prod --token=${{ secrets.VERCEL_WEBSITE_TOKEN }} + env: + WEBSITE_ASHBY_API_KEY: ${{ secrets.WEBSITE_ASHBY_API_KEY }} + WEBSITE_ASHBY_JOB_BOARD_NAME: ${{ secrets.WEBSITE_ASHBY_JOB_BOARD_NAME }} + WEBSITE_CLOUD_API_KEY: ${{ secrets.WEBSITE_CLOUD_API_KEY }} + run: vercel build --prod - name: Deploy project artifacts to Vercel id: deploy run: | - URL=$(vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_WEBSITE_TOKEN }}) + URL=$(vercel deploy --prebuilt --prod) echo "url=$URL" >> "$GITHUB_OUTPUT" - name: Add deployment URL to summary diff --git a/.github/workflows/ci-website-build.yaml b/.github/workflows/ci-website-build.yaml index 832854c2eb..211ee86960 100644 --- a/.github/workflows/ci-website-build.yaml +++ b/.github/workflows/ci-website-build.yaml @@ -4,23 +4,29 @@ name: 'CI: Website Build' on: push: branches: [main, master, website/*] - paths: - - 'apps/website/**' - - 'packages/design-system/**' - - 'pnpm-lock.yaml' pull_request: branches-ignore: [wip/*, draft/*, temp/*] - paths: - - 'apps/website/**' - - 'packages/design-system/**' - - 'pnpm-lock.yaml' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: + changes: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + app-website-changes: ${{ steps.changes.outputs.app-website-changes }} + packages-changes: ${{ steps.changes.outputs.packages-changes }} + steps: + - uses: actions/checkout@v6 + - id: changes + uses: ./.github/actions/changes-filter + build: + needs: changes + if: ${{ needs.changes.outputs.app-website-changes == 'true' || needs.changes.outputs.packages-changes == 'true' }} runs-on: ubuntu-latest steps: @@ -30,4 +36,7 @@ jobs: uses: ./.github/actions/setup-frontend - name: Build website + env: + WEBSITE_ASHBY_API_KEY: ${{ secrets.WEBSITE_ASHBY_API_KEY }} + WEBSITE_ASHBY_JOB_BOARD_NAME: ${{ secrets.WEBSITE_ASHBY_JOB_BOARD_NAME }} run: pnpm --filter @comfyorg/website build diff --git a/.github/workflows/ci-website-e2e.yaml b/.github/workflows/ci-website-e2e.yaml new file mode 100644 index 0000000000..ea8e7f0592 --- /dev/null +++ b/.github/workflows/ci-website-e2e.yaml @@ -0,0 +1,270 @@ +name: 'CI: Website E2E' + +on: + push: + branches: [main] + pull_request: + branches-ignore: [wip/*, draft/*, temp/*] + +concurrency: + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + changes: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + app-website-changes: ${{ steps.changes.outputs.app-website-changes }} + packages-changes: ${{ steps.changes.outputs.packages-changes }} + steps: + - uses: actions/checkout@v6 + - id: changes + uses: ./.github/actions/changes-filter + + website-e2e: + needs: changes + if: ${{ needs.changes.outputs.app-website-changes == 'true' || needs.changes.outputs.packages-changes == 'true' }} + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v1.58.1-noble + timeout-minutes: 15 + permissions: + contents: read + outputs: + test-outcome: ${{ steps.tests.outcome }} + report-url: ${{ steps.deploy.outputs.url }} + screenshot-failures: ${{ steps.failures.outputs.screenshot }} + other-failures: ${{ steps.failures.outputs.other }} + # Evaluated at job level (not from a step) — static expression. + is-pr: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }} + steps: + - uses: actions/checkout@v6 + + - name: Install pnpm + run: corepack enable && corepack prepare + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build website + env: + WEBSITE_GITHUB_STARS_OVERRIDE: 110000 + run: pnpm --filter @comfyorg/website build + + - name: Run Playwright tests + id: tests + run: pnpm --filter @comfyorg/website test:e2e + + - name: Upload test report + uses: actions/upload-artifact@v6 + if: ${{ !cancelled() }} + with: + name: website-playwright-report + path: apps/website/playwright-report/ + retention-days: 30 + + - name: Deploy report to Cloudflare + id: deploy + if: always() && !cancelled() + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + HEAD_REF: ${{ github.head_ref || github.ref_name }} + run: | + BRANCH=$(echo "$HEAD_REF" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g;s/--*/-/g;s/^-\|-$//g') + DEPLOY_OK=false + for i in 1 2 3; do + echo "Deployment attempt $i of 3..." + OUTPUT=$(npx wrangler@^4.0.0 pages deploy apps/website/playwright-report \ + --project-name=comfyui-website-e2e \ + --branch="$BRANCH" 2>&1) && { DEPLOY_OK=true; break; } || echo "$OUTPUT" + [ $i -lt 3 ] && sleep 10 + done + echo "$OUTPUT" + if [ "$DEPLOY_OK" != "true" ]; then + echo "::error::All 3 deployment attempts failed" + exit 1 + fi + URL=$(echo "$OUTPUT" | grep -oE 'https://[a-zA-Z0-9.-]+\.pages\.dev\S*' | head -1) + echo "url=${URL}" >> $GITHUB_OUTPUT + + - name: Categorize failures + id: failures + if: always() && !cancelled() && steps.tests.outcome != 'success' + uses: actions/github-script@v8 + with: + script: | + const fs = require('fs') + const report = JSON.parse(fs.readFileSync('apps/website/results.json', 'utf8')) + + function isFailed(t) { return t.status === 'unexpected' || t.status === 'flaky' } + function isVisual(spec) { + return spec.file?.includes('visual') || + spec.tests?.some(t => t.results?.some(r => r.error?.message?.includes('toHaveScreenshot'))) + } + function specsOf(suite) { + return [ + ...(suite.specs || []), + ...(suite.suites || []).flatMap(specsOf) + ] + } + + // True: Visual + // False: Other + const failed = specsOf(report) + .flatMap(spec => (spec.tests || []) + .filter(isFailed) + .map(() => isVisual(spec))) + + const screenshotFailures = failed.filter(Boolean).length + core.setOutput('screenshot', screenshotFailures) + core.setOutput('other', failed.length - screenshotFailures) + + - name: Write job summary + if: always() && !cancelled() + uses: actions/github-script@v8 + env: + TEST_OUTCOME: ${{ steps.tests.outcome }} + REPORT_URL: ${{ steps.deploy.outputs.url }} + SCREENSHOT_FAILURES: ${{ steps.failures.outputs.screenshot }} + OTHER_FAILURES: ${{ steps.failures.outputs.other }} + with: + script: | + const passed = process.env.TEST_OUTCOME === 'success' + const reportUrl = process.env.REPORT_URL + const screenshotFailures = parseInt(process.env.SCREENSHOT_FAILURES) || 0 + const otherFailures = parseInt(process.env.OTHER_FAILURES) || 0 + + const lines = ['## 🌐 Website E2E', ''] + + if (passed) { + lines.push('> [!TIP]', '> All tests passed.') + } else { + lines.push('> [!CAUTION]', '> Some tests failed.') + } + + const rows = [ + ['Status', passed ? '✅ Passed' : '❌ Failed'], + ['Report', reportUrl ? `[View Report](${reportUrl})` : '_unavailable_'] + ] + if (!passed) { + rows.push( + ['Screenshot diffs', String(screenshotFailures)], + ['Other failures', String(otherFailures)] + ) + } + lines.push( + '', + '| | |', + '|---|---|', + ...rows.map(([k, v]) => `| **${k}** | ${v} |`) + ) + + await core.summary.addRaw(lines.join('\n')).write() + + post-starting-comment: + # Safe to comment from pull_request trigger: fork PRs are excluded by the guard below. + # This avoids a ci-*/pr-* workflow_run split for a comment that must appear immediately. + needs: changes + if: | + github.event_name == 'pull_request' + && github.event.pull_request.head.repo.fork == false + && (needs.changes.outputs.app-website-changes == 'true' || needs.changes.outputs.packages-changes == 'true') + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + concurrency: + group: website-pr-comment-${{ github.event.pull_request.number }} + cancel-in-progress: false + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/upsert-comment-section + with: + pr-number: ${{ github.event.pull_request.number }} + section-name: e2e + comment-marker: '' + token: ${{ secrets.GITHUB_TOKEN }} + section-content: |- + ## 🌐 Website E2E + + + > [!NOTE] + > Tests are running… [View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + post-result-comment: + needs: website-e2e + if: always() && !cancelled() && needs.website-e2e.outputs.is-pr == 'true' + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + concurrency: + group: website-pr-comment-${{ github.event.pull_request.number }} + cancel-in-progress: false + steps: + - uses: actions/checkout@v6 + + - name: Build e2e section content + id: content + uses: actions/github-script@v8 + env: + TEST_OUTCOME: ${{ needs.website-e2e.outputs.test-outcome }} + REPORT_URL: ${{ needs.website-e2e.outputs.report-url }} + SCREENSHOT_FAILURES: ${{ needs.website-e2e.outputs.screenshot-failures }} + OTHER_FAILURES: ${{ needs.website-e2e.outputs.other-failures }} + with: + script: | + const passed = process.env.TEST_OUTCOME === 'success' + const reportUrl = process.env.REPORT_URL + const screenshotFailures = parseInt(process.env.SCREENSHOT_FAILURES) || 0 + const otherFailures = parseInt(process.env.OTHER_FAILURES) || 0 + + const lines = ['## 🌐 Website E2E', '', ''] + + if (passed) { + lines.push('> [!TIP]', '> All tests passed.') + } else { + lines.push('> [!CAUTION]', '> Some tests failed.') + } + + const rows = [ + ['Status', passed ? '✅ Passed' : '❌ Failed'], + ['Report', reportUrl ? `[View Report](${reportUrl})` : '_unavailable_'] + ] + if (!passed) { + rows.push( + ['Screenshot diffs', String(screenshotFailures)], + ['Other failures', String(otherFailures)] + ) + } + lines.push( + '', + '| | |', + '|---|---|', + ...rows.map(([k, v]) => `| **${k}** | ${v} |`) + ) + + if (screenshotFailures > 0) { + const s = screenshotFailures === 1 ? '' : 's' + lines.push('', `- [ ] Update website screenshots (${screenshotFailures} screenshot diff${s})`) + } + if (otherFailures > 0) { + lines.push( + '', + '> [!WARNING]', + `> ${otherFailures} non-screenshot failure${otherFailures === 1 ? '' : 's'} — these require manual review.` + ) + } + + core.setOutput('section-content', lines.join('\n')) + + - uses: ./.github/actions/upsert-comment-section + with: + pr-number: ${{ github.event.pull_request.number }} + section-name: e2e + comment-marker: '' + token: ${{ secrets.GITHUB_TOKEN }} + section-content: ${{ steps.content.outputs.section-content }} diff --git a/.github/workflows/model-page-discovery.yaml b/.github/workflows/model-page-discovery.yaml new file mode 100644 index 0000000000..457c7546aa --- /dev/null +++ b/.github/workflows/model-page-discovery.yaml @@ -0,0 +1,123 @@ +name: Model Page Discovery + +on: + schedule: + - cron: '0 9 * * 1' + workflow_dispatch: + +jobs: + discover: + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + + steps: + - name: Fetch model labels from hub API + id: hub + shell: bash + run: | + set -euo pipefail + curl -fsSL 'https://comfy.org/api/hub/labels?type=model' -o hub-labels.json + echo "Fetched $(jq '.labels | length' hub-labels.json) model labels from hub" + + - name: Checkout ComfyUI_frontend + uses: actions/checkout@v6 + with: + sparse-checkout: apps/website/src/config/generated-models.json + + - name: Compare against existing models + id: compare + shell: bash + run: | + set -euo pipefail + + HUB_SLUGS=$(jq -r '[.labels[].name]' hub-labels.json) + + EXISTING_SLUGS=$(node -e " + const fs = require('fs'); + const models = JSON.parse( + fs.readFileSync( + 'apps/website/src/config/generated-models.json', + 'utf8' + ) + ); + console.log(JSON.stringify(models.map(m => m.slug))); + " 2>/dev/null || echo '[]') + + ADDED_SLUGS=$(node -e " + const hub = $HUB_SLUGS; + const existing = new Set($EXISTING_SLUGS); + console.log(JSON.stringify(hub.filter(s => !existing.has(s)))); + ") + + COUNT=$(node -e "console.log($ADDED_SLUGS.length)") + echo "new_count=$COUNT" >> \$GITHUB_OUTPUT + echo "new_slugs=$ADDED_SLUGS" >> \$GITHUB_OUTPUT + + if [ "\$COUNT" -eq 0 ]; then + echo "No new models found." + else + echo "Found \$COUNT new model(s)" + fi + + - name: Check for existing open discovery issue + id: existing_issue + if: steps.compare.outputs.new_count != '0' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + COUNT=$(gh issue list \ + --repo "$GITHUB_REPOSITORY" \ + --state open \ + --search 'in:title "New models detected"' \ + --json number \ + --jq 'length') + echo "open_count=$COUNT" >> $GITHUB_OUTPUT + + - name: Open GitHub issue for new models + if: | + steps.compare.outputs.new_count != '0' && + steps.existing_issue.outputs.open_count == '0' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NEW_SLUGS: ${{ steps.compare.outputs.new_slugs }} + NEW_COUNT: ${{ steps.compare.outputs.new_count }} + shell: bash + run: | + SLUG_LIST=$(node -e " + const slugs = $NEW_SLUGS; + console.log(slugs.map(s => '- \`' + s + '\`').join('\n')); + ") + + gh issue create \ + --repo "$GITHUB_REPOSITORY" \ + --title "New models detected — add to model pages" \ + --body "## $NEW_COUNT new model(s) found in hub + + The weekly model discovery scan found model labels on the hub not yet in + \`apps/website/src/config/generated-models.json\`. + + ### New slugs ($NEW_COUNT) + + $SLUG_LIST + + ### Next steps + + 1. Review which of these warrant an SEO model page + 2. For local models: run \`SKIP_THUMBNAILS=1 pnpm generate:models\` and commit the result + 3. For partner/API models: add to \`API_PROVIDER_MAP\` in \`generate-models.ts\`, regenerate, commit + + --- + *Generated by the [model-page-discovery workflow](https://github.com/$GITHUB_REPOSITORY/actions/workflows/model-page-discovery.yaml)*" + + - name: Skip — open issue already exists + if: | + steps.compare.outputs.new_count != '0' && + steps.existing_issue.outputs.open_count != '0' + run: echo "An open discovery issue already exists — skipping creation." + + - name: No new models found + if: steps.compare.outputs.new_count == '0' + run: echo "No new models found — nothing to do." diff --git a/.github/workflows/pr-report.yaml b/.github/workflows/pr-report.yaml index 839c61d7b8..d5c9dbd86f 100644 --- a/.github/workflows/pr-report.yaml +++ b/.github/workflows/pr-report.yaml @@ -30,42 +30,7 @@ jobs: - name: Resolve PR from workflow_run context id: pr-meta - uses: actions/github-script@v8 - with: - script: | - let pr = context.payload.workflow_run.pull_requests?.[0]; - if (!pr) { - const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ - owner: context.repo.owner, - repo: context.repo.repo, - commit_sha: context.payload.workflow_run.head_sha, - }); - pr = prs.find(p => p.state === 'open'); - } - - if (!pr) { - core.info('No open PR found for this workflow run — skipping.'); - core.setOutput('skip', 'true'); - return; - } - - // Verify the workflow_run head SHA matches the current PR head - const { data: livePr } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: pr.number, - }); - - if (livePr.head.sha !== context.payload.workflow_run.head_sha) { - core.info(`Stale run: workflow SHA ${context.payload.workflow_run.head_sha} != PR head ${livePr.head.sha}`); - core.setOutput('skip', 'true'); - return; - } - - core.setOutput('skip', 'false'); - core.setOutput('number', String(pr.number)); - core.setOutput('base', livePr.base.ref); - core.setOutput('head-sha', livePr.head.sha); + uses: ./.github/actions/resolve-pr-from-workflow-run - name: Find size workflow run if: steps.pr-meta.outputs.skip != 'true' diff --git a/.github/workflows/pr-update-playwright-expectations.yaml b/.github/workflows/pr-update-playwright-expectations.yaml index 308c54b74f..3291d45767 100644 --- a/.github/workflows/pr-update-playwright-expectations.yaml +++ b/.github/workflows/pr-update-playwright-expectations.yaml @@ -77,7 +77,7 @@ jobs: needs: setup runs-on: ubuntu-latest container: - image: ghcr.io/comfy-org/comfyui-ci-container:0.0.16 + image: ghcr.io/comfy-org/comfyui-ci-container:0.0.17 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr-update-website-screenshots.yaml b/.github/workflows/pr-update-website-screenshots.yaml new file mode 100644 index 0000000000..9e8d58e564 --- /dev/null +++ b/.github/workflows/pr-update-website-screenshots.yaml @@ -0,0 +1,245 @@ +name: 'PR: Update Website Screenshots' + +on: + pull_request: + types: [labeled] + issue_comment: + types: [created, edited] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.ref }} + cancel-in-progress: false + +jobs: + update-screenshots: + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v1.58.1-noble + timeout-minutes: 15 + permissions: + contents: write + issues: write + pull-requests: read + # Trigger: (1) label, (2) /slash-command, or (3) checkbox in E2E status comment + # ⚠️ This condition is duplicated on `post-starting-comment` — keep them in sync. + if: > + ( github.event_name == 'pull_request' && + github.event.label.name == 'Update Website Screenshots' ) || + ( github.event.issue.pull_request && + github.event_name == 'issue_comment' && + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' + ) && + startsWith(github.event.comment.body, '/update-website-screenshots') ) || + ( github.event.issue.pull_request && + github.event_name == 'issue_comment' && + github.event.comment.user.login == 'github-actions[bot]' && + github.actor != 'github-actions[bot]' && + contains(github.event.comment.body, '') && + contains(github.event.comment.body, '- [x] Update website screenshots') ) + outputs: + pr-number: ${{ steps.pr-info.outputs.pr-number }} + update-outcome: ${{ steps.update-screenshots.outcome }} + has-changes: ${{ steps.commit.outputs.has-changes }} + changed-count: ${{ steps.commit.outputs.changed-count }} + steps: + - name: Verify sender permissions + if: > + github.event_name == 'issue_comment' && + contains(github.event.comment.body, '') + uses: actions/github-script@v8 + with: + script: | + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + ...context.repo, + username: context.actor + }) + if (!['admin', 'write'].includes(data.permission)) { + core.setFailed(`User ${context.actor} does not have write access`) + } + + - name: Get PR info + id: pr-info + uses: actions/github-script@v8 + env: + PR_NUMBER: ${{ github.event.number || github.event.issue.number }} + with: + script: | + const prNumber = Number(process.env.PR_NUMBER) + const { data: pr } = await github.rest.pulls.get({ + ...context.repo, + pull_number: prNumber + }) + core.setOutput('pr-number', prNumber) + core.setOutput('branch', pr.head.ref) + + - uses: actions/checkout@v6 + with: + ref: ${{ steps.pr-info.outputs.branch }} + token: ${{ secrets.PR_GH_TOKEN }} + + - name: Install pnpm + run: corepack enable && corepack prepare + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build website + env: + WEBSITE_GITHUB_STARS_OVERRIDE: 110000 + run: pnpm --filter @comfyorg/website build + + - name: Update screenshots + id: update-screenshots + run: pnpm --filter @comfyorg/website test:visual:update + continue-on-error: true + + - name: Commit updated screenshots + id: commit + if: steps.update-screenshots.outcome == 'success' + run: | + git config --global --add safe.directory "$(pwd)" + git config --global user.name 'github-actions' + git config --global user.email 'github-actions@github.com' + + CHANGED=$(git status --porcelain=v1 --untracked-files=all -- apps/website/e2e/ | wc -l) + echo "changed-count=${CHANGED}" >> $GITHUB_OUTPUT + + if [ "$CHANGED" -eq 0 ]; then + echo "No screenshot changes to commit" + echo "has-changes=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "has-changes=true" >> $GITHUB_OUTPUT + git add apps/website/e2e/ + git commit -m "[automated] Update website screenshot expectations" + git push origin ${{ steps.pr-info.outputs.branch }} + + - name: Upload test report + uses: actions/upload-artifact@v6 + if: ${{ !cancelled() }} + with: + name: website-screenshot-update-report + path: apps/website/playwright-report/ + retention-days: 14 + + - name: Remove label + if: always() && github.event_name == 'pull_request' + uses: actions/github-script@v8 + env: + PR_NUMBER: ${{ steps.pr-info.outputs.pr-number }} + with: + script: | + try { + await github.rest.issues.removeLabel({ + ...context.repo, + issue_number: Number(process.env.PR_NUMBER), + name: 'Update Website Screenshots' + }) + } catch (e) { + if (e.status !== 404) { + throw e + } + core.info('Label "Update Website Screenshots" was already removed') + } + + post-starting-comment: + # Runs in parallel with update-screenshots to show "in progress" immediately. + # ⚠️ This condition is duplicated from `update-screenshots` — keep them in sync. + if: > + ( github.event_name == 'pull_request' && + github.event.label.name == 'Update Website Screenshots' ) || + ( github.event.issue.pull_request && + github.event_name == 'issue_comment' && + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' + ) && + startsWith(github.event.comment.body, '/update-website-screenshots') ) || + ( github.event.issue.pull_request && + github.event_name == 'issue_comment' && + github.event.comment.user.login == 'github-actions[bot]' && + github.actor != 'github-actions[bot]' && + contains(github.event.comment.body, '') && + contains(github.event.comment.body, '- [x] Update website screenshots') ) + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + concurrency: + group: website-pr-comment-${{ github.event.number || github.event.issue.number }} + cancel-in-progress: false + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/upsert-comment-section + with: + pr-number: ${{ github.event.number || github.event.issue.number }} + section-name: screenshot-update + comment-marker: '' + token: ${{ secrets.GITHUB_TOKEN }} + section-content: |- + ## 📸 Screenshot Update + + > [!NOTE] + > Updating screenshots… [View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + post-result-comment: + needs: update-screenshots + if: always() && !cancelled() && needs.update-screenshots.result != 'skipped' + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + concurrency: + group: website-pr-comment-${{ needs.update-screenshots.outputs.pr-number }} + cancel-in-progress: false + steps: + - uses: actions/checkout@v6 + + - name: Build screenshot-update section content + id: content + uses: actions/github-script@v8 + env: + UPDATE_OUTCOME: ${{ needs.update-screenshots.outputs.update-outcome }} + HAS_CHANGES: ${{ needs.update-screenshots.outputs.has-changes }} + CHANGED_COUNT: ${{ needs.update-screenshots.outputs.changed-count }} + with: + script: | + const outcome = process.env.UPDATE_OUTCOME + const hasChanges = process.env.HAS_CHANGES === 'true' + const changedCount = parseInt(process.env.CHANGED_COUNT) || 0 + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + + const lines = ['## 📸 Screenshot Update', ''] + + if (outcome !== 'success') { + lines.push( + '> [!CAUTION]', + `> Screenshot update failed. [View workflow run](${runUrl})` + ) + } else if (!hasChanges) { + lines.push( + '> [!TIP]', + '> All screenshots are already up to date.' + ) + } else { + const s = changedCount === 1 ? '' : 's' + lines.push( + '> [!TIP]', + `> Updated ${changedCount} screenshot${s} and pushed to the branch.` + ) + } + + core.setOutput('section-content', lines.join('\n')) + + - uses: ./.github/actions/upsert-comment-section + with: + pr-number: ${{ needs.update-screenshots.outputs.pr-number }} + section-name: screenshot-update + comment-marker: '' + token: ${{ secrets.GITHUB_TOKEN }} + section-content: ${{ steps.content.outputs.section-content }} diff --git a/.github/workflows/pr-vercel-website-preview.yaml b/.github/workflows/pr-vercel-website-preview.yaml index 480ce696ee..1f718f5047 100644 --- a/.github/workflows/pr-vercel-website-preview.yaml +++ b/.github/workflows/pr-vercel-website-preview.yaml @@ -7,14 +7,23 @@ on: types: - completed -permissions: - contents: read - pull-requests: write - actions: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: true jobs: comment: runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + actions: read + # Uses head_branch as proxy for PR number (unavailable at job-level in workflow_run). + # Preview and E2E comment writes are NOT mutually serialized — the race window is + # small and self-healing on next push. + concurrency: + group: website-pr-comment-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: false if: > github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && @@ -31,44 +40,31 @@ jobs: - name: Resolve PR number from workflow_run context id: pr-meta - uses: actions/github-script@v8 - with: - script: | - let pr = context.payload.workflow_run.pull_requests?.[0]; - if (!pr) { - const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ - owner: context.repo.owner, - repo: context.repo.repo, - commit_sha: context.payload.workflow_run.head_sha, - }); - pr = prs.find(p => p.state === 'open'); - } + uses: ./.github/actions/resolve-pr-from-workflow-run - if (!pr) { - core.info('No open PR found for this workflow run — skipping.'); - core.setOutput('skip', 'true'); - return; - } - - core.setOutput('skip', 'false'); - core.setOutput('number', String(pr.number)); - - - name: Read preview URL + - name: Read preview URLs if: steps.pr-meta.outputs.skip != 'true' - id: meta + id: urls run: | - echo "url=$(cat temp/vercel-preview/url.txt)" >> "$GITHUB_OUTPUT" + echo "stable-url=$(cat temp/vercel-preview/stable-url.txt)" >> "$GITHUB_OUTPUT" + echo "unique-url=$(cat temp/vercel-preview/url.txt)" >> "$GITHUB_OUTPUT" + echo "short-sha=${HEAD_SHA:0:7}" >> "$GITHUB_OUTPUT" + env: + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} - - name: Write report + - name: Post preview comment if: steps.pr-meta.outputs.skip != 'true' - run: | - echo "**Website Preview:** ${{ steps.meta.outputs.url }}" > preview-report.md - - - name: Post PR comment - if: steps.pr-meta.outputs.skip != 'true' - uses: ./.github/actions/post-pr-report-comment + uses: ./.github/actions/upsert-comment-section with: pr-number: ${{ steps.pr-meta.outputs.number }} - report-file: ./preview-report.md - comment-marker: '' + section-name: preview + comment-marker: '' token: ${{ secrets.GITHUB_TOKEN }} + section-content: |- + ## 🔗 Website Preview + + **Website Preview:** ${{ steps.urls.outputs.stable-url }} + + This commit: ${{ steps.urls.outputs.unique-url }} + + Last updated: ${{ github.event.workflow_run.updated_at }} for `${{ steps.urls.outputs.short-sha }}` diff --git a/.github/workflows/release-website.yaml b/.github/workflows/release-website.yaml new file mode 100644 index 0000000000..2814d75315 --- /dev/null +++ b/.github/workflows/release-website.yaml @@ -0,0 +1,70 @@ +# Description: Manual workflow to refresh the apps/website Ashby roles and +# cloud nodes snapshots and open a PR. Merging the PR triggers the existing +# Vercel website production deploy via ci-vercel-website-preview.yaml. +name: 'Release: Website' + +on: + workflow_dispatch: + +concurrency: + group: release-website + cancel-in-progress: true + +jobs: + refresh-snapshots: + if: github.repository == 'Comfy-Org/ComfyUI_frontend' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + ref: main + persist-credentials: false + + - name: Refresh Ashby snapshot + uses: ./.github/actions/ashby-pull + with: + api_key: ${{ secrets.WEBSITE_ASHBY_API_KEY }} + job_board_name: ${{ secrets.WEBSITE_ASHBY_JOB_BOARD_NAME }} + + - name: Refresh cloud nodes snapshot + uses: ./.github/actions/cloud-nodes-pull + with: + api_key: ${{ secrets.WEBSITE_CLOUD_API_KEY }} + + - name: Create Pull Request + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + with: + token: ${{ secrets.PR_GH_TOKEN }} + commit-message: 'chore(website): refresh Ashby and cloud nodes snapshots' + title: 'chore(website): refresh Ashby and cloud nodes snapshots' + body: | + Automated refresh of remote-data snapshots used by the website + build: + + - `apps/website/src/data/ashby-roles.snapshot.json` — Ashby job + board API + - `apps/website/src/data/cloud-nodes.snapshot.json` — Comfy Cloud + `/api/object_info` + + **Flow:** + 1. `Release: Website` workflow ran (manual trigger). + 2. This PR opens with the regenerated snapshots. + 3. `CI: Vercel Website Preview` deploys a preview for review. + 4. Merging to `main` triggers the production Vercel deploy. + + The snapshot fallback in `apps/website/src/utils/ashby.ts` and + `apps/website/src/utils/cloudNodes.ts` remains intact: builds + without the respective API keys continue to use the committed + snapshot (with a warning annotation in CI). + + Triggered by workflow run `${{ github.run_id }}`. + branch: chore/refresh-website-snapshots-${{ github.run_id }} + base: main + labels: | + Release:Website + delete-branch: true diff --git a/.husky/pre-push b/.husky/pre-push index dcb5564518..642dd8ac88 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,5 +1,13 @@ #!/usr/bin/env bash +# Skip in CI: the canonical knip check runs in ci-lint-format on every +# PR, and bot workflows (e.g. i18n-update-core) populate ComfyUI/ via +# setup-comfyui-server, which contaminates knip's project glob with the +# devtools copy under custom_nodes and produces false-positive failures. +if [ -n "${CI-}" ]; then + exit 0 +fi + # Run Knip with cache via package script pnpm knip 1>&2 diff --git a/.oxlintrc.json b/.oxlintrc.json index 19700f80c2..66142ab5ca 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -85,6 +85,15 @@ "typescript/no-unused-vars": "off", "unicorn/no-empty-file": "off", "vitest/require-mock-type-parameters": "off", + "vitest/consistent-each-for": [ + "error", + { + "test": "for", + "it": "for", + "describe": "for", + "suite": "for" + } + ], "unicorn/no-new-array": "off", "unicorn/no-single-promise-in-promise-methods": "off", "unicorn/no-useless-fallback-in-spread": "off", diff --git a/.stylelintrc.json b/.stylelintrc.json index 4eaf9d1e23..4c1dc87c2d 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -67,6 +67,7 @@ "ignoreFiles": [ "node_modules/**", "dist/**", + "**/dist/**", "playwright-report/**", "public/**", "src/lib/litegraph/**" diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 9cbac42d76..0d2b45aacc 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,16 +1,12 @@ { "recommendations": [ "antfu.vite", - "austenc.tailwind-docs", "bradlc.vscode-tailwindcss", "davidanson.vscode-markdownlint", "dbaeumer.vscode-eslint", "donjayamanne.githistory", "eamodio.gitlens", - "github.vscode-github-actions", "github.vscode-pull-request-github", - "hbenl.vscode-test-explorer", - "kisstkondoros.vscode-codemetrics", "lokalise.i18n-ally", "ms-playwright.playwright", "oxc.oxc-vscode", diff --git a/AGENTS.md b/AGENTS.md index 9463e5357d..b16dd225ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,7 @@ This project uses **pnpm**. Always prefer scripts defined in `package.json` (e.g ## Build, Test, and Development Commands - `pnpm dev`: Start Vite dev server. +- `pnpm dev:cloud`: Dev server connected to cloud backend (testcloud.comfy.org) - `pnpm dev:electron`: Dev server with Electron API mocks - `pnpm build`: Type-check then production build to `dist/` - `pnpm preview`: Preview the production build locally @@ -311,7 +312,7 @@ When referencing Comfy-Org repos: - Instead use a semantic value from the `style.css` theme - e.g. `bg-node-component-surface` - NEVER use `:class="[]"` to merge class names - - Always use `import { cn } from '@/utils/tailwindUtil'` + - Always use `import { cn } from '@comfyorg/tailwind-utils'` - e.g. `
` - Use `cn()` inline in the template when feasible instead of creating a `computed` to hold the value - NEVER use `!important` or the `!` important prefix for tailwind classes diff --git a/apps/desktop-ui/package.json b/apps/desktop-ui/package.json index 689b9cb72f..bac277c19f 100644 --- a/apps/desktop-ui/package.json +++ b/apps/desktop-ui/package.json @@ -12,6 +12,7 @@ "dependencies": { "@comfyorg/comfyui-electron-types": "catalog:", "@comfyorg/shared-frontend-utils": "workspace:*", + "@comfyorg/tailwind-utils": "workspace:*", "@primevue/core": "catalog:", "@primevue/themes": "catalog:", "@vueuse/core": "catalog:", diff --git a/apps/desktop-ui/src/assets/css/style.css b/apps/desktop-ui/src/assets/css/style.css index 1ff895f064..af95d1cd53 100644 --- a/apps/desktop-ui/src/assets/css/style.css +++ b/apps/desktop-ui/src/assets/css/style.css @@ -20,15 +20,15 @@ } .p-button-danger { - background-color: var(--color-coral-red-600); + background-color: var(--color-coral-700); } .p-button-danger:hover { - background-color: var(--color-coral-red-500); + background-color: var(--color-coral-600); } .p-button-danger:active { - background-color: var(--color-coral-red-400); + background-color: var(--color-coral-500); } .task-div .p-card { diff --git a/apps/desktop-ui/src/components/bottomPanel/tabs/terminal/BaseTerminal.vue b/apps/desktop-ui/src/components/bottomPanel/tabs/terminal/BaseTerminal.vue index 4589aedc70..4983dc8917 100644 --- a/apps/desktop-ui/src/components/bottomPanel/tabs/terminal/BaseTerminal.vue +++ b/apps/desktop-ui/src/components/bottomPanel/tabs/terminal/BaseTerminal.vue @@ -32,7 +32,7 @@ import { useI18n } from 'vue-i18n' import { useTerminal } from '@/composables/bottomPanelTabs/useTerminal' import { electronAPI, isElectron } from '@/utils/envUtil' -import { cn } from '@/utils/tailwindUtil' +import { cn } from '@comfyorg/tailwind-utils' const { t } = useI18n() diff --git a/apps/desktop-ui/src/components/install/HardwareOption.vue b/apps/desktop-ui/src/components/install/HardwareOption.vue index 9acc9e79cf..1ac514f3e3 100644 --- a/apps/desktop-ui/src/components/install/HardwareOption.vue +++ b/apps/desktop-ui/src/components/install/HardwareOption.vue @@ -4,7 +4,7 @@ @@ -36,7 +36,7 @@ +
+``` + +The localized `/zh-CN/contact` page uses the same portal and script with form +ID `6885750c-02ef-4aa2-ba0d-213be9cccf93`. + +This keeps submission handling, validation, anti-spam updates, and field +configuration in HubSpot. The local implementation in +`src/components/contact/HubspotFormEmbed.vue` only loads the hosted script and +renders the documented embed container. + +## Scripts + +- `pnpm dev` — Astro dev server +- `pnpm build` — production build to `dist/` +- `pnpm typecheck` — `astro check` +- `pnpm test:unit` — Vitest unit tests +- `pnpm test:e2e` — Playwright E2E tests (requires `pnpm build` first) +- `pnpm ashby:refresh-snapshot` — refresh the committed careers snapshot +- `pnpm cloud-nodes:refresh-snapshot` — refresh the committed cloud nodes snapshot diff --git a/apps/website/astro.config.ts b/apps/website/astro.config.ts index ca3bdabcd3..4a10cf5bdd 100644 --- a/apps/website/astro.config.ts +++ b/apps/website/astro.config.ts @@ -3,16 +3,55 @@ import sitemap from '@astrojs/sitemap' import vue from '@astrojs/vue' import tailwindcss from '@tailwindcss/vite' +const LOCALES = ['en', 'zh-CN'] as const +const DEFAULT_LOCALE = 'en' +const PAYMENT_STATUSES = ['success', 'failed'] as const +const LOCALE_PREFIXES = LOCALES.map((locale) => + locale === DEFAULT_LOCALE ? '' : `/${locale}` +) +const SITEMAP_EXCLUDED_PATHNAMES = new Set( + LOCALE_PREFIXES.flatMap((prefix) => + PAYMENT_STATUSES.map((status) => `${prefix}/payment/${status}`) + ) +) + +function isExcludedFromSitemap(page: string): boolean { + const pathname = new URL(page).pathname.replace(/\/$/, '') + return SITEMAP_EXCLUDED_PATHNAMES.has(pathname) +} + export default defineConfig({ site: 'https://comfy.org', output: 'static', - integrations: [vue(), sitemap()], + prefetch: { prefetchAll: true }, + redirects: { + '/cloud/enterprise-case-studies/comfyui-at-architectural-scale-how-moment-factory-reimagined-3d-projection-mapping': + '/customers/moment-factory/', + '/cloud/enterprise-case-studies/how-series-entertainment-rebuilt-game-and-video-production-with-comfyui': + '/customers/series-entertainment/', + '/zh-CN/terms-of-service': '/terms-of-service' + }, + build: { + assets: '_website' + }, + devToolbar: { enabled: !process.env.NO_TOOLBAR }, + integrations: [ + vue(), + sitemap({ + filter: (page) => !isExcludedFromSitemap(page) + }) + ], vite: { - plugins: [tailwindcss()] + plugins: [tailwindcss()], + server: { + watch: { + ignored: ['**/playwright-report/**'] + } + } }, i18n: { - locales: ['en', 'zh-CN'], - defaultLocale: 'en', + locales: [...LOCALES], + defaultLocale: DEFAULT_LOCALE, routing: { prefixDefaultLocale: false } diff --git a/apps/website/e2e/assets/placeholder-1x1.webp b/apps/website/e2e/assets/placeholder-1x1.webp new file mode 100644 index 0000000000..fdc2745ef4 Binary files /dev/null and b/apps/website/e2e/assets/placeholder-1x1.webp differ diff --git a/apps/website/e2e/assets/placeholder.webm b/apps/website/e2e/assets/placeholder.webm new file mode 100644 index 0000000000..4ca8e16620 Binary files /dev/null and b/apps/website/e2e/assets/placeholder.webm differ diff --git a/apps/website/e2e/careers.spec.ts b/apps/website/e2e/careers.spec.ts new file mode 100644 index 0000000000..22263c1bfc --- /dev/null +++ b/apps/website/e2e/careers.spec.ts @@ -0,0 +1,81 @@ +import { expect } from '@playwright/test' + +import { test } from './fixtures/blockExternalMedia' + +test.describe('Careers page @smoke', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/careers') + }) + + test('has correct title', async ({ page }) => { + await expect(page).toHaveTitle('Careers — Comfy') + }) + + test('Roles section heading is visible', async ({ page }) => { + await expect( + page.getByRole('heading', { name: 'Roles', level: 2 }) + ).toBeVisible() + }) + + test('renders at least one role from the snapshot', async ({ page }) => { + const roles = page.getByTestId('careers-role-link') + await expect(roles.first()).toBeVisible() + expect(await roles.count()).toBeGreaterThan(0) + }) + + test('clicking a department button scrolls to and activates that section', async ({ + page + }) => { + const rolesSection = page.getByTestId('careers-roles') + await rolesSection.scrollIntoViewIfNeeded() + await expect(rolesSection).toBeVisible() + + const allCount = await page.getByTestId('careers-role-link').count() + + const engineeringButton = page.getByRole('button', { + name: 'ENGINEERING', + exact: true + }) + + // RolesSection is hydrated via `client:visible`. Once the button responds + // to a click by flipping aria-pressed, Vue is hydrated and the rest of + // the locator logic is in effect. + await expect(async () => { + await engineeringButton.click() + await expect(engineeringButton).toHaveAttribute('aria-pressed', 'true', { + timeout: 1_000 + }) + }).toPass({ timeout: 10_000 }) + + const engineeringSection = page.locator('#careers-dept-engineering') + await expect(engineeringSection).toBeInViewport() + + expect(await page.getByTestId('careers-role-link').count()).toBe(allCount) + }) +}) + +test.describe('Careers page role links', () => { + test('each role links to the Ashby job description page, not the application form', async ({ + page + }) => { + await page.goto('/careers') + const roles = page.getByTestId('careers-role-link') + const count = await roles.count() + for (let i = 0; i < count; i++) { + const href = await roles.nth(i).getAttribute('href') + expect(href).toMatch(/^https:\/\/jobs\.ashbyhq\.com\//) + expect(href).not.toMatch(/\/application\/?$/) + } + }) +}) + +test.describe('Careers page (zh-CN) @smoke', () => { + test('renders localized heading and roles', async ({ page }) => { + await page.goto('/zh-CN/careers') + await expect(page).toHaveTitle('招聘 — Comfy') + await expect( + page.getByRole('heading', { name: '职位', level: 2 }) + ).toBeVisible() + await expect(page.getByTestId('careers-role-link').first()).toBeVisible() + }) +}) diff --git a/apps/website/e2e/cloud-nodes.spec.ts b/apps/website/e2e/cloud-nodes.spec.ts new file mode 100644 index 0000000000..9f99bf453b --- /dev/null +++ b/apps/website/e2e/cloud-nodes.spec.ts @@ -0,0 +1,169 @@ +import { expect } from '@playwright/test' + +import { test } from './fixtures/blockExternalMedia' + +test.describe('Cloud nodes page @smoke', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/cloud/supported-nodes') + }) + + test('has correct title', async ({ page }) => { + await expect(page).toHaveTitle( + 'Custom-node packs on Comfy Cloud — supported by default' + ) + }) + + test('renders at least one pack card', async ({ page }) => { + const cards = page.getByTestId('cloud-node-pack-card') + await expect(cards.first()).toBeVisible() + expect(await cards.count()).toBeGreaterThan(0) + }) + + test('search input filters cards down', async ({ page }) => { + const cards = page.getByTestId('cloud-node-pack-card') + const initialCount = await cards.count() + + await page.getByTestId('cloud-nodes-search').fill('impact') + const filteredCards = page.getByTestId('cloud-node-pack-card') + await expect(filteredCards.first()).toBeVisible() + const filteredCount = await filteredCards.count() + + expect(filteredCount).toBeGreaterThan(0) + expect(filteredCount).toBeLessThanOrEqual(initialCount) + }) + + test('renders banner image or fallback element', async ({ page }) => { + const banners = page.getByTestId('cloud-node-pack-banner') + await expect(banners.first()).toBeVisible() + }) + + test('opens pack detail page from first card', async ({ page }) => { + const firstCard = page.getByTestId('cloud-node-pack-card').first() + await expect(firstCard).toBeVisible() + + await firstCard.locator('a').first().click() + + await expect(page).toHaveURL(/\/cloud\/supported-nodes\/[a-z0-9-]+$/) + await expect(page.getByTestId('cloud-node-pack-detail')).toBeVisible() + }) + + test('direct pack detail route renders node entries', async ({ page }) => { + await page.goto('/cloud/supported-nodes/comfyui-impact-pack') + await expect(page.getByTestId('cloud-node-pack-detail')).toBeVisible() + await expect( + page.getByTestId('cloud-node-pack-detail-node').first() + ).toBeVisible() + }) + + test('search with no matches shows empty state', async ({ page }) => { + await page + .getByTestId('cloud-nodes-search') + .fill('zzzz-this-pack-does-not-exist') + await expect(page.getByTestId('cloud-node-pack-card')).toHaveCount(0) + await expect(page.getByText('No matching packs')).toBeVisible() + }) + + test('clearing search restores the full list', async ({ page }) => { + const cards = page.getByTestId('cloud-node-pack-card') + const initialCount = await cards.count() + + await page.getByTestId('cloud-nodes-search').fill('impact') + await expect(cards.first()).toBeVisible() + + await page.getByTestId('cloud-nodes-search').fill('') + await expect(cards).toHaveCount(initialCount) + }) + + test('search matches against node display names, not just pack names', async ({ + page + }) => { + await page.getByTestId('cloud-nodes-search').fill('FaceDetailer') + await expect(page.getByTestId('cloud-node-pack-card')).toHaveCount(1) + await expect( + page.getByTestId('cloud-node-pack-card-link').first() + ).toContainText('Impact Pack') + }) + + test('switching sort to A → Z reorders cards alphabetically', async ({ + page + }) => { + await page.locator('#cloud-nodes-sort').selectOption('az') + const firstName = await page + .getByTestId('cloud-node-pack-card-link') + .first() + .textContent() + expect(firstName?.trim().toLowerCase().charAt(0)).toMatch(/^[a-c]/) + }) + + test('list grid carries a localized aria-label', async ({ page }) => { + await expect( + page.getByRole('list', { + name: 'Custom-node packs supported on Comfy Cloud' + }) + ).toBeVisible() + }) + + test('clicking the back link returns to the index from a detail page', async ({ + page + }) => { + await page.goto('/cloud/supported-nodes/comfyui-impact-pack') + await page.getByRole('link', { name: 'Back to all packs' }).click() + await expect(page).toHaveURL(/\/cloud\/supported-nodes\/?$/) + await expect(page.getByTestId('cloud-node-pack-card').first()).toBeVisible() + }) + + test('detail page renders publisher and external repo link', async ({ + page + }) => { + await page.goto('/cloud/supported-nodes/comfyui-impact-pack') + const repoLink = page.getByRole('link', { + name: /github\.com\/ltdrdata\/ComfyUI-Impact-Pack/ + }) + await expect(repoLink).toBeVisible() + await expect(repoLink).toHaveAttribute('rel', /noopener/) + }) + + test('unknown pack slug 404s', async ({ page }) => { + const response = await page.goto( + '/cloud/supported-nodes/this-pack-does-not-exist' + ) + expect(response?.status()).toBe(404) + }) + + test('JSON-LD ItemList is emitted on the index page', async ({ page }) => { + const jsonLd = page.locator('script[type="application/ld+json"]') + const ldBlocks = await jsonLd.allTextContents() + expect(ldBlocks.some((b) => b.includes('"@type":"ItemList"'))).toBeTruthy() + }) + + test('JSON-LD payload escapes <-sequences', async ({ page }) => { + const ldBlocks = await page + .locator('script[type="application/ld+json"]') + .allTextContents() + for (const block of ldBlocks) { + expect(block).not.toContain(' { + test('renders localized title and packs', async ({ page }) => { + await page.goto('/zh-CN/cloud/supported-nodes') + await expect(page).toHaveTitle('Comfy Cloud 自定义节点包合集——开箱即用') + await expect(page.getByTestId('cloud-node-pack-card').first()).toBeVisible() + await expect( + page.getByTestId('cloud-node-pack-banner').first() + ).toBeVisible() + }) + + test('opens pack detail page from first card', async ({ page }) => { + await page.goto('/zh-CN/cloud/supported-nodes') + const firstCard = page.getByTestId('cloud-node-pack-card').first() + await expect(firstCard).toBeVisible() + + await firstCard.locator('a').first().click() + + await expect(page).toHaveURL(/\/zh-CN\/cloud\/supported-nodes\/[a-z0-9-]+$/) + await expect(page.getByTestId('cloud-node-pack-detail')).toBeVisible() + }) +}) diff --git a/apps/website/e2e/cloud.spec.ts b/apps/website/e2e/cloud.spec.ts index 4393a45e38..b086bb09b1 100644 --- a/apps/website/e2e/cloud.spec.ts +++ b/apps/website/e2e/cloud.spec.ts @@ -1,4 +1,6 @@ -import { expect, test } from '@playwright/test' +import { expect } from '@playwright/test' + +import { test } from './fixtures/blockExternalMedia' test.describe('Cloud page @smoke', () => { test.beforeEach(async ({ page }) => { @@ -41,13 +43,11 @@ test.describe('Cloud page @smoke', () => { test('AIModelsSection heading and 5 model cards are visible', async ({ page }) => { - await expect( - page.getByRole('heading', { name: /leading AI models/i }) - ).toBeVisible() + const heading = page.getByRole('heading', { name: /leading AI models/i }) + await expect(heading).toBeVisible() - const grid = page.locator('.grid', { - has: page.getByText('Grok Imagine') - }) + const section = heading.locator('xpath=ancestor::section') + const grid = section.locator('.grid') const modelCards = grid.locator('a[href="https://comfy.org/workflows"]') await expect(modelCards).toHaveCount(5) }) @@ -100,38 +100,44 @@ test.describe('Cloud FAQ accordion @interaction', () => { await page.goto('/cloud') }) - test('all FAQs are expanded by default', async ({ page }) => { - await expect( - page.getByText(/Comfy Cloud is a version of ComfyUI/i) - ).toBeVisible() - }) - - test('clicking an expanded FAQ collapses it', async ({ page }) => { - const firstQuestion = page.getByRole('button', { - name: /What is Comfy Cloud/i - }) - await firstQuestion.scrollIntoViewIfNeeded() - await firstQuestion.click() - + test('all FAQs are collapsed by default', async ({ page }) => { await expect( page.getByText(/Comfy Cloud is a version of ComfyUI/i) ).toBeHidden() }) - test('clicking a collapsed FAQ expands it again', async ({ page }) => { + test('clicking a collapsed FAQ expands it', async ({ page }) => { const firstQuestion = page.getByRole('button', { name: /What is Comfy Cloud/i }) await firstQuestion.scrollIntoViewIfNeeded() - + // Gate: wait for Vue hydration to bind aria-expanded + await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false') await firstQuestion.click() - await expect( - page.getByText(/Comfy Cloud is a version of ComfyUI/i) - ).toBeHidden() - await firstQuestion.click() await expect( page.getByText(/Comfy Cloud is a version of ComfyUI/i) ).toBeVisible() }) + + test('clicking an expanded FAQ collapses it again', async ({ page }) => { + const firstQuestion = page.getByRole('button', { + name: /What is Comfy Cloud/i + }) + await firstQuestion.scrollIntoViewIfNeeded() + // Gate: wait for Vue hydration to bind aria-expanded + await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false') + + await firstQuestion.click() + await expect(firstQuestion).toHaveAttribute('aria-expanded', 'true') + await expect( + page.getByText(/Comfy Cloud is a version of ComfyUI/i) + ).toBeVisible() + + await firstQuestion.click() + await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false') + await expect( + page.getByText(/Comfy Cloud is a version of ComfyUI/i) + ).toBeHidden() + }) }) diff --git a/apps/website/e2e/content-section.spec.ts b/apps/website/e2e/content-section.spec.ts new file mode 100644 index 0000000000..6d1a6d9265 --- /dev/null +++ b/apps/website/e2e/content-section.spec.ts @@ -0,0 +1,61 @@ +import { expect } from '@playwright/test' + +import { test } from './fixtures/blockExternalMedia' + +const M4_PRO_14_INCH_VIEWPORT = { width: 2016, height: 1310 } +const LAST_SECTION_HASH = '#contact' + +test.describe( + 'ContentSection scroll-spy @smoke', + { + annotation: [ + { + type: 'issue', + description: + 'https://linear.app/comfyorg/issue/FE-604/bug-bottom-badge-not-activating-on-scroll-at-high-resolution-3024x1964' + }, + { + type: 'environment', + description: + '14" MacBook M4 Pro logical viewport reported in FE-604; /privacy-policy reproduces because of its short trailing sections' + } + ] + }, + () => { + test.use({ viewport: M4_PRO_14_INCH_VIEWPORT }) + + test('activates the last badge when user scrolls to the bottom', async ({ + page + }) => { + await page.goto('/privacy-policy') + + const sidebarNav = page.getByRole('navigation', { + name: 'Category filter' + }) + const badges = sidebarNav.getByRole('button') + const lastBadge = badges.last() + + await expect(badges.first()).toHaveAttribute('aria-pressed', 'true') + await expect(lastBadge).toHaveAttribute('aria-pressed', 'false') + + await page.evaluate(() => + window.scrollTo(0, document.documentElement.scrollHeight) + ) + + await expect(lastBadge).toHaveAttribute('aria-pressed', 'true') + }) + + test('activates the last badge when page mounts already at the bottom via trailing hash', async ({ + page + }) => { + await page.goto(`/privacy-policy${LAST_SECTION_HASH}`) + + const sidebarNav = page.getByRole('navigation', { + name: 'Category filter' + }) + const lastBadge = sidebarNav.getByRole('button').last() + + await expect(lastBadge).toHaveAttribute('aria-pressed', 'true') + }) + } +) diff --git a/apps/website/e2e/customers.spec.ts b/apps/website/e2e/customers.spec.ts new file mode 100644 index 0000000000..5d073473da --- /dev/null +++ b/apps/website/e2e/customers.spec.ts @@ -0,0 +1,33 @@ +import { expect } from '@playwright/test' + +import { test } from './fixtures/blockExternalMedia' + +test.describe('Customers @smoke', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/customers') + }) + + test('hero image declares intrinsic dimensions so layout reserves space before load', async ({ + page + }) => { + const heroImage = page.locator('img[alt="Comfy 3D logo"]') + await expect(heroImage).toBeVisible() + await expect(heroImage).toHaveAttribute('width', /^\d+$/) + await expect(heroImage).toHaveAttribute('height', /^\d+$/) + + // Regression guard: an unloaded without intrinsic dimensions + // collapses to ~0px, then jumps to its natural size on load and pushes + // the video below it. Reserved space must persist before bytes arrive. + const heightWhileUnloaded = await page.evaluate(() => { + const img = document.querySelector( + 'img[alt="Comfy 3D logo"]' + ) + if (!img) return null + img.removeAttribute('src') + return img.getBoundingClientRect().height + }) + + expect(heightWhileUnloaded).not.toBeNull() + expect(heightWhileUnloaded!).toBeGreaterThan(100) + }) +}) diff --git a/apps/website/e2e/demos.spec.ts b/apps/website/e2e/demos.spec.ts new file mode 100644 index 0000000000..bbf333e3e6 --- /dev/null +++ b/apps/website/e2e/demos.spec.ts @@ -0,0 +1,79 @@ +import { expect, test } from '@playwright/test' + +import { demos, getNextDemo } from '../src/config/demos' +import { t } from '../src/i18n/translations' + +const escapeRegExp = (value: string): string => + value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + +test.describe('Demo pages @smoke', () => { + for (const demo of demos) { + const nextDemo = getNextDemo(demo.slug) + + test(`/demos/${demo.slug} renders hero, embed, transcript, and next-demo nav`, async ({ + page + }) => { + await page.goto(`/demos/${demo.slug}`) + + const heading = page.getByRole('heading', { level: 1 }) + await expect(heading).toBeVisible() + await expect(heading).toContainText(t(demo.title, 'en')) + + const ogImage = page.locator('head meta[property="og:image"]') + await expect(ogImage).toHaveAttribute( + 'content', + new RegExp(`${escapeRegExp(demo.slug)}-og\\.png`) + ) + + const iframe = page.locator( + `iframe[title*="${t('demos.embed.label', 'en')}"]` + ) + await expect(iframe).toBeAttached() + await expect(iframe).toHaveAttribute( + 'src', + new RegExp(escapeRegExp(demo.arcadeId)) + ) + + await expect( + page.getByRole('button', { name: /demo transcript/i }) + ).toBeVisible() + + await expect( + page.getByText(t(nextDemo.title, 'en')).first() + ).toBeVisible() + const nextThumb = page.locator(`img[src="${nextDemo.thumbnail}"]`).first() + await expect(nextThumb).toBeAttached() + await expect(nextThumb).toBeVisible() + const naturalWidth = await nextThumb.evaluate( + (img) => (img as HTMLImageElement).naturalWidth + ) + expect(naturalWidth).toBeGreaterThan(1) + }) + + test(`/zh-CN/demos/${demo.slug} renders localized content`, async ({ + page + }) => { + await page.goto(`/zh-CN/demos/${demo.slug}`) + + await expect(page).toHaveURL(/\/zh-CN\/demos\//) + + const heading = page.getByRole('heading', { level: 1 }) + await expect(heading).toContainText(t(demo.title, 'zh-CN')) + await expect(heading).toContainText(/[\u4E00-\u9FFF]/) + + await expect( + page.getByText(t(nextDemo.title, 'zh-CN')).first() + ).toBeVisible() + }) + } + + test('demo library page renders', async ({ page }) => { + await page.goto('/demos') + await expect(page.getByText('Coming Soon')).toBeVisible() + }) + + test('non-existent demo returns 404', async ({ page }) => { + const response = await page.goto('/demos/nonexistent') + expect(response?.status()).toBe(404) + }) +}) diff --git a/apps/website/e2e/download.spec.ts b/apps/website/e2e/download.spec.ts index b6e77fc812..1e837ac950 100644 --- a/apps/website/e2e/download.spec.ts +++ b/apps/website/e2e/download.spec.ts @@ -1,4 +1,9 @@ -import { expect, test } from '@playwright/test' +import { devices, expect } from '@playwright/test' + +import { test } from './fixtures/blockExternalMedia' + +const WINDOWS_UA = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' test.describe('Download page @smoke', () => { test.beforeEach(async ({ page }) => { @@ -22,7 +27,11 @@ test.describe('Download page @smoke', () => { await expect(page.getByText(/The full ComfyUI engine/)).toBeVisible() }) - test('HeroSection has download and GitHub buttons', async ({ page }) => { + test('HeroSection has download and GitHub buttons', async ({ browser }) => { + const context = await browser.newContext({ userAgent: WINDOWS_UA }) + const page = await context.newPage() + await page.goto('/download') + const hero = page.locator('section', { has: page.getByRole('heading', { name: /Run on your hardware/i, @@ -37,8 +46,10 @@ test.describe('Download page @smoke', () => { await expect(githubBtn).toBeVisible() await expect(githubBtn).toHaveAttribute( 'href', - 'https://github.com/Comfy-Org/ComfyUI' + 'https://github.com/Comfy-Org/ComfyUI#installing' ) + + await context.close() }) test('ReasonSection heading and reasons are visible', async ({ page }) => { @@ -93,40 +104,46 @@ test.describe('FAQ accordion @interaction', () => { await page.goto('/download') }) - test('all FAQs are expanded by default', async ({ page }) => { + test('all FAQs are collapsed by default', async ({ page }) => { await expect( page.getByText(/A dedicated GPU is strongly recommended/i) - ).toBeVisible() - await expect(page.getByText(/ComfyUI is lightweight/i)).toBeVisible() + ).toBeHidden() + await expect(page.getByText(/ComfyUI is lightweight/i)).toBeHidden() }) - test('clicking an expanded FAQ collapses it', async ({ page }) => { + test('clicking a collapsed FAQ expands it', async ({ page }) => { const firstQuestion = page.getByRole('button', { name: /Do I need a GPU/i }) await firstQuestion.scrollIntoViewIfNeeded() + // Gate: wait for Vue hydration to bind aria-expanded + await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false') await firstQuestion.click() await expect( page.getByText(/A dedicated GPU is strongly recommended/i) - ).toBeHidden() + ).toBeVisible() }) - test('clicking a collapsed FAQ expands it again', async ({ page }) => { + test('clicking an expanded FAQ collapses it again', async ({ page }) => { const firstQuestion = page.getByRole('button', { name: /Do I need a GPU/i }) await firstQuestion.scrollIntoViewIfNeeded() + // Gate: wait for Vue hydration to bind aria-expanded + await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false') await firstQuestion.click() - await expect( - page.getByText(/A dedicated GPU is strongly recommended/i) - ).toBeHidden() - - await firstQuestion.click() + await expect(firstQuestion).toHaveAttribute('aria-expanded', 'true') await expect( page.getByText(/A dedicated GPU is strongly recommended/i) ).toBeVisible() + + await firstQuestion.click() + await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false') + await expect( + page.getByText(/A dedicated GPU is strongly recommended/i) + ).toBeHidden() }) }) @@ -145,7 +162,14 @@ test.describe('Download page mobile @mobile', () => { ).toBeVisible() }) - test('download buttons are stacked vertically', async ({ page }) => { + test('download buttons are stacked vertically', async ({ browser }) => { + const context = await browser.newContext({ + ...devices['Pixel 5'], + userAgent: WINDOWS_UA + }) + const page = await context.newPage() + await page.goto('/download') + const hero = page.locator('section', { has: page.getByRole('heading', { name: /Run on your hardware/i, @@ -155,13 +179,18 @@ test.describe('Download page mobile @mobile', () => { const downloadBtn = hero.getByRole('link', { name: /DOWNLOAD LOCAL/i }) const githubBtn = hero.getByRole('link', { name: /INSTALL FROM GITHUB/i }) - await downloadBtn.scrollIntoViewIfNeeded() + await expect(downloadBtn).toBeVisible() + await expect(githubBtn).toBeVisible() - const downloadBox = await downloadBtn.boundingBox() - const githubBox = await githubBtn.boundingBox() + await expect + .poll(async () => { + const downloadBox = await downloadBtn.boundingBox() + const githubBox = await githubBtn.boundingBox() + if (!downloadBox || !githubBox) return false + return githubBox.y > downloadBox.y + }) + .toBe(true) - expect(downloadBox, 'download button bounding box').not.toBeNull() - expect(githubBox, 'github button bounding box').not.toBeNull() - expect(githubBox!.y).toBeGreaterThan(downloadBox!.y) + await context.close() }) }) diff --git a/apps/website/e2e/fixtures/blockExternalMedia.ts b/apps/website/e2e/fixtures/blockExternalMedia.ts new file mode 100644 index 0000000000..2d327ef649 --- /dev/null +++ b/apps/website/e2e/fixtures/blockExternalMedia.ts @@ -0,0 +1,47 @@ +import { fileURLToPath } from 'node:url' + +import type { Route } from '@playwright/test' +import { test as base } from '@playwright/test' + +function assetPath(relativePath: string) { + return fileURLToPath(new URL(relativePath, import.meta.url)) +} + +const IMAGE_PLACEHOLDER = assetPath('../assets/placeholder-1x1.webp') +const VIDEO_PLACEHOLDER = assetPath('../assets/placeholder.webm') + +const ANALYTICS_PATTERN = '**/va.vercel-scripts.com/**' as const +const MEDIA_PATTERN = + /^https:\/\/media\.comfy\.org\/.*\.(webp|webm|mp4|png|jpg|jpeg|vtt)(\?.*)?$/i +const VIDEO_PATTERN = /\.(webm|mp4)(\?|$)/i +const SUBTITLE_PATTERN = /\.vtt(\?|$)/i + +function blockAnalytics(route: Route) { + return route.abort('blockedbyclient') +} + +async function fulfillMedia(route: Route) { + const url = route.request().url() + if (VIDEO_PATTERN.test(url)) + return route.fulfill({ path: VIDEO_PLACEHOLDER, status: 200 }) + + if (SUBTITLE_PATTERN.test(url)) + return route.fulfill({ + status: 200, + contentType: 'text/vtt', + body: 'WEBVTT\n' + }) + + await route.fulfill({ path: IMAGE_PLACEHOLDER, status: 200 }) +} + +export const test = base.extend<{ blockExternalMedia: void }>({ + blockExternalMedia: [ + async ({ page }, use) => { + await page.route(ANALYTICS_PATTERN, blockAnalytics) + await page.route(MEDIA_PATTERN, fulfillMedia) + await use() + }, + { auto: true } + ] +}) diff --git a/apps/website/e2e/homepage.spec.ts b/apps/website/e2e/homepage.spec.ts index c60df2486b..111c8ca5ee 100644 --- a/apps/website/e2e/homepage.spec.ts +++ b/apps/website/e2e/homepage.spec.ts @@ -1,4 +1,15 @@ -import { expect, test } from '@playwright/test' +import { fileURLToPath } from 'node:url' + +import { expect } from '@playwright/test' + +import { test } from './fixtures/blockExternalMedia' + +const caseStudyVideoPath = fileURLToPath( + new URL( + '../../../public/assets/images/cloud-subscription.webm', + import.meta.url + ) +) test.describe('Homepage @smoke', () => { test.beforeEach(async ({ page }) => { @@ -58,6 +69,50 @@ test.describe('Homepage @smoke', () => { ).toBeVisible() }) + test('CaseStudySpotlight CTA sizes to its content, not the column', async ({ + page + }) => { + const contentColumn = page.getByTestId('case-study-content') + const cta = contentColumn.getByRole('link', { + name: /see all case studies/i + }) + + await cta.scrollIntoViewIfNeeded() + await expect(cta).toBeVisible() + + const [columnBox, ctaBox] = await Promise.all([ + contentColumn.boundingBox(), + cta.boundingBox() + ]) + + expect(columnBox).not.toBeNull() + expect(ctaBox).not.toBeNull() + expect(ctaBox!.width).toBeLessThan(columnBox!.width * 0.7) + }) + + test('CaseStudySpotlight CTA has breathing room above it on mobile @mobile', async ({ + page + }) => { + const contentColumn = page.getByTestId('case-study-content') + const subheading = contentColumn.getByText( + /Videos & case studies from teams/i + ) + const cta = contentColumn.getByRole('link', { + name: /see all case studies/i + }) + + await cta.scrollIntoViewIfNeeded() + + const [subBox, ctaBox] = await Promise.all([ + subheading.boundingBox(), + cta.boundingBox() + ]) + + expect(subBox).not.toBeNull() + expect(ctaBox).not.toBeNull() + expect(ctaBox!.y - (subBox!.y + subBox!.height)).toBeGreaterThanOrEqual(24) + }) + test('BuildWhatSection is visible', async ({ page }) => { // "DOESN'T EXIST" is the actual badge text rendered in the Build What section await expect(page.getByText("DOESN'T EXIST")).toBeVisible() @@ -83,17 +138,56 @@ test.describe('Product showcase accordion @interaction', () => { .first() await secondFeature.scrollIntoViewIfNeeded() - - await expect(async () => { - await secondFeature.click() - await expect( - page.getByText(/If you are new to ComfyUI/).first() - ).toBeVisible({ timeout: 1000 }) - }).toPass({ timeout: 10000 }) + await secondFeature.click() await expect( - page.getByText(/Build powerful AI pipelines by connecting nodes/).first() - ).toBeHidden() + secondFeature.getByText(/If you are new to ComfyUI/) + ).toBeVisible() + + const firstFeature = page + .getByRole('button', { name: /Full Control with Nodes/i }) + .first() + + await expect(firstFeature).not.toHaveClass(/bg-primary-comfy-yellow/) + await expect(secondFeature).toHaveClass(/bg-primary-comfy-yellow/) + }) +}) + +test.describe('Video player @interaction', () => { + test.beforeEach(async ({ page }) => { + await page.route( + 'https://media.comfy.org/website/customers/blackmath/video.webm', + (route) => + route.fulfill({ + contentType: 'video/webm', + path: caseStudyVideoPath + }) + ) + + await page.goto('/') + }) + + test('clicking play advances playback', async ({ page }) => { + const section = page.locator('section', { + has: page.getByText('Customer Stories') + }) + const video = section.locator('video') + + await expect + .poll( + async () => + video.evaluate((element: HTMLVideoElement) => element.duration), + { timeout: 15_000 } + ) + .toBeGreaterThan(0) + + await section.getByRole('button', { name: 'Play' }).click() + + await expect + .poll(async () => + video.evaluate((element: HTMLVideoElement) => element.currentTime) + ) + .toBeGreaterThan(0) }) }) @@ -125,6 +219,6 @@ test.describe('Get started section links @smoke', () => { const cloudLink = section.getByRole('link', { name: 'Launch Cloud' }) await expect(cloudLink).toBeVisible() - await expect(cloudLink).toHaveAttribute('href', 'https://app.comfy.org') + await expect(cloudLink).toHaveAttribute('href', 'https://cloud.comfy.org') }) }) diff --git a/apps/website/e2e/navigation.spec.ts b/apps/website/e2e/navigation.spec.ts index 895e92a7c3..864770ed9a 100644 --- a/apps/website/e2e/navigation.spec.ts +++ b/apps/website/e2e/navigation.spec.ts @@ -1,4 +1,6 @@ -import { expect, test } from '@playwright/test' +import { expect } from '@playwright/test' + +import { test } from './fixtures/blockExternalMedia' test.describe('Desktop navigation @smoke', () => { test.beforeEach(async ({ page }) => { diff --git a/apps/website/e2e/payment.spec.ts b/apps/website/e2e/payment.spec.ts new file mode 100644 index 0000000000..2a2e8f535c --- /dev/null +++ b/apps/website/e2e/payment.spec.ts @@ -0,0 +1,115 @@ +import type { Page } from '@playwright/test' +import { expect } from '@playwright/test' + +import { externalLinks } from '../src/config/routes' +import { test } from './fixtures/blockExternalMedia' + +const CLOUD_URL = externalLinks.cloud +const PLATFORM_USAGE_URL = externalLinks.platformUsage +const SUPPORT_URL = externalLinks.support +const DOCS_SUBSCRIPTION_URL = externalLinks.docsSubscription + +async function expectNoIndex(page: Page) { + await expect(page.locator('meta[name="robots"]')).toHaveAttribute( + 'content', + 'noindex, nofollow' + ) +} + +test.describe('Payment success page @smoke', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/payment/success') + }) + + test('has correct title and is noindex', async ({ page }) => { + await expect(page).toHaveTitle('Payment Successful — Comfy') + await expectNoIndex(page) + }) + + test('shows success heading and subtitle', async ({ page }) => { + await expect( + page.getByRole('heading', { name: /Payment successful/i, level: 1 }) + ).toBeVisible() + await expect(page.getByText(/Thanks for your purchase/i)).toBeVisible() + }) + + test('primary CTA links to Comfy Cloud', async ({ page }) => { + const cta = page.getByRole('link', { name: /CONTINUE TO COMFY CLOUD/i }) + await expect(cta).toBeVisible() + await expect(cta).toHaveAttribute('href', CLOUD_URL) + }) + + test('secondary CTA links to platform usage & payments page', async ({ + page + }) => { + const cta = page.getByRole('link', { name: /VIEW USAGE & PAYMENTS/i }) + await expect(cta).toBeVisible() + await expect(cta).toHaveAttribute('href', PLATFORM_USAGE_URL) + }) +}) + +test.describe('Payment failed page @smoke', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/payment/failed') + }) + + test('has correct title and is noindex', async ({ page }) => { + await expect(page).toHaveTitle('Payment Failed — Comfy') + await expectNoIndex(page) + }) + + test('shows failure heading and subtitle', async ({ page }) => { + await expect( + page.getByRole('heading', { + name: /Unable to complete payment/i, + level: 1 + }) + ).toBeVisible() + await expect(page.getByText(/payment didn't go through/i)).toBeVisible() + }) + + test('primary CTA links to support help center', async ({ page }) => { + const cta = page.getByRole('link', { name: /CONTACT SUPPORT/i }) + await expect(cta).toBeVisible() + await expect(cta).toHaveAttribute('href', SUPPORT_URL) + }) + + test('secondary CTA links to subscription docs', async ({ page }) => { + const cta = page.getByRole('link', { name: /READ SUBSCRIPTION DOCS/i }) + await expect(cta).toBeVisible() + await expect(cta).toHaveAttribute('href', DOCS_SUBSCRIPTION_URL) + }) +}) + +test.describe('Payment pages zh-CN @smoke', () => { + test('zh-CN success page renders and links correctly', async ({ page }) => { + await page.goto('/zh-CN/payment/success') + await expect(page).toHaveTitle('支付成功 — Comfy') + await expectNoIndex(page) + await expect( + page.getByRole('heading', { name: '支付成功', level: 1 }) + ).toBeVisible() + await expect( + page.getByRole('link', { name: '前往 COMFY CLOUD' }) + ).toHaveAttribute('href', CLOUD_URL) + await expect( + page.getByRole('link', { name: '查看用量与支付' }) + ).toHaveAttribute('href', PLATFORM_USAGE_URL) + }) + + test('zh-CN failed page renders and links correctly', async ({ page }) => { + await page.goto('/zh-CN/payment/failed') + await expect(page).toHaveTitle('支付失败 — Comfy') + await expectNoIndex(page) + await expect( + page.getByRole('heading', { name: '无法完成支付', level: 1 }) + ).toBeVisible() + await expect(page.getByRole('link', { name: '联系支持' })).toHaveAttribute( + 'href', + SUPPORT_URL + ) + await expect( + page.getByRole('link', { name: '查看订阅文档' }) + ).toHaveAttribute('href', DOCS_SUBSCRIPTION_URL) + }) +}) diff --git a/apps/website/e2e/pricing.spec.ts b/apps/website/e2e/pricing.spec.ts new file mode 100644 index 0000000000..05520f0e5b --- /dev/null +++ b/apps/website/e2e/pricing.spec.ts @@ -0,0 +1,56 @@ +import { expect } from '@playwright/test' + +import { test } from './fixtures/blockExternalMedia' + +test.describe('Pricing page @smoke', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/cloud/pricing') + }) + + test('shows the three paid tiers and Enterprise', async ({ page }) => { + const pricingGrid = page + .locator('section', { + has: page.getByRole('heading', { name: /Pricing/i }) + }) + .locator('.lg\\:grid') + + for (const label of ['STANDARD', 'CREATOR', 'PRO']) { + await expect( + pricingGrid.locator('span', { hasText: new RegExp(`^${label}$`) }) + ).toBeVisible() + } + + await expect( + page.getByRole('heading', { name: /Looking for Enterprise Solutions/i }) + ).toBeVisible() + }) + + test('does not show the Free tier when SHOW_FREE_TIER is disabled', async ({ + page + }) => { + const pricingGrid = page + .locator('section', { + has: page.getByRole('heading', { name: /Pricing/i }) + }) + .locator('.lg\\:grid') + + await expect( + pricingGrid.locator('span', { hasText: /^FREE$/ }) + ).toHaveCount(0) + await expect(page.getByRole('link', { name: /^START FREE$/ })).toHaveCount( + 0 + ) + await expect(page.getByText(/Everything in Free, plus:/i)).toHaveCount(0) + }) +}) + +test.describe('Cloud pricing teaser @smoke', () => { + test('does not show the "Start free" tagline when SHOW_FREE_TIER is disabled', async ({ + page + }) => { + await page.goto('/cloud') + await expect( + page.getByText(/Start free\.\s*Upgrade when you're ready\./i) + ).toHaveCount(0) + }) +}) diff --git a/apps/website/e2e/responsive.spec.ts b/apps/website/e2e/responsive.spec.ts index ab6d127067..6488170457 100644 --- a/apps/website/e2e/responsive.spec.ts +++ b/apps/website/e2e/responsive.spec.ts @@ -1,4 +1,7 @@ -import { expect, test } from '@playwright/test' +import type { Page } from '@playwright/test' +import { expect } from '@playwright/test' + +import { test } from './fixtures/blockExternalMedia' test.describe('Desktop layout @smoke', () => { test.beforeEach(async ({ page }) => { @@ -45,4 +48,105 @@ test.describe('Mobile layout @mobile', () => { const mobileContainer = page.getByTestId('social-proof-mobile') await expect(mobileContainer).toBeVisible() }) + + test.describe('SocialProofBar seamless marquee', () => { + test.use({ contextOptions: { reducedMotion: 'no-preference' } }) + + test('mobile forward marquee loops seamlessly', async ({ page }) => { + const geometry = await measureMarqueeLoopGeometry( + page, + '[data-testid="social-proof-mobile"] .animate-marquee' + ) + expectSeamlessForwardLoop(geometry) + }) + + test('mobile reverse marquee loops seamlessly', async ({ page }) => { + const geometry = await measureMarqueeLoopGeometry( + page, + '[data-testid="social-proof-mobile"] .animate-marquee-reverse' + ) + expectSeamlessReverseLoop(geometry) + }) + }) }) + +test.describe('Desktop SocialProofBar @smoke', () => { + test.use({ contextOptions: { reducedMotion: 'no-preference' } }) + + test.beforeEach(async ({ page }) => { + await page.goto('/') + }) + + test('desktop marquee loops seamlessly', async ({ page }) => { + const geometry = await measureMarqueeLoopGeometry( + page, + '[data-testid="social-proof-desktop"] .animate-marquee' + ) + expectSeamlessForwardLoop(geometry) + }) +}) + +type MarqueeGeometry = { + copyWidths: number[] + startPositions: number[] + endPositions: number[] +} + +async function measureMarqueeLoopGeometry( + page: Page, + selector: string +): Promise { + await page.locator(selector).first().waitFor() + return page.evaluate((sel) => { + const tracks = Array.from( + document.querySelectorAll(sel) + ).slice(0, 2) + const firstAnimation = tracks[0]?.getAnimations()[0] + if (!firstAnimation) { + throw new Error(`No CSS animation found on ${sel}`) + } + const duration = firstAnimation.effect?.getTiming().duration + if (typeof duration !== 'number' || duration <= 1) { + throw new Error( + `Animation on ${sel} has unusable duration: ${String(duration)}` + ) + } + const setAllTimes = (time: number) => { + for (const track of tracks) { + for (const anim of track.getAnimations()) { + anim.currentTime = time + } + } + void document.body.offsetWidth + } + const readX = () => tracks.map((track) => track.getBoundingClientRect().x) + setAllTimes(0) + const startPositions = readX() + const copyWidths = tracks.map( + (track) => track.getBoundingClientRect().width + ) + setAllTimes(duration - 0.1) + const endPositions = readX() + return { copyWidths, startPositions, endPositions } + }, selector) +} + +function expectTwoMatchingCopies(geometry: MarqueeGeometry) { + const { copyWidths } = geometry + expect(copyWidths.length, 'expected two duplicate marquee tracks').toBe(2) + expect(copyWidths[0]).toBeGreaterThan(0) + expect(copyWidths[1]).toBeCloseTo(copyWidths[0], 0) +} + +function expectSeamlessForwardLoop(geometry: MarqueeGeometry) { + expectTwoMatchingCopies(geometry) + // Copy 2 ends the cycle exactly where copy 1 started, so the restart + // (when copy 1 jumps back to its start position) is visually indistinguishable. + expect(geometry.endPositions[1]).toBeCloseTo(geometry.startPositions[0], 0) +} + +function expectSeamlessReverseLoop(geometry: MarqueeGeometry) { + expectTwoMatchingCopies(geometry) + // Reverse marquee: copy 1 ends the cycle where copy 2 started. + expect(geometry.endPositions[0]).toBeCloseTo(geometry.startPositions[1], 0) +} diff --git a/apps/website/e2e/viewports.ts b/apps/website/e2e/viewports.ts new file mode 100644 index 0000000000..1b1e288aa7 --- /dev/null +++ b/apps/website/e2e/viewports.ts @@ -0,0 +1,6 @@ +export const VIEWPORTS = [ + { name: '1-sm', width: 393, height: 851 }, + { name: '2-md', width: 768, height: 1024 }, + { name: '3-lg', width: 1280, height: 800 }, + { name: '4-xl', width: 1536, height: 864 } +] as const diff --git a/apps/website/e2e/visual-responsive.spec.ts b/apps/website/e2e/visual-responsive.spec.ts new file mode 100644 index 0000000000..060e75cfd8 --- /dev/null +++ b/apps/website/e2e/visual-responsive.spec.ts @@ -0,0 +1,146 @@ +import type { Page } from '@playwright/test' + +import { expect } from '@playwright/test' + +import { test } from './fixtures/blockExternalMedia' +import { VIEWPORTS } from './viewports' + +test.describe.configure({ timeout: 60_000 }) + +const SMALL_VIEWPORTS = VIEWPORTS.filter( + (v) => v.name === '1-sm' || v.name === '2-md' +) + +async function assertNoOverflow(page: Page) { + await expect + .poll( + () => + page.evaluate( + () => + document.documentElement.scrollWidth > + document.documentElement.clientWidth + ), + { message: 'page has horizontal overflow', timeout: 5_000 } + ) + .toBe(false) +} + +async function navigateAndSettle(page: Page, url: string) { + await page.goto(url, { waitUntil: 'domcontentloaded' }) + await page.waitForLoadState('load') +} + +test.describe('Home', { tag: '@visual' }, () => { + for (const vp of VIEWPORTS) { + test.describe(vp.name, () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize({ width: vp.width, height: vp.height }) + await navigateAndSettle(page, '/') + }) + + test('product-cards screenshot', async ({ page }) => { + const section = page.locator('section', { + has: page.getByRole('heading', { name: /The AI creation/i }) + }) + await expect(section).toBeVisible() + await section.scrollIntoViewIfNeeded() + await expect(page).toHaveScreenshot(`home-product-cards-${vp.name}.png`) + }) + + test('get-started screenshot', async ({ page }) => { + const section = page.locator('section', { + has: page.getByRole('heading', { name: /Get started/i }) + }) + await expect(section).toBeVisible() + await section.scrollIntoViewIfNeeded() + await expect(page).toHaveScreenshot(`home-get-started-${vp.name}.png`) + }) + }) + } +}) + +test.describe('Pricing', { tag: '@visual' }, () => { + for (const vp of VIEWPORTS) { + test(`pricing-tiers-${vp.name}`, async ({ page }) => { + await page.setViewportSize({ width: vp.width, height: vp.height }) + await navigateAndSettle(page, '/cloud/pricing') + await assertNoOverflow(page) + + const section = page.locator('section', { + has: page.getByRole('heading', { name: /Pricing/i }) + }) + await expect(section).toBeVisible() + await section.scrollIntoViewIfNeeded() + await expect(page).toHaveScreenshot(`pricing-tiers-${vp.name}.png`) + }) + } +}) + +test.describe('Contact', { tag: '@visual' }, () => { + for (const vp of SMALL_VIEWPORTS) { + test(`form-${vp.name}`, async ({ page }) => { + await page.setViewportSize({ width: vp.width, height: vp.height }) + await navigateAndSettle(page, '/contact') + + const section = page.locator('section', { + has: page.getByRole('heading', { name: /Create powerful workflows/i }) + }) + await expect(section).toBeVisible() + await section.scrollIntoViewIfNeeded() + await expect(page).toHaveScreenshot(`contact-form-${vp.name}.png`) + }) + } +}) + +test.describe('Gallery', { tag: '@visual' }, () => { + for (const vp of SMALL_VIEWPORTS) { + test(`gallery-grid-${vp.name}`, async ({ page }) => { + await page.setViewportSize({ width: vp.width, height: vp.height }) + await navigateAndSettle(page, '/gallery') + + const section = page.getByTestId('gallery-grid') + await expect(section).toBeVisible() + await section.scrollIntoViewIfNeeded() + await expect(page).toHaveScreenshot(`gallery-grid-${vp.name}.png`) + }) + } +}) + +test.describe('About', { tag: '@visual' }, () => { + for (const vp of SMALL_VIEWPORTS) { + test(`hero-${vp.name}`, async ({ page }) => { + await page.setViewportSize({ width: vp.width, height: vp.height }) + await navigateAndSettle(page, '/about') + + const hero = page.locator('section', { + has: page.getByRole('heading', { name: /Build the tools/i }) + }) + await expect(hero).toBeVisible() + await hero.scrollIntoViewIfNeeded() + await expect(page).toHaveScreenshot(`about-hero-${vp.name}.png`) + }) + } +}) + +test.describe('Overflow guards', { tag: '@visual' }, () => { + const pages = [ + '/', + '/cloud', + '/cloud/enterprise', + '/cloud/pricing', + '/contact', + '/download', + '/gallery', + '/about', + '/careers' + ] + for (const url of pages) { + for (const vp of VIEWPORTS) { + test(`${url} ${vp.name} no overflow`, async ({ page }) => { + await page.setViewportSize({ width: vp.width, height: vp.height }) + await page.goto(url) + await assertNoOverflow(page) + }) + } + } +}) diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/about-hero-1-sm-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/about-hero-1-sm-visual-linux.png new file mode 100644 index 0000000000..a112240867 Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/about-hero-1-sm-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/about-hero-2-md-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/about-hero-2-md-visual-linux.png new file mode 100644 index 0000000000..a4ae65db45 Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/about-hero-2-md-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/contact-form-1-sm-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/contact-form-1-sm-visual-linux.png new file mode 100644 index 0000000000..a0f2f36815 Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/contact-form-1-sm-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/contact-form-2-md-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/contact-form-2-md-visual-linux.png new file mode 100644 index 0000000000..384c231446 Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/contact-form-2-md-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/gallery-grid-1-sm-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/gallery-grid-1-sm-visual-linux.png new file mode 100644 index 0000000000..ac2fc80fea Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/gallery-grid-1-sm-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/gallery-grid-2-md-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/gallery-grid-2-md-visual-linux.png new file mode 100644 index 0000000000..2de5bcb901 Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/gallery-grid-2-md-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-get-started-1-sm-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-get-started-1-sm-visual-linux.png new file mode 100644 index 0000000000..bdfa934d5f Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-get-started-1-sm-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-get-started-2-md-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-get-started-2-md-visual-linux.png new file mode 100644 index 0000000000..33bbea1737 Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-get-started-2-md-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-get-started-3-lg-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-get-started-3-lg-visual-linux.png new file mode 100644 index 0000000000..5ce1c80434 Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-get-started-3-lg-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-get-started-4-xl-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-get-started-4-xl-visual-linux.png new file mode 100644 index 0000000000..695a4684aa Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-get-started-4-xl-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-product-cards-1-sm-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-product-cards-1-sm-visual-linux.png new file mode 100644 index 0000000000..2cd8c19c3a Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-product-cards-1-sm-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-product-cards-2-md-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-product-cards-2-md-visual-linux.png new file mode 100644 index 0000000000..374ae14e40 Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-product-cards-2-md-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-product-cards-3-lg-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-product-cards-3-lg-visual-linux.png new file mode 100644 index 0000000000..780cc7b555 Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-product-cards-3-lg-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-product-cards-4-xl-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-product-cards-4-xl-visual-linux.png new file mode 100644 index 0000000000..0290e4e062 Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/home-product-cards-4-xl-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/pricing-tiers-1-sm-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/pricing-tiers-1-sm-visual-linux.png new file mode 100644 index 0000000000..13f924097d Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/pricing-tiers-1-sm-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/pricing-tiers-2-md-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/pricing-tiers-2-md-visual-linux.png new file mode 100644 index 0000000000..70b26c7ca3 Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/pricing-tiers-2-md-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/pricing-tiers-3-lg-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/pricing-tiers-3-lg-visual-linux.png new file mode 100644 index 0000000000..d5129a7fb1 Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/pricing-tiers-3-lg-visual-linux.png differ diff --git a/apps/website/e2e/visual-responsive.spec.ts-snapshots/pricing-tiers-4-xl-visual-linux.png b/apps/website/e2e/visual-responsive.spec.ts-snapshots/pricing-tiers-4-xl-visual-linux.png new file mode 100644 index 0000000000..d6f83d0fa2 Binary files /dev/null and b/apps/website/e2e/visual-responsive.spec.ts-snapshots/pricing-tiers-4-xl-visual-linux.png differ diff --git a/apps/website/package.json b/apps/website/package.json index 36bbb73060..8a34eca2cc 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -5,20 +5,35 @@ "type": "module", "scripts": { "dev": "astro dev", + "dev:no-toolbar": "cross-env NO_TOOLBAR=1 astro dev", "build": "astro build", "preview": "astro preview", "typecheck": "astro check", + "test:unit": "vitest run", + "test:coverage": "vitest run --coverage", "test:e2e": "playwright test", - "test:e2e:local": "PLAYWRIGHT_LOCAL=1 playwright test" + "test:e2e:local": "cross-env PLAYWRIGHT_LOCAL=1 playwright test", + "test:visual": "playwright test --project visual", + "test:visual:update": "playwright test --project visual --update-snapshots", + "ashby:refresh-snapshot": "tsx ./scripts/refresh-ashby-snapshot.ts", + "cloud-nodes:refresh-snapshot": "tsx ./scripts/refresh-cloud-nodes-snapshot.ts", + "generate:models": "tsx ./scripts/generate-models.ts" }, "dependencies": { "@astrojs/sitemap": "catalog:", "@comfyorg/design-system": "workspace:*", + "@comfyorg/object-info-parser": "workspace:*", + "@comfyorg/shared-frontend-utils": "workspace:*", "@comfyorg/tailwind-utils": "workspace:*", "@vercel/analytics": "catalog:", + "@vueuse/core": "catalog:", + "cva": "catalog:", "gsap": "catalog:", "lenis": "catalog:", - "vue": "catalog:" + "posthog-js": "catalog:", + "three": "catalog:", + "vue": "catalog:", + "zod": "catalog:" }, "devDependencies": { "@astrojs/check": "catalog:", @@ -27,7 +42,9 @@ "@tailwindcss/vite": "catalog:", "astro": "catalog:", "tailwindcss": "catalog:", - "typescript": "catalog:" + "tsx": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" }, "nx": { "tags": [ @@ -84,6 +101,22 @@ "command": "astro check" } }, + "test:unit": { + "executor": "nx:run-commands", + "cache": true, + "options": { + "cwd": "apps/website", + "command": "vitest run" + } + }, + "test:coverage": { + "executor": "nx:run-commands", + "cache": true, + "options": { + "cwd": "apps/website", + "command": "vitest run --coverage" + } + }, "test:e2e": { "executor": "nx:run-commands", "dependsOn": [ diff --git a/apps/website/playwright.config.ts b/apps/website/playwright.config.ts index 62eb091815..b6100286bc 100644 --- a/apps/website/playwright.config.ts +++ b/apps/website/playwright.config.ts @@ -1,15 +1,36 @@ +import type { PlaywrightTestConfig } from '@playwright/test' import { defineConfig, devices } from '@playwright/test' +const maybeLocalOptions: PlaywrightTestConfig = process.env.PLAYWRIGHT_LOCAL + ? { + timeout: 30_000, + retries: 0, + workers: 1, + use: { + baseURL: 'http://localhost:4321', + trace: 'on', + video: 'on' + } + } + : { + retries: process.env.CI ? 2 : 0, + use: { + baseURL: 'http://localhost:4321', + trace: 'on-first-retry' + } + } + export default defineConfig({ testDir: './e2e', fullyParallel: true, forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - reporter: 'html', - use: { - baseURL: 'http://localhost:4321', - trace: process.env.CI ? 'on-first-retry' : 'on' + reporter: process.env.CI + ? [['html'], ['json', { outputFile: 'results.json' }]] + : 'html', + expect: { + toHaveScreenshot: { maxDiffPixels: 100 } }, + ...maybeLocalOptions, webServer: { command: 'pnpm preview', port: 4321, @@ -19,12 +40,18 @@ export default defineConfig({ { name: 'desktop', use: { ...devices['Desktop Chrome'] }, - grepInvert: /@mobile/ + grepInvert: /@mobile|@visual/ }, { name: 'mobile', use: { ...devices['Pixel 5'] }, grep: /@mobile/ + }, + { + name: 'visual', + use: { ...devices['Desktop Chrome'] }, + grep: /@visual/, + fullyParallel: false } ] }) diff --git a/apps/website/public/assets/images/fallback-gradient-avatar.svg b/apps/website/public/assets/images/fallback-gradient-avatar.svg new file mode 100644 index 0000000000..90b860f619 --- /dev/null +++ b/apps/website/public/assets/images/fallback-gradient-avatar.svg @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/apps/website/public/favicon.svg b/apps/website/public/favicon.svg new file mode 100644 index 0000000000..f41c8445d7 --- /dev/null +++ b/apps/website/public/favicon.svg @@ -0,0 +1,14 @@ + + + + + + + diff --git a/apps/website/public/fonts/PPFormula-CondensedSemibold.woff2 b/apps/website/public/fonts/PPFormula-CondensedSemibold.woff2 deleted file mode 100644 index e39dfc2782..0000000000 Binary files a/apps/website/public/fonts/PPFormula-CondensedSemibold.woff2 and /dev/null differ diff --git a/apps/website/public/fonts/PPFormula-NarrowSemibold.woff2 b/apps/website/public/fonts/PPFormula-NarrowSemibold.woff2 new file mode 100644 index 0000000000..108621f9b0 Binary files /dev/null and b/apps/website/public/fonts/PPFormula-NarrowSemibold.woff2 differ diff --git a/apps/website/public/icons/check.svg b/apps/website/public/icons/check.svg new file mode 100644 index 0000000000..29188112c4 --- /dev/null +++ b/apps/website/public/icons/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/website/public/icons/clock.svg b/apps/website/public/icons/clock.svg new file mode 100644 index 0000000000..0c9168c81c --- /dev/null +++ b/apps/website/public/icons/clock.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/website/public/icons/investors/abstract.svg b/apps/website/public/icons/investors/abstract.svg new file mode 100644 index 0000000000..af7236d9a2 --- /dev/null +++ b/apps/website/public/icons/investors/abstract.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/website/public/icons/investors/sapphire-ventures.svg b/apps/website/public/icons/investors/sapphire-ventures.svg deleted file mode 100644 index 5b52e71c42..0000000000 --- a/apps/website/public/icons/investors/sapphire-ventures.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/apps/website/public/images/about/c-logo.webp b/apps/website/public/images/about/c-logo.webp deleted file mode 100644 index 5a8af2f794..0000000000 Binary files a/apps/website/public/images/about/c-logo.webp and /dev/null differ diff --git a/apps/website/public/images/about/c.webp b/apps/website/public/images/about/c.webp deleted file mode 100644 index a750217f29..0000000000 Binary files a/apps/website/public/images/about/c.webp and /dev/null differ diff --git a/apps/website/public/images/about/team.webp b/apps/website/public/images/about/team.webp deleted file mode 100644 index 6024d7fd99..0000000000 Binary files a/apps/website/public/images/about/team.webp and /dev/null differ diff --git a/apps/website/public/images/careers/hero.webp b/apps/website/public/images/careers/hero.webp deleted file mode 100644 index 9af84955ac..0000000000 Binary files a/apps/website/public/images/careers/hero.webp and /dev/null differ diff --git a/apps/website/public/images/careers/team0.webp b/apps/website/public/images/careers/team0.webp deleted file mode 100644 index 2baebb0796..0000000000 Binary files a/apps/website/public/images/careers/team0.webp and /dev/null differ diff --git a/apps/website/public/images/careers/team1.webp b/apps/website/public/images/careers/team1.webp deleted file mode 100644 index 63fd8b7e6a..0000000000 Binary files a/apps/website/public/images/careers/team1.webp and /dev/null differ diff --git a/apps/website/public/images/careers/team2.webp b/apps/website/public/images/careers/team2.webp deleted file mode 100644 index 539bcd49f9..0000000000 Binary files a/apps/website/public/images/careers/team2.webp and /dev/null differ diff --git a/apps/website/public/images/careers/team3.webp b/apps/website/public/images/careers/team3.webp deleted file mode 100644 index 5154a39ffc..0000000000 Binary files a/apps/website/public/images/careers/team3.webp and /dev/null differ diff --git a/apps/website/public/images/cloud/ai-models/grok-video.vtt b/apps/website/public/images/cloud/ai-models/grok-video.vtt deleted file mode 100644 index cbe1f7e3e3..0000000000 --- a/apps/website/public/images/cloud/ai-models/grok-video.vtt +++ /dev/null @@ -1,4 +0,0 @@ -WEBVTT - -00:00:00.000 --> 00:00:06.000 -AI-generated video showcasing Grok Imagine image generation capabilities diff --git a/apps/website/public/images/cloud/ai-models/grok-video.webm b/apps/website/public/images/cloud/ai-models/grok-video.webm deleted file mode 100644 index 0deb02f481..0000000000 Binary files a/apps/website/public/images/cloud/ai-models/grok-video.webm and /dev/null differ diff --git a/apps/website/public/images/cloud/ai-models/nano-banana-pro.webp b/apps/website/public/images/cloud/ai-models/nano-banana-pro.webp deleted file mode 100644 index cccc66c034..0000000000 Binary files a/apps/website/public/images/cloud/ai-models/nano-banana-pro.webp and /dev/null differ diff --git a/apps/website/public/images/cloud/ai-models/qwen-image-edit.webp b/apps/website/public/images/cloud/ai-models/qwen-image-edit.webp deleted file mode 100644 index 811440ae71..0000000000 Binary files a/apps/website/public/images/cloud/ai-models/qwen-image-edit.webp and /dev/null differ diff --git a/apps/website/public/images/cloud/ai-models/seedance-20.vtt b/apps/website/public/images/cloud/ai-models/seedance-20.vtt deleted file mode 100644 index fd5849b4da..0000000000 --- a/apps/website/public/images/cloud/ai-models/seedance-20.vtt +++ /dev/null @@ -1,4 +0,0 @@ -WEBVTT - -00:00:00.000 --> 00:00:05.000 -AI-generated video showcasing Seedance 2.0 video generation capabilities diff --git a/apps/website/public/images/cloud/ai-models/seedance-20.webm b/apps/website/public/images/cloud/ai-models/seedance-20.webm deleted file mode 100644 index 24a75c4eeb..0000000000 Binary files a/apps/website/public/images/cloud/ai-models/seedance-20.webm and /dev/null differ diff --git a/apps/website/public/images/cloud/ai-models/wan-22.vtt b/apps/website/public/images/cloud/ai-models/wan-22.vtt deleted file mode 100644 index 16db091c12..0000000000 --- a/apps/website/public/images/cloud/ai-models/wan-22.vtt +++ /dev/null @@ -1,4 +0,0 @@ -WEBVTT - -00:00:00.000 --> 00:00:05.000 -AI-generated video showcasing Wan 2.2 image-to-video generation capabilities diff --git a/apps/website/public/images/cloud/ai-models/wan-22.webm b/apps/website/public/images/cloud/ai-models/wan-22.webm deleted file mode 100644 index 339b417826..0000000000 Binary files a/apps/website/public/images/cloud/ai-models/wan-22.webm and /dev/null differ diff --git a/apps/website/public/images/cloud/audience-creator.webp b/apps/website/public/images/cloud/audience-creator.webp deleted file mode 100644 index aa436454b6..0000000000 Binary files a/apps/website/public/images/cloud/audience-creator.webp and /dev/null differ diff --git a/apps/website/public/images/cloud/audience-team.webp b/apps/website/public/images/cloud/audience-team.webp deleted file mode 100644 index fb1bf361c9..0000000000 Binary files a/apps/website/public/images/cloud/audience-team.webp and /dev/null differ diff --git a/apps/website/public/images/cloud/hero-cube.svg b/apps/website/public/images/cloud/hero-cube.svg deleted file mode 100644 index eb28acbd9d..0000000000 --- a/apps/website/public/images/cloud/hero-cube.svg +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/website/public/images/contact/c-projection.png b/apps/website/public/images/contact/c-projection.png deleted file mode 100644 index 2a08f195b8..0000000000 Binary files a/apps/website/public/images/contact/c-projection.png and /dev/null differ diff --git a/apps/website/public/images/contact/c-projection.webp b/apps/website/public/images/contact/c-projection.webp deleted file mode 100644 index a56232663a..0000000000 Binary files a/apps/website/public/images/contact/c-projection.webp and /dev/null differ diff --git a/apps/website/public/images/customers/c-projection.webp b/apps/website/public/images/customers/c-projection.webp deleted file mode 100644 index 45f3ccdc2c..0000000000 Binary files a/apps/website/public/images/customers/c-projection.webp and /dev/null differ diff --git a/apps/website/public/images/customers/detail-big-image.webp b/apps/website/public/images/customers/detail-big-image.webp deleted file mode 100644 index f2a22f9fa4..0000000000 Binary files a/apps/website/public/images/customers/detail-big-image.webp and /dev/null differ diff --git a/apps/website/public/images/demos/community-workflows-og.png b/apps/website/public/images/demos/community-workflows-og.png new file mode 100644 index 0000000000..7e46105c88 Binary files /dev/null and b/apps/website/public/images/demos/community-workflows-og.png differ diff --git a/apps/website/public/images/demos/community-workflows-thumb.webp b/apps/website/public/images/demos/community-workflows-thumb.webp new file mode 100644 index 0000000000..0e2427c14c Binary files /dev/null and b/apps/website/public/images/demos/community-workflows-thumb.webp differ diff --git a/apps/website/public/images/demos/image-to-video-og.png b/apps/website/public/images/demos/image-to-video-og.png new file mode 100644 index 0000000000..144ca94c41 Binary files /dev/null and b/apps/website/public/images/demos/image-to-video-og.png differ diff --git a/apps/website/public/images/demos/image-to-video-thumb.webp b/apps/website/public/images/demos/image-to-video-thumb.webp new file mode 100644 index 0000000000..24119e7b64 Binary files /dev/null and b/apps/website/public/images/demos/image-to-video-thumb.webp differ diff --git a/apps/website/public/images/demos/workflow-templates-og.png b/apps/website/public/images/demos/workflow-templates-og.png new file mode 100644 index 0000000000..f13a49c448 Binary files /dev/null and b/apps/website/public/images/demos/workflow-templates-og.png differ diff --git a/apps/website/public/images/demos/workflow-templates-thumb.webp b/apps/website/public/images/demos/workflow-templates-thumb.webp new file mode 100644 index 0000000000..7c2e78ab4a Binary files /dev/null and b/apps/website/public/images/demos/workflow-templates-thumb.webp differ diff --git a/apps/website/public/images/gallery/gallery.webp b/apps/website/public/images/gallery/gallery.webp deleted file mode 100644 index 83124d58fa..0000000000 Binary files a/apps/website/public/images/gallery/gallery.webp and /dev/null differ diff --git a/apps/website/public/images/homepage/use-case-left-1.webp b/apps/website/public/images/homepage/use-case-left-1.webp deleted file mode 100644 index 275ca285b9..0000000000 Binary files a/apps/website/public/images/homepage/use-case-left-1.webp and /dev/null differ diff --git a/apps/website/public/images/homepage/use-case-left-2.webp b/apps/website/public/images/homepage/use-case-left-2.webp deleted file mode 100644 index a556ba9780..0000000000 Binary files a/apps/website/public/images/homepage/use-case-left-2.webp and /dev/null differ diff --git a/apps/website/public/images/homepage/use-case-left-3.webp b/apps/website/public/images/homepage/use-case-left-3.webp deleted file mode 100644 index 528dd4230f..0000000000 Binary files a/apps/website/public/images/homepage/use-case-left-3.webp and /dev/null differ diff --git a/apps/website/public/images/homepage/use-case-left-4.webp b/apps/website/public/images/homepage/use-case-left-4.webp deleted file mode 100644 index 4055ed25f4..0000000000 Binary files a/apps/website/public/images/homepage/use-case-left-4.webp and /dev/null differ diff --git a/apps/website/public/images/homepage/use-case-left-5.webp b/apps/website/public/images/homepage/use-case-left-5.webp deleted file mode 100644 index 932e122a1b..0000000000 Binary files a/apps/website/public/images/homepage/use-case-left-5.webp and /dev/null differ diff --git a/apps/website/public/images/homepage/use-case-right-1.webp b/apps/website/public/images/homepage/use-case-right-1.webp deleted file mode 100644 index dfe40a1f1c..0000000000 Binary files a/apps/website/public/images/homepage/use-case-right-1.webp and /dev/null differ diff --git a/apps/website/public/images/homepage/use-case-right-2.webp b/apps/website/public/images/homepage/use-case-right-2.webp deleted file mode 100644 index bb88cd15d7..0000000000 Binary files a/apps/website/public/images/homepage/use-case-right-2.webp and /dev/null differ diff --git a/apps/website/public/images/homepage/use-case-right-3.webp b/apps/website/public/images/homepage/use-case-right-3.webp deleted file mode 100644 index 752451c98a..0000000000 Binary files a/apps/website/public/images/homepage/use-case-right-3.webp and /dev/null differ diff --git a/apps/website/public/images/homepage/use-case-right-4.webp b/apps/website/public/images/homepage/use-case-right-4.webp deleted file mode 100644 index 0eb7edb1fa..0000000000 Binary files a/apps/website/public/images/homepage/use-case-right-4.webp and /dev/null differ diff --git a/apps/website/public/images/homepage/use-case-right-5.webp b/apps/website/public/images/homepage/use-case-right-5.webp deleted file mode 100644 index 03cd42d075..0000000000 Binary files a/apps/website/public/images/homepage/use-case-right-5.webp and /dev/null differ diff --git a/apps/website/public/llms.txt b/apps/website/public/llms.txt new file mode 100644 index 0000000000..8f95cf6645 --- /dev/null +++ b/apps/website/public/llms.txt @@ -0,0 +1,58 @@ +# Comfy + +> Comfy is the AI creation engine for visual professionals who demand control over every model, every parameter, and every output. Built around ComfyUI — the open-source node-graph runtime with 60,000+ community nodes and thousands of shared workflows — Comfy ships as a free local app, a managed cloud, an API, and an enterprise platform. + +The Comfy ecosystem spans four surfaces: + +- **ComfyUI (local)** — the open-source node-graph runtime that runs models on your own hardware. +- **Comfy Cloud** — managed ComfyUI in the browser, with hosted models and storage. +- **Comfy API** — a REST API for triggering workflows from your own apps and pipelines. +- **Comfy Enterprise** — single-tenant deployments, BYO keys, data ownership, and orchestration for teams. + +Studios building with Comfy include Series Entertainment, Moment Factory, Open Story Movement, and Ubisoft (La Forge). Use cases concentrate in VFX & animation, advertising & creative studios, gaming, and eCommerce/fashion. + +## Product + +- [Homepage](https://comfy.org/): Overview of Comfy and the four product surfaces (Local, Cloud, API, Enterprise). +- [Download Comfy (Local)](https://comfy.org/download/): Free desktop app for macOS, Windows, and Linux — runs ComfyUI on your own GPU. +- [Comfy Cloud](https://comfy.org/cloud/): Managed ComfyUI in the browser with hosted models and storage; no local install required. +- [Comfy Cloud Pricing](https://comfy.org/cloud/pricing/): Plans and per-credit pricing for individuals and teams using Comfy Cloud. +- [Comfy API](https://comfy.org/api/): REST API for triggering ComfyUI workflows programmatically from external apps. +- [Comfy Enterprise](https://comfy.org/cloud/enterprise/): Single-tenant ComfyUI deployments with BYO keys, orchestration, and data-ownership guarantees. + +## Workflows and Gallery + +- [Workflow Gallery](https://comfy.org/gallery/): Curated showcase of ComfyUI outputs — images, video, and 3D — produced by the community. +- [Community Workflows](https://www.comfy.org/workflows/): Browseable library of community-shared ComfyUI workflows you can load and remix. + +## Customers and Case Studies + +- [Customer Stories](https://comfy.org/customers/): Index of named customers and how they use ComfyUI in production. +- [Series Entertainment](https://comfy.org/customers/series-entertainment/): How Series Entertainment rebuilt game and video production around ComfyUI. +- [Moment Factory](https://comfy.org/customers/moment-factory/): Architectural-scale 3D projection mapping reimagined with ComfyUI at Moment Factory. +- [Ubisoft — Chord](https://comfy.org/customers/ubisoft-chord/): Ubisoft La Forge open-sourcing the Chord model and its ComfyUI integration. +- [Open Story Movement](https://comfy.org/customers/open-story-movement/): How an open-source movement around AI storytelling builds on ComfyUI. + +## Developers and Documentation + +- [ComfyUI Docs](https://docs.comfy.org/): Official documentation for installing, configuring, and extending ComfyUI. +- [ComfyUI on GitHub](https://github.com/comfyanonymous/ComfyUI): Source repository for the open-source ComfyUI runtime. +- [Comfy-Org on GitHub](https://github.com/Comfy-Org): Organization-wide repositories — frontend, registry, manager, docs, and tooling. +- [Comfy Registry](https://registry.comfy.org/): Public registry of ComfyUI custom nodes and extensions, with versioning and search. + +## Company + +- [About Comfy](https://comfy.org/about/): Company background, mission, and the team behind ComfyUI. +- [Careers](https://comfy.org/careers/): Open roles across engineering, design, product, and go-to-market. +- [Contact](https://comfy.org/contact/): Sales, partnership, and general contact form. +- [Blog](https://blog.comfy.org/): Product announcements, technical deep-dives, and customer stories. +- [Privacy Policy](https://comfy.org/privacy-policy/): How Comfy collects, uses, and protects personal information. +- [Terms of Service](https://comfy.org/terms-of-service/): Terms governing use of ComfyUI and related Comfy services. + +## Optional + +- [简体中文 / Chinese homepage](https://comfy.org/zh-CN/): Simplified Chinese localization of the main site. +- [Series Entertainment — long-form case study](https://comfy.org/cloud/enterprise-case-studies/how-series-entertainment-rebuilt-game-and-video-production-with-comfyui): Extended write-up of the Series Entertainment deployment. +- [Moment Factory — long-form case study](https://comfy.org/cloud/enterprise-case-studies/comfyui-at-architectural-scale-how-moment-factory-reimagined-3d-projection-mapping): Extended write-up of Moment Factory's projection-mapping pipeline. +- [Ubisoft Chord announcement (blog)](https://blog.comfy.org/p/ubisoft-open-sources-the-chord-model): Original blog post announcing Ubisoft's open-source Chord model. +- [Open-source storytelling (blog)](https://blog.comfy.org/p/how-open-source-is-fueling-the-open): Blog post on how open source is fueling the Open Story Movement. diff --git a/apps/website/public/robots.txt b/apps/website/public/robots.txt index b1931edbd8..da06a725ac 100644 --- a/apps/website/public/robots.txt +++ b/apps/website/public/robots.txt @@ -1,4 +1,58 @@ +# robots.txt for comfy.org +# Open to all crawlers — including AI/LLM bots — for maximum visibility +# in AI-powered search, chat-based answer engines, and traditional search. +# Granular UAs are listed explicitly to signal intent; rules are shared +# via stacked user-agent records (RFC 9309 §2.2). + User-agent: * +User-agent: Googlebot +User-agent: Bingbot +User-agent: DuckDuckBot +User-agent: GPTBot +User-agent: ChatGPT-User +User-agent: OAI-SearchBot +User-agent: Google-Extended +User-agent: ClaudeBot +User-agent: Claude-Web +User-agent: anthropic-ai +User-agent: PerplexityBot +User-agent: Perplexity-User +User-agent: Applebot +User-agent: Applebot-Extended +User-agent: Bytespider +User-agent: Amazonbot +User-agent: CCBot +User-agent: Meta-ExternalAgent +User-agent: Meta-ExternalFetcher +User-agent: Diffbot +Allow: / +Disallow: /_astro/ +Disallow: /_website/ +Disallow: /_vercel/ +Disallow: /payment/ + +User-agent: GPTBot +Allow: / + +User-agent: OAI-SearchBot +Allow: / + +User-agent: ChatGPT-User +Allow: / + +User-agent: ClaudeBot +Allow: / + +User-agent: Claude-User +Allow: / + +User-agent: Claude-SearchBot +Allow: / + +User-agent: PerplexityBot +Allow: / + +User-agent: Google-Extended Allow: / Sitemap: https://comfy.org/sitemap-index.xml diff --git a/apps/website/public/videos/footer-logo-seq/Logo00.webp b/apps/website/public/videos/footer-logo-seq/Logo00.webp deleted file mode 100644 index 2df99a7929..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo00.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo01.webp b/apps/website/public/videos/footer-logo-seq/Logo01.webp deleted file mode 100644 index 39aee52523..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo01.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo02.webp b/apps/website/public/videos/footer-logo-seq/Logo02.webp deleted file mode 100644 index 770e985bb2..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo02.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo03.webp b/apps/website/public/videos/footer-logo-seq/Logo03.webp deleted file mode 100644 index 79d85e61aa..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo03.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo04.webp b/apps/website/public/videos/footer-logo-seq/Logo04.webp deleted file mode 100644 index 84a7c8a81e..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo04.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo05.webp b/apps/website/public/videos/footer-logo-seq/Logo05.webp deleted file mode 100644 index 68e917783b..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo05.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo06.webp b/apps/website/public/videos/footer-logo-seq/Logo06.webp deleted file mode 100644 index d9e1686455..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo06.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo07.webp b/apps/website/public/videos/footer-logo-seq/Logo07.webp deleted file mode 100644 index e10df09c11..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo07.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo08.webp b/apps/website/public/videos/footer-logo-seq/Logo08.webp deleted file mode 100644 index 8621ad07dc..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo08.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo09.webp b/apps/website/public/videos/footer-logo-seq/Logo09.webp deleted file mode 100644 index 0a4227dc51..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo09.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo10.webp b/apps/website/public/videos/footer-logo-seq/Logo10.webp deleted file mode 100644 index b2e832dd22..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo10.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo11.webp b/apps/website/public/videos/footer-logo-seq/Logo11.webp deleted file mode 100644 index fe8f82b016..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo11.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo12.webp b/apps/website/public/videos/footer-logo-seq/Logo12.webp deleted file mode 100644 index f81bca8b19..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo12.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo13.webp b/apps/website/public/videos/footer-logo-seq/Logo13.webp deleted file mode 100644 index 4ea76b84f4..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo13.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo14.webp b/apps/website/public/videos/footer-logo-seq/Logo14.webp deleted file mode 100644 index 3badbe247c..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo14.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo15.webp b/apps/website/public/videos/footer-logo-seq/Logo15.webp deleted file mode 100644 index 7ae74acb4d..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo15.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo16.webp b/apps/website/public/videos/footer-logo-seq/Logo16.webp deleted file mode 100644 index de331ba650..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo16.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo17.webp b/apps/website/public/videos/footer-logo-seq/Logo17.webp deleted file mode 100644 index 609d105052..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo17.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo18.webp b/apps/website/public/videos/footer-logo-seq/Logo18.webp deleted file mode 100644 index 38cfaf4e04..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo18.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo19.webp b/apps/website/public/videos/footer-logo-seq/Logo19.webp deleted file mode 100644 index ef7e91d348..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo19.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo20.webp b/apps/website/public/videos/footer-logo-seq/Logo20.webp deleted file mode 100644 index 7d9c9c0b1d..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo20.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo21.webp b/apps/website/public/videos/footer-logo-seq/Logo21.webp deleted file mode 100644 index c8e15e5a1e..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo21.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo22.webp b/apps/website/public/videos/footer-logo-seq/Logo22.webp deleted file mode 100644 index 56f7bf2161..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo22.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo23.webp b/apps/website/public/videos/footer-logo-seq/Logo23.webp deleted file mode 100644 index 24fb6be9fa..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo23.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo24.webp b/apps/website/public/videos/footer-logo-seq/Logo24.webp deleted file mode 100644 index a520d21ca3..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo24.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo25.webp b/apps/website/public/videos/footer-logo-seq/Logo25.webp deleted file mode 100644 index 3551df38b5..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo25.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo26.webp b/apps/website/public/videos/footer-logo-seq/Logo26.webp deleted file mode 100644 index 1962d54c51..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo26.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo27.webp b/apps/website/public/videos/footer-logo-seq/Logo27.webp deleted file mode 100644 index 3bccd44e52..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo27.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo28.webp b/apps/website/public/videos/footer-logo-seq/Logo28.webp deleted file mode 100644 index 23fdce9bf2..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo28.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo29.webp b/apps/website/public/videos/footer-logo-seq/Logo29.webp deleted file mode 100644 index b2491f32ed..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo29.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo30.webp b/apps/website/public/videos/footer-logo-seq/Logo30.webp deleted file mode 100644 index 132311d6d7..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo30.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo31.webp b/apps/website/public/videos/footer-logo-seq/Logo31.webp deleted file mode 100644 index 2a7517bb71..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo31.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo32.webp b/apps/website/public/videos/footer-logo-seq/Logo32.webp deleted file mode 100644 index 293b3ec8e7..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo32.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo33.webp b/apps/website/public/videos/footer-logo-seq/Logo33.webp deleted file mode 100644 index 4a64f380c0..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo33.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo34.webp b/apps/website/public/videos/footer-logo-seq/Logo34.webp deleted file mode 100644 index d4a5c63ddf..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo34.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo35.webp b/apps/website/public/videos/footer-logo-seq/Logo35.webp deleted file mode 100644 index d23094d4a1..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo35.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo36.webp b/apps/website/public/videos/footer-logo-seq/Logo36.webp deleted file mode 100644 index cae43c76e4..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo36.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo37.webp b/apps/website/public/videos/footer-logo-seq/Logo37.webp deleted file mode 100644 index 7d91915458..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo37.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo38.webp b/apps/website/public/videos/footer-logo-seq/Logo38.webp deleted file mode 100644 index 1f54724b9b..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo38.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo39.webp b/apps/website/public/videos/footer-logo-seq/Logo39.webp deleted file mode 100644 index dfbe642c49..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo39.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo40.webp b/apps/website/public/videos/footer-logo-seq/Logo40.webp deleted file mode 100644 index 1b0a7b0e66..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo40.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo41.webp b/apps/website/public/videos/footer-logo-seq/Logo41.webp deleted file mode 100644 index b8ce0167cf..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo41.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo42.webp b/apps/website/public/videos/footer-logo-seq/Logo42.webp deleted file mode 100644 index 00287026e5..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo42.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo43.webp b/apps/website/public/videos/footer-logo-seq/Logo43.webp deleted file mode 100644 index 68b4fa26be..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo43.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo44.webp b/apps/website/public/videos/footer-logo-seq/Logo44.webp deleted file mode 100644 index d04c72c72f..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo44.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo45.webp b/apps/website/public/videos/footer-logo-seq/Logo45.webp deleted file mode 100644 index fec1704086..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo45.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo46.webp b/apps/website/public/videos/footer-logo-seq/Logo46.webp deleted file mode 100644 index 2f264ad203..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo46.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo47.webp b/apps/website/public/videos/footer-logo-seq/Logo47.webp deleted file mode 100644 index 9bef152016..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo47.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo48.webp b/apps/website/public/videos/footer-logo-seq/Logo48.webp deleted file mode 100644 index 0fdd7e6402..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo48.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo49.webp b/apps/website/public/videos/footer-logo-seq/Logo49.webp deleted file mode 100644 index ede3d38e99..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo49.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo50.webp b/apps/website/public/videos/footer-logo-seq/Logo50.webp deleted file mode 100644 index ea98bf15ca..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo50.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo51.webp b/apps/website/public/videos/footer-logo-seq/Logo51.webp deleted file mode 100644 index ba709840d6..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo51.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo52.webp b/apps/website/public/videos/footer-logo-seq/Logo52.webp deleted file mode 100644 index f52ed6ec10..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo52.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo53.webp b/apps/website/public/videos/footer-logo-seq/Logo53.webp deleted file mode 100644 index 9bbbd52a5f..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo53.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo54.webp b/apps/website/public/videos/footer-logo-seq/Logo54.webp deleted file mode 100644 index 6f2e5c7901..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo54.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo55.webp b/apps/website/public/videos/footer-logo-seq/Logo55.webp deleted file mode 100644 index 825af5809f..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo55.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo56.webp b/apps/website/public/videos/footer-logo-seq/Logo56.webp deleted file mode 100644 index 7d7964f1e6..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo56.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo57.webp b/apps/website/public/videos/footer-logo-seq/Logo57.webp deleted file mode 100644 index 96a1a793ad..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo57.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo58.webp b/apps/website/public/videos/footer-logo-seq/Logo58.webp deleted file mode 100644 index 437dd3d2d4..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo58.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo59.webp b/apps/website/public/videos/footer-logo-seq/Logo59.webp deleted file mode 100644 index 76ba6f8c04..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo59.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo60.webp b/apps/website/public/videos/footer-logo-seq/Logo60.webp deleted file mode 100644 index dd6c659025..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo60.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo61.webp b/apps/website/public/videos/footer-logo-seq/Logo61.webp deleted file mode 100644 index 42d6830be5..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo61.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo62.webp b/apps/website/public/videos/footer-logo-seq/Logo62.webp deleted file mode 100644 index a6e2729e42..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo62.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo63.webp b/apps/website/public/videos/footer-logo-seq/Logo63.webp deleted file mode 100644 index dbf2f7a9fd..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo63.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo64.webp b/apps/website/public/videos/footer-logo-seq/Logo64.webp deleted file mode 100644 index 778bd67aab..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo64.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo65.webp b/apps/website/public/videos/footer-logo-seq/Logo65.webp deleted file mode 100644 index e4d17385de..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo65.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo66.webp b/apps/website/public/videos/footer-logo-seq/Logo66.webp deleted file mode 100644 index 6bad22efc1..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo66.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo67.webp b/apps/website/public/videos/footer-logo-seq/Logo67.webp deleted file mode 100644 index 33068ba6a1..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo67.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo68.webp b/apps/website/public/videos/footer-logo-seq/Logo68.webp deleted file mode 100644 index 1485172f7e..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo68.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo69.webp b/apps/website/public/videos/footer-logo-seq/Logo69.webp deleted file mode 100644 index a679a0cc5e..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo69.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo70.webp b/apps/website/public/videos/footer-logo-seq/Logo70.webp deleted file mode 100644 index 0027fde458..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo70.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo71.webp b/apps/website/public/videos/footer-logo-seq/Logo71.webp deleted file mode 100644 index fabb774a60..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo71.webp and /dev/null differ diff --git a/apps/website/public/videos/footer-logo-seq/Logo72.webp b/apps/website/public/videos/footer-logo-seq/Logo72.webp deleted file mode 100644 index ab3a4895e1..0000000000 Binary files a/apps/website/public/videos/footer-logo-seq/Logo72.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo00.webp b/apps/website/public/videos/hero-logo-seq/Logo00.webp deleted file mode 100644 index 5a8af2f794..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo00.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo01.webp b/apps/website/public/videos/hero-logo-seq/Logo01.webp deleted file mode 100644 index 1a4665d739..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo01.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo02.webp b/apps/website/public/videos/hero-logo-seq/Logo02.webp deleted file mode 100644 index 55b2580824..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo02.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo03.webp b/apps/website/public/videos/hero-logo-seq/Logo03.webp deleted file mode 100644 index 875b3810e4..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo03.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo04.webp b/apps/website/public/videos/hero-logo-seq/Logo04.webp deleted file mode 100644 index 6f668f5a29..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo04.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo05.webp b/apps/website/public/videos/hero-logo-seq/Logo05.webp deleted file mode 100644 index c24f0500c7..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo05.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo06.webp b/apps/website/public/videos/hero-logo-seq/Logo06.webp deleted file mode 100644 index b27aa683e6..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo06.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo07.webp b/apps/website/public/videos/hero-logo-seq/Logo07.webp deleted file mode 100644 index e3357f8203..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo07.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo08.webp b/apps/website/public/videos/hero-logo-seq/Logo08.webp deleted file mode 100644 index 93e5f6d4eb..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo08.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo09.webp b/apps/website/public/videos/hero-logo-seq/Logo09.webp deleted file mode 100644 index bb50a02364..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo09.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo10.webp b/apps/website/public/videos/hero-logo-seq/Logo10.webp deleted file mode 100644 index d2a6d8b433..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo10.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo11.webp b/apps/website/public/videos/hero-logo-seq/Logo11.webp deleted file mode 100644 index 6017ec869b..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo11.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo12.webp b/apps/website/public/videos/hero-logo-seq/Logo12.webp deleted file mode 100644 index edea70da33..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo12.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo13.webp b/apps/website/public/videos/hero-logo-seq/Logo13.webp deleted file mode 100644 index b4b23662e7..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo13.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo14.webp b/apps/website/public/videos/hero-logo-seq/Logo14.webp deleted file mode 100644 index 1f1e515543..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo14.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo15.webp b/apps/website/public/videos/hero-logo-seq/Logo15.webp deleted file mode 100644 index 2dc2c1fdcb..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo15.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo16.webp b/apps/website/public/videos/hero-logo-seq/Logo16.webp deleted file mode 100644 index 3abed7885a..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo16.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo17.webp b/apps/website/public/videos/hero-logo-seq/Logo17.webp deleted file mode 100644 index 985522fd9a..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo17.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo18.webp b/apps/website/public/videos/hero-logo-seq/Logo18.webp deleted file mode 100644 index 27f673a7b3..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo18.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo19.webp b/apps/website/public/videos/hero-logo-seq/Logo19.webp deleted file mode 100644 index 72b7777fb1..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo19.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo20.webp b/apps/website/public/videos/hero-logo-seq/Logo20.webp deleted file mode 100644 index b2b7c31f4d..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo20.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo21.webp b/apps/website/public/videos/hero-logo-seq/Logo21.webp deleted file mode 100644 index 69ad85865f..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo21.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo22.webp b/apps/website/public/videos/hero-logo-seq/Logo22.webp deleted file mode 100644 index c4e839e21a..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo22.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo23.webp b/apps/website/public/videos/hero-logo-seq/Logo23.webp deleted file mode 100644 index 1de04f36e9..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo23.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo24.webp b/apps/website/public/videos/hero-logo-seq/Logo24.webp deleted file mode 100644 index 368f39f909..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo24.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo25.webp b/apps/website/public/videos/hero-logo-seq/Logo25.webp deleted file mode 100644 index df1a8aa5ae..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo25.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo26.webp b/apps/website/public/videos/hero-logo-seq/Logo26.webp deleted file mode 100644 index 92809d468d..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo26.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo27.webp b/apps/website/public/videos/hero-logo-seq/Logo27.webp deleted file mode 100644 index f5b7daa3c7..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo27.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo28.webp b/apps/website/public/videos/hero-logo-seq/Logo28.webp deleted file mode 100644 index d0c6022916..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo28.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo29.webp b/apps/website/public/videos/hero-logo-seq/Logo29.webp deleted file mode 100644 index dad37c6c9c..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo29.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo30.webp b/apps/website/public/videos/hero-logo-seq/Logo30.webp deleted file mode 100644 index 096b12096a..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo30.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo31.webp b/apps/website/public/videos/hero-logo-seq/Logo31.webp deleted file mode 100644 index 6786649bbf..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo31.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo32.webp b/apps/website/public/videos/hero-logo-seq/Logo32.webp deleted file mode 100644 index 3ad1e165ca..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo32.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo33.webp b/apps/website/public/videos/hero-logo-seq/Logo33.webp deleted file mode 100644 index d548fb3869..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo33.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo34.webp b/apps/website/public/videos/hero-logo-seq/Logo34.webp deleted file mode 100644 index b93e424332..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo34.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo35.webp b/apps/website/public/videos/hero-logo-seq/Logo35.webp deleted file mode 100644 index 7bae894363..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo35.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo36.webp b/apps/website/public/videos/hero-logo-seq/Logo36.webp deleted file mode 100644 index 5326f60c83..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo36.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo37.webp b/apps/website/public/videos/hero-logo-seq/Logo37.webp deleted file mode 100644 index 9a41d58c52..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo37.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo38.webp b/apps/website/public/videos/hero-logo-seq/Logo38.webp deleted file mode 100644 index ef12408289..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo38.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo39.webp b/apps/website/public/videos/hero-logo-seq/Logo39.webp deleted file mode 100644 index 5d8ff38bb4..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo39.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo40.webp b/apps/website/public/videos/hero-logo-seq/Logo40.webp deleted file mode 100644 index 110ff16810..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo40.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo41.webp b/apps/website/public/videos/hero-logo-seq/Logo41.webp deleted file mode 100644 index c67574a72c..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo41.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo42.webp b/apps/website/public/videos/hero-logo-seq/Logo42.webp deleted file mode 100644 index 820b1f1a9e..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo42.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo43.webp b/apps/website/public/videos/hero-logo-seq/Logo43.webp deleted file mode 100644 index 7271487d06..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo43.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo44.webp b/apps/website/public/videos/hero-logo-seq/Logo44.webp deleted file mode 100644 index a6a44876eb..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo44.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo45.webp b/apps/website/public/videos/hero-logo-seq/Logo45.webp deleted file mode 100644 index 55a517c576..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo45.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo46.webp b/apps/website/public/videos/hero-logo-seq/Logo46.webp deleted file mode 100644 index a2da0b2b89..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo46.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo47.webp b/apps/website/public/videos/hero-logo-seq/Logo47.webp deleted file mode 100644 index 1b4ad289f0..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo47.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo48.webp b/apps/website/public/videos/hero-logo-seq/Logo48.webp deleted file mode 100644 index e586ca6a75..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo48.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo49.webp b/apps/website/public/videos/hero-logo-seq/Logo49.webp deleted file mode 100644 index fc6fed8cf6..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo49.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo50.webp b/apps/website/public/videos/hero-logo-seq/Logo50.webp deleted file mode 100644 index dcdec2b9b2..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo50.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo51.webp b/apps/website/public/videos/hero-logo-seq/Logo51.webp deleted file mode 100644 index 8ff8929b0e..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo51.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo52.webp b/apps/website/public/videos/hero-logo-seq/Logo52.webp deleted file mode 100644 index 47fbe15059..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo52.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo53.webp b/apps/website/public/videos/hero-logo-seq/Logo53.webp deleted file mode 100644 index b4db489c79..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo53.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo54.webp b/apps/website/public/videos/hero-logo-seq/Logo54.webp deleted file mode 100644 index 70fc036b57..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo54.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo55.webp b/apps/website/public/videos/hero-logo-seq/Logo55.webp deleted file mode 100644 index 3777287468..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo55.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo56.webp b/apps/website/public/videos/hero-logo-seq/Logo56.webp deleted file mode 100644 index bb845dc26a..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo56.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo57.webp b/apps/website/public/videos/hero-logo-seq/Logo57.webp deleted file mode 100644 index a291121054..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo57.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo58.webp b/apps/website/public/videos/hero-logo-seq/Logo58.webp deleted file mode 100644 index 1336e1a02b..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo58.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo59.webp b/apps/website/public/videos/hero-logo-seq/Logo59.webp deleted file mode 100644 index 77cc784d9c..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo59.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo60.webp b/apps/website/public/videos/hero-logo-seq/Logo60.webp deleted file mode 100644 index 3b5568586e..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo60.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo61.webp b/apps/website/public/videos/hero-logo-seq/Logo61.webp deleted file mode 100644 index 0d53689080..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo61.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo62.webp b/apps/website/public/videos/hero-logo-seq/Logo62.webp deleted file mode 100644 index 119702596c..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo62.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo63.webp b/apps/website/public/videos/hero-logo-seq/Logo63.webp deleted file mode 100644 index 084bf82382..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo63.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo64.webp b/apps/website/public/videos/hero-logo-seq/Logo64.webp deleted file mode 100644 index 5f692bd9c3..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo64.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo65.webp b/apps/website/public/videos/hero-logo-seq/Logo65.webp deleted file mode 100644 index e973ef53b5..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo65.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo66.webp b/apps/website/public/videos/hero-logo-seq/Logo66.webp deleted file mode 100644 index 33ad4c8735..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo66.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo67.webp b/apps/website/public/videos/hero-logo-seq/Logo67.webp deleted file mode 100644 index b60647c8a7..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo67.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo68.webp b/apps/website/public/videos/hero-logo-seq/Logo68.webp deleted file mode 100644 index d587a2b647..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo68.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo69.webp b/apps/website/public/videos/hero-logo-seq/Logo69.webp deleted file mode 100644 index 08c1a1c68d..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo69.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo70.webp b/apps/website/public/videos/hero-logo-seq/Logo70.webp deleted file mode 100644 index 7e4ba4f797..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo70.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo71.webp b/apps/website/public/videos/hero-logo-seq/Logo71.webp deleted file mode 100644 index 9ecb064371..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo71.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo72.webp b/apps/website/public/videos/hero-logo-seq/Logo72.webp deleted file mode 100644 index f838a1cc3f..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo72.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo73.webp b/apps/website/public/videos/hero-logo-seq/Logo73.webp deleted file mode 100644 index 1f134284c7..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo73.webp and /dev/null differ diff --git a/apps/website/public/videos/hero-logo-seq/Logo74.webp b/apps/website/public/videos/hero-logo-seq/Logo74.webp deleted file mode 100644 index b4b07d3d3f..0000000000 Binary files a/apps/website/public/videos/hero-logo-seq/Logo74.webp and /dev/null differ diff --git a/apps/website/scripts/README.md b/apps/website/scripts/README.md new file mode 100644 index 0000000000..3420245b11 --- /dev/null +++ b/apps/website/scripts/README.md @@ -0,0 +1,83 @@ +# Website Scripts + +## `refresh-ashby-snapshot.ts` + +Pulls the latest job postings from Ashby and writes +`src/data/ashby-roles.snapshot.json`. Invoked by the `Release: Website` +GitHub Actions workflow; also runnable locally via +`pnpm --filter @comfyorg/website ashby:refresh-snapshot`. + +## `process-videos.sh` + +Generates multi-resolution VP9/WebM + H.264/MP4 variants and a poster +frame for marketing videos using `ffmpeg`. Run **locally** before +uploading the outputs to `media.comfy.org`; this is not wired into CI. + +```sh +apps/website/scripts/process-videos.sh \ + ./video-sources \ + ./dist/videos \ + "640 960 1280 1920" +``` + +### Output + +For each source video at `./video-sources/foo.mp4`, you get: + +```text +foo-640.webm foo-640.mp4 +foo-960.webm foo-960.mp4 +foo-1280.webm foo-1280.mp4 +foo-1920.webm foo-1920.mp4 +foo-poster.jpg +``` + +The naming convention is enforced by `buildVideoSources()` in +`src/utils/video.ts`, which the `` Vue component uses to +emit `` URLs. + +### Pairing with `` + +Once the assets are uploaded, render them with: + +```vue + +``` + +### `` vs `` + +- **`SiteVideo`** — lightweight multi-source `