diff --git a/.claude/commands/comprehensive-pr-review.md b/.claude/commands/comprehensive-pr-review.md
index 84708564e..1b4047e78 100644
--- a/.claude/commands/comprehensive-pr-review.md
+++ b/.claude/commands/comprehensive-pr-review.md
@@ -67,9 +67,9 @@ This is critical for better file inspection:
Use git locally for much faster analysis:
-1. Get list of changed files: `git diff --name-only "origin/$BASE_BRANCH" > changed_files.txt`
-2. Get the full diff: `git diff "origin/$BASE_BRANCH" > pr_diff.txt`
-3. Get detailed file changes with status: `git diff --name-status "origin/$BASE_BRANCH" > file_changes.txt`
+1. Get list of changed files: `git diff --name-only "$BASE_SHA" > changed_files.txt`
+2. Get the full diff: `git diff "$BASE_SHA" > pr_diff.txt`
+3. Get detailed file changes with status: `git diff --name-status "$BASE_SHA" > file_changes.txt`
### Step 1.5: Create Analysis Cache
diff --git a/.claude/commands/create-frontend-release.md b/.claude/commands/create-frontend-release.md
index de6695710..f16189d42 100644
--- a/.claude/commands/create-frontend-release.md
+++ b/.claude/commands/create-frontend-release.md
@@ -294,7 +294,6 @@ echo "Last stable release: $LAST_STABLE"
1. Run complete test suite:
```bash
pnpm test:unit
- pnpm test:component
```
2. Run type checking:
```bash
@@ -459,15 +458,15 @@ echo "Workflow triggered. Waiting for PR creation..."
3. **IMMEDIATELY CHECK**: Did release workflow trigger?
```bash
sleep 10
- gh run list --workflow=release.yaml --limit=1
+ gh run list --workflow=release-draft-create.yaml --limit=1
```
-4. **For Minor/Major Version Releases**: The create-release-candidate-branch workflow will automatically:
+4. **For Minor/Major Version Releases**: The release-branch-create workflow will automatically:
- Create a `core/x.yy` branch for the PREVIOUS minor version
- Apply branch protection rules
- Document the feature freeze policy
```bash
# Monitor branch creation (for minor/major releases)
- gh run list --workflow=create-release-candidate-branch.yaml --limit=1
+ gh run list --workflow=release-branch-create.yaml --limit=1
```
4. If workflow didn't trigger due to [skip ci]:
```bash
@@ -478,7 +477,7 @@ echo "Workflow triggered. Waiting for PR creation..."
```
5. If workflow triggered, monitor execution:
```bash
- WORKFLOW_RUN_ID=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
+ WORKFLOW_RUN_ID=$(gh run list --workflow=release-draft-create.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
gh run watch ${WORKFLOW_RUN_ID}
```
diff --git a/.claude/commands/create-hotfix-release.md b/.claude/commands/create-hotfix-release.md
index de314309d..cc9c37ef5 100644
--- a/.claude/commands/create-hotfix-release.md
+++ b/.claude/commands/create-hotfix-release.md
@@ -1,30 +1,85 @@
# 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
-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
Examples:
-- `abc123,def456,ghi789` (commits)
-- `#1234,#5678` (PRs)
-- `abc123,#1234,def456` (mixed)
+- `#1234,#5678` (PRs - preferred)
+- `abc123,def456` (commit hashes)
+- `#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.
## Prerequisites
-Before starting, ensure:
-- You have push access to the repository
-- GitHub CLI (`gh`) is authenticated
-- You're on a clean working tree
-- You understand the commits/PRs you're cherry-picking
+- Push access to repository
+- GitHub CLI (`gh`) authenticated
+- Clean working tree
+- Understanding of what fixes need backporting
## 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:
```bash
@@ -36,7 +91,7 @@ Before starting, ensure:
5. Verify the core branch exists: `git ls-remote origin refs/heads/core/*`
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
2. For each item:
@@ -49,7 +104,7 @@ Before starting, ensure:
- **CONFIRMATION REQUIRED**: Use merge commit or cherry-pick individual commits?
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:
- Display commit hash, author, date
@@ -60,7 +115,7 @@ Before starting, ensure:
2. Identify potential conflicts by checking changed files
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`)
2. Pull latest changes: `git pull origin core/X.Y`
@@ -69,7 +124,7 @@ Before starting, ensure:
- Example: `hotfix/1.23.4-20241120`
5. **CONFIRMATION REQUIRED**: Created branch correctly?
-### Step 5: Cherry-pick Changes
+### Step 6: Cherry-pick Changes
For each commit:
1. Attempt cherry-pick: `git cherry-pick `
@@ -83,7 +138,7 @@ For each commit:
- Run validation: `pnpm typecheck && pnpm lint`
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/-`
2. Create PR using gh CLI:
@@ -100,7 +155,7 @@ For each commit:
- Impact assessment
5. **CONFIRMATION REQUIRED**: PR created correctly?
-### Step 7: Wait for Tests
+### Step 8: Wait for Tests
1. Monitor PR checks: `gh pr checks`
2. Display test results as they complete
@@ -111,7 +166,7 @@ For each commit:
4. Wait for all required checks to pass
5. **CONFIRMATION REQUIRED**: All tests passing?
-### Step 8: Merge Hotfix PR
+### Step 9: Merge Hotfix PR
1. Verify all checks have passed
2. Check for required approvals
@@ -119,7 +174,7 @@ For each commit:
4. Delete the hotfix branch
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`
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"`
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`
2. Create PR with Release label:
@@ -184,14 +239,14 @@ For each commit:
```
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
2. **FINAL CONFIRMATION**: Ready to trigger release by merging?
3. Merge the PR: `gh pr merge --merge`
4. Monitor release workflow:
```bash
- gh run list --workflow=release.yaml --limit=1
+ gh run list --workflow=release-draft-create.yaml --limit=1
gh run watch
```
5. Track progress:
@@ -199,7 +254,102 @@ For each commit:
- PyPI upload
- 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 <> $GITHUB_OUTPUT
+
+ - name: Cache Playwright Browsers
+ uses: actions/cache@v4
+ id: cache-playwright-browsers
+ with:
+ path: '~/.cache/ms-playwright'
+ key: ${{ runner.os }}-playwright-browsers-${{ steps.detect-version.outputs.playwright-version }}
+
+ - name: Install Playwright Browsers
+ if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
+ shell: bash
+ run: pnpm exec playwright install chromium --with-deps
+
+ - name: Install Playwright Browsers (operating system dependencies)
+ if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
+ shell: bash
+ run: pnpm exec playwright install-deps
diff --git a/.github/workflows/README.md b/.github/workflows/README.md
new file mode 100644
index 000000000..f62e91eda
--- /dev/null
+++ b/.github/workflows/README.md
@@ -0,0 +1,21 @@
+# GitHub Workflows
+
+## Naming Convention
+
+Workflow files follow a consistent naming pattern: `-.yaml`
+
+### Category Prefixes
+
+| Prefix | Purpose | Example |
+| ---------- | ----------------------------------- | ------------------------------------ |
+| `ci-` | Testing, linting, validation | `ci-tests-e2e.yaml` |
+| `release-` | Version management, publishing | `release-version-bump.yaml` |
+| `pr-` | PR automation (triggered by labels) | `pr-claude-review.yaml` |
+| `api-` | External Api type generation | `api-update-registry-api-types.yaml` |
+| `i18n-` | Internationalization updates | `i18n-update-core.yaml` |
+
+## Documentation
+
+Each workflow file contains comments explaining its purpose, triggers, and behavior. For specific details about what each workflow does, refer to the comments at the top of each `.yaml` file.
+
+For GitHub Actions documentation, see [Events that trigger workflows](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows).
diff --git a/.github/workflows/update-electron-types.yaml b/.github/workflows/api-update-electron-api-types.yaml
similarity index 78%
rename from .github/workflows/update-electron-types.yaml
rename to .github/workflows/api-update-electron-api-types.yaml
index 04d8cc436..90a08e02c 100644
--- a/.github/workflows/update-electron-types.yaml
+++ b/.github/workflows/api-update-electron-api-types.yaml
@@ -1,4 +1,5 @@
-name: Update Electron Types
+name: 'Api: Update Electron API Types'
+description: 'When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo'
on:
workflow_dispatch:
@@ -12,7 +13,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -35,12 +36,12 @@ jobs:
electron-types-tools-cache-${{ runner.os }}-
- 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
id: get-version
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].dependencies."@comfyorg/comfyui-electron-types".version')
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Create Pull Request
diff --git a/.github/workflows/update-manager-types.yaml b/.github/workflows/api-update-manager-api-types.yaml
similarity index 91%
rename from .github/workflows/update-manager-types.yaml
rename to .github/workflows/api-update-manager-api-types.yaml
index 244127dc2..5a1e8ec74 100644
--- a/.github/workflows/update-manager-types.yaml
+++ b/.github/workflows/api-update-manager-api-types.yaml
@@ -1,4 +1,5 @@
-name: Update ComfyUI-Manager API Types
+name: 'Api: Update Manager API Types'
+description: 'When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo'
on:
# Manual trigger
@@ -17,7 +18,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -51,7 +52,7 @@ jobs:
comfyui-manager-repo-${{ runner.os }}-
- name: Checkout ComfyUI-Manager repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
repository: Comfy-Org/ComfyUI-Manager
path: ComfyUI-Manager
@@ -68,7 +69,7 @@ jobs:
- name: Generate Manager API types
run: |
echo "Generating TypeScript types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}..."
- npx openapi-typescript ./ComfyUI-Manager/openapi.yaml --output ./src/types/generatedManagerTypes.ts
+ pnpm dlx openapi-typescript ./ComfyUI-Manager/openapi.yaml --output ./src/types/generatedManagerTypes.ts
- name: Validate generated types
run: |
diff --git a/.github/workflows/update-registry-types.yaml b/.github/workflows/api-update-registry-api-types.yaml
similarity index 81%
rename from .github/workflows/update-registry-types.yaml
rename to .github/workflows/api-update-registry-api-types.yaml
index 0cd2c41da..69b4ad5b2 100644
--- a/.github/workflows/update-registry-types.yaml
+++ b/.github/workflows/api-update-registry-api-types.yaml
@@ -1,4 +1,5 @@
-name: Update Comfy Registry API Types
+name: 'Api: Update Registry API Types'
+description: 'When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo'
on:
# Manual trigger
@@ -16,7 +17,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -50,7 +51,7 @@ jobs:
comfy-api-repo-${{ runner.os }}-
- name: Checkout comfy-api repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
repository: Comfy-Org/comfy-api
path: comfy-api
@@ -68,17 +69,18 @@ jobs:
- name: Generate API types
run: |
echo "Generating TypeScript types from comfy-api@${{ steps.api-info.outputs.commit }}..."
- npx openapi-typescript ./comfy-api/openapi.yml --output ./src/types/comfyRegistryTypes.ts
+ mkdir -p ./packages/registry-types/src
+ pnpm dlx openapi-typescript ./comfy-api/openapi.yml --output ./packages/registry-types/src/comfyRegistryTypes.ts
- name: Validate generated types
run: |
- if [ ! -f ./src/types/comfyRegistryTypes.ts ]; then
+ if [ ! -f ./packages/registry-types/src/comfyRegistryTypes.ts ]; then
echo "Error: Types file was not generated."
exit 1
fi
# Check if file is not empty
- if [ ! -s ./src/types/comfyRegistryTypes.ts ]; then
+ if [ ! -s ./packages/registry-types/src/comfyRegistryTypes.ts ]; then
echo "Error: Generated types file is empty."
exit 1
fi
@@ -86,12 +88,12 @@ jobs:
- name: Lint generated types
run: |
echo "Linting generated Comfy Registry API types..."
- pnpm lint:fix:no-cache -- ./src/types/comfyRegistryTypes.ts
+ pnpm lint:fix:no-cache -- ./packages/registry-types/src/comfyRegistryTypes.ts
- name: Check for changes
id: check-changes
run: |
- if [[ -z $(git status --porcelain ./src/types/comfyRegistryTypes.ts) ]]; then
+ if [[ -z $(git status --porcelain ./packages/registry-types/src/comfyRegistryTypes.ts) ]]; then
echo "No changes to Comfy Registry API types detected."
echo "changed=false" >> $GITHUB_OUTPUT
exit 0
@@ -121,4 +123,4 @@ jobs:
labels: CNR
delete-branch: true
add-paths: |
- src/types/comfyRegistryTypes.ts
+ packages/registry-types/src/comfyRegistryTypes.ts
diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml
deleted file mode 100644
index f9106caee..000000000
--- a/.github/workflows/backport.yaml
+++ /dev/null
@@ -1,165 +0,0 @@
-name: Auto Backport
-
-on:
- pull_request_target:
- types: [closed]
- branches: [main]
-
-jobs:
- backport:
- if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-backport')
- runs-on: ubuntu-latest
- permissions:
- contents: write
- pull-requests: write
- issues: write
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Configure git
- run: |
- git config user.name "github-actions[bot]"
- git config user.email "github-actions[bot]@users.noreply.github.com"
-
- - name: Extract version labels
- id: versions
- run: |
- # Extract version labels (e.g., "1.24", "1.22")
- VERSIONS=""
- LABELS='${{ toJSON(github.event.pull_request.labels) }}'
- for label in $(echo "$LABELS" | jq -r '.[].name'); do
- # Match version labels like "1.24" (major.minor only)
- if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
- # Validate the branch exists before adding to list
- if git ls-remote --exit-code origin "core/${label}" >/dev/null 2>&1; then
- VERSIONS="${VERSIONS}${label} "
- else
- echo "::warning::Label '${label}' found but branch 'core/${label}' does not exist"
- fi
- fi
- done
-
- if [ -z "$VERSIONS" ]; then
- echo "::error::No version labels found (e.g., 1.24, 1.22)"
- exit 1
- fi
-
- echo "versions=${VERSIONS}" >> $GITHUB_OUTPUT
- echo "Found version labels: ${VERSIONS}"
-
- - name: Backport commits
- id: backport
- env:
- PR_NUMBER: ${{ github.event.pull_request.number }}
- PR_TITLE: ${{ github.event.pull_request.title }}
- MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }}
- run: |
- FAILED=""
- SUCCESS=""
-
- for version in ${{ steps.versions.outputs.versions }}; do
- echo "::group::Backporting to core/${version}"
-
- TARGET_BRANCH="core/${version}"
- BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${version}"
-
- # Fetch target branch (fail if doesn't exist)
- if ! git fetch origin "${TARGET_BRANCH}"; then
- echo "::error::Target branch ${TARGET_BRANCH} does not exist"
- FAILED="${FAILED}${version}:branch-missing "
- echo "::endgroup::"
- continue
- fi
-
- # Create backport branch
- git checkout -b "${BACKPORT_BRANCH}" "origin/${TARGET_BRANCH}"
-
- # Try cherry-pick
- if git cherry-pick "${MERGE_COMMIT}"; then
- git push origin "${BACKPORT_BRANCH}"
- SUCCESS="${SUCCESS}${version}:${BACKPORT_BRANCH} "
- echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
- # Return to main (keep the branch, we need it for PR)
- git checkout main
- else
- # Get conflict info
- CONFLICTS=$(git diff --name-only --diff-filter=U | tr '\n' ',')
- git cherry-pick --abort
-
- echo "::error::Cherry-pick failed due to conflicts"
- FAILED="${FAILED}${version}:conflicts:${CONFLICTS} "
-
- # Clean up the failed branch
- git checkout main
- git branch -D "${BACKPORT_BRANCH}"
- fi
-
- echo "::endgroup::"
- done
-
- echo "success=${SUCCESS}" >> $GITHUB_OUTPUT
- echo "failed=${FAILED}" >> $GITHUB_OUTPUT
-
- if [ -n "${FAILED}" ]; then
- exit 1
- fi
-
- - name: Create PR for each successful backport
- if: steps.backport.outputs.success
- env:
- GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
- run: |
- PR_TITLE="${{ github.event.pull_request.title }}"
- PR_NUMBER="${{ github.event.pull_request.number }}"
- PR_AUTHOR="${{ github.event.pull_request.user.login }}"
-
- for backport in ${{ steps.backport.outputs.success }}; do
- IFS=':' read -r version branch <<< "${backport}"
-
- if PR_URL=$(gh pr create \
- --base "core/${version}" \
- --head "${branch}" \
- --title "[backport ${version}] ${PR_TITLE}" \
- --body "Backport of #${PR_NUMBER} to \`core/${version}\`"$'\n\n'"Automatically created by backport workflow." \
- --label "backport" 2>&1); then
-
- # Extract PR number from URL
- PR_NUM=$(echo "${PR_URL}" | grep -o '[0-9]*$')
-
- if [ -n "${PR_NUM}" ]; then
- gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Successfully backported to #${PR_NUM}"
- fi
- else
- echo "::error::Failed to create PR for ${version}: ${PR_URL}"
- # Still try to comment on the original PR about the failure
- gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`core/${version}\`. Please create the PR manually from branch \`${branch}\`"
- fi
- done
-
- - name: Comment on failures
- if: failure() && steps.backport.outputs.failed
- env:
- GH_TOKEN: ${{ github.token }}
- run: |
- PR_NUMBER="${{ github.event.pull_request.number }}"
- PR_AUTHOR="${{ github.event.pull_request.user.login }}"
- MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
-
- for failure in ${{ steps.backport.outputs.failed }}; do
- IFS=':' read -r version reason conflicts <<< "${failure}"
-
- if [ "${reason}" = "branch-missing" ]; then
- gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`core/${version}\` does not exist"
-
- elif [ "${reason}" = "conflicts" ]; then
- # Convert comma-separated conflicts back to newlines for display
- CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /')
-
- COMMENT_BODY="@${PR_AUTHOR} Backport to \`core/${version}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`core/${version}\` branch."$'\n\n'"Conflicting files
"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'" "
- gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
- fi
- done
diff --git a/.github/workflows/chromatic.yaml b/.github/workflows/chromatic.yaml
deleted file mode 100644
index 127186d68..000000000
--- a/.github/workflows/chromatic.yaml
+++ /dev/null
@@ -1,57 +0,0 @@
-name: 'Chromatic'
-
-# - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ )
-
-on:
- workflow_dispatch: # Allow manual triggering
- pull_request:
- branches: [main]
-
-jobs:
- chromatic-deployment:
- runs-on: ubuntu-latest
- # Only run for PRs from version-bump-* branches or manual triggers
- if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'version-bump-')
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- fetch-depth: 0 # Required for Chromatic baseline
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
- with:
- version: 10
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '20'
- cache: 'pnpm'
-
-
- - name: Cache tool outputs
- uses: actions/cache@v4
- with:
- path: |
- .cache
- storybook-static
- tsconfig.tsbuildinfo
- key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
- restore-keys: |
- storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
- storybook-cache-${{ runner.os }}-
- storybook-tools-cache-${{ runner.os }}-
-
- - name: Install dependencies
- run: pnpm install --frozen-lockfile
-
- - name: Build Storybook and run Chromatic
- id: chromatic
- uses: chromaui/action@latest
- with:
- projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- buildScriptName: build-storybook
- autoAcceptChanges: 'main' # Auto-accept changes on main branch
- exitOnceUploaded: true # Don't wait for UI tests to complete
-
diff --git a/.github/workflows/ci-json-validation.yaml b/.github/workflows/ci-json-validation.yaml
new file mode 100644
index 000000000..0d8c9392f
--- /dev/null
+++ b/.github/workflows/ci-json-validation.yaml
@@ -0,0 +1,16 @@
+name: "CI: JSON Validation"
+description: "Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq"
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+
+jobs:
+ json-lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - name: Validate JSON syntax
+ run: ./scripts/cicd/check-json.sh
diff --git a/.github/workflows/lint-and-format.yaml b/.github/workflows/ci-lint-format.yaml
similarity index 90%
rename from .github/workflows/lint-and-format.yaml
rename to .github/workflows/ci-lint-format.yaml
index 3b6bf1538..5c4e0011b 100644
--- a/.github/workflows/lint-and-format.yaml
+++ b/.github/workflows/ci-lint-format.yaml
@@ -1,9 +1,14 @@
-name: Lint and Format
+name: "CI: Lint Format"
+description: "Linting and code formatting validation for pull requests"
on:
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
permissions:
contents: write
pull-requests: write
@@ -13,11 +18,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout PR
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
- token: ${{ secrets.GITHUB_TOKEN }}
- ref: ${{ github.event.pull_request.head.ref }}
- fetch-depth: 0
+ ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || github.ref }}
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -69,7 +72,7 @@ jobs:
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add .
- git commit -m "[auto-fix] Apply ESLint and Prettier fixes"
+ git commit -m "[automated] Apply ESLint and Prettier fixes"
git push
- name: Final validation
@@ -102,4 +105,4 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
body: '## ⚠️ Linting/Formatting Issues Found\n\nThis PR has linting or formatting issues that need to be fixed.\n\n**Since this PR is from a fork, auto-fix cannot be applied automatically.**\n\n### Option 1: Set up pre-commit hooks (recommended)\nRun this once to automatically format code on every commit:\n```bash\npnpm prepare\n```\n\n### Option 2: Fix manually\nRun these commands and push the changes:\n```bash\npnpm lint:fix\npnpm format\n```\n\nSee [CONTRIBUTING.md](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/CONTRIBUTING.md#git-pre-commit-hooks) for more details.'
- })
\ No newline at end of file
+ })
diff --git a/.github/workflows/ci-python-validation.yaml b/.github/workflows/ci-python-validation.yaml
new file mode 100644
index 000000000..698f467bf
--- /dev/null
+++ b/.github/workflows/ci-python-validation.yaml
@@ -0,0 +1,27 @@
+name: "CI: Python Validation"
+description: "Validates Python code in tools/devtools directory"
+
+on:
+ pull_request:
+ paths:
+ - 'tools/devtools/**'
+ push:
+ branches: [ main ]
+ paths:
+ - 'tools/devtools/**'
+
+jobs:
+ syntax:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Validate Python syntax
+ run: python3 -m compileall -q tools/devtools
diff --git a/.github/workflows/ci-tests-e2e-forks.yaml b/.github/workflows/ci-tests-e2e-forks.yaml
new file mode 100644
index 000000000..5d3767f8e
--- /dev/null
+++ b/.github/workflows/ci-tests-e2e-forks.yaml
@@ -0,0 +1,93 @@
+name: "CI: Tests E2E (Deploy for Forks)"
+description: "Deploys test results from forked PRs (forks can't access deployment secrets)"
+
+on:
+ workflow_run:
+ workflows: ["CI: Tests E2E"]
+ 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@v5
+
+ - 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"
\ No newline at end of file
diff --git a/.github/workflows/ci-tests-e2e.yaml b/.github/workflows/ci-tests-e2e.yaml
new file mode 100644
index 000000000..690417a04
--- /dev/null
+++ b/.github/workflows/ci-tests-e2e.yaml
@@ -0,0 +1,236 @@
+name: "CI: Tests E2E"
+description: "End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages"
+
+on:
+ push:
+ branches: [main, master, core/*, desktop/*]
+ pull_request:
+ branches-ignore:
+ [wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ setup:
+ runs-on: ubuntu-latest
+ outputs:
+ cache-key: ${{ steps.cache-key.outputs.key }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+
+ # Setup Test Environment, build frontend but do not start server yet
+ - name: Setup ComfyUI server
+ uses: ./.github/actions/setup-comfyui-server
+ - name: Setup frontend
+ uses: ./.github/actions/setup-frontend
+ with:
+ include_build_step: true
+ - name: Setup Playwright
+ uses: ./.github/actions/setup-playwright # Setup Playwright and cache browsers
+
+ # Save the entire workspace as cache for later test jobs to restore
+ - name: Generate cache key
+ id: cache-key
+ run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
+ - name: Save cache
+ uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
+ with:
+ path: .
+ key: comfyui-setup-${{ steps.cache-key.outputs.key }}
+
+ # Sharded chromium tests
+ playwright-tests-chromium-sharded:
+ needs: setup
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ strategy:
+ fail-fast: false
+ matrix:
+ shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
+ shardTotal: [8]
+ steps:
+ # download built frontend repo from setup job
+ - name: Wait for cache propagation
+ run: sleep 10
+ - name: Restore cached setup
+ uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
+ with:
+ fail-on-cache-miss: true
+ path: .
+ key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
+
+ # Setup Test Environment for this runner, start server, use cached built frontend ./dist from 'setup' job
+ - name: Setup ComfyUI server
+ uses: ./.github/actions/setup-comfyui-server
+ with:
+ launch_server: true
+ - name: Setup nodejs, pnpm, reuse built frontend
+ uses: ./.github/actions/setup-frontend
+ - name: Setup Playwright
+ uses: ./.github/actions/setup-playwright
+
+ # Run sharded tests and upload sharded reports
+ - name: Run Playwright tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
+ id: playwright
+ run: pnpm exec playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob
+ env:
+ PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report
+
+ - name: Upload 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:
+ # download built frontend repo from setup job
+ - name: Wait for cache propagation
+ run: sleep 10
+ - name: Restore cached setup
+ uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
+ with:
+ fail-on-cache-miss: true
+ path: .
+ key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
+
+ # Setup Test Environment for this runner, start server, use cached built frontend ./dist from 'setup' job
+ - name: Setup ComfyUI server
+ uses: ./.github/actions/setup-comfyui-server
+ with:
+ launch_server: true
+ - name: Setup nodejs, pnpm, reuse built frontend
+ uses: ./.github/actions/setup-frontend
+ - name: Setup Playwright
+ uses: ./.github/actions/setup-playwright
+
+ # Run tests and upload reports
+ - name: Run Playwright tests (${{ matrix.browser }})
+ id: playwright
+ run: |
+ # Run tests with both HTML and JSON reporters
+ PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
+ pnpm exec playwright test --project=${{ matrix.browser }} \
+ --reporter=list \
+ --reporter=html \
+ --reporter=json
+
+ - name: Upload Playwright report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: playwright-report-${{ matrix.browser }}
+ path: ./playwright-report/
+ retention-days: 30
+
+ # Merge sharded test reports
+ merge-reports:
+ needs: [playwright-tests-chromium-sharded]
+ runs-on: ubuntu-latest
+ if: ${{ !cancelled() }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+
+ # Setup Test Environment, we only need playwright to merge reports
+ - name: Setup frontend
+ uses: ./.github/actions/setup-frontend
+ - name: Setup Playwright
+ uses: ./.github/actions/setup-playwright
+
+ - name: Download blob reports
+ uses: actions/download-artifact@v4
+ with:
+ path: ./all-blob-reports
+ pattern: blob-report-chromium-*
+ merge-multiple: true
+
+ - name: Merge into HTML Report
+ run: |
+ # Generate HTML report
+ pnpm exec playwright merge-reports --reporter=html ./all-blob-reports
+ # Generate JSON report separately with explicit output path
+ PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
+ pnpm exec playwright merge-reports --reporter=json ./all-blob-reports
+
+ - name: Upload HTML report
+ uses: actions/upload-artifact@v4
+ with:
+ name: playwright-report-chromium
+ path: ./playwright-report/
+ retention-days: 30
+
+ #### BEGIN Deployment and commenting (non-forked PRs only)
+ # when using pull_request event, we have permission to comment directly
+ # if its a forked repo, we need to use workflow_run event in a separate workflow (pr-playwright-deploy.yaml)
+
+ # Post starting comment for non-forked PRs
+ comment-on-pr-start:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
+ permissions:
+ pull-requests: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+
+ - 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@v5
+
+ - name: Download all playwright reports
+ uses: actions/download-artifact@v4
+ with:
+ pattern: playwright-report-*
+ path: reports
+
+ - name: Deploy reports and comment on PR
+ env:
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ GITHUB_TOKEN: ${{ github.token }}
+ run: |
+ bash ./scripts/cicd/pr-playwright-deploy-and-comment.sh \
+ "${{ github.event.pull_request.number }}" \
+ "${{ github.head_ref }}" \
+ "completed"
+ #### END Deployment and commenting (non-forked PRs only)
diff --git a/.github/workflows/ci-tests-storybook-forks.yaml b/.github/workflows/ci-tests-storybook-forks.yaml
new file mode 100644
index 000000000..083a85f1d
--- /dev/null
+++ b/.github/workflows/ci-tests-storybook-forks.yaml
@@ -0,0 +1,91 @@
+name: "CI: Tests Storybook (Deploy for Forks)"
+description: "Deploys Storybook previews from forked PRs (forks can't access deployment secrets)"
+
+on:
+ workflow_run:
+ workflows: ["CI: Tests Storybook"]
+ 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@v5
+
+ - 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 Storybook Start
+ if: steps.pr.outputs.result != 'null' && github.event.action == 'requested'
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ run: |
+ chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
+ ./scripts/cicd/pr-storybook-deploy-and-comment.sh \
+ "${{ steps.pr.outputs.result }}" \
+ "${{ github.event.workflow_run.head_branch }}" \
+ "starting" \
+ "$(date -u '${{ env.DATE_FORMAT }}')"
+
+ - name: Download and Deploy Storybook
+ if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' && github.event.workflow_run.conclusion == 'success'
+ uses: actions/download-artifact@v4
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ run-id: ${{ github.event.workflow_run.id }}
+ name: storybook-static
+ path: storybook-static
+
+ - name: Handle Storybook 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 }}
+ WORKFLOW_CONCLUSION: ${{ github.event.workflow_run.conclusion }}
+ WORKFLOW_URL: ${{ github.event.workflow_run.html_url }}
+ run: |
+ chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
+ ./scripts/cicd/pr-storybook-deploy-and-comment.sh \
+ "${{ steps.pr.outputs.result }}" \
+ "${{ github.event.workflow_run.head_branch }}" \
+ "completed"
\ No newline at end of file
diff --git a/.github/workflows/ci-tests-storybook.yaml b/.github/workflows/ci-tests-storybook.yaml
new file mode 100644
index 000000000..65e97ed91
--- /dev/null
+++ b/.github/workflows/ci-tests-storybook.yaml
@@ -0,0 +1,230 @@
+name: "CI: Tests Storybook"
+description: "Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages"
+
+on:
+ workflow_dispatch: # Allow manual triggering
+ pull_request:
+ branches: [main]
+
+jobs:
+ # Post starting comment for non-forked PRs
+ comment-on-pr-start:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
+ permissions:
+ pull-requests: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+
+ - name: Post starting comment
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ run: |
+ chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
+ ./scripts/cicd/pr-storybook-deploy-and-comment.sh \
+ "${{ github.event.pull_request.number }}" \
+ "${{ github.head_ref }}" \
+ "starting" \
+ "$(date -u '+%m/%d/%Y, %I:%M:%S %p')"
+
+ # Build Storybook for all PRs (free Cloudflare deployment)
+ storybook-build:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request'
+ outputs:
+ conclusion: ${{ steps.job-status.outputs.conclusion }}
+ workflow-url: ${{ steps.workflow-url.outputs.url }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'pnpm'
+
+ - name: Cache tool outputs
+ uses: actions/cache@v4
+ with:
+ path: |
+ .cache
+ storybook-static
+ tsconfig.tsbuildinfo
+ key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
+ restore-keys: |
+ storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
+ storybook-cache-${{ runner.os }}-
+ storybook-tools-cache-${{ runner.os }}-
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Build Storybook
+ run: pnpm build-storybook
+
+ - name: Set job status
+ id: job-status
+ if: always()
+ run: |
+ echo "conclusion=${{ job.status }}" >> $GITHUB_OUTPUT
+
+ - name: Get workflow URL
+ id: workflow-url
+ if: always()
+ run: |
+ echo "url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_OUTPUT
+
+ - name: Upload Storybook build
+ if: success() && github.event.pull_request.head.repo.fork == false
+ uses: actions/upload-artifact@v4
+ with:
+ name: storybook-static
+ path: storybook-static/
+ retention-days: 7
+
+ # Chromatic deployment only for version-bump-* branches or manual triggers
+ chromatic-deployment:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && startsWith(github.head_ref, 'version-bump-'))
+ outputs:
+ conclusion: ${{ steps.job-status.outputs.conclusion }}
+ workflow-url: ${{ steps.workflow-url.outputs.url }}
+ chromatic-build-url: ${{ steps.chromatic.outputs.buildUrl }}
+ chromatic-storybook-url: ${{ steps.chromatic.outputs.storybookUrl }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0 # Required for Chromatic baseline
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'pnpm'
+
+ - name: Cache tool outputs
+ uses: actions/cache@v4
+ with:
+ path: |
+ .cache
+ storybook-static
+ tsconfig.tsbuildinfo
+ key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
+ restore-keys: |
+ storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
+ storybook-cache-${{ runner.os }}-
+ storybook-tools-cache-${{ runner.os }}-
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Build Storybook and run Chromatic
+ id: chromatic
+ uses: chromaui/action@latest
+ with:
+ projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
+ buildScriptName: build-storybook
+ autoAcceptChanges: 'main' # Auto-accept changes on main branch
+ exitOnceUploaded: true # Don't wait for UI tests to complete
+ onlyChanged: true # Only capture changed stories
+
+ - name: Set job status
+ id: job-status
+ if: always()
+ run: |
+ echo "conclusion=${{ job.status }}" >> $GITHUB_OUTPUT
+
+ - name: Get workflow URL
+ id: workflow-url
+ if: always()
+ run: |
+ echo "url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_OUTPUT
+
+ # Deploy and comment for non-forked PRs only
+ deploy-and-comment:
+ needs: [storybook-build]
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && always()
+ permissions:
+ pull-requests: write
+ contents: read
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+
+ - name: Download Storybook build
+ if: needs.storybook-build.outputs.conclusion == 'success'
+ uses: actions/download-artifact@v4
+ with:
+ name: storybook-static
+ path: storybook-static
+
+ - name: Make deployment script executable
+ run: chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
+
+ - name: Deploy Storybook and comment on PR
+ env:
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ GITHUB_TOKEN: ${{ github.token }}
+ WORKFLOW_CONCLUSION: ${{ needs.storybook-build.outputs.conclusion }}
+ WORKFLOW_URL: ${{ needs.storybook-build.outputs.workflow-url }}
+ run: |
+ ./scripts/cicd/pr-storybook-deploy-and-comment.sh \
+ "${{ github.event.pull_request.number }}" \
+ "${{ github.head_ref }}" \
+ "completed"
+
+ # Update comment with Chromatic URLs for version-bump branches
+ update-comment-with-chromatic:
+ needs: [chromatic-deployment, deploy-and-comment]
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && startsWith(github.head_ref, 'version-bump-') && needs.chromatic-deployment.outputs.chromatic-build-url != ''
+ permissions:
+ pull-requests: write
+ steps:
+ - name: Update comment with Chromatic URLs
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const buildUrl = '${{ needs.chromatic-deployment.outputs.chromatic-build-url }}';
+ const storybookUrl = '${{ needs.chromatic-deployment.outputs.chromatic-storybook-url }}';
+
+ // Find the existing Storybook comment
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: ${{ github.event.pull_request.number }}
+ });
+
+ const storybookComment = comments.find(comment =>
+ comment.body.includes('')
+ );
+
+ if (storybookComment && buildUrl && storybookUrl) {
+ // Append Chromatic info to existing comment
+ const updatedBody = storybookComment.body.replace(
+ /---\n(.*)$/s,
+ `---\n### 🎨 Chromatic Visual Tests\n- 📊 [View Chromatic Build](${buildUrl})\n- 📚 [View Chromatic Storybook](${storybookUrl})\n\n$1`
+ );
+
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: storybookComment.id,
+ body: updatedBody
+ });
+ }
diff --git a/.github/workflows/ci-tests-unit.yaml b/.github/workflows/ci-tests-unit.yaml
new file mode 100644
index 000000000..152f78885
--- /dev/null
+++ b/.github/workflows/ci-tests-unit.yaml
@@ -0,0 +1,49 @@
+name: "CI: Tests Unit"
+description: "Unit and component testing with Vitest"
+
+on:
+ push:
+ branches: [main, master, dev*, core/*, desktop/*]
+ pull_request:
+ branches-ignore: [wip/*, draft/*, temp/*]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10
+
+ - name: Use Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "lts/*"
+ cache: "pnpm"
+
+ - name: Cache tool outputs
+ uses: actions/cache@v4
+ with:
+ path: |
+ .cache
+ coverage
+ .vitest-cache
+ key: vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', 'vitest.config.*', 'tsconfig.json') }}
+ restore-keys: |
+ vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
+ vitest-cache-${{ runner.os }}-
+ test-tools-cache-${{ runner.os }}-
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Run Vitest tests
+ run: pnpm test:unit
diff --git a/.github/workflows/i18n.yaml b/.github/workflows/i18n-update-core.yaml
similarity index 61%
rename from .github/workflows/i18n.yaml
rename to .github/workflows/i18n-update-core.yaml
index d7df815ff..8a20af4c0 100644
--- a/.github/workflows/i18n.yaml
+++ b/.github/workflows/i18n-update-core.yaml
@@ -1,4 +1,5 @@
-name: Update Locales
+name: "i18n: Update Core"
+description: "Generates and updates translations for core ComfyUI components using OpenAI"
on:
# Manual dispatch for urgent translation updates
@@ -14,42 +15,35 @@ jobs:
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.head_ref, 'version-bump-'))
runs-on: ubuntu-latest
steps:
- - uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
-
- - name: Cache tool outputs
- uses: actions/cache@v4
+ - name: Checkout repository
+ uses: actions/checkout@v5
+
+ # Setup playwright environment
+ - name: Setup ComfyUI Frontend
+ uses: ./.github/actions/setup-frontend
with:
- path: |
- ComfyUI_frontend/.cache
- ComfyUI_frontend/.cache
- key: i18n-tools-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
- restore-keys: |
- i18n-tools-cache-${{ runner.os }}-
- - name: Cache Playwright browsers
- uses: actions/cache@v4
+ include_build_step: true
+ - name: Setup ComfyUI Server
+ uses: ./.github/actions/setup-comfyui-server
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
- run: npx playwright install chromium --with-deps
- working-directory: ComfyUI_frontend
+ launch_server: true
+ - name: Setup Playwright
+ uses: ./.github/actions/setup-playwright
+
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
- working-directory: ComfyUI_frontend
+
+ # Update locales, collect new strings and update translations using OpenAI, then commit changes
- name: Update en.json
run: pnpm collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
- working-directory: ComfyUI_frontend
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- working-directory: ComfyUI_frontend
- name: Commit updated locales
run: |
git config --global user.name 'github-actions'
@@ -61,6 +55,5 @@ jobs:
# Apply the stashed changes if any
git stash pop || true
git add src/locales/
- git diff --staged --quiet || git commit -m "Update locales [skip ci]"
+ git diff --staged --quiet || git commit -m "Update locales"
git push origin HEAD:${{ github.head_ref }}
- working-directory: ComfyUI_frontend
diff --git a/.github/workflows/i18n-custom-nodes.yaml b/.github/workflows/i18n-update-custom-nodes.yaml
similarity index 68%
rename from .github/workflows/i18n-custom-nodes.yaml
rename to .github/workflows/i18n-update-custom-nodes.yaml
index a5617c196..61076f031 100644
--- a/.github/workflows/i18n-custom-nodes.yaml
+++ b/.github/workflows/i18n-update-custom-nodes.yaml
@@ -1,4 +1,4 @@
-name: Update Locales for given custom node repository
+name: i18n Update Custom Nodes
on:
workflow_dispatch:
@@ -21,92 +21,64 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- - name: Checkout ComfyUI
- uses: actions/checkout@v4
+ - name: Checkout repository
+ uses: actions/checkout@v5
+
+ # Setup playwright environment with custom node repository
+ - name: Setup ComfyUI Server (without launching)
+ uses: ./.github/actions/setup-comfyui-server
+ - name: Setup frontend
+ uses: ./.github/actions/setup-frontend
with:
- repository: comfyanonymous/ComfyUI
- path: ComfyUI
- ref: master
- - name: Checkout ComfyUI_frontend
- uses: actions/checkout@v4
- with:
- repository: Comfy-Org/ComfyUI_frontend
- path: ComfyUI_frontend
- - name: Checkout ComfyUI_devtools
- uses: actions/checkout@v4
- with:
- repository: Comfy-Org/ComfyUI_devtools
- path: ComfyUI/custom_nodes/ComfyUI_devtools
+ include_build_step: 'true'
+ - name: Setup Playwright
+ uses: ./.github/actions/setup-playwright
+
+ # Install the custom node repository
- name: Checkout custom node repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
repository: ${{ inputs.owner }}/${{ inputs.repository }}
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
- - name: Install pnpm
- uses: pnpm/action-setup@v4
- with:
- version: 10
- - uses: actions/setup-node@v4
- with:
- node-version: 'lts/*'
- cache: 'pnpm'
- - uses: actions/setup-python@v4
- with:
- python-version: '3.10'
- - name: Install ComfyUI requirements
- run: |
- python -m pip install --upgrade pip
- pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
- pip install -r requirements.txt
- pip install wait-for-it
- working-directory: ComfyUI
- - name: Install custom node requirements
+ - name: Install custom node Python requirements
+ working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
if [ -f "requirements.txt" ]; then
pip install -r requirements.txt
fi
- working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
- - name: Build & Install ComfyUI_frontend
- run: |
- pnpm install --frozen-lockfile
- pnpm build
- rm -rf ../ComfyUI/web/*
- mv dist/* ../ComfyUI/web/
- working-directory: ComfyUI_frontend
- - name: Start ComfyUI server
- run: |
- python main.py --cpu --multi-user &
- wait-for-it --service 127.0.0.1:8188 -t 600
+
+ # Start ComfyUI Server
+ - name: Start ComfyUI Server
+ shell: bash
working-directory: ComfyUI
- - name: Install Playwright Browsers
- run: npx playwright install chromium --with-deps
- working-directory: ComfyUI_frontend
+ run: |
+ python main.py --cpu --multi-user --front-end-root ../dist --custom-node-path ../ComfyUI/custom_nodes/${{ inputs.repository }} &
+ wait-for-it --service
+
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
- working-directory: ComfyUI_frontend
+
- name: Capture base i18n
- run: npx tsx scripts/diff-i18n capture
- working-directory: ComfyUI_frontend
+ run: pnpm exec tsx scripts/diff-i18n capture
- name: Update en.json
run: pnpm collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
- working-directory: ComfyUI_frontend
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- working-directory: ComfyUI_frontend
- name: Diff base vs updated i18n
- run: npx tsx scripts/diff-i18n diff
- working-directory: ComfyUI_frontend
+ run: pnpm exec tsx scripts/diff-i18n diff
- name: Update i18n in custom node repository
run: |
LOCALE_DIR=ComfyUI/custom_nodes/${{ inputs.repository }}/locales/
install -d "$LOCALE_DIR"
cp -rf ComfyUI_frontend/temp/diff/* "$LOCALE_DIR"
+
+ # Git ops for pushing changes and creating PR
- name: Check and create fork of custom node repository
run: |
# Try to fork the repository
diff --git a/.github/workflows/i18n-node-defs.yaml b/.github/workflows/i18n-update-nodes.yaml
similarity index 74%
rename from .github/workflows/i18n-node-defs.yaml
rename to .github/workflows/i18n-update-nodes.yaml
index 1327db3cf..0b9f1534d 100644
--- a/.github/workflows/i18n-node-defs.yaml
+++ b/.github/workflows/i18n-update-nodes.yaml
@@ -1,4 +1,4 @@
-name: Update Node Definitions Locales
+name: i18n Update Nodes
on:
workflow_dispatch:
@@ -13,25 +13,32 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- - uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
- - name: Install Playwright Browsers
- run: npx playwright install chromium --with-deps
- working-directory: ComfyUI_frontend
+ - name: Checkout repository
+ uses: actions/checkout@v5
+ # Setup playwright environment
+ - name: Setup ComfyUI Server (and start)
+ uses: ./.github/actions/setup-comfyui-server
+ with:
+ launch_server: true
+ - name: Setup frontend
+ uses: ./.github/actions/setup-frontend
+ with:
+ include_build_step: true
+ - name: Setup Playwright
+ uses: ./.github/actions/setup-playwright
+
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
- working-directory: ComfyUI_frontend
- name: Update en.json
run: pnpm collect-i18n -- scripts/collect-i18n-node-defs.ts
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
- working-directory: ComfyUI_frontend
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- working-directory: ComfyUI_frontend
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
with:
@@ -45,4 +52,3 @@ jobs:
branch: update-locales-node-defs-${{ github.event.inputs.trigger_type }}-${{ github.run_id }}
base: main
labels: dependencies
- path: ComfyUI_frontend
\ No newline at end of file
diff --git a/.github/workflows/pr-backport.yaml b/.github/workflows/pr-backport.yaml
new file mode 100644
index 000000000..1c9a29230
--- /dev/null
+++ b/.github/workflows/pr-backport.yaml
@@ -0,0 +1,289 @@
+name: PR Backport
+
+on:
+ pull_request_target:
+ types: [closed, labeled]
+ branches: [main]
+ workflow_dispatch:
+ inputs:
+ pr_number:
+ description: 'PR number to backport'
+ required: true
+ type: string
+ force_rerun:
+ description: 'Force rerun even if backports exist'
+ required: false
+ type: boolean
+ default: false
+
+jobs:
+ backport:
+ if: >
+ (github.event_name == 'pull_request_target' &&
+ github.event.pull_request.merged == true &&
+ contains(github.event.pull_request.labels.*.name, 'needs-backport')) ||
+ github.event_name == 'workflow_dispatch'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ issues: write
+
+ steps:
+ - name: Validate inputs for manual triggers
+ if: github.event_name == 'workflow_dispatch'
+ run: |
+ # Validate PR number format
+ if ! [[ "${{ inputs.pr_number }}" =~ ^[0-9]+$ ]]; then
+ echo "::error::Invalid PR number format. Must be a positive integer."
+ exit 1
+ fi
+
+ # Validate PR exists and is merged
+ if ! gh pr view "${{ inputs.pr_number }}" --json merged >/dev/null 2>&1; then
+ echo "::error::PR #${{ inputs.pr_number }} not found or inaccessible."
+ exit 1
+ fi
+
+ MERGED=$(gh pr view "${{ inputs.pr_number }}" --json merged --jq '.merged')
+ if [ "$MERGED" != "true" ]; then
+ echo "::error::PR #${{ inputs.pr_number }} is not merged. Only merged PRs can be backported."
+ exit 1
+ fi
+
+ # Validate PR has needs-backport label
+ if ! gh pr view "${{ inputs.pr_number }}" --json labels --jq '.labels[].name' | grep -q "needs-backport"; then
+ echo "::error::PR #${{ inputs.pr_number }} does not have 'needs-backport' label."
+ exit 1
+ fi
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Checkout repository
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - name: Configure git
+ run: |
+ git config user.name "github-actions[bot]"
+ 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_name == 'workflow_dispatch' && inputs.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
+
+ # For manual triggers with force_rerun, proceed anyway
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.force_rerun }}" = "true" ]; then
+ echo "skip=false" >> $GITHUB_OUTPUT
+ echo "::warning::Force rerun requested - existing backports will be updated"
+ 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: Collect backport targets
+ if: steps.check-existing.outputs.skip != 'true'
+ id: targets
+ run: |
+ TARGETS=()
+ declare -A SEEN=()
+
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
+ else
+ LABELS='${{ toJSON(github.event.pull_request.labels) }}'
+ LABELS=$(echo "$LABELS" | jq -r '.[].name')
+ fi
+
+ add_target() {
+ local label="$1"
+ local target="$2"
+
+ if [ -z "$target" ]; then
+ return
+ fi
+
+ target=$(echo "$target" | xargs)
+
+ if [ -z "$target" ] || [ -n "${SEEN[$target]}" ]; then
+ return
+ fi
+
+ if git ls-remote --exit-code origin "$target" >/dev/null 2>&1; then
+ TARGETS+=("$target")
+ SEEN["$target"]=1
+ else
+ echo "::warning::Label '${label}' references missing branch '${target}'"
+ fi
+ }
+
+ while IFS= read -r label; do
+ [ -z "$label" ] && continue
+
+ if [[ "$label" =~ ^branch:(.+)$ ]]; then
+ add_target "$label" "${BASH_REMATCH[1]}"
+ elif [[ "$label" =~ ^backport:(.+)$ ]]; then
+ add_target "$label" "${BASH_REMATCH[1]}"
+ elif [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
+ add_target "$label" "core/${label}"
+ fi
+ done <<< "$LABELS"
+
+ if [ "${#TARGETS[@]}" -eq 0 ]; then
+ echo "::error::No backport targets found (use labels like '1.24' or 'branch:release/hotfix')"
+ exit 1
+ fi
+
+ echo "targets=${TARGETS[*]}" >> $GITHUB_OUTPUT
+ echo "Found backport targets: ${TARGETS[*]}"
+
+ - name: Backport commits
+ if: steps.check-existing.outputs.skip != 'true'
+ id: backport
+ env:
+ PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
+ run: |
+ FAILED=""
+ SUCCESS=""
+
+ # Get PR data for manual triggers
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit)
+ PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
+ MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
+ else
+ PR_TITLE="${{ github.event.pull_request.title }}"
+ MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
+ fi
+
+ for target in ${{ steps.targets.outputs.targets }}; do
+ TARGET_BRANCH="${target}"
+ SAFE_TARGET=$(echo "$TARGET_BRANCH" | tr '/' '-')
+ BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
+
+ echo "::group::Backporting to ${TARGET_BRANCH}"
+
+ # Fetch target branch (fail if doesn't exist)
+ if ! git fetch origin "${TARGET_BRANCH}"; then
+ echo "::error::Target branch ${TARGET_BRANCH} does not exist"
+ FAILED="${FAILED}${TARGET_BRANCH}:branch-missing "
+ echo "::endgroup::"
+ continue
+ fi
+
+ # Create backport branch
+ git checkout -b "${BACKPORT_BRANCH}" "origin/${TARGET_BRANCH}"
+
+ # Try cherry-pick
+ if git cherry-pick "${MERGE_COMMIT}"; then
+ git push origin "${BACKPORT_BRANCH}"
+ SUCCESS="${SUCCESS}${TARGET_BRANCH}:${BACKPORT_BRANCH} "
+ echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
+ # Return to main (keep the branch, we need it for PR)
+ git checkout main
+ else
+ # Get conflict info
+ CONFLICTS=$(git diff --name-only --diff-filter=U | tr '\n' ',')
+ git cherry-pick --abort
+
+ echo "::error::Cherry-pick failed due to conflicts"
+ FAILED="${FAILED}${TARGET_BRANCH}:conflicts:${CONFLICTS} "
+
+ # Clean up the failed branch
+ git checkout main
+ git branch -D "${BACKPORT_BRANCH}"
+ fi
+
+ echo "::endgroup::"
+ done
+
+ echo "success=${SUCCESS}" >> $GITHUB_OUTPUT
+ echo "failed=${FAILED}" >> $GITHUB_OUTPUT
+
+ if [ -n "${FAILED}" ]; then
+ exit 1
+ fi
+
+ - name: Create PR for each successful backport
+ if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success
+ env:
+ GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
+ PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
+ run: |
+ # Get PR data for manual triggers
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,author)
+ PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
+ PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
+ else
+ PR_TITLE="${{ github.event.pull_request.title }}"
+ PR_AUTHOR="${{ github.event.pull_request.user.login }}"
+ fi
+
+ for backport in ${{ steps.backport.outputs.success }}; do
+ IFS=':' read -r target branch <<< "${backport}"
+
+ if PR_URL=$(gh pr create \
+ --base "${target}" \
+ --head "${branch}" \
+ --title "[backport ${target}] ${PR_TITLE}" \
+ --body "Backport of #${PR_NUMBER} to \`${target}\`"$'\n\n'"Automatically created by backport workflow." \
+ --label "backport" 2>&1); then
+
+ # Extract PR number from URL
+ PR_NUM=$(echo "${PR_URL}" | grep -o '[0-9]*$')
+
+ if [ -n "${PR_NUM}" ]; then
+ gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Successfully backported to #${PR_NUM}"
+ fi
+ else
+ echo "::error::Failed to create PR for ${target}: ${PR_URL}"
+ # Still try to comment on the original PR about the failure
+ gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`${target}\`. Please create the PR manually from branch \`${branch}\`"
+ fi
+ done
+
+ - name: Comment on failures
+ if: steps.check-existing.outputs.skip != 'true' && failure() && steps.backport.outputs.failed
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json author,mergeCommit)
+ PR_NUMBER="${{ inputs.pr_number }}"
+ PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
+ MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
+ else
+ PR_NUMBER="${{ github.event.pull_request.number }}"
+ PR_AUTHOR="${{ github.event.pull_request.user.login }}"
+ MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
+ fi
+
+ for failure in ${{ steps.backport.outputs.failed }}; do
+ IFS=':' read -r target reason conflicts <<< "${failure}"
+
+ if [ "${reason}" = "branch-missing" ]; then
+ gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist"
+
+ elif [ "${reason}" = "conflicts" ]; then
+ # Convert comma-separated conflicts back to newlines for display
+ CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /')
+
+ COMMENT_BODY="@${PR_AUTHOR} Backport to \`${target}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`${target}\` branch."$'\n\n'"Conflicting files
"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'" "
+ gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
+ fi
+ done
diff --git a/.github/workflows/claude-pr-review.yml b/.github/workflows/pr-claude-review.yaml
similarity index 67%
rename from .github/workflows/claude-pr-review.yml
rename to .github/workflows/pr-claude-review.yaml
index 3ec61cb3c..b09fde14f 100644
--- a/.github/workflows/claude-pr-review.yml
+++ b/.github/workflows/pr-claude-review.yaml
@@ -1,4 +1,5 @@
-name: Claude PR Review
+name: "PR: Claude Review"
+description: "AI-powered code review triggered by adding the 'claude-review' label to a PR"
permissions:
contents: read
@@ -11,6 +12,10 @@ on:
pull_request:
types: [labeled]
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
wait-for-ci:
runs-on: ubuntu-latest
@@ -29,11 +34,9 @@ jobs:
- name: Check if we should proceed
id: check-status
run: |
- # Get all check runs for this commit
- CHECK_RUNS=$(gh api repos/${{ github.repository }}/commits/${{ github.event.pull_request.head.sha }}/check-runs --jq '.check_runs[] | select(.name | test("lint-and-format|test|playwright-tests")) | {name, conclusion}')
-
- # Check if any required checks failed
- if echo "$CHECK_RUNS" | grep -q '"conclusion": "failure"'; then
+ CHECK_RUNS=$(gh api repos/${{ github.repository }}/commits/${{ github.event.pull_request.head.sha }}/check-runs --jq '.check_runs[] | select(.name | test("lint-and-format")) | {name, conclusion}')
+
+ if echo "$CHECK_RUNS" | grep -Eq '"conclusion": "(failure|cancelled|timed_out|action_required)"'; then
echo "Some CI checks failed - skipping Claude review"
echo "proceed=false" >> $GITHUB_OUTPUT
else
@@ -47,11 +50,13 @@ jobs:
needs: wait-for-ci
if: needs.wait-for-ci.outputs.should-proceed == 'true'
runs-on: ubuntu-latest
+ timeout-minutes: 30
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
fetch-depth: 0
+ ref: refs/pull/${{ github.event.pull_request.number }}/head
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -69,22 +74,26 @@ jobs:
pnpm install -g typescript @vue/compiler-sfc
- name: Run Claude PR Review
- uses: anthropics/claude-code-action@main
+ uses: anthropics/claude-code-action@v1.0.6
with:
label_trigger: "claude-review"
- direct_prompt: |
- 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.
- DO NOT create a summary comment.
+ prompt: |
+ 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.
+ DO NOT create a summary comment.
Each issue must be posted as a separate inline comment on the specific line of code.
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- max_turns: 256
- 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"
+ 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'"
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
- REPOSITORY: ${{ github.repository }}
\ No newline at end of file
+ REPOSITORY: ${{ github.repository }}
+
+ - name: Remove claude-review label
+ if: always()
+ run: gh pr edit ${{ github.event.pull_request.number }} --remove-label "claude-review"
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/pr-playwright-deploy.yaml b/.github/workflows/pr-playwright-deploy.yaml
deleted file mode 100644
index 12051fa99..000000000
--- a/.github/workflows/pr-playwright-deploy.yaml
+++ /dev/null
@@ -1,280 +0,0 @@
-name: PR Playwright Deploy and Comment
-
-on:
- workflow_run:
- workflows: ["Tests CI"]
- types: [requested, completed]
-
-env:
- DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
-
-jobs:
- deploy-reports:
- runs-on: ubuntu-latest
- if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && github.event.action == 'completed'
- permissions:
- actions: read
- strategy:
- fail-fast: false
- matrix:
- browser: [chromium, chromium-2x, chromium-0.5x, mobile-chrome]
- steps:
- - name: Get PR info
- id: pr-info
- 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 { number: null, sanitized_branch: null };
- }
-
- const pr = pullRequests[0];
- const branchName = context.payload.workflow_run.head_branch;
- const sanitizedBranch = branchName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/--+/g, '-').replace(/^-|-$/g, '');
-
- return {
- number: pr.number,
- sanitized_branch: sanitizedBranch
- };
-
- - name: Set project name
- if: fromJSON(steps.pr-info.outputs.result).number != null
- id: project-name
- run: |
- if [ "${{ matrix.browser }}" = "chromium-0.5x" ]; then
- echo "name=comfyui-playwright-chromium-0-5x" >> $GITHUB_OUTPUT
- else
- echo "name=comfyui-playwright-${{ matrix.browser }}" >> $GITHUB_OUTPUT
- fi
- echo "branch=${{ fromJSON(steps.pr-info.outputs.result).sanitized_branch }}" >> $GITHUB_OUTPUT
-
- - name: Download playwright report
- if: fromJSON(steps.pr-info.outputs.result).number != null
- uses: actions/download-artifact@v4
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- run-id: ${{ github.event.workflow_run.id }}
- name: playwright-report-${{ matrix.browser }}
- path: playwright-report
-
- - name: Install Wrangler
- if: fromJSON(steps.pr-info.outputs.result).number != null
- run: npm install -g wrangler
-
- - name: Deploy to Cloudflare Pages (${{ matrix.browser }})
- if: fromJSON(steps.pr-info.outputs.result).number != null
- id: cloudflare-deploy
- continue-on-error: true
- run: |
- # Retry logic for wrangler deploy (3 attempts)
- RETRY_COUNT=0
- MAX_RETRIES=3
- SUCCESS=false
-
- while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ $SUCCESS = false ]; do
- RETRY_COUNT=$((RETRY_COUNT + 1))
- echo "Deployment attempt $RETRY_COUNT of $MAX_RETRIES..."
-
- if npx wrangler pages deploy 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 }}
-
- comment-tests-starting:
- runs-on: ubuntu-latest
- if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && github.event.action == 'requested'
- 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: Get completion time
- id: completion-time
- run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
-
- - name: Generate comment body for start
- if: steps.pr.outputs.result != 'null'
- id: comment-body-start
- run: |
- echo "" > comment.md
- echo "## 🎭 Playwright Test Results" >> comment.md
- echo "" >> comment.md
- echo "
**Tests are starting...** " >> comment.md
- echo "" >> comment.md
- echo "⏰ Started at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
- echo "" >> comment.md
- echo "### 🚀 Running Tests" >> comment.md
- echo "- 🧪 **chromium**: Running tests..." >> comment.md
- echo "- 🧪 **chromium-0.5x**: Running tests..." >> comment.md
- echo "- 🧪 **chromium-2x**: Running tests..." >> comment.md
- echo "- 🧪 **mobile-chrome**: Running tests..." >> comment.md
- echo "" >> comment.md
- echo "---" >> comment.md
- echo "⏱️ Please wait while tests are running across all browsers..." >> comment.md
-
- - name: Comment PR - Tests Started
- if: steps.pr.outputs.result != 'null'
- uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
- with:
- issue-number: ${{ steps.pr.outputs.result }}
- body-includes: ''
- comment-author: 'github-actions[bot]'
- edit-mode: replace
- body-path: comment.md
-
- comment-tests-completed:
- runs-on: ubuntu-latest
- needs: deploy-reports
- if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && github.event.action == 'completed' && always()
- 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: Download all deployment info
- if: steps.pr.outputs.result != 'null'
- uses: actions/download-artifact@v4
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- run-id: ${{ github.event.workflow_run.id }}
- pattern: deployment-info-*
- merge-multiple: true
- path: deployment-info
-
- - name: Get completion time
- id: completion-time
- run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
-
- - name: Generate comment body for completion
- if: steps.pr.outputs.result != 'null'
- id: comment-body-completed
- run: |
- echo "" > comment.md
- echo "## 🎭 Playwright Test Results" >> comment.md
- echo "" >> comment.md
-
- # Check if all tests passed
- 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)
-
- # Validate URLs before using them in comments
- sanitized_url=$(echo "$url" | grep -E '^https://[a-z0-9.-]+\.pages\.dev(/.*)?$' || echo "INVALID_URL")
- if [ "$sanitized_url" = "INVALID_URL" ]; then
- echo "Invalid deployment URL detected: $url"
- url="#" # Use safe fallback
- fi
-
- 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
- if: steps.pr.outputs.result != 'null'
- uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
- with:
- issue-number: ${{ steps.pr.outputs.result }}
- body-includes: ''
- comment-author: 'github-actions[bot]'
- edit-mode: replace
- body-path: comment.md
\ No newline at end of file
diff --git a/.github/workflows/pr-storybook-comment.yaml b/.github/workflows/pr-storybook-comment.yaml
deleted file mode 100644
index 53691d826..000000000
--- a/.github/workflows/pr-storybook-comment.yaml
+++ /dev/null
@@ -1,126 +0,0 @@
-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: ''
- comment-author: 'github-actions[bot]'
- edit-mode: replace
- body: |
-
- ## 🎨 Storybook Build Status
-
-
**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: ''
- comment-author: 'github-actions[bot]'
- edit-mode: replace
- body: |
-
- ## 🎨 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.'
- }}
diff --git a/.github/workflows/pr-update-playwright-expectations.yaml b/.github/workflows/pr-update-playwright-expectations.yaml
new file mode 100644
index 000000000..f688c3250
--- /dev/null
+++ b/.github/workflows/pr-update-playwright-expectations.yaml
@@ -0,0 +1,108 @@
+# Setting test expectation screenshots for Playwright
+name: "PR: Update Playwright Expectations"
+
+on:
+ pull_request:
+ types: [labeled]
+ issue_comment:
+ types: [created]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ if: >
+ ( github.event_name == 'pull_request' && github.event.label.name == 'New Browser Test Expectations' ) ||
+ ( github.event.issue.pull_request &&
+ github.event_name == 'issue_comment' &&
+ (
+ github.event.comment.author_association == 'OWNER' ||
+ github.event.comment.author_association == 'MEMBER' ||
+ github.event.comment.author_association == 'COLLABORATOR'
+ ) &&
+ startsWith(github.event.comment.body, '/update-playwright') )
+ steps:
+ - name: Find Update Comment
+ uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
+ id: "find-update-comment"
+ with:
+ issue-number: ${{ github.event.number || github.event.issue.number }}
+ comment-author: "github-actions[bot]"
+ body-includes: "Updating Playwright Expectations"
+
+ - name: Add Starting Reaction
+ uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
+ with:
+ comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
+ issue-number: ${{ github.event.number || github.event.issue.number }}
+ body: |
+ Updating Playwright Expectations
+ edit-mode: replace
+ reactions: eyes
+
+ - name: Get Branch SHA
+ id: "get-branch"
+ run: echo ::set-output name=branch::$(gh pr view $PR_NO --repo $REPO --json headRefName --jq '.headRefName')
+ env:
+ REPO: ${{ github.repository }}
+ PR_NO: ${{ github.event.number || github.event.issue.number }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Initial Checkout
+ uses: actions/checkout@v5
+ with:
+ ref: ${{ steps.get-branch.outputs.branch }}
+ - name: Setup Frontend
+ uses: ./.github/actions/setup-frontend
+ with:
+ include_build_step: true
+ - name: Setup ComfyUI Server
+ uses: ./.github/actions/setup-comfyui-server
+ with:
+ launch_server: true
+ - name: Setup Playwright
+ uses: ./.github/actions/setup-playwright
+ - name: Run Playwright tests and update snapshots
+ id: playwright-tests
+ run: pnpm exec playwright test --update-snapshots
+ continue-on-error: true
+ - uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: playwright-report
+ path: ./playwright-report/
+ retention-days: 30
+ - name: Debugging info
+ run: |
+ echo "PR: ${{ github.event.issue.number }}"
+ echo "Branch: ${{ steps.get-branch.outputs.branch }}"
+ git status
+ - name: Commit updated expectations
+ run: |
+ git config --global user.name 'github-actions'
+ git config --global user.email 'github-actions@github.com'
+ git add browser_tests
+ if git diff --cached --quiet; then
+ echo "No changes to commit"
+ else
+ git commit -m "[automated] Update test expectations"
+ git push origin ${{ steps.get-branch.outputs.branch }}
+ fi
+
+ - name: Add Done Reaction
+ uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
+ if: github.event_name == 'issue_comment'
+ with:
+ comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
+ issue-number: ${{ github.event.number || github.event.issue.number }}
+ reactions: +1
+ reactions-edit-mode: replace
+
+ - name: Remove New Browser Test Expectations label
+ if: always() && github.event_name == 'pull_request'
+ run: gh pr edit ${{ github.event.pull_request.number }} --remove-label "New Browser Test Expectations"
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/publish-desktop-ui-on-merge.yaml b/.github/workflows/publish-desktop-ui-on-merge.yaml
new file mode 100644
index 000000000..b1adaa0c2
--- /dev/null
+++ b/.github/workflows/publish-desktop-ui-on-merge.yaml
@@ -0,0 +1,59 @@
+name: Publish Desktop UI on PR Merge
+
+on:
+ pull_request:
+ types: [ closed ]
+ branches: [ main, core/* ]
+ paths:
+ - 'apps/desktop-ui/package.json'
+
+jobs:
+ resolve:
+ name: Resolve Version and Dist Tag
+ runs-on: ubuntu-latest
+ if: >
+ github.event.pull_request.merged == true &&
+ contains(github.event.pull_request.labels.*.name, 'Release')
+ outputs:
+ version: ${{ steps.get_version.outputs.version }}
+ dist_tag: ${{ steps.dist.outputs.dist_tag }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+ with:
+ ref: ${{ github.event.pull_request.merge_commit_sha }}
+ persist-credentials: false
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v5
+ with:
+ node-version: '24.x'
+
+ - name: Read desktop-ui version
+ id: get_version
+ run: |
+ VERSION=$(node -p "require('./apps/desktop-ui/package.json').version")
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+
+ - name: Determine dist-tag
+ id: dist
+ env:
+ VERSION: ${{ steps.get_version.outputs.version }}
+ run: |
+ if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+- ]]; then
+ echo "dist_tag=next" >> $GITHUB_OUTPUT
+ else
+ echo "dist_tag=latest" >> $GITHUB_OUTPUT
+ fi
+
+ publish:
+ name: Publish Desktop UI to npm
+ needs: resolve
+ uses: ./.github/workflows/publish-desktop-ui.yaml
+ with:
+ version: ${{ needs.resolve.outputs.version }}
+ dist_tag: ${{ needs.resolve.outputs.dist_tag }}
+ ref: ${{ github.event.pull_request.merge_commit_sha }}
+ secrets:
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+
diff --git a/.github/workflows/publish-desktop-ui.yaml b/.github/workflows/publish-desktop-ui.yaml
new file mode 100644
index 000000000..356cf9e78
--- /dev/null
+++ b/.github/workflows/publish-desktop-ui.yaml
@@ -0,0 +1,206 @@
+name: Publish Desktop UI
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Version to publish (e.g., 1.2.3)'
+ required: true
+ type: string
+ dist_tag:
+ description: 'npm dist-tag to use'
+ required: true
+ default: latest
+ type: string
+ ref:
+ description: 'Git ref to checkout (commit SHA, tag, or branch)'
+ required: false
+ type: string
+ workflow_call:
+ inputs:
+ version:
+ required: true
+ type: string
+ dist_tag:
+ required: false
+ type: string
+ default: latest
+ ref:
+ required: false
+ type: string
+ secrets:
+ NPM_TOKEN:
+ required: true
+
+concurrency:
+ group: publish-desktop-ui-${{ github.workflow }}-${{ inputs.version }}-${{ inputs.dist_tag }}
+ cancel-in-progress: false
+
+jobs:
+ publish_desktop_ui:
+ name: Publish @comfyorg/desktop-ui
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ env:
+ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
+ ENABLE_MINIFY: 'true'
+ steps:
+ - name: Validate inputs
+ env:
+ VERSION: ${{ inputs.version }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ SEMVER_REGEX='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$'
+ if [[ ! "$VERSION" =~ $SEMVER_REGEX ]]; then
+ echo "::error title=Invalid version::Version '$VERSION' must follow semantic versioning (x.y.z[-suffix][+build])" >&2
+ exit 1
+ fi
+
+ - name: Determine ref to checkout
+ id: resolve_ref
+ env:
+ REF: ${{ inputs.ref }}
+ VERSION: ${{ inputs.version }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ if [ -n "$REF" ]; then
+ if ! git check-ref-format --allow-onelevel "$REF"; then
+ echo "::error title=Invalid ref::Ref '$REF' fails git check-ref-format validation." >&2
+ exit 1
+ fi
+ echo "ref=$REF" >> "$GITHUB_OUTPUT"
+ else
+ echo "ref=refs/tags/v$VERSION" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Checkout repository
+ uses: actions/checkout@v5
+ with:
+ ref: ${{ steps.resolve_ref.outputs.ref }}
+ fetch-depth: 1
+ persist-credentials: false
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v5
+ with:
+ node-version: '24.x'
+ cache: 'pnpm'
+ registry-url: https://registry.npmjs.org
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile --ignore-scripts
+
+ - name: Build Desktop UI
+ run: pnpm build:desktop
+
+ - name: Prepare npm package
+ id: pkg
+ shell: bash
+ run: |
+ set -euo pipefail
+ APP_PKG=apps/desktop-ui/package.json
+ ROOT_PKG=package.json
+
+ NAME=$(jq -r .name "$APP_PKG")
+ APP_VERSION=$(jq -r .version "$APP_PKG")
+ ROOT_LICENSE=$(jq -r .license "$ROOT_PKG")
+ REPO=$(jq -r .repository "$ROOT_PKG")
+
+ if [ -z "$NAME" ] || [ "$NAME" = "null" ]; then
+ echo "::error title=Missing name::apps/desktop-ui/package.json is missing 'name'" >&2
+ exit 1
+ fi
+
+ INPUT_VERSION="${{ inputs.version }}"
+ if [ "$APP_VERSION" != "$INPUT_VERSION" ]; then
+ echo "::error title=Version mismatch::apps/desktop-ui version $APP_VERSION does not match input $INPUT_VERSION" >&2
+ exit 1
+ fi
+
+ if [ ! -d apps/desktop-ui/dist ]; then
+ echo "::error title=Missing build::apps/desktop-ui/dist not found. Did build succeed?" >&2
+ exit 1
+ fi
+
+ PUBLISH_DIR=apps/desktop-ui/.npm-publish
+ rm -rf "$PUBLISH_DIR"
+ mkdir -p "$PUBLISH_DIR"
+ cp -R apps/desktop-ui/dist "$PUBLISH_DIR/dist"
+
+ INPUT_VERSION="${{ inputs.version }}"
+ jq -n \
+ --arg name "$NAME" \
+ --arg version "$INPUT_VERSION" \
+ --arg description "Static assets for the ComfyUI Desktop UI" \
+ --arg license "$ROOT_LICENSE" \
+ --arg repository "$REPO" \
+ '{
+ name: $name,
+ version: $version,
+ description: $description,
+ license: $license,
+ repository: $repository,
+ type: "module",
+ private: false,
+ files: ["dist"],
+ publishConfig: { access: "public" }
+ }' > "$PUBLISH_DIR/package.json"
+
+ if [ -f apps/desktop-ui/README.md ]; then
+ cp apps/desktop-ui/README.md "$PUBLISH_DIR/README.md"
+ fi
+
+ echo "publish_dir=$PUBLISH_DIR" >> "$GITHUB_OUTPUT"
+ echo "name=$NAME" >> "$GITHUB_OUTPUT"
+
+ - name: Pack (preview only)
+ shell: bash
+ working-directory: ${{ steps.pkg.outputs.publish_dir }}
+ run: |
+ set -euo pipefail
+ npm pack --json | tee pack-result.json
+
+ - name: Upload package tarball artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: desktop-ui-npm-tarball-${{ inputs.version }}
+ path: ${{ steps.pkg.outputs.publish_dir }}/*.tgz
+ if-no-files-found: error
+
+ - name: Check if version already on npm
+ id: check_npm
+ env:
+ NAME: ${{ steps.pkg.outputs.name }}
+ VER: ${{ inputs.version }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ STATUS=0
+ OUTPUT=$(npm view "${NAME}@${VER}" --json 2>&1) || STATUS=$?
+ if [ "$STATUS" -eq 0 ]; then
+ echo "exists=true" >> "$GITHUB_OUTPUT"
+ echo "::warning title=Already published::${NAME}@${VER} already exists on npm. Skipping publish."
+ else
+ if echo "$OUTPUT" | grep -q "E404"; then
+ echo "exists=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "::error title=Registry lookup failed::$OUTPUT" >&2
+ exit "$STATUS"
+ fi
+ fi
+
+ - name: Publish package
+ if: steps.check_npm.outputs.exists == 'false'
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ DIST_TAG: ${{ inputs.dist_tag }}
+ run: pnpm publish --access public --tag "$DIST_TAG" --no-git-checks --ignore-scripts
+ working-directory: ${{ steps.pkg.outputs.publish_dir }}
diff --git a/.github/workflows/create-release-candidate-branch.yaml b/.github/workflows/release-branch-create.yaml
similarity index 78%
rename from .github/workflows/create-release-candidate-branch.yaml
rename to .github/workflows/release-branch-create.yaml
index 84b545478..992e779dd 100644
--- a/.github/workflows/create-release-candidate-branch.yaml
+++ b/.github/workflows/release-branch-create.yaml
@@ -1,4 +1,4 @@
-name: Create Release Branch
+name: Release Branch Create
on:
pull_request:
@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
fetch-depth: 0
token: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
@@ -128,45 +128,6 @@ jobs:
echo "- Critical security patches"
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
if: steps.check_version.outputs.is_minor_bump == 'true'
diff --git a/.github/workflows/release.yaml b/.github/workflows/release-draft-create.yaml
similarity index 79%
rename from .github/workflows/release.yaml
rename to .github/workflows/release-draft-create.yaml
index 8958ce147..d26744f56 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release-draft-create.yaml
@@ -1,4 +1,4 @@
-name: Create Release Draft
+name: Release Draft Create
on:
pull_request:
@@ -18,7 +18,7 @@ jobs:
is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }}
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
@@ -55,6 +55,7 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
+ ENABLE_MINIFY: 'true'
USE_PROD_CONFIG: 'true'
run: |
pnpm install --frozen-lockfile
@@ -73,7 +74,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Download dist artifact
uses: actions/download-artifact@v4
with:
@@ -98,7 +99,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Download dist artifact
uses: actions/download-artifact@v4
with:
@@ -126,34 +127,8 @@ jobs:
publish_types:
needs: build
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - name: Install pnpm
- uses: pnpm/action-setup@v4
- with:
- version: 10
- - uses: actions/setup-node@v4
- with:
- node-version: 'lts/*'
- cache: 'pnpm'
- registry-url: https://registry.npmjs.org
-
- - name: Cache tool outputs
- uses: actions/cache@v4
- with:
- path: |
- .cache
- tsconfig.tsbuildinfo
- dist
- key: types-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
- restore-keys: |
- types-tools-cache-${{ runner.os }}-
-
- - run: pnpm install --frozen-lockfile
- - run: pnpm build:types
- - name: Publish package
- run: pnpm publish --access public
- working-directory: dist
- env:
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ uses: ./.github/workflows/release-npm-types.yaml
+ with:
+ version: ${{ needs.build.outputs.version }}
+ ref: ${{ github.event.pull_request.merge_commit_sha }}
+ secrets: inherit
diff --git a/.github/workflows/release-npm-types.yaml b/.github/workflows/release-npm-types.yaml
new file mode 100644
index 000000000..23f0cc016
--- /dev/null
+++ b/.github/workflows/release-npm-types.yaml
@@ -0,0 +1,139 @@
+name: Release NPM Types
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Version to publish (e.g., 1.26.7)'
+ required: true
+ type: string
+ dist_tag:
+ description: 'npm dist-tag to use'
+ required: true
+ default: latest
+ type: string
+ ref:
+ description: 'Git ref to checkout (commit SHA, tag, or branch)'
+ required: false
+ type: string
+ workflow_call:
+ inputs:
+ version:
+ required: true
+ type: string
+ dist_tag:
+ required: false
+ type: string
+ default: latest
+ ref:
+ required: false
+ type: string
+
+concurrency:
+ group: publish-frontend-types-${{ github.workflow }}-${{ inputs.version }}-${{ inputs.dist_tag }}
+ cancel-in-progress: false
+
+jobs:
+ publish_types_manual:
+ name: Publish @comfyorg/comfyui-frontend-types
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - name: Validate inputs
+ shell: bash
+ run: |
+ set -euo pipefail
+ VERSION="${{ inputs.version }}"
+ SEMVER_REGEX='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$'
+ if [[ ! "$VERSION" =~ $SEMVER_REGEX ]]; then
+ echo "::error title=Invalid version::Version '$VERSION' must follow semantic versioning (x.y.z[-suffix][+build])" >&2
+ exit 1
+ fi
+
+ - name: Determine ref to checkout
+ id: resolve_ref
+ shell: bash
+ run: |
+ set -euo pipefail
+ REF="${{ inputs.ref }}"
+ VERSION="${{ inputs.version }}"
+ if [ -n "$REF" ]; then
+ if ! git check-ref-format --allow-onelevel "$REF"; then
+ echo "::error title=Invalid ref::Ref '$REF' fails git check-ref-format validation." >&2
+ exit 1
+ fi
+ echo "ref=$REF" >> "$GITHUB_OUTPUT"
+ else
+ echo "ref=refs/tags/v$VERSION" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Checkout repository
+ uses: actions/checkout@v5
+ with:
+ ref: ${{ steps.resolve_ref.outputs.ref }}
+ fetch-depth: 1
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v5
+ with:
+ node-version: 'lts/*'
+ cache: 'pnpm'
+ registry-url: https://registry.npmjs.org
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+ env:
+ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
+
+ - name: Build types
+ run: pnpm build:types
+
+ - name: Verify version matches input
+ id: verify
+ shell: bash
+ run: |
+ PKG_VERSION=$(node -p "require('./package.json').version")
+ TYPES_PKG_VERSION=$(node -p "require('./dist/package.json').version")
+ if [ "$PKG_VERSION" != "${{ inputs.version }}" ]; then
+ echo "Error: package.json version $PKG_VERSION does not match input ${{ inputs.version }}" >&2
+ exit 1
+ fi
+ if [ "$TYPES_PKG_VERSION" != "${{ inputs.version }}" ]; then
+ echo "Error: dist/package.json version $TYPES_PKG_VERSION does not match input ${{ inputs.version }}" >&2
+ exit 1
+ fi
+ echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
+
+ - name: Check if version already on npm
+ id: check_npm
+ shell: bash
+ run: |
+ set -euo pipefail
+ NAME=$(node -p "require('./dist/package.json').name")
+ VER="${{ steps.verify.outputs.version }}"
+ STATUS=0
+ OUTPUT=$(npm view "${NAME}@${VER}" --json 2>&1) || STATUS=$?
+ if [ "$STATUS" -eq 0 ]; then
+ echo "exists=true" >> "$GITHUB_OUTPUT"
+ echo "::warning title=Already published::${NAME}@${VER} already exists on npm. Skipping publish."
+ else
+ if echo "$OUTPUT" | grep -q "E404"; then
+ echo "exists=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "::error title=Registry lookup failed::$OUTPUT" >&2
+ exit "$STATUS"
+ fi
+ fi
+
+ - name: Publish package
+ if: steps.check_npm.outputs.exists == 'false'
+ run: pnpm publish --access public --tag "${{ inputs.dist_tag }}" --no-git-checks
+ working-directory: dist
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/dev-release.yaml b/.github/workflows/release-pypi-dev.yaml
similarity index 95%
rename from .github/workflows/dev-release.yaml
rename to .github/workflows/release-pypi-dev.yaml
index 0b45420c6..1bb375025 100644
--- a/.github/workflows/dev-release.yaml
+++ b/.github/workflows/release-pypi-dev.yaml
@@ -1,4 +1,4 @@
-name: Create Dev PyPI Package
+name: Release PyPI Dev
on:
workflow_dispatch:
@@ -15,7 +15,7 @@ jobs:
version: ${{ steps.current_version.outputs.version }}
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
@@ -44,6 +44,7 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
+ ENABLE_MINIFY: 'true'
USE_PROD_CONFIG: 'true'
run: |
pnpm install --frozen-lockfile
@@ -62,7 +63,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Download dist artifact
uses: actions/download-artifact@v4
with:
diff --git a/.github/workflows/version-bump.yaml b/.github/workflows/release-version-bump.yaml
similarity index 62%
rename from .github/workflows/version-bump.yaml
rename to .github/workflows/release-version-bump.yaml
index 77021e5c7..5f3152285 100644
--- a/.github/workflows/version-bump.yaml
+++ b/.github/workflows/release-version-bump.yaml
@@ -1,4 +1,5 @@
-name: Version Bump
+name: "Release: Version Bump"
+description: "Manual workflow to increment package version with semantic versioning support"
on:
workflow_dispatch:
@@ -14,6 +15,11 @@ on:
required: false
default: ''
type: string
+ branch:
+ description: 'Base branch to bump (e.g., main, core/1.29, core/1.30)'
+ required: true
+ default: 'main'
+ type: string
jobs:
bump-version:
@@ -24,7 +30,25 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
+ with:
+ ref: ${{ github.event.inputs.branch }}
+ fetch-depth: 0
+
+ - name: Validate branch exists
+ run: |
+ BRANCH="${{ github.event.inputs.branch }}"
+ if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
+ echo "❌ Branch '$BRANCH' does not exist"
+ echo ""
+ echo "Available core branches:"
+ git branch -r | grep 'origin/core/' | sed 's/.*origin\// - /' || echo " (none found)"
+ echo ""
+ echo "Main branch:"
+ echo " - main"
+ exit 1
+ fi
+ echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -58,7 +82,9 @@ jobs:
title: ${{ steps.bump-version.outputs.NEW_VERSION }}
body: |
${{ steps.capitalised.outputs.capitalised }} version increment to ${{ steps.bump-version.outputs.NEW_VERSION }}
+
+ **Base branch:** `${{ github.event.inputs.branch }}`
branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
- base: main
+ base: ${{ github.event.inputs.branch }}
labels: |
Release
diff --git a/.github/workflows/size-data.yml b/.github/workflows/size-data.yml
new file mode 100644
index 000000000..8da55d0c2
--- /dev/null
+++ b/.github/workflows/size-data.yml
@@ -0,0 +1,52 @@
+name: size data
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+permissions:
+ contents: read
+
+jobs:
+ collect:
+ if: github.repository == 'Comfy-Org/ComfyUI_frontend'
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4.1.0
+ with:
+ version: 10
+
+ - name: Install Node.js
+ uses: actions/setup-node@v5
+ with:
+ node-version: '24.x'
+ cache: pnpm
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Build project
+ run: pnpm build
+
+ - name: Collect size data
+ run: node scripts/size-collect.js
+
+ - name: Save PR number & base branch
+ if: ${{ github.event_name == 'pull_request' }}
+ run: |
+ echo ${{ github.event.number }} > ./temp/size/number.txt
+ echo ${{ github.base_ref }} > ./temp/size/base.txt
+
+ - name: Upload size data
+ uses: actions/upload-artifact@v4
+ with:
+ name: size-data
+ path: temp/size
diff --git a/.github/workflows/size-report.yml b/.github/workflows/size-report.yml
new file mode 100644
index 000000000..caaafd30a
--- /dev/null
+++ b/.github/workflows/size-report.yml
@@ -0,0 +1,104 @@
+name: size report
+
+on:
+ workflow_run:
+ workflows: ['size data']
+ types:
+ - completed
+ workflow_dispatch:
+ inputs:
+ pr_number:
+ description: 'PR number to report on'
+ required: true
+ type: number
+ run_id:
+ description: 'Size data workflow run ID'
+ required: true
+ type: string
+
+permissions:
+ contents: read
+ pull-requests: write
+ issues: write
+
+jobs:
+ size-report:
+ runs-on: ubuntu-latest
+ if: >
+ github.repository == 'Comfy-Org/ComfyUI_frontend' &&
+ (
+ (github.event_name == 'workflow_run' &&
+ github.event.workflow_run.event == 'pull_request' &&
+ github.event.workflow_run.conclusion == 'success') ||
+ github.event_name == 'workflow_dispatch'
+ )
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4.1.0
+ with:
+ version: 10
+
+ - name: Install Node.js
+ uses: actions/setup-node@v5
+ with:
+ node-version: '24.x'
+ cache: pnpm
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Download size data
+ uses: dawidd6/action-download-artifact@v11
+ with:
+ name: size-data
+ run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }}
+ path: temp/size
+
+ - name: Set PR number
+ id: pr-number
+ run: |
+ if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
+ echo "content=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
+ else
+ echo "content=$(cat temp/size/number.txt)" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Set base branch
+ id: pr-base
+ run: |
+ if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
+ echo "content=main" >> $GITHUB_OUTPUT
+ else
+ echo "content=$(cat temp/size/base.txt)" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Download previous size data
+ uses: dawidd6/action-download-artifact@v11
+ with:
+ branch: ${{ steps.pr-base.outputs.content }}
+ workflow: size-data.yml
+ event: push
+ name: size-data
+ path: temp/size-prev
+ if_no_artifact_found: warn
+
+ - name: Generate size report
+ run: node scripts/size-report.js > size-report.md
+
+ - name: Read size report
+ id: size-report
+ uses: juliangruber/read-file-action@v1
+ with:
+ path: ./size-report.md
+
+ - name: Create or update PR comment
+ uses: actions-cool/maintain-one-comment@v3
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ number: ${{ steps.pr-number.outputs.content }}
+ body: |
+ ${{ steps.size-report.outputs.content }}
+
+ body-include: ''
diff --git a/.github/workflows/test-browser-exp.yaml b/.github/workflows/test-browser-exp.yaml
deleted file mode 100644
index 63052c3e4..000000000
--- a/.github/workflows/test-browser-exp.yaml
+++ /dev/null
@@ -1,49 +0,0 @@
-# Setting test expectation screenshots for Playwright
-name: Update Playwright Expectations
-
-on:
- pull_request:
- types: [ labeled ]
-
-jobs:
- test:
- runs-on: ubuntu-latest
- if: github.event.label.name == 'New Browser Test Expectations'
- steps:
- - 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
- run: npx playwright install chromium --with-deps
- working-directory: ComfyUI_frontend
- - name: Run Playwright tests and update snapshots
- id: playwright-tests
- run: npx playwright test --update-snapshots
- continue-on-error: true
- working-directory: ComfyUI_frontend
- - uses: actions/upload-artifact@v4
- if: always()
- with:
- name: playwright-report
- path: ComfyUI_frontend/playwright-report/
- retention-days: 30
- - name: Debugging info
- run: |
- echo "Branch: ${{ github.head_ref }}"
- git status
- working-directory: ComfyUI_frontend
- - name: Commit updated expectations
- run: |
- git config --global user.name 'github-actions'
- git config --global user.email 'github-actions@github.com'
- git fetch origin ${{ github.head_ref }}
- git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
- git add browser_tests
- git commit -m "Update test expectations [skip ci]"
- git push origin HEAD:${{ github.head_ref }}
- working-directory: ComfyUI_frontend
diff --git a/.github/workflows/test-ui.yaml b/.github/workflows/test-ui.yaml
deleted file mode 100644
index be5c0cfc2..000000000
--- a/.github/workflows/test-ui.yaml
+++ /dev/null
@@ -1,251 +0,0 @@
-name: Tests CI
-
-on:
- push:
- branches: [main, master, core/*, desktop/*]
- pull_request:
- branches-ignore:
- [wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*]
-
-jobs:
- setup:
- runs-on: ubuntu-latest
- outputs:
- cache-key: ${{ steps.cache-key.outputs.key }}
- steps:
- - name: Checkout ComfyUI
- uses: actions/checkout@v4
- with:
- repository: 'comfyanonymous/ComfyUI'
- path: 'ComfyUI'
- ref: master
-
- - name: Checkout ComfyUI_frontend
- uses: actions/checkout@v4
- with:
- repository: 'Comfy-Org/ComfyUI_frontend'
- path: 'ComfyUI_frontend'
-
- - name: Checkout ComfyUI_devtools
- uses: actions/checkout@v4
- with:
- repository: 'Comfy-Org/ComfyUI_devtools'
- path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
- ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684'
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
- with:
- version: 10
-
- - uses: actions/setup-node@v4
- with:
- node-version: lts/*
- cache: 'pnpm'
- cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml'
-
- - name: Cache tool outputs
- uses: actions/cache@v4
- with:
- path: |
- ComfyUI_frontend/.cache
- ComfyUI_frontend/tsconfig.tsbuildinfo
- key: playwright-setup-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-${{ hashFiles('ComfyUI_frontend/src/**/*.{ts,vue,js}', 'ComfyUI_frontend/*.config.*') }}
- restore-keys: |
- playwright-setup-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-
- playwright-setup-cache-${{ runner.os }}-
- playwright-tools-cache-${{ runner.os }}-
-
- - name: Build ComfyUI_frontend
- run: |
- pnpm install --frozen-lockfile
- pnpm build
- working-directory: ComfyUI_frontend
-
- - name: Generate cache key
- id: cache-key
- run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
-
- - name: Save cache
- uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
- with:
- path: |
- ComfyUI
- ComfyUI_frontend
- key: comfyui-setup-${{ steps.cache-key.outputs.key }}
-
- # Sharded chromium tests
- playwright-tests-chromium-sharded:
- needs: setup
- runs-on: ubuntu-latest
- permissions:
- contents: read
- strategy:
- fail-fast: false
- matrix:
- shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
- shardTotal: [8]
- 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
- run: |
- python -m pip install --upgrade pip
- pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
- pip install -r requirements.txt
- pip install wait-for-it
- working-directory: ComfyUI
-
- - 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: Install Playwright Browsers
- run: npx playwright install chromium --with-deps
- working-directory: ComfyUI_frontend
-
- - 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
- run: |
- python -m pip install --upgrade pip
- pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
- pip install -r requirements.txt
- pip install wait-for-it
- working-directory: ComfyUI
-
- - 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: Install Playwright Browsers
- run: npx playwright install chromium --with-deps
- working-directory: ComfyUI_frontend
-
- - name: Run Playwright tests (${{ matrix.browser }})
- id: playwright
- run: npx playwright test --project=${{ matrix.browser }} --reporter=html
- working-directory: ComfyUI_frontend
-
- - uses: actions/upload-artifact@v4
- if: always()
- with:
- name: playwright-report-${{ matrix.browser }}
- path: ComfyUI_frontend/playwright-report/
- retention-days: 30
-
- # Merge sharded test reports
- merge-reports:
- needs: [playwright-tests-chromium-sharded]
- runs-on: ubuntu-latest
- if: ${{ !cancelled() }}
- steps:
- - name: Checkout ComfyUI_frontend
- uses: actions/checkout@v4
- with:
- repository: 'Comfy-Org/ComfyUI_frontend'
- path: 'ComfyUI_frontend'
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
- with:
- version: 10
-
- - uses: actions/setup-node@v4
- 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
- with:
- name: playwright-report-chromium
- path: ComfyUI_frontend/playwright-report/
- retention-days: 30
\ No newline at end of file
diff --git a/.github/workflows/version-bump-desktop-ui.yaml b/.github/workflows/version-bump-desktop-ui.yaml
new file mode 100644
index 000000000..123cdbd9b
--- /dev/null
+++ b/.github/workflows/version-bump-desktop-ui.yaml
@@ -0,0 +1,95 @@
+name: Version Bump Desktop UI
+
+on:
+ workflow_dispatch:
+ inputs:
+ version_type:
+ description: 'Version increment type'
+ required: true
+ default: 'patch'
+ type: 'choice'
+ options: [patch, minor, major, prepatch, preminor, premajor, prerelease]
+ pre_release:
+ description: Pre-release ID (suffix)
+ required: false
+ default: ''
+ type: string
+ branch:
+ description: 'Base branch to bump (e.g., main, core/1.29, core/1.30)'
+ required: true
+ default: 'main'
+ type: string
+
+jobs:
+ bump-version-desktop-ui:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+ with:
+ ref: ${{ github.event.inputs.branch }}
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Validate branch exists
+ run: |
+ BRANCH="${{ github.event.inputs.branch }}"
+ if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
+ echo "❌ Branch '$BRANCH' does not exist"
+ echo ""
+ echo "Available core branches:"
+ git branch -r | grep 'origin/core/' | sed 's/.*origin\// - /' || echo " (none found)"
+ echo ""
+ echo "Main branch:"
+ echo " - main"
+ exit 1
+ fi
+ echo "✅ Branch '$BRANCH' exists"
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v5
+ with:
+ node-version: '24.x'
+ cache: 'pnpm'
+
+ - name: Bump desktop-ui version
+ id: bump-version
+ env:
+ VERSION_TYPE: ${{ github.event.inputs.version_type }}
+ PRE_RELEASE: ${{ github.event.inputs.pre_release }}
+ run: |
+ pnpm -C apps/desktop-ui version "$VERSION_TYPE" --preid "$PRE_RELEASE" --no-git-tag-version
+ NEW_VERSION=$(node -p "require('./apps/desktop-ui/package.json').version")
+ echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
+
+ - name: Format PR string
+ id: capitalised
+ env:
+ VERSION_TYPE: ${{ github.event.inputs.version_type }}
+ run: |
+ echo "capitalised=${VERSION_TYPE@u}" >> $GITHUB_OUTPUT
+
+ - name: Create Pull Request
+ uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
+ with:
+ token: ${{ secrets.PR_GH_TOKEN }}
+ commit-message: '[release] Increment desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}'
+ title: desktop-ui ${{ steps.bump-version.outputs.NEW_VERSION }}
+ body: |
+ ${{ steps.capitalised.outputs.capitalised }} version increment for @comfyorg/desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}
+
+ **Base branch:** `${{ github.event.inputs.branch }}`
+ branch: desktop-ui-version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
+ base: ${{ github.event.inputs.branch }}
+ labels: |
+ Release
+
diff --git a/.github/workflows/vitest.yaml b/.github/workflows/vitest.yaml
deleted file mode 100644
index cba1dbe05..000000000
--- a/.github/workflows/vitest.yaml
+++ /dev/null
@@ -1,46 +0,0 @@
-name: Vitest Tests
-
-on:
- push:
- branches: [ main, master, dev*, core/*, desktop/* ]
- pull_request:
- branches-ignore: [ wip/*, draft/*, temp/* ]
-
-jobs:
- test:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
- with:
- version: 10
-
- - name: Use Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 'lts/*'
- cache: 'pnpm'
-
- - name: Cache tool outputs
- uses: actions/cache@v4
- with:
- path: |
- .cache
- coverage
- .vitest-cache
- key: vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', 'vitest.config.*', 'tsconfig.json') }}
- restore-keys: |
- vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
- vitest-cache-${{ runner.os }}-
- test-tools-cache-${{ runner.os }}-
-
- - name: Install dependencies
- run: pnpm install --frozen-lockfile
-
- - name: Run Vitest tests
- run: |
- pnpm test:component
- pnpm test:unit
diff --git a/.gitignore b/.gitignore
index db0b8454c..8f69ce164 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@ yarn.lock
# Cache files
.eslintcache
.prettiercache
+.stylelintcache
node_modules
dist
@@ -22,6 +23,8 @@ dist-ssr
*.local
# Claude configuration
.claude/*.local.json
+.claude/*.local.md
+.claude/*.local.txt
CLAUDE.local.md
# Editor directories and files
@@ -29,6 +32,7 @@ CLAUDE.local.md
*.code-workspace
!.vscode/extensions.json
!.vscode/tailwind.json
+!.vscode/custom-css.json
!.vscode/settings.json.default
!.vscode/launch.json.default
.idea
@@ -44,6 +48,7 @@ components.d.ts
tests-ui/data/*
tests-ui/ComfyUI_examples
tests-ui/workflows/examples
+coverage/
# Browser tests
/test-results/
@@ -51,6 +56,7 @@ tests-ui/workflows/examples
/blob-report/
/playwright/.cache/
browser_tests/**/*-win32.png
+browser_tests/local/
.env
@@ -77,8 +83,8 @@ vite.config.mts.timestamp-*.mjs
*storybook.log
storybook-static
-
-
+# MCP Servers
+.playwright-mcp/*
.nx/cache
.nx/workspace-data
diff --git a/.husky/pre-commit b/.husky/pre-commit
index 6b8a399e4..c0b5cf437 100755
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
-npx lint-staged
-npx tsx scripts/check-unused-i18n-keys.ts
\ No newline at end of file
+pnpm exec lint-staged
+pnpm exec tsx scripts/check-unused-i18n-keys.ts
\ No newline at end of file
diff --git a/.husky/pre-push b/.husky/pre-push
new file mode 100755
index 000000000..ec1fc17d0
--- /dev/null
+++ b/.husky/pre-push
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+# Run Knip with cache via package script
+pnpm knip
+
diff --git a/.i18nrc.cjs b/.i18nrc.cjs
index 0429c3578..2efe5f966 100644
--- a/.i18nrc.cjs
+++ b/.i18nrc.cjs
@@ -9,7 +9,7 @@ module.exports = defineConfig({
entry: 'src/locales/en',
entryLocale: 'en',
output: 'src/locales',
- outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar'],
+ outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr'],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.
diff --git a/.mcp.json b/.mcp.json
deleted file mode 100644
index 82e215be0..000000000
--- a/.mcp.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "mcpServers": {
- "playwright": {
- "command": "npx",
- "args": ["-y", "@executeautomation/playwright-mcp-server"]
- },
- "context7": {
- "command": "npx",
- "args": ["-y", "@upstash/context7-mcp"]
- }
- }
-}
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 000000000..1b28e47ce
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,2 @@
+ignore-workspace-root-check=true
+catalog-mode=prefer
diff --git a/.prettierignore b/.prettierignore
index cccae51c9..4403edd8e 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,2 +1,2 @@
-src/types/comfyRegistryTypes.ts
-src/types/generatedManagerTypes.ts
\ No newline at end of file
+packages/registry-types/src/comfyRegistryTypes.ts
+src/types/generatedManagerTypes.ts
diff --git a/.storybook/CLAUDE.md b/.storybook/CLAUDE.md
index 3877181a2..ca8248784 100644
--- a/.storybook/CLAUDE.md
+++ b/.storybook/CLAUDE.md
@@ -4,7 +4,7 @@
- `pnpm storybook`: Start Storybook development server
- `pnpm build-storybook`: Build static Storybook
-- `pnpm test:component`: Run component tests (includes Storybook components)
+- `pnpm test:unit`: Run unit tests (includes Storybook components)
## Development Workflow for Storybook
diff --git a/.storybook/README.md b/.storybook/README.md
index 0d34474ec..be5405e51 100644
--- a/.storybook/README.md
+++ b/.storybook/README.md
@@ -211,18 +211,17 @@ This Storybook setup includes:
## Icon Usage in Storybook
-In this project, the `` syntax from unplugin-icons is not supported in Storybook.
+In this project, only the `` syntax from unplugin-icons is supported in Storybook.
**Example:**
```vue
-
-
+
+
```
diff --git a/.storybook/main.ts b/.storybook/main.ts
index a799ec143..d61f72eae 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -15,26 +15,37 @@ const config: StorybookConfig = {
async viteFinal(config) {
// Use dynamic import to avoid CJS deprecation warning
const { mergeConfig } = await import('vite')
+ const { default: tailwindcss } = await import('@tailwindcss/vite')
// Filter out any plugins that might generate import maps
if (config.plugins) {
- config.plugins = config.plugins.filter((plugin: any) => {
- if (plugin && plugin.name && plugin.name.includes('import-map')) {
- return false
- }
- return true
- })
+ config.plugins = config.plugins
+ // Type guard: ensure we have valid plugin objects with names
+ .filter(
+ (plugin): plugin is NonNullable & { name: string } => {
+ return (
+ plugin !== null &&
+ plugin !== undefined &&
+ typeof plugin === 'object' &&
+ 'name' in plugin &&
+ typeof plugin.name === 'string'
+ )
+ }
+ )
+ // Business logic: filter out import-map plugins
+ .filter((plugin) => !plugin.name.includes('import-map'))
}
return mergeConfig(config, {
// Replace plugins entirely to avoid inheritance issues
plugins: [
// Only include plugins we explicitly need for Storybook
+ tailwindcss(),
Icons({
compiler: 'vue3',
customCollections: {
comfy: FileSystemIconLoader(
- process.cwd() + '/src/assets/icons/custom'
+ process.cwd() + '/packages/design-system/src/icons'
)
}
}),
@@ -65,11 +76,6 @@ const config: StorybookConfig = {
},
build: {
rollupOptions: {
- external: () => {
- // Don't externalize any modules in Storybook build
- // This ensures PrimeVue and other dependencies are bundled
- return false
- },
onwarn: (warning, warn) => {
// Suppress specific warnings
if (
diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html
index 76aca2401..05e082ef0 100644
--- a/.storybook/preview-head.html
+++ b/.storybook/preview-head.html
@@ -57,9 +57,8 @@
/* Override Storybook's problematic & selector styles */
/* Reset only the specific properties that Storybook injects */
- #storybook-root li+li,
- #storybook-docs li+li {
- margin: inherit;
- padding: inherit;
+ li+li {
+ margin: 0;
+ padding: revert-layer;
}
\ No newline at end of file
diff --git a/.storybook/preview.ts b/.storybook/preview.ts
index 747bbe802..bfe81f431 100644
--- a/.storybook/preview.ts
+++ b/.storybook/preview.ts
@@ -1,7 +1,7 @@
import { definePreset } from '@primevue/themes'
import Aura from '@primevue/themes/aura'
import { setup } from '@storybook/vue3'
-import type { Preview } from '@storybook/vue3-vite'
+import type { Preview, StoryContext, StoryFn } from '@storybook/vue3-vite'
import { createPinia } from 'pinia'
import 'primeicons/primeicons.css'
import PrimeVue from 'primevue/config'
@@ -9,11 +9,9 @@ import ConfirmationService from 'primevue/confirmationservice'
import ToastService from 'primevue/toastservice'
import Tooltip from 'primevue/tooltip'
-import '../src/assets/css/style.css'
-import { i18n } from '../src/i18n'
-import '../src/lib/litegraph/public/css/litegraph.css'
-import { useWidgetStore } from '../src/stores/widgetStore'
-import { useColorPaletteStore } from '../src/stores/workspace/colorPaletteStore'
+import '@/assets/css/style.css'
+import { i18n } from '@/i18n'
+import '@/lib/litegraph/public/css/litegraph.css'
const ComfyUIPreset = definePreset(Aura, {
semantic: {
@@ -25,13 +23,11 @@ const ComfyUIPreset = definePreset(Aura, {
// Setup Vue app for Storybook
setup((app) => {
app.directive('tooltip', Tooltip)
+
+ // Create Pinia instance
const pinia = createPinia()
+
app.use(pinia)
-
- // Initialize stores
- useColorPaletteStore(pinia)
- useWidgetStore(pinia)
-
app.use(i18n)
app.use(PrimeVue, {
theme: {
@@ -50,8 +46,8 @@ setup((app) => {
app.use(ToastService)
})
-// Dark theme decorator
-export const withTheme = (Story: any, context: any) => {
+// Theme and dialog decorator
+export const withTheme = (Story: StoryFn, context: StoryContext) => {
const theme = context.globals.theme || 'light'
// Apply theme class to document root
@@ -63,7 +59,7 @@ export const withTheme = (Story: any, context: any) => {
document.body.classList.remove('dark-theme')
}
- return Story()
+ return Story(context.args, context)
}
const preview: Preview = {
diff --git a/.stylelintrc.json b/.stylelintrc.json
new file mode 100644
index 000000000..276ac8156
--- /dev/null
+++ b/.stylelintrc.json
@@ -0,0 +1,74 @@
+{
+ "extends": [],
+ "overrides": [
+ {
+ "files": ["*.vue", "**/*.vue"],
+ "customSyntax": "postcss-html"
+ }
+ ],
+ "rules": {
+ "import-notation": "string",
+ "font-family-no-missing-generic-family-keyword": true,
+ "declaration-property-value-no-unknown": [
+ true,
+ {
+ "ignoreProperties": {
+ "speak": ["none"],
+ "app-region": ["drag", "no-drag"],
+ "/^(width|height)$/": ["/^v-bind/"]
+ }
+ }
+ ],
+ "color-function-notation": "modern",
+ "shorthand-property-no-redundant-values": true,
+ "selector-pseudo-element-colon-notation": "double",
+ "no-duplicate-selectors": true,
+ "font-weight-notation": "numeric",
+ "length-zero-no-unit": true,
+ "color-no-invalid-hex": true,
+ "number-max-precision": 4,
+ "property-no-vendor-prefix": true,
+ "value-no-vendor-prefix": true,
+ "selector-no-vendor-prefix": true,
+ "media-feature-name-no-vendor-prefix": true,
+ "selector-max-universal": 1,
+ "selector-max-type": 2,
+ "declaration-block-no-duplicate-properties": true,
+ "block-no-empty": true,
+ "no-descending-specificity": null,
+ "no-duplicate-at-import-rules": true,
+ "at-rule-no-unknown": [
+ true,
+ {
+ "ignoreAtRules": [
+ "tailwind",
+ "apply",
+ "layer",
+ "config",
+ "theme",
+ "reference",
+ "plugin",
+ "custom-variant",
+ "utility"
+ ]
+ }
+ ],
+ "function-no-unknown": [
+ true,
+ {
+ "ignoreFunctions": [
+ "theme",
+ "v-bind"
+ ]
+ }
+ ]
+ },
+ "ignoreFiles": [
+ "node_modules/**",
+ "dist/**",
+ "playwright-report/**",
+ "public/**",
+ "src/lib/litegraph/**"
+ ],
+ "files": ["**/*.css", "**/*.vue"]
+}
diff --git a/.vscode/custom-css.json b/.vscode/custom-css.json
new file mode 100644
index 000000000..67aa038b4
--- /dev/null
+++ b/.vscode/custom-css.json
@@ -0,0 +1,50 @@
+{
+ "version": 1.1,
+ "properties": [
+ {
+ "name": "app-region",
+ "description": "Electron-specific CSS property that defines draggable regions in custom title bar windows. Setting 'drag' marks a rectangular area as draggable for moving the window; 'no-drag' excludes areas from the draggable region.",
+ "values": [
+ {
+ "name": "drag",
+ "description": "Marks the element as draggable for moving the Electron window"
+ },
+ {
+ "name": "no-drag",
+ "description": "Excludes the element from being used to drag the Electron window"
+ }
+ ],
+ "references": [
+ {
+ "name": "Electron Window Customization",
+ "url": "https://www.electronjs.org/docs/latest/tutorial/window-customization"
+ }
+ ]
+ },
+ {
+ "name": "speak",
+ "description": "Deprecated CSS2 aural stylesheet property for controlling screen reader speech. Use ARIA attributes instead.",
+ "values": [
+ {
+ "name": "auto",
+ "description": "Content is read aurally if element is not a block and is visible"
+ },
+ {
+ "name": "never",
+ "description": "Content will not be read aurally"
+ },
+ {
+ "name": "always",
+ "description": "Content will be read aurally regardless of display settings"
+ }
+ ],
+ "references": [
+ {
+ "name": "CSS-Tricks Reference",
+ "url": "https://css-tricks.com/almanac/properties/s/speak/"
+ }
+ ],
+ "status": "obsolete"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json.default b/.vscode/settings.json.default
index f1ed5fba6..18a614290 100644
--- a/.vscode/settings.json.default
+++ b/.vscode/settings.json.default
@@ -1,5 +1,6 @@
{
"css.customData": [
- ".vscode/tailwind.json"
+ ".vscode/tailwind.json",
+ ".vscode/custom-css.json"
]
}
diff --git a/.vscode/tailwind.json b/.vscode/tailwind.json
index 96a1f5797..659c28859 100644
--- a/.vscode/tailwind.json
+++ b/.vscode/tailwind.json
@@ -2,52 +2,92 @@
"version": 1.1,
"atDirectives": [
{
- "name": "@tailwind",
- "description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
+ "name": "@import",
+ "description": "Use the `@import` directive to inline CSS files, including Tailwind itself, into your stylesheet.",
"references": [
{
"name": "Tailwind Documentation",
- "url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
+ "url": "https://tailwindcss.com/docs/functions-and-directives#import-directive"
+ }
+ ]
+ },
+ {
+ "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-directive"
+ }
+ ]
+ },
+ {
+ "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/theme#layers"
}
]
},
{
"name": "@apply",
- "description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
+ "description": "DO NOT USE. IF YOU ARE CAUGHT USING @apply YOU WILL FACE SEVERE CONSEQUENCES.",
"references": [
{
"name": "Tailwind Documentation",
- "url": "https://tailwindcss.com/docs/functions-and-directives#apply"
+ "url": "https://tailwindcss.com/docs/functions-and-directives#apply-directive"
}
]
},
{
- "name": "@responsive",
- "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",
+ "name": "@config",
+ "description": "Use the `@config` directive to load a legacy JavaScript-based Tailwind configuration file.",
"references": [
{
"name": "Tailwind Documentation",
- "url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
+ "url": "https://tailwindcss.com/docs/functions-and-directives#config-directive"
}
]
},
{
- "name": "@screen",
- "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",
+ "name": "@reference",
+ "description": "Use the `@reference` directive to import theme variables, custom utilities, and custom variants from other files without duplicating CSS.",
"references": [
{
"name": "Tailwind Documentation",
- "url": "https://tailwindcss.com/docs/functions-and-directives#screen"
+ "url": "https://tailwindcss.com/docs/functions-and-directives#reference-directive"
}
]
},
{
- "name": "@variants",
- "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",
+ "name": "@plugin",
+ "description": "Use the `@plugin` directive to load a legacy JavaScript-based Tailwind plugin.",
"references": [
{
"name": "Tailwind Documentation",
- "url": "https://tailwindcss.com/docs/functions-and-directives#variants"
+ "url": "https://tailwindcss.com/docs/functions-and-directives#plugin-directive"
+ }
+ ]
+ },
+ {
+ "name": "@custom-variant",
+ "description": "Use the `@custom-variant` directive to add a custom variant to your project. Custom variants can be used with utilities like `hover`, `focus`, and responsive breakpoints. Use `@slot` inside the variant to indicate where the utility's styles should be inserted.",
+ "references": [
+ {
+ "name": "Tailwind Documentation",
+ "url": "https://tailwindcss.com/docs/adding-custom-styles#adding-custom-variants"
+ }
+ ]
+ },
+ {
+ "name": "@utility",
+ "description": "Use the `@utility` directive to add custom utilities to your project. Custom utilities work with all variants like `hover`, `focus`, and responsive variants. Use `--value()` to create functional utilities that accept arguments.",
+ "references": [
+ {
+ "name": "Tailwind Documentation",
+ "url": "https://tailwindcss.com/docs/adding-custom-styles#adding-custom-utilities"
}
]
}
diff --git a/AGENTS.md b/AGENTS.md
index 5cec6b810..0d060af1f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -5,15 +5,14 @@
- Routing/i18n/entry: `src/router.ts`, `src/i18n.ts`, `src/main.ts`.
- Tests: unit/component in `tests-ui/` and `src/components/**/*.{test,spec}.ts`; E2E in `browser_tests/`.
- Public assets: `public/`. Build output: `dist/`.
-- Config: `vite.config.mts`, `vitest.config.ts`, `playwright.config.ts`, `eslint.config.js`, `.prettierrc`.
+- Config: `vite.config.mts`, `vitest.config.ts`, `playwright.config.ts`, `eslint.config.ts`, `.prettierrc`.
## Build, Test, and Development Commands
- `pnpm dev`: Start Vite dev server.
- `pnpm dev:electron`: Dev server with Electron API mocks.
- `pnpm build`: Type-check then production build to `dist/`.
- `pnpm preview`: Preview the production build locally.
-- `pnpm test:unit`: Run Vitest unit tests (`tests-ui/`).
-- `pnpm test:component`: Run component tests (`src/components/`).
+- `pnpm test:unit`: Run Vitest unit tests.
- `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`).
- `pnpm lint` / `pnpm lint:fix`: Lint (ESLint). `pnpm format` / `format:check`: Prettier.
- `pnpm typecheck`: Vue TSC type checking.
@@ -31,10 +30,9 @@
- Playwright: place tests in `browser_tests/`; optional tags like `@mobile`, `@2x` are respected by config.
## Commit & Pull Request Guidelines
-- Commits: Prefer Conventional Commits (e.g., `feat(ui): add sidebar`), `refactor(litegraph): …`. Use `[skip ci]` for locale-only updates when appropriate.
-- PRs: Include clear description, linked issues (`Fixes #123`), and screenshots/GIFs for UI changes. Add/adjust tests and i18n strings when applicable.
+- Commits: Use `[skip ci]` for locale-only updates when appropriate.
+- PRs: Include clear description, linked issues (`- Fixes #123`), and screenshots/GIFs for UI changes.
- Quality gates: `pnpm lint`, `pnpm typecheck`, and relevant tests must pass. Keep PRs focused and small.
## Security & Configuration Tips
- Secrets: Use `.env` (see `.env_example`); do not commit secrets.
-- Backend: Dev server expects ComfyUI backend at `localhost:8188` by default; configure via `.env`.
diff --git a/CLAUDE.md b/CLAUDE.md
index 2ac2ab06e..0b187fbfc 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -18,7 +18,6 @@ This bootstraps the monorepo with dependencies, builds, tests, and dev server ve
- `pnpm build`: Build for production (via nx)
- `pnpm lint`: Linting (via nx)
- `pnpm format`: Prettier formatting
-- `pnpm test:component`: Run component tests with browser environment
- `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
@@ -127,3 +126,5 @@ const value = api.getServerFeature('config_name', defaultValue) // Get config
- NEVER use `--no-verify` flag when committing
- NEVER delete or disable tests to make them pass
- NEVER circumvent quality checks
+- NEVER use `dark:` or `dark-theme:` tailwind variants. Instead use a semantic value from the `style.css` theme, e.g. `bg-node-component-surface`
+- NEVER use `:class="[]"` to merge class names - always use `import { cn } from '@/utils/tailwindUtil'`, for example: ``
diff --git a/CODEOWNERS b/CODEOWNERS
index 8d4e4a90f..d754859b1 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1,17 +1,63 @@
-# Admins
-* @Comfy-Org/comfy_frontend_devs
+# Desktop/Electron
+/apps/desktop-ui/ @webfiltered
+/src/stores/electronDownloadStore.ts @webfiltered
+/src/extensions/core/electronAdapter.ts @webfiltered
+/vite.electron.config.mts @webfiltered
-# Maintainers
-*.md @Comfy-Org/comfy_maintainer
-/tests-ui/ @Comfy-Org/comfy_maintainer
-/browser_tests/ @Comfy-Org/comfy_maintainer
-/.env_example @Comfy-Org/comfy_maintainer
+# Common UI Components
+/src/components/chip/ @viva-jinyi
+/src/components/card/ @viva-jinyi
+/src/components/button/ @viva-jinyi
+/src/components/input/ @viva-jinyi
-# Translations (AIGODLIKE team + shinshin86)
-/src/locales/ @Yorha4D @KarryCharon @DorotaLuna @shinshin86 @Comfy-Org/comfy_maintainer
+# Topbar
+/src/components/topbar/ @pythongosssss
-# Load 3D extension
-/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs
+# Thumbnail
+/src/renderer/core/thumbnail/ @pythongosssss
-# Mask Editor extension
-/src/extensions/core/maskeditor.ts @brucew4yn3rp @trsommer @Comfy-Org/comfy_frontend_devs
+# Legacy UI
+/scripts/ui/ @pythongosssss
+
+# Link rendering
+/src/renderer/core/canvas/links/ @benceruleanlu
+
+# Node help system
+/src/utils/nodeHelpUtil.ts @benceruleanlu
+/src/stores/workspace/nodeHelpStore.ts @benceruleanlu
+/src/services/nodeHelpService.ts @benceruleanlu
+
+# Selection toolbox
+/src/components/graph/selectionToolbox/ @Myestery
+
+# Minimap
+/src/renderer/extensions/minimap/ @jtydhr88
+
+# Assets
+/src/platform/assets/ @arjansingh
+
+# Workflow Templates
+/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki
+/src/components/templates/ @Myestery @christian-byrne @comfyui-wiki
+
+# Mask Editor
+/src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp
+/src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp
+/src/extensions/core/maskEditorOld.ts @trsommer @brucew4yn3rp
+
+# 3D
+/src/extensions/core/load3d.ts @jtydhr88
+/src/components/load3d/ @jtydhr88
+
+# Manager
+/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata
+
+# Translations
+/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer
+
+# LLM Instructions (blank on purpose)
+.claude/
+.cursor/
+.cursorrules
+**/AGENTS.md
+**/CLAUDE.md
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6f4fd8db8..f1c9341d5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -213,12 +213,6 @@ Here's how Claude Code can use the Playwright MCP server to inspect the interfac
- `pnpm i` to install all dependencies
- `pnpm test:unit` to execute all unit tests
-### Component Tests
-
-Component tests verify Vue components in `src/components/`.
-
-- `pnpm test:component` to execute all component tests
-
### Playwright Tests
Playwright tests verify the whole app. See [browser_tests/README.md](browser_tests/README.md) for details.
@@ -229,7 +223,6 @@ Before submitting a PR, ensure all tests pass:
```bash
pnpm test:unit
-pnpm test:component
pnpm test:browser
pnpm typecheck
pnpm lint
@@ -262,12 +255,12 @@ pnpm format
The project supports three types of icons, all with automatic imports (no manual imports needed):
1. **PrimeIcons** - Built-in PrimeVue icons using CSS classes: ``
-2. **Iconify Icons** - 200,000+ icons from various libraries: ``, ``
+2. **Iconify Icons** - 200,000+ icons from various libraries: ``, ``
3. **Custom Icons** - Your own SVG icons: ``
-Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `src/assets/icons/custom/` and processed by `build/customIconCollection.ts` with automatic validation.
+Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `packages/design-system/src/icons/` and processed by `packages/design-system/src/iconCollection.ts` with automatic validation.
-For detailed instructions and code examples, see [src/assets/icons/README.md](src/assets/icons/README.md).
+For detailed instructions and code examples, see [packages/design-system/src/icons/README.md](packages/design-system/src/icons/README.md).
## Working with litegraph.js
diff --git a/apps/desktop-ui/.storybook/main.ts b/apps/desktop-ui/.storybook/main.ts
new file mode 100644
index 000000000..91c29eb7a
--- /dev/null
+++ b/apps/desktop-ui/.storybook/main.ts
@@ -0,0 +1,103 @@
+import type { StorybookConfig } from '@storybook/vue3-vite'
+import { FileSystemIconLoader } from 'unplugin-icons/loaders'
+import IconsResolver from 'unplugin-icons/resolver'
+import Icons from 'unplugin-icons/vite'
+import Components from 'unplugin-vue-components/vite'
+import type { InlineConfig } from 'vite'
+
+const config: StorybookConfig = {
+ stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
+ addons: ['@storybook/addon-docs'],
+ framework: {
+ name: '@storybook/vue3-vite',
+ options: {}
+ },
+ staticDirs: [{ from: '../public', to: '/' }],
+ async viteFinal(config) {
+ // Use dynamic import to avoid CJS deprecation warning
+ const { mergeConfig } = await import('vite')
+ const { default: tailwindcss } = await import('@tailwindcss/vite')
+
+ // Filter out any plugins that might generate import maps
+ if (config.plugins) {
+ config.plugins = config.plugins
+ // Type guard: ensure we have valid plugin objects with names
+ .filter(
+ (plugin): plugin is NonNullable & { name: string } => {
+ return (
+ plugin !== null &&
+ plugin !== undefined &&
+ typeof plugin === 'object' &&
+ 'name' in plugin &&
+ typeof plugin.name === 'string'
+ )
+ }
+ )
+ // Business logic: filter out import-map plugins
+ .filter((plugin) => !plugin.name.includes('import-map'))
+ }
+
+ return mergeConfig(config, {
+ // Replace plugins entirely to avoid inheritance issues
+ plugins: [
+ // Only include plugins we explicitly need for Storybook
+ tailwindcss(),
+ Icons({
+ compiler: 'vue3',
+ customCollections: {
+ comfy: FileSystemIconLoader(
+ process.cwd() + '/../../packages/design-system/src/icons'
+ )
+ }
+ }),
+ Components({
+ dts: false, // Disable dts generation in Storybook
+ resolvers: [
+ IconsResolver({
+ customCollections: ['comfy']
+ })
+ ],
+ dirs: [
+ process.cwd() + '/src/components',
+ process.cwd() + '/src/views'
+ ],
+ deep: true,
+ extensions: ['vue'],
+ directoryAsNamespace: true
+ })
+ ],
+ server: {
+ allowedHosts: true
+ },
+ resolve: {
+ alias: {
+ '@': process.cwd() + '/src',
+ '@frontend-locales': process.cwd() + '/../../src/locales'
+ }
+ },
+ build: {
+ rollupOptions: {
+ onwarn: (warning, warn) => {
+ // Suppress specific warnings
+ if (
+ warning.code === 'UNUSED_EXTERNAL_IMPORT' &&
+ warning.message?.includes('resolveComponent')
+ ) {
+ return
+ }
+ // Suppress Storybook font asset warnings
+ if (
+ warning.code === 'UNRESOLVED_IMPORT' &&
+ warning.message?.includes('nunito-sans')
+ ) {
+ return
+ }
+ warn(warning)
+ }
+ },
+ chunkSizeWarningLimit: 1000
+ }
+ } satisfies InlineConfig)
+ }
+}
+export default config
diff --git a/apps/desktop-ui/.storybook/preview.ts b/apps/desktop-ui/.storybook/preview.ts
new file mode 100644
index 000000000..a0ead30cc
--- /dev/null
+++ b/apps/desktop-ui/.storybook/preview.ts
@@ -0,0 +1,88 @@
+import { definePreset } from '@primevue/themes'
+import Aura from '@primevue/themes/aura'
+import { setup } from '@storybook/vue3'
+import type { Preview, StoryContext, StoryFn } from '@storybook/vue3-vite'
+import { createPinia } from 'pinia'
+import 'primeicons/primeicons.css'
+import PrimeVue from 'primevue/config'
+import ConfirmationService from 'primevue/confirmationservice'
+import ToastService from 'primevue/toastservice'
+import Tooltip from 'primevue/tooltip'
+
+import '@/assets/css/style.css'
+import { i18n } from '@/i18n'
+
+const ComfyUIPreset = definePreset(Aura, {
+ semantic: {
+ // @ts-expect-error prime type quirk
+ primary: Aura['primitive'].blue
+ }
+})
+
+setup((app) => {
+ app.directive('tooltip', Tooltip)
+
+ const pinia = createPinia()
+
+ app.use(pinia)
+ app.use(i18n)
+ app.use(PrimeVue, {
+ theme: {
+ preset: ComfyUIPreset,
+ options: {
+ prefix: 'p',
+ cssLayer: { name: 'primevue', order: 'primevue, tailwind-utilities' },
+ darkModeSelector: '.dark-theme, :root:has(.dark-theme)'
+ }
+ }
+ })
+ app.use(ConfirmationService)
+ app.use(ToastService)
+})
+
+export const withTheme = (Story: StoryFn, context: StoryContext) => {
+ const theme = context.globals.theme || 'light'
+ if (theme === 'dark') {
+ document.documentElement.classList.add('dark-theme')
+ document.body.classList.add('dark-theme')
+ } else {
+ document.documentElement.classList.remove('dark-theme')
+ document.body.classList.remove('dark-theme')
+ }
+
+ return Story(context.args, context)
+}
+
+const preview: Preview = {
+ parameters: {
+ controls: {
+ matchers: { color: /(background|color)$/i, date: /Date$/i }
+ },
+ backgrounds: {
+ default: 'light',
+ values: [
+ { name: 'light', value: '#ffffff' },
+ { name: 'dark', value: '#0a0a0a' }
+ ]
+ }
+ },
+ globalTypes: {
+ theme: {
+ name: 'Theme',
+ description: 'Global theme for components',
+ defaultValue: 'light',
+ toolbar: {
+ icon: 'circlehollow',
+ items: [
+ { value: 'light', icon: 'sun', title: 'Light' },
+ { value: 'dark', icon: 'moon', title: 'Dark' }
+ ],
+ showName: true,
+ dynamicTitle: true
+ }
+ }
+ },
+ decorators: [withTheme]
+}
+
+export default preview
diff --git a/apps/desktop-ui/index.html b/apps/desktop-ui/index.html
new file mode 100644
index 000000000..5cb34ffc2
--- /dev/null
+++ b/apps/desktop-ui/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+ ComfyUI Desktop
+
+
+
+
+
+
+
diff --git a/apps/desktop-ui/package.json b/apps/desktop-ui/package.json
new file mode 100644
index 000000000..686d35233
--- /dev/null
+++ b/apps/desktop-ui/package.json
@@ -0,0 +1,117 @@
+{
+ "name": "@comfyorg/desktop-ui",
+ "version": "0.0.1",
+ "type": "module",
+ "nx": {
+ "tags": [
+ "scope:desktop",
+ "type:app"
+ ],
+ "targets": {
+ "dev": {
+ "executor": "nx:run-commands",
+ "continuous": true,
+ "options": {
+ "cwd": "apps/desktop-ui",
+ "command": "vite --config vite.config.mts"
+ }
+ },
+ "serve": {
+ "executor": "nx:run-commands",
+ "continuous": true,
+ "options": {
+ "cwd": "apps/desktop-ui",
+ "command": "vite --config vite.config.mts"
+ }
+ },
+ "build": {
+ "executor": "nx:run-commands",
+ "cache": true,
+ "dependsOn": [
+ "^build"
+ ],
+ "options": {
+ "cwd": "apps/desktop-ui",
+ "command": "vite build --config vite.config.mts"
+ },
+ "outputs": [
+ "{projectRoot}/dist"
+ ]
+ },
+ "preview": {
+ "executor": "nx:run-commands",
+ "continuous": true,
+ "dependsOn": [
+ "build"
+ ],
+ "options": {
+ "cwd": "apps/desktop-ui",
+ "command": "vite preview --config vite.config.mts"
+ }
+ },
+ "storybook": {
+ "executor": "nx:run-commands",
+ "continuous": true,
+ "options": {
+ "cwd": "apps/desktop-ui",
+ "command": "storybook dev -p 6007"
+ }
+ },
+ "build-storybook": {
+ "executor": "nx:run-commands",
+ "cache": true,
+ "options": {
+ "cwd": "apps/desktop-ui",
+ "command": "storybook build -o dist/storybook"
+ },
+ "outputs": [
+ "{projectRoot}/dist/storybook"
+ ]
+ },
+ "lint": {
+ "executor": "nx:run-commands",
+ "cache": true,
+ "options": {
+ "cwd": "apps/desktop-ui",
+ "command": "eslint src --cache"
+ }
+ },
+ "typecheck": {
+ "executor": "nx:run-commands",
+ "cache": true,
+ "options": {
+ "cwd": "apps/desktop-ui",
+ "command": "vue-tsc --noEmit -p tsconfig.json"
+ }
+ }
+ }
+ },
+ "scripts": {
+ "storybook": "storybook dev -p 6007",
+ "build-storybook": "storybook build -o dist/storybook"
+ },
+ "dependencies": {
+ "@comfyorg/comfyui-electron-types": "0.4.73-0",
+ "@comfyorg/shared-frontend-utils": "workspace:*",
+ "@primevue/core": "catalog:",
+ "@primevue/themes": "catalog:",
+ "@vueuse/core": "catalog:",
+ "pinia": "catalog:",
+ "primeicons": "catalog:",
+ "primevue": "catalog:",
+ "vue": "catalog:",
+ "vue-i18n": "catalog:",
+ "vue-router": "catalog:"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "catalog:",
+ "@vitejs/plugin-vue": "catalog:",
+ "dotenv": "catalog:",
+ "unplugin-icons": "catalog:",
+ "unplugin-vue-components": "catalog:",
+ "vite": "catalog:",
+ "vite-plugin-html": "catalog:",
+ "vite-plugin-vue-devtools": "catalog:",
+ "vue-tsc": "catalog:"
+ }
+}
diff --git a/public/assets/images/Git-Logo-White.svg b/apps/desktop-ui/public/assets/images/Git-Logo-White.svg
similarity index 100%
rename from public/assets/images/Git-Logo-White.svg
rename to apps/desktop-ui/public/assets/images/Git-Logo-White.svg
diff --git a/public/assets/images/apple-mps-logo.png b/apps/desktop-ui/public/assets/images/apple-mps-logo.png
similarity index 100%
rename from public/assets/images/apple-mps-logo.png
rename to apps/desktop-ui/public/assets/images/apple-mps-logo.png
diff --git a/apps/desktop-ui/public/assets/images/comfy-brand-mark.svg b/apps/desktop-ui/public/assets/images/comfy-brand-mark.svg
new file mode 100644
index 000000000..9056ae149
--- /dev/null
+++ b/apps/desktop-ui/public/assets/images/comfy-brand-mark.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/apps/desktop-ui/public/assets/images/nvidia-logo-square.jpg b/apps/desktop-ui/public/assets/images/nvidia-logo-square.jpg
new file mode 100644
index 000000000..c57f20b5d
Binary files /dev/null and b/apps/desktop-ui/public/assets/images/nvidia-logo-square.jpg differ
diff --git a/public/assets/images/sad_girl.png b/apps/desktop-ui/public/assets/images/sad_girl.png
similarity index 100%
rename from public/assets/images/sad_girl.png
rename to apps/desktop-ui/public/assets/images/sad_girl.png
diff --git a/apps/desktop-ui/src/App.vue b/apps/desktop-ui/src/App.vue
new file mode 100644
index 000000000..a43d49c99
--- /dev/null
+++ b/apps/desktop-ui/src/App.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/apps/desktop-ui/src/assets/css/style.css b/apps/desktop-ui/src/assets/css/style.css
new file mode 100644
index 000000000..0eef377ae
--- /dev/null
+++ b/apps/desktop-ui/src/assets/css/style.css
@@ -0,0 +1,6 @@
+@import '@comfyorg/design-system/css/style.css';
+
+#desktop-app {
+ position: absolute;
+ inset: 0;
+}
diff --git a/apps/desktop-ui/src/components/bottomPanel/tabs/terminal/BaseTerminal.vue b/apps/desktop-ui/src/components/bottomPanel/tabs/terminal/BaseTerminal.vue
new file mode 100644
index 000000000..ca0dcf34e
--- /dev/null
+++ b/apps/desktop-ui/src/components/bottomPanel/tabs/terminal/BaseTerminal.vue
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
diff --git a/src/components/common/RefreshButton.vue b/apps/desktop-ui/src/components/common/RefreshButton.vue
similarity index 94%
rename from src/components/common/RefreshButton.vue
rename to apps/desktop-ui/src/components/common/RefreshButton.vue
index b564ac278..2569d0fbd 100644
--- a/src/components/common/RefreshButton.vue
+++ b/apps/desktop-ui/src/components/common/RefreshButton.vue
@@ -32,7 +32,7 @@
import Button from 'primevue/button'
import ProgressSpinner from 'primevue/progressspinner'
-import { PrimeVueSeverity } from '@/types/primeVueTypes'
+import type { PrimeVueSeverity } from '@/types/primeVueTypes'
const {
disabled,
diff --git a/apps/desktop-ui/src/components/common/StartupDisplay.vue b/apps/desktop-ui/src/components/common/StartupDisplay.vue
new file mode 100644
index 000000000..bd42c14e4
--- /dev/null
+++ b/apps/desktop-ui/src/components/common/StartupDisplay.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
+

+
+
+
+
+
+ {{ title }}
+
+
+ {{ statusText }}
+
+
+
+
+
+
+
diff --git a/apps/desktop-ui/src/components/common/UrlInput.vue b/apps/desktop-ui/src/components/common/UrlInput.vue
new file mode 100644
index 000000000..91f8cfe56
--- /dev/null
+++ b/apps/desktop-ui/src/components/common/UrlInput.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
diff --git a/src/components/install/DesktopSettingsConfiguration.vue b/apps/desktop-ui/src/components/install/DesktopSettingsConfiguration.vue
similarity index 87%
rename from src/components/install/DesktopSettingsConfiguration.vue
rename to apps/desktop-ui/src/components/install/DesktopSettingsConfiguration.vue
index 2868d11a7..f1caa62fa 100644
--- a/src/components/install/DesktopSettingsConfiguration.vue
+++ b/apps/desktop-ui/src/components/install/DesktopSettingsConfiguration.vue
@@ -10,14 +10,14 @@
-
+
{{ $t('install.settings.autoUpdate') }}
-
+
{{ $t('install.settings.autoUpdateDescription') }}
@@ -32,14 +32,10 @@
{{ $t('install.settings.allowMetrics') }}
-
+
{{ $t('install.settings.allowMetricsDescription') }}
-
+
{{ $t('install.settings.learnMoreAboutData') }}
@@ -51,7 +47,9 @@