Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci-website-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jobs:
- name: Build website
env:
WEBSITE_GITHUB_STARS_OVERRIDE: 110000
WEBSITE_CLOUD_NODES_FIXTURE: e2e/fixtures/cloud-nodes.fixture.json
run: pnpm --filter @comfyorg/website build

- name: Run Playwright tests
Expand Down
156 changes: 156 additions & 0 deletions apps/website/e2e/fixtures/cloud-nodes.fixture.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
{
"fetchedAt": "2026-01-01T00:00:00.000Z",
"packs": [
{
"id": "comfyui-impact-pack",
"registryId": "comfyui-impact-pack",
"displayName": "ComfyUI Impact Pack",
"description": "Production-grade detailer, detector, and SEG (segmentation) tooling. The most-used pack for face restoration, region-based refinement, and iterative upscaling on Comfy Cloud.",
"bannerUrl": "https://media.comfy.org/cloud-nodes/comfyui-impact-pack-banner.webp",
"iconUrl": "https://media.comfy.org/cloud-nodes/comfyui-impact-pack-icon.webp",
"repoUrl": "https://github.com/ltdrdata/ComfyUI-Impact-Pack",
"publisher": {
"id": "drltdata",
"name": "Dr.Lt.Data"
},
"downloads": 2618646,
"githubStars": 3092,
"latestVersion": "8.28.3",
"license": "GPL-3.0",
"lastUpdated": "2026-04-19T17:08:04.993918Z",
"nodes": [
{
"name": "FaceDetailer",
"displayName": "FaceDetailer",
"category": "ImpactPack/Detailer",
"description": "Detect and refine faces with iterative passes."
},
{
"name": "DetailerForEach",
"displayName": "DetailerForEach",
"category": "ImpactPack/Detailer",
"description": "Run iterative detail refinement over detected SEG regions."
},
{
"name": "UltralyticsDetectorProvider",
"displayName": "UltralyticsDetectorProvider",
"category": "ImpactPack/Detector",
"description": "Provide detector models powered by Ultralytics YOLO."
}
]
},
{
"id": "comfyui-crystools",
"registryId": "ComfyUI-Crystools",
"displayName": "ComfyUI-Crystools",
"description": "Live system monitoring (GPU, RAM, disk) and rich image inspection inside your workflow.",
"bannerUrl": "https://media.comfy.org/cloud-nodes/comfyui-crystools-banner.webp",
"iconUrl": "https://media.comfy.org/cloud-nodes/comfyui-crystools-icon.webp",
"repoUrl": "https://github.com/crystian/ComfyUI-Crystools",
"publisher": {
"id": "crystian",
"name": "Crystian"
},
"downloads": 1671447,
"githubStars": 1855,
"latestVersion": "1.27.4",
"license": "MIT",
"lastUpdated": "2025-10-26T19:11:09.943366Z",
"nodes": [
{
"name": "CCrystools_Show_Resources",
"displayName": "CCrystools_Show_Resources",
"category": "crystools/show",
"description": "Display GPU, RAM and disk usage live in the workflow."
},
{
"name": "CCrystools_Show_Image",
"displayName": "CCrystools_Show_Image",
"category": "crystools/show",
"description": "Inspect images at full resolution with metadata overlays."
}
]
},
{
"id": "alpha-test-pack",
"registryId": "alpha-test-pack",
"displayName": "Alpha Test Pack",
"description": "Deterministic fixture pack used to anchor the A\u2192Z sort assertion in Playwright. Its display name starts with 'A' so it surfaces first when sorted alphabetically.",
"bannerUrl": "https://media.comfy.org/cloud-nodes/alpha-test-pack-banner.webp",
"repoUrl": "https://github.com/Comfy-Org/alpha-test-pack",
"publisher": {
"id": "comfy-org",
"name": "Comfy Org"
},
"downloads": 42,
"githubStars": 7,
"latestVersion": "0.1.0",
"license": "MIT",
"lastUpdated": "2026-01-01T00:00:00.000Z",
"nodes": [
{
"name": "AlphaProbe",
"displayName": "AlphaProbe",
"category": "alpha/test",
"description": "Deterministic node used to verify alphabetical ordering."
}
]
},
{
"id": "rgthree-comfy",
"registryId": "rgthree-comfy",
"displayName": "rgthree-comfy",
"description": "Quality-of-life nodes from rgthree: Power Lora Loader, Display Any, Image Comparer, and more.",
"bannerUrl": "https://media.comfy.org/cloud-nodes/rgthree-comfy-banner.webp",
"repoUrl": "https://github.com/rgthree/rgthree-comfy",
"publisher": {
"id": "rgthree",
"name": "rgthree"
},
"downloads": 987654,
"githubStars": 1500,
"latestVersion": "2.0.0",
"license": "MIT",
"lastUpdated": "2026-03-15T00:00:00.000Z",
"nodes": [
{
"name": "PowerLoraLoader",
"displayName": "Power Lora Loader",
"category": "rgthree/loaders",
"description": "Load multiple LoRAs at once with strength control."
},
{
"name": "DisplayAny",
"displayName": "Display Any",
"category": "rgthree/display",
"description": "Display the value of any wire for debugging."
}
]
},
{
"id": "was-node-suite-comfyui",
"registryId": "was-node-suite-comfyui",
"displayName": "WAS Node Suite",
"description": "Large collection of utility nodes for image processing, text manipulation, and workflow control.",
"bannerUrl": "https://media.comfy.org/cloud-nodes/was-node-suite-banner.webp",
"repoUrl": "https://github.com/WASasquatch/was-node-suite-comfyui",
"publisher": {
"id": "wasasquatch",
"name": "WASasquatch"
},
"downloads": 1234567,
"githubStars": 1700,
"latestVersion": "1.0.0",
"license": "MIT",
"lastUpdated": "2026-02-01T00:00:00.000Z",
"nodes": [
{
"name": "WASImageBlend",
"displayName": "Image Blend",
"category": "WAS/image",
"description": "Blend two images using a configurable mode."
}
]
}
]
}
12 changes: 6 additions & 6 deletions apps/website/src/data/cloud-nodes.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
]
},
{
"id": "ComfyUI-Crystools",
"id": "comfyui-crystools",
"registryId": "ComfyUI-Crystools",
"displayName": "ComfyUI-Crystools",
"description": "Live system monitoring (GPU, RAM, disk) and rich image inspection inside your workflow. The most-installed quality-of-life pack on the registry.",
Expand Down Expand Up @@ -201,7 +201,7 @@
"id": "comfyui-easy-use",
"registryId": "comfyui-easy-use",
"displayName": "ComfyUI-Easy-Use",
"description": "Simplified, opinionated nodes that bundle common patterns into single drop-ins full loader, pre-sampling, easy KSampler, and XY plotting.",
"description": "Simplified, opinionated nodes that bundle common patterns into single drop-ins \u2014 full loader, pre-sampling, easy KSampler, and XY plotting.",
"iconUrl": "https://mintlify.s3.us-west-1.amazonaws.com/yolain/images/logo.svg",
"repoUrl": "https://github.com/yolain/ComfyUI-Easy-Use",
"publisher": {
Expand Down Expand Up @@ -250,7 +250,7 @@
"id": "comfyui-advanced-controlnet",
"registryId": "comfyui-advanced-controlnet",
"displayName": "ComfyUI-Advanced-ControlNet",
"description": "ControlNet with timestep keyframes, per-frame masks, and advanced strength scheduling essential for animation and batched-latent workflows.",
"description": "ControlNet with timestep keyframes, per-frame masks, and advanced strength scheduling \u2014 essential for animation and batched-latent workflows.",
"repoUrl": "https://github.com/Kosinkadink/ComfyUI-Advanced-ControlNet",
"publisher": {
"id": "kosinkadink",
Expand Down Expand Up @@ -298,7 +298,7 @@
"id": "was-node-suite-comfyui",
"registryId": "was-node-suite-comfyui",
"displayName": "WAS Node Suite",
"description": "A broad utility suite covering image adjustments, compositing, text, math, and I/O the original \"kitchen sink\" pack still relied on by thousands of workflows.",
"description": "A broad utility suite covering image adjustments, compositing, text, math, and I/O \u2014 the original \"kitchen sink\" pack still relied on by thousands of workflows.",
"repoUrl": "https://github.com/WASasquatch/was-node-suite-comfyui",
"publisher": {
"id": "was",
Expand Down Expand Up @@ -343,10 +343,10 @@
]
},
{
"id": "comfyui_ipadapter_plus",
"id": "comfyui-ipadapter-plus",
"registryId": "comfyui_ipadapter_plus",
"displayName": "ComfyUI_IPAdapter_plus",
"description": "Reference-image conditioning with IPAdapter style transfer, Face ID, and multi-image embeddings. The most-installed conditioning pack on the registry, used in countless portrait, product, and animation workflows.",
"description": "Reference-image conditioning with IPAdapter \u2014 style transfer, Face ID, and multi-image embeddings. The most-installed conditioning pack on the registry, used in countless portrait, product, and animation workflows.",
"repoUrl": "https://github.com/cubiq/ComfyUI_IPAdapter_plus",
"publisher": {
"id": "matteo",
Expand Down
61 changes: 58 additions & 3 deletions apps/website/src/utils/cloudNodes.build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import type { FetchOutcome } from './cloudNodes'
import type { NodesSnapshot } from '../data/cloudNodes'

const fetchCloudNodesMock = vi.hoisted(() =>
vi.fn<() => Promise<FetchOutcome>>()
vi.fn<
(options?: { snapshotUrl?: URL; apiKey?: string }) => Promise<FetchOutcome>
>()
)
const reportCloudNodesOutcomeMock = vi.hoisted(() => vi.fn())

Expand Down Expand Up @@ -33,19 +35,26 @@ const SNAPSHOT: NodesSnapshot = {

describe('loadPacksForBuild', () => {
const savedVercelEnv = process.env.VERCEL_ENV
const savedFixture = process.env.WEBSITE_CLOUD_NODES_FIXTURE

beforeEach(() => {
fetchCloudNodesMock.mockReset()
reportCloudNodesOutcomeMock.mockReset()
delete process.env.VERCEL_ENV
delete process.env.WEBSITE_CLOUD_NODES_FIXTURE
})

afterEach(() => {
if (savedVercelEnv === undefined) {
delete process.env.VERCEL_ENV
return
} else {
process.env.VERCEL_ENV = savedVercelEnv
}
if (savedFixture === undefined) {
delete process.env.WEBSITE_CLOUD_NODES_FIXTURE
} else {
process.env.WEBSITE_CLOUD_NODES_FIXTURE = savedFixture
}
process.env.VERCEL_ENV = savedVercelEnv
})

it('returns packs when fetch is fresh', async () => {
Expand Down Expand Up @@ -125,4 +134,50 @@ describe('loadPacksForBuild', () => {
await expect(loadPacksForBuild()).rejects.toThrow()
expect(reportCloudNodesOutcomeMock).toHaveBeenCalledTimes(1)
})

it('forwards WEBSITE_CLOUD_NODES_FIXTURE as snapshotUrl with an empty api key', async () => {
process.env.WEBSITE_CLOUD_NODES_FIXTURE =
'e2e/fixtures/cloud-nodes.fixture.json'
fetchCloudNodesMock.mockResolvedValue({
status: 'stale',
snapshot: SNAPSHOT,
reason: 'missing WEBSITE_CLOUD_API_KEY'
})

await loadPacksForBuild()
const call = fetchCloudNodesMock.mock.calls[0]?.[0]
expect(call?.snapshotUrl).toBeInstanceOf(URL)
expect(call?.snapshotUrl?.protocol).toBe('file:')
expect(call?.snapshotUrl?.pathname).toMatch(
/apps\/website\/e2e\/fixtures\/cloud-nodes\.fixture\.json$/
)
expect(call?.apiKey).toBe('')
})

it('accepts an absolute path for WEBSITE_CLOUD_NODES_FIXTURE', async () => {
process.env.WEBSITE_CLOUD_NODES_FIXTURE = '/etc/cloud-nodes.fixture.json'
fetchCloudNodesMock.mockResolvedValue({
status: 'stale',
snapshot: SNAPSHOT,
reason: 'missing WEBSITE_CLOUD_API_KEY'
})

await loadPacksForBuild()
const call = fetchCloudNodesMock.mock.calls[0]?.[0]
expect(call?.snapshotUrl?.pathname).toBe('/etc/cloud-nodes.fixture.json')
})

it('does not throw on stale-in-production when the fixture override is set', async () => {
process.env.VERCEL_ENV = 'production'
process.env.WEBSITE_CLOUD_NODES_FIXTURE =
'e2e/fixtures/cloud-nodes.fixture.json'
fetchCloudNodesMock.mockResolvedValue({
status: 'stale',
snapshot: SNAPSHOT,
reason: 'missing WEBSITE_CLOUD_API_KEY'
})

const packs = await loadPacksForBuild()
expect(packs).toBe(SNAPSHOT.packs)
})
})
27 changes: 25 additions & 2 deletions apps/website/src/utils/cloudNodes.build.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { isAbsolute, resolve as resolvePath } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'

import type { Pack } from '../data/cloudNodes'

import { fetchCloudNodesForBuild } from './cloudNodes'
Expand All @@ -7,10 +10,21 @@ const REFRESH_HINT =
'Run `pnpm --filter @comfyorg/website cloud-nodes:refresh-snapshot` locally and commit the snapshot, ' +
'or re-run the `Release: Website` workflow with a valid WEBSITE_CLOUD_API_KEY.'

const WEBSITE_PACKAGE_ROOT = fileURLToPath(new URL('../..', import.meta.url))

function isProductionBuild(): boolean {
return process.env.VERCEL_ENV === 'production'
}

function fixtureSnapshotUrl(): URL | undefined {
const fixturePath = process.env.WEBSITE_CLOUD_NODES_FIXTURE
if (!fixturePath) return undefined
const absolute = isAbsolute(fixturePath)
? fixturePath
: resolvePath(WEBSITE_PACKAGE_ROOT, fixturePath)
return pathToFileURL(absolute)
}

/**
* Resolve the list of packs to render at build time.
*
Expand All @@ -23,9 +37,18 @@ function isProductionBuild(): boolean {
* Production builds (VERCEL_ENV=production) fail hard on a stale outcome
* to prevent silently shipping out-of-date snapshot data. Preview and
* local builds continue to use the committed snapshot.
*
* Setting `WEBSITE_CLOUD_NODES_FIXTURE=<path>` overrides the bundled
* snapshot with a fixture file on disk. This is used by the e2e build
* step in CI so Playwright assertions can be written against deterministic
* pack content instead of whatever the upstream registry happens to expose
* at the moment of the test run. The override never fires the live cloud
* API; the fixture path goes straight to the snapshot-fallback branch.
*/
export async function loadPacksForBuild(): Promise<Pack[]> {
const outcome = await fetchCloudNodesForBuild()
const snapshotUrl = fixtureSnapshotUrl()
const options = snapshotUrl ? { snapshotUrl, apiKey: '' } : {}
const outcome = await fetchCloudNodesForBuild(options)
reportCloudNodesOutcome(outcome)

if (outcome.status === 'failed') {
Expand All @@ -34,7 +57,7 @@ export async function loadPacksForBuild(): Promise<Pack[]> {
)
}

if (outcome.status === 'stale' && isProductionBuild()) {
if (outcome.status === 'stale' && isProductionBuild() && !snapshotUrl) {
throw new Error(
`Cloud nodes fetch returned stale data in a production build (VERCEL_ENV=production). ` +
`Reason: ${outcome.reason}. ${REFRESH_HINT}`
Expand Down
Loading
Loading