Skip to content

Commit 32c7f10

Browse files
dante01yoonampagent
andcommitted
Merge origin/main into comfydesigner/enterprise-tier-rendering
Amp-Thread-ID: https://ampcode.com/threads/T-01a02452-d1ab-71a8-a05a-a44cde538f9c Co-authored-by: Amp <amp@ampcode.com>
2 parents b38acfa + 848cd39 commit 32c7f10

115 files changed

Lines changed: 4161 additions & 953 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci-oss-assets-validation.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ jobs:
112112
--summary \
113113
--excludePackages '@comfyorg/comfyui-frontend;@comfyorg/design-system;@comfyorg/ingest-types;@comfyorg/registry-types;@comfyorg/shared-frontend-utils;@comfyorg/tailwind-utils;@comfyorg/comfyui-electron-types' \
114114
--clarificationsFile .github/license-clarifications.json \
115-
--onlyAllow 'MIT;MIT*;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;0BSD;BlueOak-1.0.0;Python-2.0;CC0-1.0;Unlicense;(MIT OR Apache-2.0);(MIT OR GPL-3.0);(Apache-2.0 OR MIT);(MPL-2.0 OR Apache-2.0);CC-BY-4.0;CC-BY-3.0;GPL-3.0-only'; then
115+
--onlyAllow 'MIT;MIT*;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;0BSD;BlueOak-1.0.0;Python-2.0;CC0-1.0;Unlicense;(MIT OR Apache-2.0);(MIT OR GPL-3.0);(Apache-2.0 OR MIT);(MPL-2.0 OR Apache-2.0);MPL-2.0;CC-BY-4.0;CC-BY-3.0;GPL-3.0-only'; then
116116
echo ''
117117
echo '✅ All production dependency licenses are approved!'
118118
else

.github/workflows/release-draft-create.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@ jobs:
106106
ref: ${{ github.event.pull_request.merge_commit_sha }}
107107
secrets: inherit
108108

109+
# GITHUB_TOKEN releases emit no events, so the reconciler's own trigger never fires.
110+
enforce_latest:
111+
name: Enforce Latest Release
112+
needs: draft_release
113+
permissions:
114+
contents: write
115+
uses: ./.github/workflows/release-enforce-latest.yaml
116+
secrets:
117+
PR_GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
118+
109119
comment_release_summary:
110120
name: Comment Release Summary
111121
needs:
@@ -125,6 +135,7 @@ jobs:
125135
fetch-depth: 2
126136

127137
- name: Post release summary comment
138+
continue-on-error: true
128139
uses: ./.github/actions/comment-release-links
129140
with:
130141
issue-number: ${{ github.event.pull_request.number }}
Lines changed: 19 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,38 @@
11
name: Enforce Greatest-Semver-Wins Latest Release
22

3-
# Keeps GitHub's "latest release" flag pinned to the highest stable semver
4-
# tag, so `--front-end-version latest` can never resolve to an older release
5-
# than what's already shipped.
6-
#
7-
# Un-publishing the current latest release is treated as an implicit
8-
# rollback: latest reassigns to the next-highest published stable release.
9-
# This is intentional.
3+
# Un-publishing the latest release rolls Latest back on purpose.
104

115
on:
126
release:
137
types: [published, edited, unpublished, deleted]
148
workflow_dispatch: {}
9+
workflow_call:
10+
secrets:
11+
PR_GH_TOKEN:
12+
required: true
1513
schedule:
16-
- cron: '0 4 * * *' # daily backstop in case a release webhook is dropped
14+
- cron: '0 4 * * *'
1715

18-
# Serialize runs so two near-simultaneous release edits can't race each
19-
# other's `gh release edit --latest` calls. Do NOT cancel-in-progress: a
20-
# queued run still needs to re-check state after the run ahead of it finishes.
2116
concurrency:
2217
group: enforce-latest-release
23-
cancel-in-progress: false
2418

2519
jobs:
2620
enforce-latest:
2721
runs-on: ubuntu-latest
28-
# Defense in depth: GH_TOKEN edits don't retrigger `release` events, but
29-
# this guards against a future PAT/App-token swap that would.
30-
if: github.triggering_actor != 'github-actions[bot]'
22+
# On workflow_call the actor is whoever merged the release PR, often a bot.
23+
if: github.event_name != 'release' || github.triggering_actor != 'github-actions[bot]'
3124
permissions:
32-
contents: write # required: gh release edit / releases API write access
25+
contents: write
3326
steps:
34-
- name: Reconcile "latest" flag to the highest stable semver release
27+
# workflow_call inherits the caller's refs/pull/N/merge, gone once it closes.
28+
- uses: actions/checkout@v7
29+
with:
30+
ref: main
31+
sparse-checkout: scripts/cicd/reconcile-latest-release.sh
32+
sparse-checkout-cone-mode: false
33+
34+
- name: Reconcile Latest to the highest stable semver release
3535
env:
36-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36+
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
3737
REPO: ${{ github.repository }}
38-
run: |
39-
set -euo pipefail
40-
41-
echo "Fetching all releases for $REPO..."
42-
RELEASES_JSON=$(gh release list --repo "$REPO" --limit 1000 \
43-
--json tagName,isDraft,isPrerelease,isLatest)
44-
45-
COUNT=$(echo "$RELEASES_JSON" | jq 'length')
46-
if [ "$COUNT" -ge 1000 ]; then
47-
echo "::warning::Release count hit --limit 1000 cap. Results may be truncated."
48-
fi
49-
50-
STABLE_TAGS=$(echo "$RELEASES_JSON" | jq -r \
51-
'.[] | select(.isDraft == false and .isPrerelease == false) | .tagName')
52-
53-
if [ -z "$STABLE_TAGS" ]; then
54-
echo "::warning::No stable (non-draft, non-prerelease) releases found. '--front-end-version latest' will 404 until one exists."
55-
exit 0
56-
fi
57-
58-
# Defensive filter: only strict [v]X.Y.Z tags -- skips "-rc"/"-beta"
59-
# tags mis-flagged as stable, and other monorepo tags (design-system,
60-
# desktop-ui, npm-types, etc.) that aren't ours.
61-
CANDIDATES=()
62-
while IFS= read -r tag; do
63-
if [[ "$tag" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
64-
CANDIDATES+=("$tag")
65-
fi
66-
done <<< "$STABLE_TAGS"
67-
68-
if [ "${#CANDIDATES[@]}" -eq 0 ]; then
69-
echo "::warning::No semver-shaped stable tags found among releases. '--front-end-version latest' will 404 until one exists."
70-
exit 0
71-
fi
72-
73-
# greatest-semver-wins: sort numerically (v-prefix stripped), not by
74-
# tag name or publish date.
75-
TRUE_LATEST_VER=$(printf '%s\n' "${CANDIDATES[@]}" | sed 's/^v//' | sort -V | tail -1)
76-
77-
TRUE_LATEST_TAG=""
78-
for tag in "${CANDIDATES[@]}"; do
79-
if [[ "${tag#v}" == "$TRUE_LATEST_VER" ]]; then
80-
TRUE_LATEST_TAG="$tag"
81-
break
82-
fi
83-
done
84-
85-
[ -n "$TRUE_LATEST_TAG" ] || { echo "::error::BUG: could not resolve TRUE_LATEST_TAG"; exit 1; }
86-
87-
echo "Highest stable semver release: $TRUE_LATEST_TAG"
88-
89-
CURRENT_LATEST_TAG=$(echo "$RELEASES_JSON" | jq -r \
90-
'[.[] | select(.isLatest == true)][0].tagName // empty')
91-
echo "GitHub-flagged 'latest' release: ${CURRENT_LATEST_TAG:-<none>}"
92-
93-
if [ "$CURRENT_LATEST_TAG" == "$TRUE_LATEST_TAG" ]; then
94-
echo "OK: 'latest' already matches the highest stable semver release. No action needed."
95-
exit 0
96-
fi
97-
98-
echo "::warning::'latest' is currently '${CURRENT_LATEST_TAG:-<none>}' but the highest stable semver release is '$TRUE_LATEST_TAG'. Reassigning 'latest' to '$TRUE_LATEST_TAG'."
99-
100-
gh release edit "$TRUE_LATEST_TAG" --repo "$REPO" --latest
101-
102-
{
103-
echo "## Latest-release auto-correction"
104-
echo ""
105-
echo "- Previously flagged as \`latest\`: \`${CURRENT_LATEST_TAG:-<none>}\`"
106-
echo "- Highest stable semver release: \`$TRUE_LATEST_TAG\`"
107-
echo "- Action taken: re-assigned \`latest\` to \`$TRUE_LATEST_TAG\` via \`gh release edit --latest\`"
108-
} >> "$GITHUB_STEP_SUMMARY"
38+
run: ./scripts/cicd/reconcile-latest-release.sh

.github/workflows/release-weekly-comfyui.yaml

Lines changed: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -179,37 +179,34 @@ jobs:
179179
180180
publish-pypi:
181181
needs: [resolve-version, trigger-release-if-needed]
182+
outputs:
183+
pypi_confirmed: ${{ steps.pypi-wait.outputs.confirmed }}
182184
if: >
183185
always() &&
184186
needs.resolve-version.result == 'success' &&
185187
(needs.trigger-release-if-needed.result == 'success' ||
186188
needs.trigger-release-if-needed.result == 'skipped')
187189
runs-on: ubuntu-latest
188190
steps:
189-
- name: Wait for release PR to be created and merged
191+
- name: Check out release scripts
192+
uses: actions/checkout@v7
193+
with:
194+
sparse-checkout: scripts/cicd
195+
sparse-checkout-cone-mode: false
196+
197+
# The tag checkout below replaces the workspace, and the tag predates these.
198+
- name: Stage release scripts
199+
run: cp -r scripts/cicd "$RUNNER_TEMP/cicd"
200+
201+
- name: Wait for the release tag
190202
if: needs.trigger-release-if-needed.result == 'success'
191203
env:
192204
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
193-
run: |
194-
set -euo pipefail
195-
196-
TARGET_VERSION="${{ needs.resolve-version.outputs.target_version }}"
197-
TARGET_BRANCH="${{ needs.resolve-version.outputs.target_branch }}"
198-
echo "Waiting for version bump PR for v${TARGET_VERSION} on ${TARGET_BRANCH} to be merged..."
199-
200-
# Poll for up to 30 minutes (a human or automation needs to merge the version bump PR)
201-
for i in $(seq 1 60); do
202-
# Check if the tag exists (release-draft-create creates a tag on merge)
203-
if gh api "repos/Comfy-Org/ComfyUI_frontend/git/ref/tags/v${TARGET_VERSION}" --silent 2>/dev/null; then
204-
echo "✅ Tag v${TARGET_VERSION} found — release PR has been merged"
205-
exit 0
206-
fi
207-
echo "Attempt $i/60: Tag v${TARGET_VERSION} not found yet, waiting 30s..."
208-
sleep 30
209-
done
210-
211-
echo "❌ Timed out waiting for tag v${TARGET_VERSION}"
212-
exit 1
205+
REPO: ${{ github.repository }}
206+
TARGET_VERSION: ${{ needs.resolve-version.outputs.target_version }}
207+
TARGET_BRANCH: ${{ needs.resolve-version.outputs.target_branch }}
208+
RUN_ID: ${{ github.run_id }}
209+
run: '$RUNNER_TEMP/cicd/wait-for-release-tag.sh'
213210

214211
- name: Checkout code at target version
215212
uses: actions/checkout@v7
@@ -262,35 +259,20 @@ jobs:
262259
password: ${{ secrets.PYPI_TOKEN }}
263260
packages-dir: comfyui_frontend_package/dist
264261

265-
- name: Wait for PyPI propagation
266-
run: |
267-
set -euo pipefail
268-
269-
TARGET_VERSION="${{ needs.resolve-version.outputs.target_version }}"
270-
PACKAGE="comfyui-frontend-package"
271-
echo "Waiting for ${PACKAGE}==${TARGET_VERSION} to be available on PyPI..."
272-
273-
# Wait up to 15 minutes (polling every 30 seconds)
274-
for i in $(seq 1 30); do
275-
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/${PACKAGE}/${TARGET_VERSION}/json")
276-
if [ "$HTTP_CODE" = "200" ]; then
277-
echo "✅ ${PACKAGE}==${TARGET_VERSION} is available on PyPI"
278-
exit 0
279-
fi
280-
echo "Attempt $i/30: PyPI returned HTTP ${HTTP_CODE}, waiting 30s..."
281-
sleep 30
282-
done
283-
284-
echo "❌ Timed out waiting for ${PACKAGE}==${TARGET_VERSION} on PyPI"
285-
exit 1
262+
- name: Wait for the version to be installable
263+
id: pypi-wait
264+
env:
265+
PACKAGE: comfyui-frontend-package
266+
TARGET_VERSION: ${{ needs.resolve-version.outputs.target_version }}
267+
run: '$RUNNER_TEMP/cicd/wait-for-pypi-version.sh'
286268

287269
- name: Summary
288270
run: |
289271
echo "## PyPI Publishing" >> $GITHUB_STEP_SUMMARY
290272
echo "" >> $GITHUB_STEP_SUMMARY
291273
echo "- Package: comfyui-frontend-package" >> $GITHUB_STEP_SUMMARY
292274
echo "- Version: ${{ needs.resolve-version.outputs.target_version }}" >> $GITHUB_STEP_SUMMARY
293-
echo "- Status: ✅ Published and confirmed available" >> $GITHUB_STEP_SUMMARY
275+
echo "- Installable from PyPI: ${{ steps.pypi-wait.outputs.confirmed }}" >> $GITHUB_STEP_SUMMARY
294276
295277
create-comfyui-pr:
296278
needs:
@@ -372,7 +354,11 @@ jobs:
372354
EOF
373355
)
374356
375-
PYPI_NOTE="✅ **PyPI package confirmed available** — \`comfyui-frontend-package==${{ needs.resolve-version.outputs.target_version }}\` has been published and verified."
357+
if [ "${{ needs.publish-pypi.outputs.pypi_confirmed }}" = "true" ]; then
358+
PYPI_NOTE="✅ **PyPI package confirmed available** — \`comfyui-frontend-package==${{ needs.resolve-version.outputs.target_version }}\` has been published and verified."
359+
else
360+
PYPI_NOTE="⚠️ **PyPI availability unconfirmed** — \`comfyui-frontend-package==${{ needs.resolve-version.outputs.target_version }}\` was not installable from PyPI before the wait expired. CI here may fail until the index catches up."
361+
fi
376362
BODY=$''"${PYPI_NOTE}"$'\n\n'"${BODY}"
377363
378364
# Save to file for later use
@@ -459,3 +445,21 @@ jobs:
459445
echo "" >> $GITHUB_STEP_SUMMARY
460446
echo "### PR Body:" >> $GITHUB_STEP_SUMMARY
461447
cat pr-body.txt >> $GITHUB_STEP_SUMMARY
448+
449+
release-done:
450+
name: Verify release reached users
451+
needs: [resolve-version, publish-pypi]
452+
if: always() && needs.publish-pypi.result == 'success'
453+
runs-on: ubuntu-latest
454+
steps:
455+
- name: Checkout ComfyUI_frontend
456+
uses: actions/checkout@v7
457+
with:
458+
fetch-depth: 0
459+
460+
- name: Assert the release reached users
461+
env:
462+
PACKAGE: comfyui-frontend-package
463+
TARGET_VERSION: ${{ needs.resolve-version.outputs.target_version }}
464+
TARGET_BRANCH: ${{ needs.resolve-version.outputs.target_branch }}
465+
run: ./scripts/cicd/assert-release-shipped.sh

apps/website/e2e/events.spec.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,15 +194,27 @@ test.describe('Events page — desktop @smoke', () => {
194194
await expect(row).toContainText(event.location![locale])
195195
await expect(row).toContainText(event.dateLabel![locale])
196196

197-
const livestreamLink = row.getByRole('link', {
198-
name: new RegExp(t('events.upcoming.livestream', locale))
197+
// In-person events override the CTA label (e.g. "Register"); the rest
198+
// fall back to the default "Livestream" label.
199+
const ctaLabel =
200+
event.ctaLabel?.[locale] ?? t('events.upcoming.livestream', locale)
201+
const ctaLink = row.getByRole('link', {
202+
name: `${event.title[locale]}${ctaLabel}`,
203+
exact: true
199204
})
200205
// Events with a stream open their own detail page (dialog over the
201206
// directory); the rest link to the event's page.
207+
const eventLink = event.link
202208
const expectedHref = eventVideoId(event)
203209
? localizeHref(eventPath(event), locale)
204-
: event.link!.href[locale]
205-
await expect(livestreamLink).toHaveAttribute('href', expectedHref)
210+
: eventLink?.href[locale]
211+
if (expectedHref) {
212+
await expect(ctaLink).toHaveAttribute('href', expectedHref)
213+
}
214+
// External registration links open in a new tab.
215+
if (!eventVideoId(event) && eventLink?.newTab) {
216+
await expect(ctaLink).toHaveAttribute('target', '_blank')
217+
}
206218
}
207219
}
208220
})

apps/website/e2e/seedance.spec.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from '@playwright/test'
22

3-
import { externalLinks, getRoutes } from '../src/config/routes'
3+
import { getRoutes } from '../src/config/routes'
44
import { creatorReviews } from '../src/data/creatorReviews'
55
import { seedancePage } from '../src/data/seedance'
66
import { t } from '../src/i18n/translations'
@@ -20,7 +20,10 @@ const HERO_PRIMARY_CTA: ModelLaunchCta | undefined =
2020
if (!HERO_PRIMARY_CTA)
2121
throw new Error('seedancePage must configure a hero primary CTA')
2222
const SEEDANCE_RUN: string = HERO_PRIMARY_CTA.href
23-
const CLOUD_WORKFLOWS_HUB = externalLinks.workflows
23+
// The hub's Seedance family page, which lists the shipped 2.5 workflows. The
24+
// CTA used to open the hub root, leaving the reader to find the model they had
25+
// just read about.
26+
const SEEDANCE_HUB_PAGE: string = seedancePage.hero.secondaryCta?.href ?? ''
2427
const PROMPT_CTA = t('seedance.hero.promptCta', 'en')
2528
const COPY_PROMPT = t('modelLaunch.copyPrompt', 'en')
2629
// `faq` is optional on the template (Wan Animate 2 ships without one), but
@@ -106,7 +109,9 @@ test.describe('Seedance 2.5 page — link targets', () => {
106109
await expect(primary).toHaveAttribute('href', /video_wan2_2/)
107110
})
108111

109-
test('the hero run CTA opens the same Cloud workflow', async ({ page }) => {
112+
test('the hero CTAs open Cloud to run and the hub to browse', async ({
113+
page
114+
}) => {
110115
const hero = page.locator('section').filter({
111116
has: page.getByRole('heading', { level: 1, name: HERO_TITLE })
112117
})
@@ -116,7 +121,10 @@ test.describe('Seedance 2.5 page — link targets', () => {
116121

117122
await expect(
118123
hero.getByRole('link', { name: t('seedance.hero.secondaryCta', 'en') })
119-
).toHaveAttribute('href', CLOUD_WORKFLOWS_HUB)
124+
).toHaveAttribute('href', SEEDANCE_HUB_PAGE)
125+
await expect(
126+
hero.getByRole('link', { name: t('seedance.hero.secondaryCta', 'en') })
127+
).toHaveAttribute('href', /\/workflows\/model\/seedance$/)
120128
})
121129

122130
test('renders one step card per configured step', async ({ page }) => {

0 commit comments

Comments
 (0)