From f176d18fe0bbd82f39de613785f0429c8985413d Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Thu, 14 May 2026 02:49:00 -0700 Subject: [PATCH] fix(website): refresh cloud nodes snapshot in release workflow + strict production builds (#12219) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *PR Created by the Glary-Bot Agent* --- ## Summary `Release: Website` only refreshed the Ashby snapshot, so the cloud-nodes snapshot (`apps/website/src/data/cloud-nodes.snapshot.json`) was stale on every release. `loadPacksForBuild()` then silently fell back to that snapshot because `WEBSITE_CLOUD_API_KEY` was never plumbed through CI or Vercel, leaving production at `/cloud/supported-nodes` with placeholder data (e.g. `rgthree-comfy` listed as supported when it isn't — visible at line 104 of the committed snapshot, last fetched 2026-05-04). ## Changes - **New composite action `.github/actions/cloud-nodes-pull`** mirroring `ashby-pull`: runs `pnpm --filter @comfyorg/website cloud-nodes:refresh-snapshot` with `WEBSITE_CLOUD_API_KEY`. The script already `process.exit(1)`s on any non-`fresh` outcome, so refresh failures are loud. - **`release-website.yaml`** now runs both refreshes and opens a single PR with both updated snapshots. Renamed the job to `refresh-snapshots`, updated branch/commit/title/body for the wider scope, and kept the existing `Release:Website` label so downstream automation is unaffected. - **`cloudNodes.build.ts`** throws when the outcome is `'stale'` **and** `VERCEL_ENV === 'production'`. Preview / local builds keep the snapshot fallback so contributors without key access are unaffected. The CI reporter still runs first so the GitHub annotation explaining *why* it's stale is visible in the failed job. - **`ci-vercel-website-preview.yaml`**: passes `WEBSITE_CLOUD_API_KEY` to `vercel build` in both preview and production jobs, and adds a preflight step on `deploy-production` that hard-fails before `vercel build --prod` if the secret is missing — surfacing config drift with a maintainer-friendly error annotation instead of mid-build. - **`apps/website/README.md`**: documents the production-strictness behavior, the new required secret (GitHub Actions + Vercel env), and the manual refresh path. - **New unit tests** in `cloudNodes.build.test.ts` (6 cases): fresh, stale-no-VERCEL_ENV, stale-on-preview, stale-on-production, failed-regardless, and "still reports on stale-in-production before throwing". ## Manual / one-time steps required before merging This PR cannot finish the job alone. A maintainer must also: 1. Add `WEBSITE_CLOUD_API_KEY` as a **GitHub Actions repo secret** in `Comfy-Org/ComfyUI_frontend`. 2. Add `WEBSITE_CLOUD_API_KEY` to the **Vercel project environment** (`production` env at minimum; `preview` recommended). 3. Investigate why `rgthree-comfy` is in the current snapshot — either the Cloud API was actually returning it on 2026-05-04, the snapshot was generated against a non-production environment, or it was hand-edited. The first manual run of `Release: Website` after this PR merges will confirm. Without step 1, the new `Release: Website` job will fail loudly (the refresh script exits 1 with `missing WEBSITE_CLOUD_API_KEY`). Without step 2, the new preflight will fail the production deploy with a clear error annotation pointing at `apps/website/README.md`. Both failure modes are intentional — they replace today's silent stale snapshot. ## Related (out of scope for this PR) The other half of the original report — production 404s on `/p/supported-models/*`, `/cloud/supported-nodes/*`, `/demos/community-workflows` from PRs #11892 / #11903 / #11942 — is a `comfy-router` allow-list gap (those paths exist in the Vercel build as pre-rendered static HTML). That fix needs to land in `Comfy-Org/comfy-router` and is being handled separately since glary doesn't have access to that repo. ## Verification - `pnpm --filter @comfyorg/website test:unit` — 75/75 pass (6 new in `cloudNodes.build.test.ts`) - `pnpm --filter @comfyorg/website typecheck` — 0 errors, 0 warnings (2 pre-existing hints unrelated to this PR) - `pnpm format` + `pnpm exec eslint` on changed files — clean - `js-yaml` validates `release-website.yaml`, `cloud-nodes-pull/action.yaml`, `ci-vercel-website-preview.yaml` - Oracle code review (round 1) raised 1 warning + 1 suggestion; both addressed in commit 2. **Manual verification not applicable**: the runtime changes are GitHub Actions workflows and a Vercel-env-gated branch in a build-time module — they cannot meaningfully run outside of GitHub Actions / Vercel, and the strict-on-stale path is exhaustively covered by the 6 unit tests (including the exact assertions a manual run would check: throws on `VERCEL_ENV=production` + stale, passes on preview, reports observability annotation before throwing). The end-to-end behavior will be verified by the first `Release: Website` dispatch and the next production deploy after the maintainer adds the secret. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12219-fix-website-refresh-cloud-nodes-snapshot-in-release-workflow-strict-production-build-35f6d73d3650816d8f32d403cb39d733) by [Unito](https://www.unito.io) --------- Co-authored-by: glary --- .github/actions/cloud-nodes-pull/action.yaml | 19 +++ .../workflows/ci-vercel-website-preview.yaml | 11 ++ .github/workflows/release-website.yaml | 37 +++-- apps/website/README.md | 38 ++++++ .../src/utils/cloudNodes.build.test.ts | 128 ++++++++++++++++++ apps/website/src/utils/cloudNodes.build.ts | 22 ++- 6 files changed, 240 insertions(+), 15 deletions(-) create mode 100644 .github/actions/cloud-nodes-pull/action.yaml create mode 100644 apps/website/src/utils/cloudNodes.build.test.ts 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/workflows/ci-vercel-website-preview.yaml b/.github/workflows/ci-vercel-website-preview.yaml index 99e2016b47..7e298abb81 100644 --- a/.github/workflows/ci-vercel-website-preview.yaml +++ b/.github/workflows/ci-vercel-website-preview.yaml @@ -58,6 +58,7 @@ jobs: 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 @@ -151,10 +152,20 @@ jobs: - name: Pull Vercel environment information 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 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 diff --git a/.github/workflows/release-website.yaml b/.github/workflows/release-website.yaml index 8ec080bddd..2814d75315 100644 --- a/.github/workflows/release-website.yaml +++ b/.github/workflows/release-website.yaml @@ -1,6 +1,6 @@ -# Description: Manual workflow to refresh the apps/website Ashby roles snapshot -# and open a PR. Merging the PR triggers the existing Vercel website production -# deploy via ci-vercel-website-preview.yaml. +# 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: @@ -11,7 +11,7 @@ concurrency: cancel-in-progress: true jobs: - refresh-snapshot: + refresh-snapshots: if: github.repository == 'Comfy-Org/ComfyUI_frontend' runs-on: ubuntu-latest permissions: @@ -31,28 +31,39 @@ jobs: 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 roles snapshot' - title: 'chore(website): refresh Ashby roles snapshot' + commit-message: 'chore(website): refresh Ashby and cloud nodes snapshots' + title: 'chore(website): refresh Ashby and cloud nodes snapshots' body: | - Automated refresh of `apps/website/src/data/ashby-roles.snapshot.json` - from the Ashby job board API. + 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 snapshot. + 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` remains - intact: builds without `WEBSITE_ASHBY_API_KEY` continue to use the - committed snapshot. + 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-ashby-snapshot-${{ github.run_id }} + branch: chore/refresh-website-snapshots-${{ github.run_id }} base: main labels: | Release:Website diff --git a/apps/website/README.md b/apps/website/README.md index 94468614c4..1b2cc9033d 100644 --- a/apps/website/README.md +++ b/apps/website/README.md @@ -119,6 +119,44 @@ snapshots can't be accidentally committed. Build-time env var: `WEBSITE_CLOUD_API_KEY` (Cloud `/api/object_info` auth; the build falls back to the committed snapshot when unset). Must also be set in the Vercel project environment. +### Production strictness + +`src/utils/cloudNodes.build.ts` throws when `fetchCloudNodesForBuild()` returns +`{ status: 'stale' }` **and** `process.env.VERCEL_ENV === 'production'`. This +prevents the production deploy from silently shipping an out-of-date snapshot +when the Cloud API is unreachable or `WEBSITE_CLOUD_API_KEY` is missing. Preview +and local builds continue to use the committed snapshot with a warning +annotation. + +### Required GitHub Actions / Vercel secrets + +| Name | Where | Purpose | +| ----------------------- | ----------------------------------------------- | ---------------------------------------------------------------------- | +| `WEBSITE_CLOUD_API_KEY` | GitHub Actions repo secret + Vercel project env | Auth for Cloud `/api/object_info`. Required for fresh production data. | + +The `Release: Website` workflow uses the GitHub Actions secret to regenerate +`apps/website/src/data/cloud-nodes.snapshot.json` via +`.github/actions/cloud-nodes-pull/action.yaml`. The Vercel environment value is +read at build time by `vercel build` in `ci-vercel-website-preview.yaml`; the +`deploy-production` job hard-fails before `vercel build --prod` if the secret +is missing. + +### Refreshing the snapshot + +To update the committed snapshot manually (e.g. after onboarding new packs +to Comfy Cloud): + +```bash +WEBSITE_CLOUD_API_KEY=… \ + pnpm --filter @comfyorg/website cloud-nodes:refresh-snapshot +git commit apps/website/src/data/cloud-nodes.snapshot.json +``` + +The script exits non-zero on any non-fresh outcome so stale/empty snapshots +can't be accidentally committed. Otherwise the `Release: Website` GitHub +Actions workflow runs the same step on every manual dispatch and opens a PR +with the refreshed snapshot. + ## HubSpot contact form The contact page uses HubSpot's hosted form embed for the interest form: diff --git a/apps/website/src/utils/cloudNodes.build.test.ts b/apps/website/src/utils/cloudNodes.build.test.ts new file mode 100644 index 0000000000..0359987e19 --- /dev/null +++ b/apps/website/src/utils/cloudNodes.build.test.ts @@ -0,0 +1,128 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import type { FetchOutcome } from './cloudNodes' +import type { NodesSnapshot } from '../data/cloudNodes' + +const fetchCloudNodesMock = vi.hoisted(() => + vi.fn<() => Promise>() +) +const reportCloudNodesOutcomeMock = vi.hoisted(() => vi.fn()) + +vi.mock('./cloudNodes', () => ({ + fetchCloudNodesForBuild: fetchCloudNodesMock +})) + +vi.mock('./cloudNodes.ci', () => ({ + reportCloudNodesOutcome: reportCloudNodesOutcomeMock +})) + +import { loadPacksForBuild } from './cloudNodes.build' + +const SNAPSHOT: NodesSnapshot = { + fetchedAt: '2026-04-01T00:00:00.000Z', + packs: [ + { + id: 'snapshot-pack', + displayName: 'Snapshot Pack', + nodes: [ + { name: 'SnapshotNode', displayName: 'Snapshot Node', category: 'x' } + ] + } + ] +} + +describe('loadPacksForBuild', () => { + const savedVercelEnv = process.env.VERCEL_ENV + + beforeEach(() => { + fetchCloudNodesMock.mockReset() + reportCloudNodesOutcomeMock.mockReset() + delete process.env.VERCEL_ENV + }) + + afterEach(() => { + if (savedVercelEnv === undefined) { + delete process.env.VERCEL_ENV + return + } + process.env.VERCEL_ENV = savedVercelEnv + }) + + it('returns packs when fetch is fresh', async () => { + fetchCloudNodesMock.mockResolvedValue({ + status: 'fresh', + snapshot: SNAPSHOT, + droppedCount: 0, + droppedNodes: [] + }) + + const packs = await loadPacksForBuild() + expect(packs).toBe(SNAPSHOT.packs) + expect(reportCloudNodesOutcomeMock).toHaveBeenCalledTimes(1) + }) + + it('returns snapshot packs when outcome is stale outside production', async () => { + fetchCloudNodesMock.mockResolvedValue({ + status: 'stale', + snapshot: SNAPSHOT, + reason: 'missing WEBSITE_CLOUD_API_KEY' + }) + + const packs = await loadPacksForBuild() + expect(packs).toBe(SNAPSHOT.packs) + expect(reportCloudNodesOutcomeMock).toHaveBeenCalledTimes(1) + }) + + it('returns snapshot packs when outcome is stale on Vercel preview', async () => { + process.env.VERCEL_ENV = 'preview' + fetchCloudNodesMock.mockResolvedValue({ + status: 'stale', + snapshot: SNAPSHOT, + reason: 'HTTP 503' + }) + + const packs = await loadPacksForBuild() + expect(packs).toBe(SNAPSHOT.packs) + expect(reportCloudNodesOutcomeMock).toHaveBeenCalledTimes(1) + }) + + it('throws when outcome is stale on Vercel production', async () => { + process.env.VERCEL_ENV = 'production' + fetchCloudNodesMock.mockResolvedValue({ + status: 'stale', + snapshot: SNAPSHOT, + reason: 'missing WEBSITE_CLOUD_API_KEY' + }) + + await expect(loadPacksForBuild()).rejects.toThrow( + /stale data in a production build/ + ) + await expect(loadPacksForBuild()).rejects.toThrow( + /missing WEBSITE_CLOUD_API_KEY/ + ) + }) + + it('throws when outcome is failed regardless of environment', async () => { + fetchCloudNodesMock.mockResolvedValue({ + status: 'failed', + reason: 'network error: ECONNREFUSED' + }) + + await expect(loadPacksForBuild()).rejects.toThrow( + /Cloud nodes fetch failed and no snapshot is available/ + ) + await expect(loadPacksForBuild()).rejects.toThrow(/ECONNREFUSED/) + }) + + it('still reports outcome before throwing on stale-in-production', async () => { + process.env.VERCEL_ENV = 'production' + fetchCloudNodesMock.mockResolvedValue({ + status: 'stale', + snapshot: SNAPSHOT, + reason: 'HTTP 503' + }) + + await expect(loadPacksForBuild()).rejects.toThrow() + expect(reportCloudNodesOutcomeMock).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/website/src/utils/cloudNodes.build.ts b/apps/website/src/utils/cloudNodes.build.ts index d874adf80c..12ae56828d 100644 --- a/apps/website/src/utils/cloudNodes.build.ts +++ b/apps/website/src/utils/cloudNodes.build.ts @@ -3,6 +3,14 @@ import type { Pack } from '../data/cloudNodes' import { fetchCloudNodesForBuild } from './cloudNodes' import { reportCloudNodesOutcome } from './cloudNodes.ci' +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.' + +function isProductionBuild(): boolean { + return process.env.VERCEL_ENV === 'production' +} + /** * Resolve the list of packs to render at build time. * @@ -11,6 +19,10 @@ import { reportCloudNodesOutcome } from './cloudNodes.ci' * same source. `fetchCloudNodesForBuild` is memoized on a module-level * `inflight` promise, so repeated calls in the same build process share a * single network round-trip and the same outcome. + * + * 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. */ export async function loadPacksForBuild(): Promise { const outcome = await fetchCloudNodesForBuild() @@ -18,8 +30,14 @@ export async function loadPacksForBuild(): Promise { if (outcome.status === 'failed') { throw new Error( - `Cloud nodes fetch failed and no snapshot is available. Reason: ${outcome.reason}. ` + - 'Run `pnpm --filter @comfyorg/website cloud-nodes:refresh-snapshot` locally and commit the snapshot.' + `Cloud nodes fetch failed and no snapshot is available. Reason: ${outcome.reason}. ${REFRESH_HINT}` + ) + } + + if (outcome.status === 'stale' && isProductionBuild()) { + throw new Error( + `Cloud nodes fetch returned stale data in a production build (VERCEL_ENV=production). ` + + `Reason: ${outcome.reason}. ${REFRESH_HINT}` ) }