Merge main into austin/widgets-v2

This commit is contained in:
Austin Mroz
2025-09-11 13:04:34 -05:00
745 changed files with 44060 additions and 10711 deletions

View File

@@ -1,30 +1,85 @@
# Create Hotfix Release # Create Hotfix Release
This command guides you through creating a patch/hotfix release for ComfyUI Frontend with comprehensive safety checks and human confirmations at each step. This command creates patch/hotfix releases for ComfyUI Frontend by backporting fixes to stable core branches. It handles both automated backports (preferred) and manual cherry-picking (fallback).
**Process Overview:**
1. **Check automated backports first** (via labels)
2. **Skip to version bump** if backports already merged
3. **Manual cherry-picking** if automation failed
4. **Create patch release** with version bump
5. **Publish GitHub release** (manually uncheck "latest")
6. **Update ComfyUI requirements.txt** via PR
<task> <task>
Create a hotfix release by cherry-picking commits or PR commits from main to a core branch: $ARGUMENTS Create a hotfix release by backporting commits/PRs from main to a core branch: $ARGUMENTS
Expected format: Comma-separated list of commits or PR numbers Expected format: Comma-separated list of commits or PR numbers
Examples: Examples:
- `abc123,def456,ghi789` (commits) - `#1234,#5678` (PRs - preferred)
- `#1234,#5678` (PRs) - `abc123,def456` (commit hashes)
- `abc123,#1234,def456` (mixed) - `#1234,abc123` (mixed)
If no arguments provided, the command will help identify the correct core branch and guide you through selecting commits/PRs. If no arguments provided, the command will guide you through identifying commits/PRs to backport.
</task> </task>
## Prerequisites ## Prerequisites
Before starting, ensure: - Push access to repository
- You have push access to the repository - GitHub CLI (`gh`) authenticated
- GitHub CLI (`gh`) is authenticated - Clean working tree
- You're on a clean working tree - Understanding of what fixes need backporting
- You understand the commits/PRs you're cherry-picking
## Hotfix Release Process ## Hotfix Release Process
### Step 1: Identify Target Core Branch ### Step 1: Try Automated Backports First
**Check if automated backports were attempted:**
1. **For each PR, check existing backport labels:**
```bash
gh pr view #1234 --json labels | jq -r '.labels[].name'
```
2. **If no backport labels exist, add them now:**
```bash
# Add backport labels (this triggers automated backports)
gh pr edit #1234 --add-label "needs-backport"
gh pr edit #1234 --add-label "1.24" # Replace with target version
```
3. **Check for existing backport PRs:**
```bash
# Check for backport PRs created by automation
PR_NUMBER=${ARGUMENTS%%,*} # Extract first PR number from arguments
PR_NUMBER=${PR_NUMBER#\#} # Remove # prefix
gh pr list --search "backport-${PR_NUMBER}-to" --json number,title,state,baseRefName
```
4. **Handle existing backport scenarios:**
**Scenario A: Automated backports already merged**
```bash
# Check if backport PRs were merged to core branches
gh pr list --search "backport-${PR_NUMBER}-to" --state merged
```
- If backport PRs are merged → Skip to Step 10 (Version Bump)
- **CONFIRMATION**: Automated backports completed, proceeding to version bump?
**Scenario B: Automated backport PRs exist but not merged**
```bash
# Show open backport PRs that need merging
gh pr list --search "backport-${PR_NUMBER}-to" --state open
```
- **ACTION REQUIRED**: Merge the existing backport PRs first
- Use: `gh pr merge [PR_NUMBER] --merge` for each backport PR
- After merging, return to this command and skip to Step 10 (Version Bump)
- **CONFIRMATION**: Have you merged all backport PRs? Ready to proceed to version bump?
**Scenario C: No automated backports or they failed**
- Continue to Step 2 for manual cherry-picking
- **CONFIRMATION**: Proceeding with manual cherry-picking because automation failed?
### Step 2: Identify Target Core Branch
1. Fetch the current ComfyUI requirements.txt from master branch: 1. Fetch the current ComfyUI requirements.txt from master branch:
```bash ```bash
@@ -36,7 +91,7 @@ Before starting, ensure:
5. Verify the core branch exists: `git ls-remote origin refs/heads/core/*` 5. Verify the core branch exists: `git ls-remote origin refs/heads/core/*`
6. **CONFIRMATION REQUIRED**: Is `core/X.Y` the correct target branch? 6. **CONFIRMATION REQUIRED**: Is `core/X.Y` the correct target branch?
### Step 2: Parse and Validate Arguments ### Step 3: Parse and Validate Arguments
1. Parse the comma-separated list of commits/PRs 1. Parse the comma-separated list of commits/PRs
2. For each item: 2. For each item:
@@ -49,7 +104,7 @@ Before starting, ensure:
- **CONFIRMATION REQUIRED**: Use merge commit or cherry-pick individual commits? - **CONFIRMATION REQUIRED**: Use merge commit or cherry-pick individual commits?
4. Validate all commit hashes exist in the repository 4. Validate all commit hashes exist in the repository
### Step 3: Analyze Target Changes ### Step 4: Analyze Target Changes
1. For each commit/PR to cherry-pick: 1. For each commit/PR to cherry-pick:
- Display commit hash, author, date - Display commit hash, author, date
@@ -60,7 +115,7 @@ Before starting, ensure:
2. Identify potential conflicts by checking changed files 2. Identify potential conflicts by checking changed files
3. **CONFIRMATION REQUIRED**: Proceed with these commits? 3. **CONFIRMATION REQUIRED**: Proceed with these commits?
### Step 4: Create Hotfix Branch ### Step 5: Create Hotfix Branch
1. Checkout the core branch (e.g., `core/1.23`) 1. Checkout the core branch (e.g., `core/1.23`)
2. Pull latest changes: `git pull origin core/X.Y` 2. Pull latest changes: `git pull origin core/X.Y`
@@ -69,7 +124,7 @@ Before starting, ensure:
- Example: `hotfix/1.23.4-20241120` - Example: `hotfix/1.23.4-20241120`
5. **CONFIRMATION REQUIRED**: Created branch correctly? 5. **CONFIRMATION REQUIRED**: Created branch correctly?
### Step 5: Cherry-pick Changes ### Step 6: Cherry-pick Changes
For each commit: For each commit:
1. Attempt cherry-pick: `git cherry-pick <commit>` 1. Attempt cherry-pick: `git cherry-pick <commit>`
@@ -83,7 +138,7 @@ For each commit:
- Run validation: `pnpm typecheck && pnpm lint` - Run validation: `pnpm typecheck && pnpm lint`
4. **CONFIRMATION REQUIRED**: Cherry-pick successful and valid? 4. **CONFIRMATION REQUIRED**: Cherry-pick successful and valid?
### Step 6: Create PR to Core Branch ### Step 7: Create PR to Core Branch
1. Push the hotfix branch: `git push origin hotfix/<version>-<timestamp>` 1. Push the hotfix branch: `git push origin hotfix/<version>-<timestamp>`
2. Create PR using gh CLI: 2. Create PR using gh CLI:
@@ -100,7 +155,7 @@ For each commit:
- Impact assessment - Impact assessment
5. **CONFIRMATION REQUIRED**: PR created correctly? 5. **CONFIRMATION REQUIRED**: PR created correctly?
### Step 7: Wait for Tests ### Step 8: Wait for Tests
1. Monitor PR checks: `gh pr checks` 1. Monitor PR checks: `gh pr checks`
2. Display test results as they complete 2. Display test results as they complete
@@ -111,7 +166,7 @@ For each commit:
4. Wait for all required checks to pass 4. Wait for all required checks to pass
5. **CONFIRMATION REQUIRED**: All tests passing? 5. **CONFIRMATION REQUIRED**: All tests passing?
### Step 8: Merge Hotfix PR ### Step 9: Merge Hotfix PR
1. Verify all checks have passed 1. Verify all checks have passed
2. Check for required approvals 2. Check for required approvals
@@ -119,7 +174,7 @@ For each commit:
4. Delete the hotfix branch 4. Delete the hotfix branch
5. **CONFIRMATION REQUIRED**: PR merged successfully? 5. **CONFIRMATION REQUIRED**: PR merged successfully?
### Step 9: Create Version Bump ### Step 10: Create Version Bump
1. Checkout the core branch: `git checkout core/X.Y` 1. Checkout the core branch: `git checkout core/X.Y`
2. Pull latest changes: `git pull origin core/X.Y` 2. Pull latest changes: `git pull origin core/X.Y`
@@ -131,7 +186,7 @@ For each commit:
7. Commit: `git commit -m "[release] Bump version to 1.23.5"` 7. Commit: `git commit -m "[release] Bump version to 1.23.5"`
8. **CONFIRMATION REQUIRED**: Version bump correct? 8. **CONFIRMATION REQUIRED**: Version bump correct?
### Step 10: Create Release PR ### Step 11: Create Release PR
1. Push release branch: `git push origin release/1.23.5` 1. Push release branch: `git push origin release/1.23.5`
2. Create PR with Release label: 2. Create PR with Release label:
@@ -184,7 +239,7 @@ For each commit:
``` ```
5. **CONFIRMATION REQUIRED**: Release PR has "Release" label? 5. **CONFIRMATION REQUIRED**: Release PR has "Release" label?
### Step 11: Monitor Release Process ### Step 12: Monitor Release Process
1. Wait for PR checks to pass 1. Wait for PR checks to pass
2. **FINAL CONFIRMATION**: Ready to trigger release by merging? 2. **FINAL CONFIRMATION**: Ready to trigger release by merging?
@@ -199,7 +254,102 @@ For each commit:
- PyPI upload - PyPI upload
- pnpm types publication - pnpm types publication
### Step 12: Post-Release Verification ### Step 13: Manually Publish Draft Release
**CRITICAL**: The release workflow creates a DRAFT release. You must manually publish it:
1. **Go to GitHub Releases:** https://github.com/Comfy-Org/ComfyUI_frontend/releases
2. **Find the DRAFT release** (e.g., "v1.23.5 Draft")
3. **Click "Edit release"**
4. **UNCHECK "Set as the latest release"** ⚠️ **CRITICAL**
- This prevents the hotfix from showing as "latest"
- Main branch should always be "latest release"
5. **Click "Publish release"**
6. **CONFIRMATION REQUIRED**: Draft release published with "latest" unchecked?
### Step 14: Create ComfyUI Requirements.txt Update PR
**IMPORTANT**: Create PR to update ComfyUI's requirements.txt via fork:
1. **Setup fork (if needed):**
```bash
# Check if fork already exists
if gh repo view ComfyUI --json owner | jq -r '.owner.login' | grep -q "$(gh api user --jq .login)"; then
echo "Fork already exists"
else
# Fork the ComfyUI repository
gh repo fork comfyanonymous/ComfyUI --clone=false
echo "Created fork of ComfyUI"
fi
```
2. **Clone fork and create branch:**
```bash
# Clone your fork (or use existing clone)
GITHUB_USER=$(gh api user --jq .login)
if [ ! -d "ComfyUI-fork" ]; then
gh repo clone ${GITHUB_USER}/ComfyUI ComfyUI-fork
fi
cd ComfyUI-fork
git checkout master
git pull origin master
# Create update branch
BRANCH_NAME="update-frontend-${NEW_VERSION}"
git checkout -b ${BRANCH_NAME}
```
3. **Update requirements.txt:**
```bash
# Update the version in requirements.txt
sed -i "s/comfyui-frontend-package==[0-9].*$/comfyui-frontend-package==${NEW_VERSION}/" requirements.txt
# Verify the change
grep "comfyui-frontend-package" requirements.txt
# Commit the change
git add requirements.txt
git commit -m "Bump frontend to ${NEW_VERSION}"
git push origin ${BRANCH_NAME}
```
4. **Create PR from fork:**
```bash
# Create PR using gh CLI from fork
gh pr create \
--repo comfyanonymous/ComfyUI \
--title "Bump frontend to ${NEW_VERSION}" \
--body "$(cat <<EOF
Bump frontend to ${NEW_VERSION}
\`\`\`
python main.py --front-end-version Comfy-Org/ComfyUI_frontend@${NEW_VERSION}
\`\`\`
- Diff: [Comfy-Org/ComfyUI_frontend: v${OLD_VERSION}...v${NEW_VERSION}](https://github.com/Comfy-Org/ComfyUI_frontend/compare/v${OLD_VERSION}...v${NEW_VERSION})
- PyPI Package: https://pypi.org/project/comfyui-frontend-package/${NEW_VERSION}/
- npm Types: https://www.npmjs.com/package/@comfyorg/comfyui-frontend-types/v/${NEW_VERSION}
## Changes
- Fix: [Brief description of hotfixes included]
EOF
)"
```
5. **Clean up:**
```bash
# Return to original directory
cd ..
# Keep fork directory for future updates
echo "Fork directory 'ComfyUI-fork' kept for future use"
```
6. **CONFIRMATION REQUIRED**: ComfyUI requirements.txt PR created from fork?
### Step 15: Post-Release Verification
1. Verify GitHub release: 1. Verify GitHub release:
```bash ```bash
@@ -213,12 +363,14 @@ For each commit:
```bash ```bash
pnpm view @comfyorg/comfyui-frontend-types@1.23.5 pnpm view @comfyorg/comfyui-frontend-types@1.23.5
``` ```
4. Generate release summary with: 4. Monitor ComfyUI requirements.txt PR for approval/merge
5. Generate release summary with:
- Version released - Version released
- Commits included - Commits included
- Issues fixed - Issues fixed
- Distribution status - Distribution status
5. **CONFIRMATION REQUIRED**: Release completed successfully? - ComfyUI integration status
6. **CONFIRMATION REQUIRED**: Hotfix release fully completed?
## Safety Checks ## Safety Checks
@@ -240,19 +392,28 @@ If something goes wrong:
## Important Notes ## Important Notes
- **Always try automated backports first** - This command is for when automation fails
- Core branch version will be behind main - this is expected - Core branch version will be behind main - this is expected
- The "Release" label triggers the PyPI/npm publication - The "Release" label triggers the PyPI/npm publication
- **CRITICAL**: Always uncheck "Set as latest release" for hotfix releases
- **Must create ComfyUI requirements.txt PR** - Hotfix isn't complete without it
- PR numbers must include the `#` prefix - PR numbers must include the `#` prefix
- Mixed commits/PRs are supported but review carefully - Mixed commits/PRs are supported but review carefully
- Always wait for full test suite before proceeding - Always wait for full test suite before proceeding
## Expected Timeline ## Modern Workflow Context
- Step 1-3: ~10 minutes (analysis) **Primary Backport Method:** Automated via `needs-backport` + `X.YY` labels
- Steps 4-6: ~15-30 minutes (cherry-picking) **This Command Usage:**
- Step 7: ~10-20 minutes (tests) - Smart path detection - skip to version bump if backports already merged
- Steps 8-10: ~10 minutes (version bump) - Fallback to manual cherry-picking only when automation fails/has conflicts
- Step 11-12: ~15-20 minutes (release) **Complete Hotfix:** Includes GitHub release publishing + ComfyUI requirements.txt integration
- Total: ~60-90 minutes
This process ensures a safe, verified hotfix release with multiple confirmation points and clear tracking of what changes are being released. ## Workflow Paths
- **Path A:** Backports already merged → Skip to Step 10 (Version Bump)
- **Path B:** Backport PRs need merging → Merge them → Skip to Step 10 (Version Bump)
- **Path C:** No/failed backports → Manual cherry-picking (Steps 2-9) → Version Bump (Step 10)
This process ensures a complete hotfix release with proper GitHub publishing, ComfyUI integration, and multiple safety checkpoints.

View File

@@ -0,0 +1,158 @@
# Setup Repository
Bootstrap the ComfyUI Frontend monorepo with all necessary dependencies and verification checks.
## Overview
This command will:
1. Install pnpm package manager (if not present)
2. Install all project dependencies
3. Verify the project builds successfully
4. Run unit tests to ensure functionality
5. Start development server to verify frontend boots correctly
## Prerequisites Check
First, let's verify the environment:
```bash
# Check Node.js version (should be >= 24)
node --version
# Check if we're in a git repository
git status
```
## Step 1: Install pnpm
```bash
# Check if pnpm is already installed
pnpm --version 2>/dev/null || {
echo "Installing pnpm..."
npm install -g pnpm
}
# Verify pnpm installation
pnpm --version
```
## Step 2: Install Dependencies
```bash
# Install all dependencies using pnpm
echo "Installing project dependencies..."
pnpm install
# Verify node_modules exists and has packages
ls -la node_modules | head -5
```
## Step 3: Verify Build
```bash
# Run TypeScript type checking
echo "Running TypeScript checks..."
pnpm typecheck
# Build the project
echo "Building project..."
pnpm build
# Verify dist folder was created
ls -la dist/
```
## Step 4: Run Unit Tests
```bash
# Run unit tests
echo "Running unit tests..."
pnpm test:unit
# If tests fail, show the output and stop
if [ $? -ne 0 ]; then
echo "❌ Unit tests failed. Please fix failing tests before continuing."
exit 1
fi
echo "✅ Unit tests passed successfully"
```
## Step 5: Verify Development Server
```bash
# Start development server in background
echo "Starting development server..."
pnpm dev &
SERVER_PID=$!
# Wait for server to start (check for port 5173 or similar)
echo "Waiting for server to start..."
sleep 10
# Check if server is running
if curl -s http://localhost:5173 > /dev/null 2>&1; then
echo "✅ Development server started successfully at http://localhost:5173"
# Kill the background server
kill $SERVER_PID
wait $SERVER_PID 2>/dev/null
else
echo "❌ Development server failed to start or is not accessible"
kill $SERVER_PID 2>/dev/null
wait $SERVER_PID 2>/dev/null
exit 1
fi
```
## Step 6: Final Verification
```bash
# Run linting to ensure code quality
echo "Running linter..."
pnpm lint
# Show project status
echo ""
echo "🎉 Repository setup complete!"
echo ""
echo "Available commands:"
echo " pnpm dev - Start development server"
echo " pnpm build - Build for production"
echo " pnpm test:unit - Run unit tests"
echo " pnpm test:component - Run component tests"
echo " pnpm typecheck - Run TypeScript checks"
echo " pnpm lint - Run ESLint"
echo " pnpm format - Format code with Prettier"
echo ""
echo "Next steps:"
echo "1. Run 'pnpm dev' to start developing"
echo "2. Open http://localhost:5173 in your browser"
echo "3. Check README.md for additional setup instructions"
```
## Troubleshooting
If any step fails:
1. **pnpm installation fails**: Try using `curl -fsSL https://get.pnpm.io/install.sh | sh -`
2. **Dependencies fail to install**: Try clearing cache with `pnpm store prune` and retry
3. **Build fails**: Check for TypeScript errors and fix them first
4. **Tests fail**: Review test output and fix failing tests
5. **Dev server fails**: Check if port 5173 is already in use
## Manual Verification Steps
After running the setup, manually verify:
1. **Dependencies installed**: `ls node_modules | wc -l` should show many packages
2. **Build artifacts**: `ls dist/` should show built files
3. **Server accessible**: Open http://localhost:5173 in browser
4. **Hot reload works**: Edit a file and see changes reflect
## Environment Requirements
- Node.js >= 24
- Git repository
- Internet connection for package downloads
- Available ports (typically 5173 for dev server)

View File

@@ -33,3 +33,4 @@ DISABLE_VUE_PLUGINS=false
# Algolia credentials required for developing with the new custom node manager. # Algolia credentials required for developing with the new custom node manager.
ALGOLIA_APP_ID=4E0RO38HS8 ALGOLIA_APP_ID=4E0RO38HS8
ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579 ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579

View File

@@ -4,3 +4,6 @@
# npm run format on litegraph merge (10,672 insertions, 7,327 deletions across 129 files) # npm run format on litegraph merge (10,672 insertions, 7,327 deletions across 129 files)
c53f197de2a3e0fa66b16dedc65c131235c1c4b6 c53f197de2a3e0fa66b16dedc65c131235c1c4b6
# Reorganize renderer components into domain-driven folder structure
c8a83a9caede7bdb5f8598c5492b07d08c339d49

1
.gitattributes vendored
View File

@@ -9,6 +9,7 @@
*.mts text eol=lf *.mts text eol=lf
*.ts text eol=lf *.ts text eol=lf
*.vue text eol=lf *.vue text eol=lf
*.yaml text eol=lf
# Generated files # Generated files
src/types/comfyRegistryTypes.ts linguist-generated=true src/types/comfyRegistryTypes.ts linguist-generated=true

View File

@@ -2,7 +2,7 @@ name: Auto Backport
on: on:
pull_request_target: pull_request_target:
types: [closed] types: [closed, labeled]
branches: [main] branches: [main]
jobs: jobs:
@@ -25,7 +25,27 @@ jobs:
git config user.name "github-actions[bot]" git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com" git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Check if backports already exist
id: check-existing
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# Check for existing backport PRs for this PR number
EXISTING_BACKPORTS=$(gh pr list --state all --search "backport-${PR_NUMBER}-to" --json title,headRefName,baseRefName | jq -r '.[].headRefName')
if [ -z "$EXISTING_BACKPORTS" ]; then
echo "skip=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "Found existing backport PRs:"
echo "$EXISTING_BACKPORTS"
echo "skip=true" >> $GITHUB_OUTPUT
echo "::warning::Backport PRs already exist for PR #${PR_NUMBER}, skipping to avoid duplicates"
- name: Extract version labels - name: Extract version labels
if: steps.check-existing.outputs.skip != 'true'
id: versions id: versions
run: | run: |
# Extract version labels (e.g., "1.24", "1.22") # Extract version labels (e.g., "1.24", "1.22")
@@ -52,6 +72,7 @@ jobs:
echo "Found version labels: ${VERSIONS}" echo "Found version labels: ${VERSIONS}"
- name: Backport commits - name: Backport commits
if: steps.check-existing.outputs.skip != 'true'
id: backport id: backport
env: env:
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
@@ -109,7 +130,7 @@ jobs:
fi fi
- name: Create PR for each successful backport - name: Create PR for each successful backport
if: steps.backport.outputs.success if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success
env: env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }} GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
run: | run: |
@@ -141,7 +162,7 @@ jobs:
done done
- name: Comment on failures - name: Comment on failures
if: failure() && steps.backport.outputs.failed if: steps.check-existing.outputs.skip != 'true' && failure() && steps.backport.outputs.failed
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
run: | run: |

View File

@@ -12,9 +12,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Only run for PRs from version-bump-* branches or manual triggers # Only run for PRs from version-bump-* branches or manual triggers
if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'version-bump-') if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'version-bump-')
permissions:
pull-requests: write
issues: write
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -32,29 +29,6 @@ jobs:
node-version: '20' node-version: '20'
cache: 'pnpm' cache: 'pnpm'
- name: Get current time
id: current-time
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
- name: Comment PR - Build Started
if: github.event_name == 'pull_request'
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
🔄 **Building Storybook and running visual tests...**
⏳ Build started at: ${{ steps.current-time.outputs.time }} UTC
---
*This comment will be updated when the build completes*
- name: Cache tool outputs - name: Cache tool outputs
uses: actions/cache@v4 uses: actions/cache@v4
@@ -81,37 +55,3 @@ jobs:
autoAcceptChanges: 'main' # Auto-accept changes on main branch autoAcceptChanges: 'main' # Auto-accept changes on main branch
exitOnceUploaded: true # Don't wait for UI tests to complete exitOnceUploaded: true # Don't wait for UI tests to complete
- name: Get completion time
id: completion-time
if: always()
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
- name: Comment PR - Build Complete
if: github.event_name == 'pull_request' && always()
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
${{ steps.chromatic.outcome == 'success' && '✅' || '❌' }} **${{ steps.chromatic.outcome == 'success' && 'Build completed successfully!' || 'Build failed!' }}**
⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC
### 📊 Build Summary
- **Components**: ${{ steps.chromatic.outputs.componentCount || '0' }}
- **Stories**: ${{ steps.chromatic.outputs.testCount || '0' }}
- **Visual changes**: ${{ steps.chromatic.outputs.changeCount || '0' }}
- **Errors**: ${{ steps.chromatic.outputs.errorCount || '0' }}
### 🔗 Links
${{ steps.chromatic.outputs.buildUrl && format('- [📸 View Chromatic Build]({0})', steps.chromatic.outputs.buildUrl) || '' }}
${{ steps.chromatic.outputs.storybookUrl && format('- [📖 Preview Storybook]({0})', steps.chromatic.outputs.storybookUrl) || '' }}
---
${{ steps.chromatic.outcome == 'success' && '🎉 Your Storybook is ready for review!' || '⚠️ Please check the workflow logs for error details.' }}

View File

@@ -47,6 +47,7 @@ jobs:
needs: wait-for-ci needs: wait-for-ci
if: needs.wait-for-ci.outputs.should-proceed == 'true' if: needs.wait-for-ci.outputs.should-proceed == 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -69,19 +70,17 @@ jobs:
pnpm install -g typescript @vue/compiler-sfc pnpm install -g typescript @vue/compiler-sfc
- name: Run Claude PR Review - name: Run Claude PR Review
uses: anthropics/claude-code-action@main uses: anthropics/claude-code-action@v1.0.6
with: with:
label_trigger: "claude-review" label_trigger: "claude-review"
direct_prompt: | prompt: |
Read the file .claude/commands/comprehensive-pr-review.md and follow ALL the instructions exactly. Read the file .claude/commands/comprehensive-pr-review.md and follow ALL the instructions exactly.
CRITICAL: You must post individual inline comments using the gh api commands shown in the file. CRITICAL: You must post individual inline comments using the gh api commands shown in the file.
DO NOT create a summary comment. DO NOT create a summary comment.
Each issue must be posted as a separate inline comment on the specific line of code. Each issue must be posted as a separate inline comment on the specific line of code.
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
max_turns: 256 claude_args: "--max-turns 256 --allowedTools 'Bash(git:*),Bash(gh api:*),Bash(gh pr:*),Bash(gh repo:*),Bash(jq:*),Bash(echo:*),Read,Write,Edit,Glob,Grep,WebFetch'"
timeout_minutes: 30
allowed_tools: "Bash(git:*),Bash(gh api:*),Bash(gh pr:*),Bash(gh repo:*),Bash(jq:*),Bash(echo:*),Read,Write,Edit,Glob,Grep,WebFetch"
env: env:
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -128,45 +128,6 @@ jobs:
echo "- Critical security patches" echo "- Critical security patches"
echo "- Documentation updates" echo "- Documentation updates"
- name: Create branch protection rules
if: steps.check_version.outputs.is_minor_bump == 'true' && env.branch_exists != 'true'
env:
GITHUB_TOKEN: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
run: |
BRANCH_NAME="${{ steps.check_version.outputs.branch_name }}"
# Create branch protection using GitHub API
echo "Setting up branch protection for $BRANCH_NAME..."
RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/branches/$BRANCH_NAME/protection" \
-d '{
"required_status_checks": {
"strict": true,
"contexts": ["lint-and-format", "test", "playwright-tests"]
},
"enforce_admins": false,
"required_pull_request_reviews": {
"required_approving_review_count": 1,
"dismiss_stale_reviews": true
},
"restrictions": null,
"allow_force_pushes": false,
"allow_deletions": false
}')
HTTP_CODE=$(echo "$RESPONSE" | tail -n 1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [[ "$HTTP_CODE" -eq 200 ]] || [[ "$HTTP_CODE" -eq 201 ]]; then
echo "✅ Branch protection successfully applied"
else
echo "⚠️ Failed to apply branch protection (HTTP $HTTP_CODE)"
echo "Response: $BODY"
# Don't fail the workflow, just warn
fi
- name: Post summary - name: Post summary
if: steps.check_version.outputs.is_minor_bump == 'true' if: steps.check_version.outputs.is_minor_bump == 'true'

View File

@@ -25,6 +25,13 @@ jobs:
key: i18n-tools-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }} key: i18n-tools-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
restore-keys: | restore-keys: |
i18n-tools-cache-${{ runner.os }}- i18n-tools-cache-${{ runner.os }}-
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
restore-keys: |
playwright-browsers-${{ runner.os }}-
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: npx playwright install chromium --with-deps run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend working-directory: ComfyUI_frontend
@@ -34,7 +41,7 @@ jobs:
run: pnpm dev:electron & run: pnpm dev:electron &
working-directory: ComfyUI_frontend working-directory: ComfyUI_frontend
- name: Update en.json - name: Update en.json
run: pnpm collect-i18n -- scripts/collect-i18n-general.ts run: pnpm collect-i18n
env: env:
PLAYWRIGHT_TEST_URL: http://localhost:5173 PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend working-directory: ComfyUI_frontend

View File

@@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0 fetch-depth: 0
- name: Install pnpm - name: Install pnpm

View File

@@ -0,0 +1,92 @@
name: PR Playwright Deploy (Forks)
on:
workflow_run:
workflows: ["Tests CI"]
types: [requested, completed]
env:
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
jobs:
deploy-and-comment-forked-pr:
runs-on: ubuntu-latest
if: |
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.head_repository != null &&
github.event.workflow_run.repository != null &&
github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name
permissions:
pull-requests: write
actions: read
steps:
- name: Log workflow trigger info
run: |
echo "Repository: ${{ github.repository }}"
echo "Event: ${{ github.event.workflow_run.event }}"
echo "Head repo: ${{ github.event.workflow_run.head_repository.full_name || 'null' }}"
echo "Base repo: ${{ github.event.workflow_run.repository.full_name || 'null' }}"
echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}"
- name: Checkout repository
uses: actions/checkout@v4
- name: Get PR Number
id: pr
uses: actions/github-script@v7
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;
- name: Handle Test Start
if: steps.pr.outputs.result != 'null' && 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 }}" \
"${{ github.event.workflow_run.head_branch }}" \
"starting" \
"$(date -u '${{ env.DATE_FORMAT }}')"
- name: Download and Deploy Reports
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
uses: actions/download-artifact@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
pattern: playwright-report-*
path: reports
- name: Handle Test Completion
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
GITHUB_TOKEN: ${{ github.token }}
run: |
# Rename merged report if exists
[ -d "reports/playwright-report-chromium-merged" ] && \
mv reports/playwright-report-chromium-merged reports/playwright-report-chromium
chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh
./scripts/cicd/pr-playwright-deploy-and-comment.sh \
"${{ steps.pr.outputs.result }}" \
"${{ github.event.workflow_run.head_branch }}" \
"completed"

View File

@@ -0,0 +1,126 @@
name: PR Storybook Comment
on:
workflow_run:
workflows: ['Chromatic']
types: [requested, completed]
jobs:
comment-storybook:
runs-on: ubuntu-latest
if: >-
github.repository == 'Comfy-Org/ComfyUI_frontend'
&& github.event.workflow_run.event == 'pull_request'
&& startsWith(github.event.workflow_run.head_branch, 'version-bump-')
permissions:
pull-requests: write
actions: read
steps:
- name: Get PR number
id: pr
uses: actions/github-script@v7
with:
script: |
const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
});
if (pullRequests.length === 0) {
console.log('No open PR found for this branch');
return null;
}
return pullRequests[0].number;
- name: Log when no PR found
if: steps.pr.outputs.result == 'null'
run: |
echo "⚠️ No open PR found for branch: ${{ github.event.workflow_run.head_branch }}"
echo "Workflow run ID: ${{ github.event.workflow_run.id }}"
echo "Repository: ${{ github.event.workflow_run.repository.full_name }}"
echo "Event: ${{ github.event.workflow_run.event }}"
- name: Get workflow run details
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
id: workflow-run
uses: actions/github-script@v7
with:
script: |
const run = await github.rest.actions.getWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
return {
conclusion: run.data.conclusion,
html_url: run.data.html_url
};
- name: Get completion time
id: completion-time
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
- name: Comment PR - Storybook Started
if: steps.pr.outputs.result != 'null' && github.event.action == 'requested'
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
with:
issue-number: ${{ steps.pr.outputs.result }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
<img alt='comfy-loading-gif' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px' style='vertical-align: middle; margin-right: 4px;' /> **Build is starting...**
⏰ Started at: ${{ steps.completion-time.outputs.time }} UTC
### 🚀 Building Storybook
- 📦 Installing dependencies...
- 🔧 Building Storybook components...
- 🎨 Running Chromatic visual tests...
---
⏱️ Please wait while the Storybook build is in progress...
- name: Comment PR - Storybook Complete
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
with:
issue-number: ${{ steps.pr.outputs.result }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
${{
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '✅'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && '⏭️'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && '🚫'
|| '❌'
}} **${{
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && 'Build completed successfully!'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && 'Build skipped.'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && 'Build cancelled.'
|| 'Build failed!'
}}**
⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC
### 🔗 Links
- [📊 View Workflow Run](${{ fromJSON(steps.workflow-run.outputs.result).html_url }})
---
${{
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '🎉 Your Storybook is ready for review!'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && ' Chromatic was skipped for this PR.'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && ' The Chromatic run was cancelled.'
|| '⚠️ Please check the workflow logs for error details.'
}}

View File

@@ -11,6 +11,13 @@ jobs:
if: github.event.label.name == 'New Browser Test Expectations' if: github.event.label.name == 'New Browser Test Expectations'
steps: steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3 - uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
restore-keys: |
playwright-browsers-${{ runner.os }}-
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: npx playwright install chromium --with-deps run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend working-directory: ComfyUI_frontend

View File

@@ -7,15 +7,12 @@ on:
branches-ignore: branches-ignore:
[wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*] [wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*]
env:
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
jobs: jobs:
setup: setup:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
cache-key: ${{ steps.cache-key.outputs.key }} cache-key: ${{ steps.cache-key.outputs.key }}
sanitized-branch: ${{ steps.branch-info.outputs.sanitized }} playwright-version: ${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}
steps: steps:
- name: Checkout ComfyUI - name: Checkout ComfyUI
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -48,30 +45,6 @@ jobs:
cache: 'pnpm' cache: 'pnpm'
cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml' cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml'
- name: Get current time
id: current-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Comment PR - Tests Started
if: github.event_name == 'pull_request'
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
<!-- PLAYWRIGHT_TEST_STATUS -->
---
<img alt='comfy-loading-gif' src="https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
<bold>[${{ steps.current-time.outputs.time }} UTC] Preparing browser tests across multiple browsers...</bold>
---
*This comment will be updated when tests complete*
- name: Cache tool outputs - name: Cache tool outputs
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -94,13 +67,12 @@ jobs:
id: cache-key id: cache-key
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
- name: Generate sanitized branch name - name: Playwright Version
id: branch-info id: playwright-version
run: | run: |
# Get branch name and sanitize it for Cloudflare branch names PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --json | jq --raw-output '.[0].devDependencies["@playwright/test"].version')
BRANCH_NAME="${{ github.head_ref || github.ref_name }}" echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT
SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') working-directory: ComfyUI_frontend
echo "sanitized=${SANITIZED_BRANCH}" >> $GITHUB_OUTPUT
- name: Save cache - name: Save cache
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
@@ -110,17 +82,17 @@ jobs:
ComfyUI_frontend ComfyUI_frontend
key: comfyui-setup-${{ steps.cache-key.outputs.key }} key: comfyui-setup-${{ steps.cache-key.outputs.key }}
playwright-tests: # Sharded chromium tests
playwright-tests-chromium-sharded:
needs: setup needs: setup
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
pull-requests: write
issues: write
contents: read contents: read
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
browser: [chromium, chromium-2x, chromium-0.5x, mobile-chrome] shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
steps: steps:
- name: Wait for cache propagation - name: Wait for cache propagation
run: sleep 10 run: sleep 10
@@ -144,32 +116,85 @@ jobs:
python-version: '3.10' python-version: '3.10'
cache: 'pip' cache: 'pip'
- name: Get current time - name: Install requirements
id: current-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Set project name
id: project-name
run: | run: |
if [ "${{ matrix.browser }}" = "chromium-0.5x" ]; then python -m pip install --upgrade pip
echo "name=comfyui-playwright-chromium-0-5x" >> $GITHUB_OUTPUT pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
else pip install -r requirements.txt
echo "name=comfyui-playwright-${{ matrix.browser }}" >> $GITHUB_OUTPUT pip install wait-for-it
fi working-directory: ComfyUI
echo "branch=${{ needs.setup.outputs.sanitized-branch }}" >> $GITHUB_OUTPUT
- name: Comment PR - Browser Test Started
if: github.event_name == 'pull_request' - name: Cache Playwright Browsers
continue-on-error: true uses: actions/cache@v4
uses: edumserrano/find-create-or-update-comment@v3 id: cache-playwright-browsers
with: with:
issue-number: ${{ github.event.pull_request.number }} path: '~/.cache/ms-playwright'
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->' key: '${{ runner.os }}-playwright-browsers-${{ needs.setup.outputs.playwright-version }}'
comment-author: 'github-actions[bot]'
edit-mode: append - name: Install Playwright Browsers
body: | if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
<img alt='comfy-loading-gif' src="https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" /> run: pnpm exec playwright install chromium --with-deps
<bold>${{ matrix.browser }}</bold>: Running tests... working-directory: ComfyUI_frontend
- name: Install Playwright Browsers (operating system dependencies)
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps
working-directory: ComfyUI_frontend
- name: Start ComfyUI server
run: |
python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist &
wait-for-it --service 127.0.0.1:8188 -t 600
working-directory: ComfyUI
- name: Run Playwright tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
id: playwright
run: npx playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob
working-directory: ComfyUI_frontend
env:
PLAYWRIGHT_BLOB_OUTPUT_DIR: ../blob-report
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: blob-report-chromium-${{ matrix.shardIndex }}
path: blob-report/
retention-days: 1
playwright-tests:
# Ideally, each shard runs test in 6 minutes, but allow up to 15 minutes
timeout-minutes: 15
needs: setup
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
browser: [chromium-2x, chromium-0.5x, mobile-chrome]
steps:
- name: Wait for cache propagation
run: sleep 10
- name: Restore cached setup
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
with:
fail-on-cache-miss: true
path: |
ComfyUI
ComfyUI_frontend
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-python@v4
with:
python-version: '3.10'
cache: 'pip'
- name: Install requirements - name: Install requirements
run: | run: |
@@ -179,215 +204,145 @@ jobs:
pip install wait-for-it pip install wait-for-it
working-directory: ComfyUI working-directory: ComfyUI
- name: Cache Playwright Browsers
uses: actions/cache@v4
id: cache-playwright-browsers
with:
path: '~/.cache/ms-playwright'
key: '${{ runner.os }}-playwright-browsers-${{ needs.setup.outputs.playwright-version }}'
- name: Install Playwright Browsers
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
run: pnpm exec playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Install Playwright Browsers (operating system dependencies)
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps
working-directory: ComfyUI_frontend
- name: Start ComfyUI server - name: Start ComfyUI server
run: | run: |
python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist & python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist &
wait-for-it --service 127.0.0.1:8188 -t 600 wait-for-it --service 127.0.0.1:8188 -t 600
working-directory: ComfyUI working-directory: ComfyUI
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-${{ matrix.browser }}
restore-keys: |
playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-
playwright-browsers-${{ runner.os }}-
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Install Wrangler
run: pnpm install -g wrangler
- name: Run Playwright tests (${{ matrix.browser }}) - name: Run Playwright tests (${{ matrix.browser }})
id: playwright id: playwright
run: npx playwright test --project=${{ matrix.browser }} --reporter=html run: npx playwright test --project=${{ matrix.browser }} --reporter=html
working-directory: ComfyUI_frontend working-directory: ComfyUI_frontend
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() # note: use always() to allow results to be upload/report even tests failed. if: always()
with: with:
name: playwright-report-${{ matrix.browser }} name: playwright-report-${{ matrix.browser }}
path: ComfyUI_frontend/playwright-report/ path: ComfyUI_frontend/playwright-report/
retention-days: 30 retention-days: 30
- name: Deploy to Cloudflare Pages (${{ matrix.browser }}) # Merge sharded test reports
id: cloudflare-deploy merge-reports:
if: always() needs: [playwright-tests-chromium-sharded]
continue-on-error: true runs-on: ubuntu-latest
run: | if: ${{ !cancelled() }}
# Retry logic for wrangler deploy (3 attempts) steps:
RETRY_COUNT=0 - name: Checkout ComfyUI_frontend
MAX_RETRIES=3 uses: actions/checkout@v4
SUCCESS=false with:
repository: 'Comfy-Org/ComfyUI_frontend'
while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ $SUCCESS = false ]; do path: 'ComfyUI_frontend'
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "Deployment attempt $RETRY_COUNT of $MAX_RETRIES..."
if npx wrangler pages deploy ComfyUI_frontend/playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }}; then
SUCCESS=true
echo "Deployment successful on attempt $RETRY_COUNT"
else
echo "Deployment failed on attempt $RETRY_COUNT"
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo "Retrying in 10 seconds..."
sleep 10
fi
fi
done
if [ $SUCCESS = false ]; then
echo "All deployment attempts failed"
exit 1
fi
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- name: Save deployment info for summary - name: Install pnpm
if: always() uses: pnpm/action-setup@v4
run: | with:
mkdir -p deployment-info version: 10
# Use step conclusion to determine test result
if [ "${{ steps.playwright.conclusion }}" = "success" ]; then
EXIT_CODE="0"
else
EXIT_CODE="1"
fi
DEPLOYMENT_URL="${{ steps.cloudflare-deploy.outputs.deployment-url || steps.cloudflare-deploy.outputs.url || format('https://{0}.{1}.pages.dev', steps.project-name.outputs.branch, steps.project-name.outputs.name) }}"
echo "${{ matrix.browser }}|${EXIT_CODE}|${DEPLOYMENT_URL}" > deployment-info/${{ matrix.browser }}.txt
- name: Upload deployment info - uses: actions/setup-node@v4
if: always() with:
node-version: lts/*
cache: 'pnpm'
cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml'
- name: Install dependencies
run: |
pnpm install --frozen-lockfile
working-directory: ComfyUI_frontend
- name: Download blob reports
uses: actions/download-artifact@v4
with:
path: ComfyUI_frontend/all-blob-reports
pattern: blob-report-chromium-*
merge-multiple: true
- name: Merge into HTML Report
run: npx playwright merge-reports --reporter html ./all-blob-reports
working-directory: ComfyUI_frontend
- name: Upload HTML report
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: deployment-info-${{ matrix.browser }} name: playwright-report-chromium
path: deployment-info/ path: ComfyUI_frontend/playwright-report/
retention-days: 1 retention-days: 30
- name: Get completion time #### BEGIN Deployment and commenting (non-forked PRs only)
id: completion-time # when using pull_request event, we have permission to comment directly
if: always() # if its a forked repo, we need to use workflow_run event in a separate workflow (pr-playwright-deploy.yaml)
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Comment PR - Browser Test Complete # Post starting comment for non-forked PRs
if: always() && github.event_name == 'pull_request' comment-on-pr-start:
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
${{ steps.playwright.conclusion == 'success' && '✅' || '❌' }} **${{ matrix.browser }}**: ${{ steps.playwright.conclusion == 'success' && 'Tests passed!' || 'Tests failed!' }} [View Report](${{ steps.cloudflare-deploy.outputs.deployment-url || format('https://{0}.{1}.pages.dev', steps.project-name.outputs.branch, steps.project-name.outputs.name) }})
comment-summary:
needs: playwright-tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() && github.event_name == 'pull_request' if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
permissions: permissions:
pull-requests: write pull-requests: write
steps: steps:
- name: Download all deployment info - name: Checkout repository
uses: actions/checkout@v4
- name: Get start time
id: start-time
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
- name: Post starting comment
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh
./scripts/cicd/pr-playwright-deploy-and-comment.sh \
"${{ github.event.pull_request.number }}" \
"${{ github.head_ref }}" \
"starting" \
"${{ steps.start-time.outputs.time }}"
# Deploy and comment for non-forked PRs only
deploy-and-comment:
needs: [playwright-tests, merge-reports]
runs-on: ubuntu-latest
if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download all playwright reports
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
pattern: deployment-info-* pattern: playwright-report-*
merge-multiple: true path: reports
path: deployment-info
- name: Make deployment script executable
- name: Get completion time run: chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh
id: completion-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT - name: Deploy reports and comment on PR
env:
- name: Generate comment body CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
id: comment-body CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
GITHUB_TOKEN: ${{ github.token }}
run: | run: |
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md ./scripts/cicd/pr-playwright-deploy-and-comment.sh \
echo "## 🎭 Playwright Test Results" >> comment.md "${{ github.event.pull_request.number }}" \
echo "" >> comment.md "${{ github.head_ref }}" \
"completed"
# Check if all tests passed #### END Deployment and commenting (non-forked PRs only)
ALL_PASSED=true
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
browser=$(basename "$file" .txt)
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
if [ "$exit_code" != "0" ]; then
ALL_PASSED=false
break
fi
fi
done
if [ "$ALL_PASSED" = "true" ]; then
echo "✅ **All tests passed across all browsers!**" >> comment.md
else
echo "❌ **Some tests failed!**" >> comment.md
fi
echo "" >> comment.md
echo "⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
echo "" >> comment.md
echo "### 📊 Test Reports by Browser" >> comment.md
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
browser=$(basename "$file" .txt)
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
url=$(echo "$info" | cut -d'|' -f3)
if [ "$exit_code" = "0" ]; then
status="✅"
else
status="❌"
fi
echo "- $status **$browser**: [View Report]($url)" >> comment.md
fi
done
echo "" >> comment.md
echo "---" >> comment.md
if [ "$ALL_PASSED" = "true" ]; then
echo "🎉 Your tests are passing across all browsers!" >> comment.md
else
echo "⚠️ Please check the test reports for details on failures." >> comment.md
fi
- name: Comment PR - Tests Complete
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body-path: comment.md
- name: Check test results and fail if needed
run: |
# Check if all tests passed and fail the job if not
ALL_PASSED=true
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
if [ "$exit_code" != "0" ]; then
ALL_PASSED=false
break
fi
fi
done
if [ "$ALL_PASSED" = "false" ]; then
echo "❌ Tests failed in one or more browsers. Failing the CI job."
exit 1
else
echo "✅ All tests passed across all browsers!"
fi

View File

@@ -35,12 +35,12 @@ jobs:
electron-types-tools-cache-${{ runner.os }}- electron-types-tools-cache-${{ runner.os }}-
- name: Update electron types - name: Update electron types
run: pnpm install @comfyorg/comfyui-electron-types@latest run: pnpm install --workspace-root @comfyorg/comfyui-electron-types@latest
- name: Get new version - name: Get new version
id: get-version id: get-version
run: | run: |
NEW_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('./pnpm-lock.yaml')).packages['node_modules/@comfyorg/comfyui-electron-types'].version)") NEW_VERSION=$(pnpm list @comfyorg/comfyui-electron-types --json --depth=0 | jq -r '.[0].version')
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Create Pull Request - name: Create Pull Request

View File

@@ -121,4 +121,4 @@ jobs:
labels: Manager labels: Manager
delete-branch: true delete-branch: true
add-paths: | add-paths: |
src/types/generatedManagerTypes.ts src/types/generatedManagerTypes.ts

11
.gitignore vendored
View File

@@ -22,6 +22,7 @@ dist-ssr
*.local *.local
# Claude configuration # Claude configuration
.claude/*.local.json .claude/*.local.json
CLAUDE.local.md
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
@@ -50,6 +51,7 @@ tests-ui/workflows/examples
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
browser_tests/**/*-win32.png browser_tests/**/*-win32.png
browser-tests/local/
.env .env
@@ -76,3 +78,12 @@ vite.config.mts.timestamp-*.mjs
*storybook.log *storybook.log
storybook-static storybook-static
.nx/cache
.nx/workspace-data
.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md
vite.config.*.timestamp*
vitest.config.*.timestamp*

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
npx lint-staged pnpm exec lint-staged
npx tsx scripts/check-unused-i18n-keys.ts pnpm exec tsx scripts/check-unused-i18n-keys.ts

5
.husky/pre-push Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# Run Knip with cache via package script
pnpm knip

View File

@@ -1,12 +0,0 @@
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@executeautomation/playwright-mcp-server"]
},
"context7": {
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"]
}
}
}

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
24

View File

@@ -4,17 +4,26 @@
transition: background-color 0.3s ease, color 0.3s ease; transition: background-color 0.3s ease, color 0.3s ease;
} }
/* Light theme default */ /* Light theme default - with explicit color to override media queries */
body { body:not(.dark-theme) {
background-color: #ffffff; background-color: #fff !important;
color: #1a1a1a; color: #000 !important;
}
/* Override browser dark mode preference for light theme */
@media (prefers-color-scheme: dark) {
body:not(.dark-theme) {
color: #000 !important;
--fg-color: #000 !important;
--bg-color: #fff !important;
}
} }
/* Dark theme styles */ /* Dark theme styles */
body.dark-theme, body.dark-theme,
.dark-theme body { .dark-theme body {
background-color: #0a0a0a; background-color: #202020;
color: #e5e5e5; color: #fff;
} }
/* Ensure Storybook canvas follows theme */ /* Ensure Storybook canvas follows theme */
@@ -24,11 +33,32 @@
.dark-theme .sb-show-main, .dark-theme .sb-show-main,
.dark-theme .docs-story { .dark-theme .docs-story {
background-color: #0a0a0a !important; background-color: #202020 !important;
} }
/* Fix for Storybook controls panel in dark mode */ /* CSS Variables for theme consistency */
.dark-theme .docblock-argstable-body { body:not(.dark-theme) {
color: #e5e5e5; --fg-color: #000;
--bg-color: #fff;
--content-bg: #e0e0e0;
--content-fg: #000;
--content-hover-bg: #adadad;
--content-hover-fg: #000;
}
body.dark-theme {
--fg-color: #fff;
--bg-color: #202020;
--content-bg: #4e4e4e;
--content-fg: #fff;
--content-hover-bg: #222;
--content-hover-fg: #fff;
}
/* Override Storybook's problematic & selector styles */
/* Reset only the specific properties that Storybook injects */
li+li {
margin: 0;
padding: revert-layer;
} }
</style> </style>

44
.vscode/tailwind.json vendored
View File

@@ -2,12 +2,32 @@
"version": 1.1, "version": 1.1,
"atDirectives": [ "atDirectives": [
{ {
"name": "@tailwind", "name": "@import",
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.", "description": "Use the `@import` directive to inline CSS files, including Tailwind itself, into your stylesheet.",
"references": [ "references": [
{ {
"name": "Tailwind Documentation", "name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind" "url": "https://tailwindcss.com/docs/functions-and-directives#import"
}
]
},
{
"name": "@theme",
"description": "Use the `@theme` directive to define custom design tokens like fonts, colors, and breakpoints.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#theme"
}
]
},
{
"name": "@layer",
"description": "Use the `@layer` directive inside `@theme` to organize custom styles into different layers like `base`, `components`, and `utilities`.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#layer"
} }
] ]
}, },
@@ -22,32 +42,32 @@
] ]
}, },
{ {
"name": "@responsive", "name": "@config",
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n", "description": "Use the `@config` directive to load a legacy JavaScript-based Tailwind configuration file.",
"references": [ "references": [
{ {
"name": "Tailwind Documentation", "name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive" "url": "https://tailwindcss.com/docs/functions-and-directives#config"
} }
] ]
}, },
{ {
"name": "@screen", "name": "@reference",
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n", "description": "Use the `@reference` directive to import theme variables, custom utilities, and custom variants from other files without duplicating CSS.",
"references": [ "references": [
{ {
"name": "Tailwind Documentation", "name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#screen" "url": "https://tailwindcss.com/docs/functions-and-directives#reference"
} }
] ]
}, },
{ {
"name": "@variants", "name": "@plugin",
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n", "description": "Use the `@plugin` directive to load a legacy JavaScript-based Tailwind plugin.",
"references": [ "references": [
{ {
"name": "Tailwind Documentation", "name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#variants" "url": "https://tailwindcss.com/docs/functions-and-directives#plugin"
} }
] ]
} }

View File

@@ -1,22 +1,52 @@
# ComfyUI Frontend Project Guidelines # ComfyUI Frontend Project Guidelines
## Repository Setup
For first-time setup, use the Claude command:
```
/setup_repo
```
This bootstraps the monorepo with dependencies, builds, tests, and dev server verification.
**Prerequisites:** Node.js >= 24, Git repository, available ports (5173, 6006)
## Quick Commands ## Quick Commands
- `pnpm`: See all available commands - `pnpm`: See all available commands
- `pnpm dev`: Start development server (port 5173, via nx)
- `pnpm typecheck`: Type checking - `pnpm typecheck`: Type checking
- `pnpm lint`: Linting - `pnpm build`: Build for production (via nx)
- `pnpm lint`: Linting (via nx)
- `pnpm format`: Prettier formatting - `pnpm format`: Prettier formatting
- `pnpm test:component`: Run component tests with browser environment - `pnpm test:component`: Run component tests with browser environment
- `pnpm test:unit`: Run all unit tests - `pnpm test:unit`: Run all unit tests
- `pnpm test:browser`: Run E2E tests via Playwright
- `pnpm test:unit -- tests-ui/tests/example.test.ts`: Run single test file - `pnpm test:unit -- tests-ui/tests/example.test.ts`: Run single test file
- `pnpm storybook`: Start Storybook development server (port 6006)
- `pnpm knip`: Detect unused code and dependencies
## Monorepo Architecture
The project now uses **Nx** for build orchestration and task management:
- **Task Orchestration**: Commands like `dev`, `build`, `lint`, and `test:browser` run via Nx
- **Caching**: Nx provides intelligent caching for faster rebuilds
- **Configuration**: Managed through `nx.json` with plugins for ESLint, Storybook, Vite, and Playwright
- **Dependencies**: Nx handles dependency graph analysis and parallel execution
Key Nx features:
- Build target caching and incremental builds
- Parallel task execution across the monorepo
- Plugin-based architecture for different tools
## Development Workflow ## Development Workflow
1. Make code changes 1. **First-time setup**: Run `/setup_repo` Claude command
2. Run tests (see subdirectory CLAUDE.md files) 2. Make code changes
3. Run typecheck, lint, format 3. Run tests (see subdirectory CLAUDE.md files)
4. Check README updates 4. Run typecheck, lint, format
5. Consider docs.comfy.org updates 5. Check README updates
6. Consider docs.comfy.org updates
## Git Conventions ## Git Conventions
@@ -52,6 +82,44 @@ When referencing Comfy-Org repos:
2. Use GitHub API for branches/PRs/metadata 2. Use GitHub API for branches/PRs/metadata
3. Curl GitHub website if needed 3. Curl GitHub website if needed
## Settings and Feature Flags Quick Reference
### Settings Usage
```typescript
const settingStore = useSettingStore()
const value = settingStore.get('Comfy.SomeSetting') // Get setting
await settingStore.set('Comfy.SomeSetting', newValue) // Update setting
```
### Dynamic Defaults
```typescript
{
id: 'Comfy.Example.Setting',
defaultValue: () => window.innerWidth < 1024 ? 'small' : 'large' // Runtime context
}
```
### Version-Based Defaults
```typescript
{
id: 'Comfy.Example.Feature',
defaultValue: 'legacy',
defaultsByInstallVersion: { '1.25.0': 'enhanced' } // Gradual rollout
}
```
### Feature Flags
```typescript
if (api.serverSupportsFeature('feature_name')) { // Check capability
// Use enhanced feature
}
const value = api.getServerFeature('config_name', defaultValue) // Get config
```
**Documentation:**
- Settings system: `docs/SETTINGS.md`
- Feature flags system: `docs/FEATURE_FLAGS.md`
## Common Pitfalls ## Common Pitfalls
- NEVER use `any` type - use proper TypeScript types - NEVER use `any` type - use proper TypeScript types
@@ -59,3 +127,6 @@ When referencing Comfy-Org repos:
- NEVER use `--no-verify` flag when committing - NEVER use `--no-verify` flag when committing
- NEVER delete or disable tests to make them pass - NEVER delete or disable tests to make them pass
- NEVER circumvent quality checks - NEVER circumvent quality checks
- NEVER use `dark:` prefix - always use `dark-theme:` for dark mode styles, for example: `dark-theme:text-white dark-theme:bg-black`
- NEVER use `:class="[]"` to merge class names - always use `import { cn } from '@/utils/tailwindUtil'`, for example: `<div :class="cn('bg-red-500', { 'bg-blue-500': condition })" />`

View File

@@ -14,4 +14,4 @@
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs /src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs
# Mask Editor extension # Mask Editor extension
/src/extensions/core/maskeditor.ts @trsommer @Comfy-Org/comfy_frontend_devs /src/extensions/core/maskeditor.ts @brucew4yn3rp @trsommer @Comfy-Org/comfy_frontend_devs

View File

@@ -17,7 +17,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
### Prerequisites & Technology Stack ### Prerequisites & Technology Stack
- **Required Software**: - **Required Software**:
- Node.js (v16 or later; v24 strongly recommended) and pnpm - Node.js (v18 or later to build; v24 for vite dev server) and pnpm
- Git for version control - Git for version control
- A running ComfyUI backend instance - A running ComfyUI backend instance

View File

@@ -10,7 +10,7 @@ import type { Position } from './types'
* - {@link Mouse.move} * - {@link Mouse.move}
* - {@link Mouse.up} * - {@link Mouse.up}
*/ */
export interface DragOptions { interface DragOptions {
button?: 'left' | 'right' | 'middle' button?: 'left' | 'right' | 'middle'
clickCount?: number clickCount?: number
steps?: number steps?: number

View File

@@ -453,6 +453,32 @@ export class ComfyPage {
await workflowsTab.close() await workflowsTab.close()
} }
/**
* Attach a screenshot to the test report.
* By default, screenshots are only taken in non-CI environments.
* @param name - Name for the screenshot attachment
* @param options - Optional configuration
* @param options.runInCI - Whether to take screenshot in CI (default: false)
* @param options.fullPage - Whether to capture full page (default: false)
*/
async attachScreenshot(
name: string,
options: { runInCI?: boolean; fullPage?: boolean } = {}
) {
const { runInCI = false, fullPage = false } = options
// Skip in CI unless explicitly requested
if (process.env.CI && !runInCI) {
return
}
const testInfo = comfyPageFixture.info()
await testInfo.attach(name, {
body: await this.page.screenshot({ fullPage }),
contentType: 'image/png'
})
}
async resetView() { async resetView() {
if (await this.resetViewButton.isVisible()) { if (await this.resetViewButton.isVisible()) {
await this.resetViewButton.click() await this.resetViewButton.click()

View File

@@ -1,5 +1,4 @@
import { test as base } from '@playwright/test' import { Page, test as base } from '@playwright/test'
import { Page } from 'playwright'
export class UserSelectPage { export class UserSelectPage {
constructor( constructor(

View File

@@ -1,7 +1,13 @@
import { Locator, Page } from '@playwright/test' import { Locator, Page, expect } from '@playwright/test'
export class Topbar { export class Topbar {
constructor(public readonly page: Page) {} private readonly menuLocator: Locator
private readonly menuTrigger: Locator
constructor(public readonly page: Page) {
this.menuLocator = page.locator('.comfy-command-menu')
this.menuTrigger = page.locator('.comfyui-logo-wrapper')
}
async getTabNames(): Promise<string[]> { async getTabNames(): Promise<string[]> {
return await this.page return await this.page
@@ -15,10 +21,33 @@ export class Topbar {
.innerText() .innerText()
} }
getMenuItem(itemLabel: string): Locator { /**
* Get a menu item by its label, optionally within a specific parent container
*/
getMenuItem(itemLabel: string, parent?: Locator): Locator {
if (parent) {
return parent.locator(`.p-tieredmenu-item:has-text("${itemLabel}")`)
}
return this.page.locator(`.p-menubar-item-label:text-is("${itemLabel}")`) return this.page.locator(`.p-menubar-item-label:text-is("${itemLabel}")`)
} }
/**
* Get the visible submenu (last visible submenu in case of nested menus)
*/
getVisibleSubmenu(): Locator {
return this.page.locator('.p-tieredmenu-submenu:visible').last()
}
/**
* Check if a menu item has an active checkmark
*/
async isMenuItemActive(menuItem: Locator): Promise<boolean> {
const checkmark = menuItem.locator('.pi-check')
const classes = await checkmark.getAttribute('class')
return classes ? !classes.includes('invisible') : false
}
getWorkflowTab(tabName: string): Locator { getWorkflowTab(tabName: string): Locator {
return this.page return this.page
.locator(`.workflow-tabs .workflow-label:has-text("${tabName}")`) .locator(`.workflow-tabs .workflow-label:has-text("${tabName}")`)
@@ -66,10 +95,50 @@ export class Topbar {
async openTopbarMenu() { async openTopbarMenu() {
await this.page.waitForTimeout(1000) await this.page.waitForTimeout(1000)
await this.page.locator('.comfyui-logo-wrapper').click() await this.menuTrigger.click()
const menu = this.page.locator('.comfy-command-menu') await this.menuLocator.waitFor({ state: 'visible' })
await menu.waitFor({ state: 'visible' }) return this.menuLocator
return menu }
/**
* Close the topbar menu by clicking outside
*/
async closeTopbarMenu() {
await this.page.locator('body').click({ position: { x: 10, y: 10 } })
await expect(this.menuLocator).not.toBeVisible()
}
/**
* Navigate to a submenu by hovering over a menu item
*/
async openSubmenu(menuItemLabel: string): Promise<Locator> {
const menuItem = this.getMenuItem(menuItemLabel)
await menuItem.hover()
const submenu = this.getVisibleSubmenu()
await submenu.waitFor({ state: 'visible' })
return submenu
}
/**
* Get theme menu items and interact with theme switching
*/
async getThemeMenuItems() {
const themeSubmenu = await this.openSubmenu('Theme')
return {
submenu: themeSubmenu,
darkTheme: this.getMenuItem('Dark (Default)', themeSubmenu),
lightTheme: this.getMenuItem('Light', themeSubmenu)
}
}
/**
* Switch to a specific theme
*/
async switchTheme(theme: 'dark' | 'light') {
const { darkTheme, lightTheme } = await this.getThemeMenuItems()
const themeItem = theme === 'dark' ? darkTheme : lightTheme
const themeLabel = themeItem.locator('.p-menubar-item-label')
await themeLabel.click()
} }
async triggerTopbarCommand(path: string[]) { async triggerTopbarCommand(path: string[]) {
@@ -79,9 +148,7 @@ export class Topbar {
const menu = await this.openTopbarMenu() const menu = await this.openTopbarMenu()
const tabName = path[0] const tabName = path[0]
const topLevelMenuItem = this.page.locator( const topLevelMenuItem = this.getMenuItem(tabName)
`.p-menubar-item-label:text-is("${tabName}")`
)
const topLevelMenu = menu const topLevelMenu = menu
.locator('.p-tieredmenu-item') .locator('.p-tieredmenu-item')
.filter({ has: topLevelMenuItem }) .filter({ has: topLevelMenuItem })

View File

@@ -134,7 +134,7 @@ export class SubgraphSlotReference {
} }
} }
export class NodeSlotReference { class NodeSlotReference {
constructor( constructor(
readonly type: 'input' | 'output', readonly type: 'input' | 'output',
readonly index: number, readonly index: number,
@@ -201,7 +201,7 @@ export class NodeSlotReference {
} }
} }
export class NodeWidgetReference { class NodeWidgetReference {
constructor( constructor(
readonly index: number, readonly index: number,
readonly node: NodeReference readonly node: NodeReference

View File

@@ -1,7 +1,7 @@
import type { Request, Route } from '@playwright/test'
import _ from 'es-toolkit/compat' import _ from 'es-toolkit/compat'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import type { Request, Route } from 'playwright'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import type { import type {

View File

@@ -0,0 +1,131 @@
import type { Locator, Page } from '@playwright/test'
import type { NodeReference } from './litegraphUtils'
/**
* VueNodeFixture provides Vue-specific testing utilities for interacting with
* Vue node components. It bridges the gap between litegraph node references
* and Vue UI components.
*/
export class VueNodeFixture {
constructor(
private readonly nodeRef: NodeReference,
private readonly page: Page
) {}
/**
* Get the node's header element using data-testid
*/
async getHeader(): Promise<Locator> {
const nodeId = this.nodeRef.id
return this.page.locator(`[data-testid="node-header-${nodeId}"]`)
}
/**
* Get the node's title element
*/
async getTitleElement(): Promise<Locator> {
const header = await this.getHeader()
return header.locator('[data-testid="node-title"]')
}
/**
* Get the current title text
*/
async getTitle(): Promise<string> {
const titleElement = await this.getTitleElement()
return (await titleElement.textContent()) || ''
}
/**
* Set a new title by double-clicking and entering text
*/
async setTitle(newTitle: string): Promise<void> {
const titleElement = await this.getTitleElement()
await titleElement.dblclick()
const input = (await this.getHeader()).locator(
'[data-testid="node-title-input"]'
)
await input.fill(newTitle)
await input.press('Enter')
}
/**
* Cancel title editing
*/
async cancelTitleEdit(): Promise<void> {
const titleElement = await this.getTitleElement()
await titleElement.dblclick()
const input = (await this.getHeader()).locator(
'[data-testid="node-title-input"]'
)
await input.press('Escape')
}
/**
* Check if the title is currently being edited
*/
async isEditingTitle(): Promise<boolean> {
const header = await this.getHeader()
const input = header.locator('[data-testid="node-title-input"]')
return await input.isVisible()
}
/**
* Get the collapse/expand button
*/
async getCollapseButton(): Promise<Locator> {
const header = await this.getHeader()
return header.locator('[data-testid="node-collapse-button"]')
}
/**
* Toggle the node's collapsed state
*/
async toggleCollapse(): Promise<void> {
const button = await this.getCollapseButton()
await button.click()
}
/**
* Get the collapse icon element
*/
async getCollapseIcon(): Promise<Locator> {
const button = await this.getCollapseButton()
return button.locator('i')
}
/**
* Get the collapse icon's CSS classes
*/
async getCollapseIconClass(): Promise<string> {
const icon = await this.getCollapseIcon()
return (await icon.getAttribute('class')) || ''
}
/**
* Check if the collapse button is visible
*/
async isCollapseButtonVisible(): Promise<boolean> {
const button = await this.getCollapseButton()
return await button.isVisible()
}
/**
* Get the node's body/content element
*/
async getBody(): Promise<Locator> {
const nodeId = this.nodeRef.id
return this.page.locator(`[data-testid="node-body-${nodeId}"]`)
}
/**
* Check if the node body is visible (not collapsed)
*/
async isBodyVisible(): Promise<boolean> {
const body = await this.getBody()
return await body.isVisible()
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -36,6 +36,10 @@ test('Does not report warning on undo/redo', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('missing/missing_nodes') await comfyPage.loadWorkflow('missing/missing_nodes')
await comfyPage.closeDialog() await comfyPage.closeDialog()
// Wait for any async operations to complete after dialog closes
await comfyPage.nextFrame()
await comfyPage.page.waitForTimeout(100)
// Make a change to the graph // Make a change to the graph
await comfyPage.doubleClickCanvas() await comfyPage.doubleClickCanvas()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
@@ -59,18 +63,6 @@ test.describe('Execution error', () => {
const executionError = comfyPage.page.locator('.comfy-error-report') const executionError = comfyPage.page.locator('.comfy-error-report')
await expect(executionError).toBeVisible() await expect(executionError).toBeVisible()
}) })
test('Can display Issue Report form', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/execution_error')
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
await comfyPage.page.getByLabel('Help Fix This').click()
const issueReportForm = comfyPage.page.getByText(
'Submit Error Report (Optional)'
)
await expect(issueReportForm).toBeVisible()
})
}) })
test.describe('Missing models warning', () => { test.describe('Missing models warning', () => {
@@ -303,37 +295,16 @@ test.describe('Settings', () => {
}) })
}) })
test.describe('Feedback dialog', () => { test.describe('Support', () => {
test('Should open from topmenu help command', async ({ comfyPage }) => { test('Should open external zendesk link', async ({ comfyPage }) => {
// Open feedback dialog from top menu
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback']) const pagePromise = comfyPage.page.context().waitForEvent('page')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support'])
const newPage = await pagePromise
// Verify feedback dialog content is visible await newPage.waitForLoadState('networkidle')
const feedbackHeader = comfyPage.page.getByRole('heading', { await expect(newPage).toHaveURL(/.*support\.comfy\.org.*/)
name: 'Feedback' await newPage.close()
})
await expect(feedbackHeader).toBeVisible()
})
test('Should close when close button clicked', async ({ comfyPage }) => {
// Open feedback dialog
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
const feedbackHeader = comfyPage.page.getByRole('heading', {
name: 'Feedback'
})
// Close feedback dialog
await comfyPage.page
.getByLabel('', { exact: true })
.getByLabel('Close')
.click()
await feedbackHeader.waitFor({ state: 'hidden' })
// Verify dialog is closed
await expect(feedbackHeader).not.toBeVisible()
}) })
}) })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Some files were not shown because too many files have changed in this diff Show More