Merge branch 'main' into sno-storybook--settings-panel
@@ -1,30 +1,85 @@
|
|||||||
# Create Hotfix Release
|
# Create Hotfix Release
|
||||||
|
|
||||||
This command guides you through creating a patch/hotfix release for ComfyUI Frontend with comprehensive safety checks and human confirmations at each step.
|
This command creates patch/hotfix releases for ComfyUI Frontend by backporting fixes to stable core branches. It handles both automated backports (preferred) and manual cherry-picking (fallback).
|
||||||
|
|
||||||
|
**Process Overview:**
|
||||||
|
1. **Check automated backports first** (via labels)
|
||||||
|
2. **Skip to version bump** if backports already merged
|
||||||
|
3. **Manual cherry-picking** if automation failed
|
||||||
|
4. **Create patch release** with version bump
|
||||||
|
5. **Publish GitHub release** (manually uncheck "latest")
|
||||||
|
6. **Update ComfyUI requirements.txt** via PR
|
||||||
|
|
||||||
<task>
|
<task>
|
||||||
Create a hotfix release by cherry-picking commits or PR commits from main to a core branch: $ARGUMENTS
|
Create a hotfix release by backporting commits/PRs from main to a core branch: $ARGUMENTS
|
||||||
|
|
||||||
Expected format: Comma-separated list of commits or PR numbers
|
Expected format: Comma-separated list of commits or PR numbers
|
||||||
Examples:
|
Examples:
|
||||||
- `abc123,def456,ghi789` (commits)
|
- `#1234,#5678` (PRs - preferred)
|
||||||
- `#1234,#5678` (PRs)
|
- `abc123,def456` (commit hashes)
|
||||||
- `abc123,#1234,def456` (mixed)
|
- `#1234,abc123` (mixed)
|
||||||
|
|
||||||
If no arguments provided, the command will help identify the correct core branch and guide you through selecting commits/PRs.
|
If no arguments provided, the command will guide you through identifying commits/PRs to backport.
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Before starting, ensure:
|
- Push access to repository
|
||||||
- You have push access to the repository
|
- GitHub CLI (`gh`) authenticated
|
||||||
- GitHub CLI (`gh`) is authenticated
|
- Clean working tree
|
||||||
- You're on a clean working tree
|
- Understanding of what fixes need backporting
|
||||||
- You understand the commits/PRs you're cherry-picking
|
|
||||||
|
|
||||||
## Hotfix Release Process
|
## Hotfix Release Process
|
||||||
|
|
||||||
### Step 1: Identify Target Core Branch
|
### Step 1: Try Automated Backports First
|
||||||
|
|
||||||
|
**Check if automated backports were attempted:**
|
||||||
|
|
||||||
|
1. **For each PR, check existing backport labels:**
|
||||||
|
```bash
|
||||||
|
gh pr view #1234 --json labels | jq -r '.labels[].name'
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **If no backport labels exist, add them now:**
|
||||||
|
```bash
|
||||||
|
# Add backport labels (this triggers automated backports)
|
||||||
|
gh pr edit #1234 --add-label "needs-backport"
|
||||||
|
gh pr edit #1234 --add-label "1.24" # Replace with target version
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Check for existing backport PRs:**
|
||||||
|
```bash
|
||||||
|
# Check for backport PRs created by automation
|
||||||
|
PR_NUMBER=${ARGUMENTS%%,*} # Extract first PR number from arguments
|
||||||
|
PR_NUMBER=${PR_NUMBER#\#} # Remove # prefix
|
||||||
|
gh pr list --search "backport-${PR_NUMBER}-to" --json number,title,state,baseRefName
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Handle existing backport scenarios:**
|
||||||
|
|
||||||
|
**Scenario A: Automated backports already merged**
|
||||||
|
```bash
|
||||||
|
# Check if backport PRs were merged to core branches
|
||||||
|
gh pr list --search "backport-${PR_NUMBER}-to" --state merged
|
||||||
|
```
|
||||||
|
- If backport PRs are merged → Skip to Step 10 (Version Bump)
|
||||||
|
- **CONFIRMATION**: Automated backports completed, proceeding to version bump?
|
||||||
|
|
||||||
|
**Scenario B: Automated backport PRs exist but not merged**
|
||||||
|
```bash
|
||||||
|
# Show open backport PRs that need merging
|
||||||
|
gh pr list --search "backport-${PR_NUMBER}-to" --state open
|
||||||
|
```
|
||||||
|
- **ACTION REQUIRED**: Merge the existing backport PRs first
|
||||||
|
- Use: `gh pr merge [PR_NUMBER] --merge` for each backport PR
|
||||||
|
- After merging, return to this command and skip to Step 10 (Version Bump)
|
||||||
|
- **CONFIRMATION**: Have you merged all backport PRs? Ready to proceed to version bump?
|
||||||
|
|
||||||
|
**Scenario C: No automated backports or they failed**
|
||||||
|
- Continue to Step 2 for manual cherry-picking
|
||||||
|
- **CONFIRMATION**: Proceeding with manual cherry-picking because automation failed?
|
||||||
|
|
||||||
|
### Step 2: Identify Target Core Branch
|
||||||
|
|
||||||
1. Fetch the current ComfyUI requirements.txt from master branch:
|
1. Fetch the current ComfyUI requirements.txt from master branch:
|
||||||
```bash
|
```bash
|
||||||
@@ -36,7 +91,7 @@ Before starting, ensure:
|
|||||||
5. Verify the core branch exists: `git ls-remote origin refs/heads/core/*`
|
5. Verify the core branch exists: `git ls-remote origin refs/heads/core/*`
|
||||||
6. **CONFIRMATION REQUIRED**: Is `core/X.Y` the correct target branch?
|
6. **CONFIRMATION REQUIRED**: Is `core/X.Y` the correct target branch?
|
||||||
|
|
||||||
### Step 2: Parse and Validate Arguments
|
### Step 3: Parse and Validate Arguments
|
||||||
|
|
||||||
1. Parse the comma-separated list of commits/PRs
|
1. Parse the comma-separated list of commits/PRs
|
||||||
2. For each item:
|
2. For each item:
|
||||||
@@ -49,7 +104,7 @@ Before starting, ensure:
|
|||||||
- **CONFIRMATION REQUIRED**: Use merge commit or cherry-pick individual commits?
|
- **CONFIRMATION REQUIRED**: Use merge commit or cherry-pick individual commits?
|
||||||
4. Validate all commit hashes exist in the repository
|
4. Validate all commit hashes exist in the repository
|
||||||
|
|
||||||
### Step 3: Analyze Target Changes
|
### Step 4: Analyze Target Changes
|
||||||
|
|
||||||
1. For each commit/PR to cherry-pick:
|
1. For each commit/PR to cherry-pick:
|
||||||
- Display commit hash, author, date
|
- Display commit hash, author, date
|
||||||
@@ -60,7 +115,7 @@ Before starting, ensure:
|
|||||||
2. Identify potential conflicts by checking changed files
|
2. Identify potential conflicts by checking changed files
|
||||||
3. **CONFIRMATION REQUIRED**: Proceed with these commits?
|
3. **CONFIRMATION REQUIRED**: Proceed with these commits?
|
||||||
|
|
||||||
### Step 4: Create Hotfix Branch
|
### Step 5: Create Hotfix Branch
|
||||||
|
|
||||||
1. Checkout the core branch (e.g., `core/1.23`)
|
1. Checkout the core branch (e.g., `core/1.23`)
|
||||||
2. Pull latest changes: `git pull origin core/X.Y`
|
2. Pull latest changes: `git pull origin core/X.Y`
|
||||||
@@ -69,7 +124,7 @@ Before starting, ensure:
|
|||||||
- Example: `hotfix/1.23.4-20241120`
|
- Example: `hotfix/1.23.4-20241120`
|
||||||
5. **CONFIRMATION REQUIRED**: Created branch correctly?
|
5. **CONFIRMATION REQUIRED**: Created branch correctly?
|
||||||
|
|
||||||
### Step 5: Cherry-pick Changes
|
### Step 6: Cherry-pick Changes
|
||||||
|
|
||||||
For each commit:
|
For each commit:
|
||||||
1. Attempt cherry-pick: `git cherry-pick <commit>`
|
1. Attempt cherry-pick: `git cherry-pick <commit>`
|
||||||
@@ -83,7 +138,7 @@ For each commit:
|
|||||||
- Run validation: `pnpm typecheck && pnpm lint`
|
- Run validation: `pnpm typecheck && pnpm lint`
|
||||||
4. **CONFIRMATION REQUIRED**: Cherry-pick successful and valid?
|
4. **CONFIRMATION REQUIRED**: Cherry-pick successful and valid?
|
||||||
|
|
||||||
### Step 6: Create PR to Core Branch
|
### Step 7: Create PR to Core Branch
|
||||||
|
|
||||||
1. Push the hotfix branch: `git push origin hotfix/<version>-<timestamp>`
|
1. Push the hotfix branch: `git push origin hotfix/<version>-<timestamp>`
|
||||||
2. Create PR using gh CLI:
|
2. Create PR using gh CLI:
|
||||||
@@ -100,7 +155,7 @@ For each commit:
|
|||||||
- Impact assessment
|
- Impact assessment
|
||||||
5. **CONFIRMATION REQUIRED**: PR created correctly?
|
5. **CONFIRMATION REQUIRED**: PR created correctly?
|
||||||
|
|
||||||
### Step 7: Wait for Tests
|
### Step 8: Wait for Tests
|
||||||
|
|
||||||
1. Monitor PR checks: `gh pr checks`
|
1. Monitor PR checks: `gh pr checks`
|
||||||
2. Display test results as they complete
|
2. Display test results as they complete
|
||||||
@@ -111,7 +166,7 @@ For each commit:
|
|||||||
4. Wait for all required checks to pass
|
4. Wait for all required checks to pass
|
||||||
5. **CONFIRMATION REQUIRED**: All tests passing?
|
5. **CONFIRMATION REQUIRED**: All tests passing?
|
||||||
|
|
||||||
### Step 8: Merge Hotfix PR
|
### Step 9: Merge Hotfix PR
|
||||||
|
|
||||||
1. Verify all checks have passed
|
1. Verify all checks have passed
|
||||||
2. Check for required approvals
|
2. Check for required approvals
|
||||||
@@ -119,7 +174,7 @@ For each commit:
|
|||||||
4. Delete the hotfix branch
|
4. Delete the hotfix branch
|
||||||
5. **CONFIRMATION REQUIRED**: PR merged successfully?
|
5. **CONFIRMATION REQUIRED**: PR merged successfully?
|
||||||
|
|
||||||
### Step 9: Create Version Bump
|
### Step 10: Create Version Bump
|
||||||
|
|
||||||
1. Checkout the core branch: `git checkout core/X.Y`
|
1. Checkout the core branch: `git checkout core/X.Y`
|
||||||
2. Pull latest changes: `git pull origin core/X.Y`
|
2. Pull latest changes: `git pull origin core/X.Y`
|
||||||
@@ -131,7 +186,7 @@ For each commit:
|
|||||||
7. Commit: `git commit -m "[release] Bump version to 1.23.5"`
|
7. Commit: `git commit -m "[release] Bump version to 1.23.5"`
|
||||||
8. **CONFIRMATION REQUIRED**: Version bump correct?
|
8. **CONFIRMATION REQUIRED**: Version bump correct?
|
||||||
|
|
||||||
### Step 10: Create Release PR
|
### Step 11: Create Release PR
|
||||||
|
|
||||||
1. Push release branch: `git push origin release/1.23.5`
|
1. Push release branch: `git push origin release/1.23.5`
|
||||||
2. Create PR with Release label:
|
2. Create PR with Release label:
|
||||||
@@ -184,7 +239,7 @@ For each commit:
|
|||||||
```
|
```
|
||||||
5. **CONFIRMATION REQUIRED**: Release PR has "Release" label?
|
5. **CONFIRMATION REQUIRED**: Release PR has "Release" label?
|
||||||
|
|
||||||
### Step 11: Monitor Release Process
|
### Step 12: Monitor Release Process
|
||||||
|
|
||||||
1. Wait for PR checks to pass
|
1. Wait for PR checks to pass
|
||||||
2. **FINAL CONFIRMATION**: Ready to trigger release by merging?
|
2. **FINAL CONFIRMATION**: Ready to trigger release by merging?
|
||||||
@@ -199,7 +254,102 @@ For each commit:
|
|||||||
- PyPI upload
|
- PyPI upload
|
||||||
- pnpm types publication
|
- pnpm types publication
|
||||||
|
|
||||||
### Step 12: Post-Release Verification
|
### Step 13: Manually Publish Draft Release
|
||||||
|
|
||||||
|
**CRITICAL**: The release workflow creates a DRAFT release. You must manually publish it:
|
||||||
|
|
||||||
|
1. **Go to GitHub Releases:** https://github.com/Comfy-Org/ComfyUI_frontend/releases
|
||||||
|
2. **Find the DRAFT release** (e.g., "v1.23.5 Draft")
|
||||||
|
3. **Click "Edit release"**
|
||||||
|
4. **UNCHECK "Set as the latest release"** ⚠️ **CRITICAL**
|
||||||
|
- This prevents the hotfix from showing as "latest"
|
||||||
|
- Main branch should always be "latest release"
|
||||||
|
5. **Click "Publish release"**
|
||||||
|
6. **CONFIRMATION REQUIRED**: Draft release published with "latest" unchecked?
|
||||||
|
|
||||||
|
### Step 14: Create ComfyUI Requirements.txt Update PR
|
||||||
|
|
||||||
|
**IMPORTANT**: Create PR to update ComfyUI's requirements.txt via fork:
|
||||||
|
|
||||||
|
1. **Setup fork (if needed):**
|
||||||
|
```bash
|
||||||
|
# Check if fork already exists
|
||||||
|
if gh repo view ComfyUI --json owner | jq -r '.owner.login' | grep -q "$(gh api user --jq .login)"; then
|
||||||
|
echo "Fork already exists"
|
||||||
|
else
|
||||||
|
# Fork the ComfyUI repository
|
||||||
|
gh repo fork comfyanonymous/ComfyUI --clone=false
|
||||||
|
echo "Created fork of ComfyUI"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Clone fork and create branch:**
|
||||||
|
```bash
|
||||||
|
# Clone your fork (or use existing clone)
|
||||||
|
GITHUB_USER=$(gh api user --jq .login)
|
||||||
|
if [ ! -d "ComfyUI-fork" ]; then
|
||||||
|
gh repo clone ${GITHUB_USER}/ComfyUI ComfyUI-fork
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd ComfyUI-fork
|
||||||
|
git checkout master
|
||||||
|
git pull origin master
|
||||||
|
|
||||||
|
# Create update branch
|
||||||
|
BRANCH_NAME="update-frontend-${NEW_VERSION}"
|
||||||
|
git checkout -b ${BRANCH_NAME}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Update requirements.txt:**
|
||||||
|
```bash
|
||||||
|
# Update the version in requirements.txt
|
||||||
|
sed -i "s/comfyui-frontend-package==[0-9].*$/comfyui-frontend-package==${NEW_VERSION}/" requirements.txt
|
||||||
|
|
||||||
|
# Verify the change
|
||||||
|
grep "comfyui-frontend-package" requirements.txt
|
||||||
|
|
||||||
|
# Commit the change
|
||||||
|
git add requirements.txt
|
||||||
|
git commit -m "Bump frontend to ${NEW_VERSION}"
|
||||||
|
git push origin ${BRANCH_NAME}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Create PR from fork:**
|
||||||
|
```bash
|
||||||
|
# Create PR using gh CLI from fork
|
||||||
|
gh pr create \
|
||||||
|
--repo comfyanonymous/ComfyUI \
|
||||||
|
--title "Bump frontend to ${NEW_VERSION}" \
|
||||||
|
--body "$(cat <<EOF
|
||||||
|
Bump frontend to ${NEW_VERSION}
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
python main.py --front-end-version Comfy-Org/ComfyUI_frontend@${NEW_VERSION}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
- Diff: [Comfy-Org/ComfyUI_frontend: v${OLD_VERSION}...v${NEW_VERSION}](https://github.com/Comfy-Org/ComfyUI_frontend/compare/v${OLD_VERSION}...v${NEW_VERSION})
|
||||||
|
- PyPI Package: https://pypi.org/project/comfyui-frontend-package/${NEW_VERSION}/
|
||||||
|
- npm Types: https://www.npmjs.com/package/@comfyorg/comfyui-frontend-types/v/${NEW_VERSION}
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Fix: [Brief description of hotfixes included]
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Clean up:**
|
||||||
|
```bash
|
||||||
|
# Return to original directory
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Keep fork directory for future updates
|
||||||
|
echo "Fork directory 'ComfyUI-fork' kept for future use"
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **CONFIRMATION REQUIRED**: ComfyUI requirements.txt PR created from fork?
|
||||||
|
|
||||||
|
### Step 15: Post-Release Verification
|
||||||
|
|
||||||
1. Verify GitHub release:
|
1. Verify GitHub release:
|
||||||
```bash
|
```bash
|
||||||
@@ -213,12 +363,14 @@ For each commit:
|
|||||||
```bash
|
```bash
|
||||||
pnpm view @comfyorg/comfyui-frontend-types@1.23.5
|
pnpm view @comfyorg/comfyui-frontend-types@1.23.5
|
||||||
```
|
```
|
||||||
4. Generate release summary with:
|
4. Monitor ComfyUI requirements.txt PR for approval/merge
|
||||||
|
5. Generate release summary with:
|
||||||
- Version released
|
- Version released
|
||||||
- Commits included
|
- Commits included
|
||||||
- Issues fixed
|
- Issues fixed
|
||||||
- Distribution status
|
- Distribution status
|
||||||
5. **CONFIRMATION REQUIRED**: Release completed successfully?
|
- ComfyUI integration status
|
||||||
|
6. **CONFIRMATION REQUIRED**: Hotfix release fully completed?
|
||||||
|
|
||||||
## Safety Checks
|
## Safety Checks
|
||||||
|
|
||||||
@@ -240,19 +392,28 @@ If something goes wrong:
|
|||||||
|
|
||||||
## Important Notes
|
## Important Notes
|
||||||
|
|
||||||
|
- **Always try automated backports first** - This command is for when automation fails
|
||||||
- Core branch version will be behind main - this is expected
|
- Core branch version will be behind main - this is expected
|
||||||
- The "Release" label triggers the PyPI/npm publication
|
- The "Release" label triggers the PyPI/npm publication
|
||||||
|
- **CRITICAL**: Always uncheck "Set as latest release" for hotfix releases
|
||||||
|
- **Must create ComfyUI requirements.txt PR** - Hotfix isn't complete without it
|
||||||
- PR numbers must include the `#` prefix
|
- PR numbers must include the `#` prefix
|
||||||
- Mixed commits/PRs are supported but review carefully
|
- Mixed commits/PRs are supported but review carefully
|
||||||
- Always wait for full test suite before proceeding
|
- Always wait for full test suite before proceeding
|
||||||
|
|
||||||
## Expected Timeline
|
## Modern Workflow Context
|
||||||
|
|
||||||
- Step 1-3: ~10 minutes (analysis)
|
**Primary Backport Method:** Automated via `needs-backport` + `X.YY` labels
|
||||||
- Steps 4-6: ~15-30 minutes (cherry-picking)
|
**This Command Usage:**
|
||||||
- Step 7: ~10-20 minutes (tests)
|
- Smart path detection - skip to version bump if backports already merged
|
||||||
- Steps 8-10: ~10 minutes (version bump)
|
- Fallback to manual cherry-picking only when automation fails/has conflicts
|
||||||
- Step 11-12: ~15-20 minutes (release)
|
**Complete Hotfix:** Includes GitHub release publishing + ComfyUI requirements.txt integration
|
||||||
- Total: ~60-90 minutes
|
|
||||||
|
|
||||||
This process ensures a safe, verified hotfix release with multiple confirmation points and clear tracking of what changes are being released.
|
## Workflow Paths
|
||||||
|
|
||||||
|
- **Path A:** Backports already merged → Skip to Step 10 (Version Bump)
|
||||||
|
- **Path B:** Backport PRs need merging → Merge them → Skip to Step 10 (Version Bump)
|
||||||
|
- **Path C:** No/failed backports → Manual cherry-picking (Steps 2-9) → Version Bump (Step 10)
|
||||||
|
|
||||||
|
|
||||||
|
This process ensures a complete hotfix release with proper GitHub publishing, ComfyUI integration, and multiple safety checkpoints.
|
||||||
@@ -4,3 +4,6 @@
|
|||||||
|
|
||||||
# npm run format on litegraph merge (10,672 insertions, 7,327 deletions across 129 files)
|
# npm run format on litegraph merge (10,672 insertions, 7,327 deletions across 129 files)
|
||||||
c53f197de2a3e0fa66b16dedc65c131235c1c4b6
|
c53f197de2a3e0fa66b16dedc65c131235c1c4b6
|
||||||
|
|
||||||
|
# Reorganize renderer components into domain-driven folder structure
|
||||||
|
c8a83a9caede7bdb5f8598c5492b07d08c339d49
|
||||||
|
|||||||
1
.gitattributes
vendored
@@ -9,6 +9,7 @@
|
|||||||
*.mts text eol=lf
|
*.mts text eol=lf
|
||||||
*.ts text eol=lf
|
*.ts text eol=lf
|
||||||
*.vue text eol=lf
|
*.vue text eol=lf
|
||||||
|
*.yaml text eol=lf
|
||||||
|
|
||||||
# Generated files
|
# Generated files
|
||||||
src/types/comfyRegistryTypes.ts linguist-generated=true
|
src/types/comfyRegistryTypes.ts linguist-generated=true
|
||||||
|
|||||||
27
.github/workflows/backport.yaml
vendored
@@ -2,7 +2,7 @@ name: Auto Backport
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [closed]
|
types: [closed, labeled]
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -25,7 +25,27 @@ jobs:
|
|||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Check if backports already exist
|
||||||
|
id: check-existing
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
run: |
|
||||||
|
# Check for existing backport PRs for this PR number
|
||||||
|
EXISTING_BACKPORTS=$(gh pr list --state all --search "backport-${PR_NUMBER}-to" --json title,headRefName,baseRefName | jq -r '.[].headRefName')
|
||||||
|
|
||||||
|
if [ -z "$EXISTING_BACKPORTS" ]; then
|
||||||
|
echo "skip=false" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Found existing backport PRs:"
|
||||||
|
echo "$EXISTING_BACKPORTS"
|
||||||
|
echo "skip=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "::warning::Backport PRs already exist for PR #${PR_NUMBER}, skipping to avoid duplicates"
|
||||||
|
|
||||||
- name: Extract version labels
|
- name: Extract version labels
|
||||||
|
if: steps.check-existing.outputs.skip != 'true'
|
||||||
id: versions
|
id: versions
|
||||||
run: |
|
run: |
|
||||||
# Extract version labels (e.g., "1.24", "1.22")
|
# Extract version labels (e.g., "1.24", "1.22")
|
||||||
@@ -52,6 +72,7 @@ jobs:
|
|||||||
echo "Found version labels: ${VERSIONS}"
|
echo "Found version labels: ${VERSIONS}"
|
||||||
|
|
||||||
- name: Backport commits
|
- name: Backport commits
|
||||||
|
if: steps.check-existing.outputs.skip != 'true'
|
||||||
id: backport
|
id: backport
|
||||||
env:
|
env:
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
@@ -109,7 +130,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Create PR for each successful backport
|
- name: Create PR for each successful backport
|
||||||
if: steps.backport.outputs.success
|
if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
@@ -141,7 +162,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
- name: Comment on failures
|
- name: Comment on failures
|
||||||
if: failure() && steps.backport.outputs.failed
|
if: steps.check-existing.outputs.skip != 'true' && failure() && steps.backport.outputs.failed
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
60
.github/workflows/chromatic.yaml
vendored
@@ -12,9 +12,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Only run for PRs from version-bump-* branches or manual triggers
|
# Only run for PRs from version-bump-* branches or manual triggers
|
||||||
if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'version-bump-')
|
if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'version-bump-')
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
issues: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -32,29 +29,6 @@ jobs:
|
|||||||
node-version: '20'
|
node-version: '20'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Get current time
|
|
||||||
id: current-time
|
|
||||||
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Comment PR - Build Started
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
continue-on-error: true
|
|
||||||
uses: edumserrano/find-create-or-update-comment@v3
|
|
||||||
with:
|
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
|
||||||
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
|
|
||||||
comment-author: 'github-actions[bot]'
|
|
||||||
edit-mode: append
|
|
||||||
body: |
|
|
||||||
<!-- STORYBOOK_BUILD_STATUS -->
|
|
||||||
## 🎨 Storybook Build Status
|
|
||||||
|
|
||||||
🔄 **Building Storybook and running visual tests...**
|
|
||||||
|
|
||||||
⏳ Build started at: ${{ steps.current-time.outputs.time }} UTC
|
|
||||||
|
|
||||||
---
|
|
||||||
*This comment will be updated when the build completes*
|
|
||||||
|
|
||||||
- name: Cache tool outputs
|
- name: Cache tool outputs
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -81,37 +55,3 @@ jobs:
|
|||||||
autoAcceptChanges: 'main' # Auto-accept changes on main branch
|
autoAcceptChanges: 'main' # Auto-accept changes on main branch
|
||||||
exitOnceUploaded: true # Don't wait for UI tests to complete
|
exitOnceUploaded: true # Don't wait for UI tests to complete
|
||||||
|
|
||||||
- name: Get completion time
|
|
||||||
id: completion-time
|
|
||||||
if: always()
|
|
||||||
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Comment PR - Build Complete
|
|
||||||
if: github.event_name == 'pull_request' && always()
|
|
||||||
continue-on-error: true
|
|
||||||
uses: edumserrano/find-create-or-update-comment@v3
|
|
||||||
with:
|
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
|
||||||
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
|
|
||||||
comment-author: 'github-actions[bot]'
|
|
||||||
edit-mode: replace
|
|
||||||
body: |
|
|
||||||
<!-- STORYBOOK_BUILD_STATUS -->
|
|
||||||
## 🎨 Storybook Build Status
|
|
||||||
|
|
||||||
${{ steps.chromatic.outcome == 'success' && '✅' || '❌' }} **${{ steps.chromatic.outcome == 'success' && 'Build completed successfully!' || 'Build failed!' }}**
|
|
||||||
|
|
||||||
⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC
|
|
||||||
|
|
||||||
### 📊 Build Summary
|
|
||||||
- **Components**: ${{ steps.chromatic.outputs.componentCount || '0' }}
|
|
||||||
- **Stories**: ${{ steps.chromatic.outputs.testCount || '0' }}
|
|
||||||
- **Visual changes**: ${{ steps.chromatic.outputs.changeCount || '0' }}
|
|
||||||
- **Errors**: ${{ steps.chromatic.outputs.errorCount || '0' }}
|
|
||||||
|
|
||||||
### 🔗 Links
|
|
||||||
${{ steps.chromatic.outputs.buildUrl && format('- [📸 View Chromatic Build]({0})', steps.chromatic.outputs.buildUrl) || '' }}
|
|
||||||
${{ steps.chromatic.outputs.storybookUrl && format('- [📖 Preview Storybook]({0})', steps.chromatic.outputs.storybookUrl) || '' }}
|
|
||||||
|
|
||||||
---
|
|
||||||
${{ steps.chromatic.outcome == 'success' && '🎉 Your Storybook is ready for review!' || '⚠️ Please check the workflow logs for error details.' }}
|
|
||||||
|
|||||||
@@ -128,45 +128,6 @@ jobs:
|
|||||||
echo "- Critical security patches"
|
echo "- Critical security patches"
|
||||||
echo "- Documentation updates"
|
echo "- Documentation updates"
|
||||||
|
|
||||||
- name: Create branch protection rules
|
|
||||||
if: steps.check_version.outputs.is_minor_bump == 'true' && env.branch_exists != 'true'
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
BRANCH_NAME="${{ steps.check_version.outputs.branch_name }}"
|
|
||||||
|
|
||||||
# Create branch protection using GitHub API
|
|
||||||
echo "Setting up branch protection for $BRANCH_NAME..."
|
|
||||||
|
|
||||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT \
|
|
||||||
-H "Authorization: token $GITHUB_TOKEN" \
|
|
||||||
-H "Accept: application/vnd.github.v3+json" \
|
|
||||||
"https://api.github.com/repos/${{ github.repository }}/branches/$BRANCH_NAME/protection" \
|
|
||||||
-d '{
|
|
||||||
"required_status_checks": {
|
|
||||||
"strict": true,
|
|
||||||
"contexts": ["lint-and-format", "test", "playwright-tests"]
|
|
||||||
},
|
|
||||||
"enforce_admins": false,
|
|
||||||
"required_pull_request_reviews": {
|
|
||||||
"required_approving_review_count": 1,
|
|
||||||
"dismiss_stale_reviews": true
|
|
||||||
},
|
|
||||||
"restrictions": null,
|
|
||||||
"allow_force_pushes": false,
|
|
||||||
"allow_deletions": false
|
|
||||||
}')
|
|
||||||
|
|
||||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n 1)
|
|
||||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
|
||||||
|
|
||||||
if [[ "$HTTP_CODE" -eq 200 ]] || [[ "$HTTP_CODE" -eq 201 ]]; then
|
|
||||||
echo "✅ Branch protection successfully applied"
|
|
||||||
else
|
|
||||||
echo "⚠️ Failed to apply branch protection (HTTP $HTTP_CODE)"
|
|
||||||
echo "Response: $BODY"
|
|
||||||
# Don't fail the workflow, just warn
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Post summary
|
- name: Post summary
|
||||||
if: steps.check_version.outputs.is_minor_bump == 'true'
|
if: steps.check_version.outputs.is_minor_bump == 'true'
|
||||||
|
|||||||
9
.github/workflows/i18n.yaml
vendored
@@ -25,6 +25,13 @@ jobs:
|
|||||||
key: i18n-tools-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
|
key: i18n-tools-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
i18n-tools-cache-${{ runner.os }}-
|
i18n-tools-cache-${{ runner.os }}-
|
||||||
|
- name: Cache Playwright browsers
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cache/ms-playwright
|
||||||
|
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
playwright-browsers-${{ runner.os }}-
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: npx playwright install chromium --with-deps
|
run: npx playwright install chromium --with-deps
|
||||||
working-directory: ComfyUI_frontend
|
working-directory: ComfyUI_frontend
|
||||||
@@ -34,7 +41,7 @@ jobs:
|
|||||||
run: pnpm dev:electron &
|
run: pnpm dev:electron &
|
||||||
working-directory: ComfyUI_frontend
|
working-directory: ComfyUI_frontend
|
||||||
- name: Update en.json
|
- name: Update en.json
|
||||||
run: pnpm collect-i18n -- scripts/collect-i18n-general.ts
|
run: pnpm collect-i18n
|
||||||
env:
|
env:
|
||||||
PLAYWRIGHT_TEST_URL: http://localhost:5173
|
PLAYWRIGHT_TEST_URL: http://localhost:5173
|
||||||
working-directory: ComfyUI_frontend
|
working-directory: ComfyUI_frontend
|
||||||
|
|||||||
2
.github/workflows/lint-and-format.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
|
|||||||
280
.github/workflows/pr-playwright-deploy.yaml
vendored
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
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 "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
|
||||||
|
echo "## 🎭 Playwright Test Results" >> comment.md
|
||||||
|
echo "" >> comment.md
|
||||||
|
echo "<img alt='comfy-loading-gif' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px' style='vertical-align: middle; margin-right: 4px;' /> **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: '<!-- PLAYWRIGHT_TEST_STATUS -->'
|
||||||
|
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 "<!-- PLAYWRIGHT_TEST_STATUS -->" > 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: '<!-- PLAYWRIGHT_TEST_STATUS -->'
|
||||||
|
comment-author: 'github-actions[bot]'
|
||||||
|
edit-mode: replace
|
||||||
|
body-path: comment.md
|
||||||
126
.github/workflows/pr-storybook-comment.yaml
vendored
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
name: PR Storybook Comment
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ['Chromatic']
|
||||||
|
types: [requested, completed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment-storybook:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
github.repository == 'Comfy-Org/ComfyUI_frontend'
|
||||||
|
&& github.event.workflow_run.event == 'pull_request'
|
||||||
|
&& startsWith(github.event.workflow_run.head_branch, 'version-bump-')
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
actions: read
|
||||||
|
steps:
|
||||||
|
- name: Get PR number
|
||||||
|
id: pr
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { data: pullRequests } = await github.rest.pulls.list({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
state: 'open',
|
||||||
|
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pullRequests.length === 0) {
|
||||||
|
console.log('No open PR found for this branch');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pullRequests[0].number;
|
||||||
|
|
||||||
|
- name: Log when no PR found
|
||||||
|
if: steps.pr.outputs.result == 'null'
|
||||||
|
run: |
|
||||||
|
echo "⚠️ No open PR found for branch: ${{ github.event.workflow_run.head_branch }}"
|
||||||
|
echo "Workflow run ID: ${{ github.event.workflow_run.id }}"
|
||||||
|
echo "Repository: ${{ github.event.workflow_run.repository.full_name }}"
|
||||||
|
echo "Event: ${{ github.event.workflow_run.event }}"
|
||||||
|
|
||||||
|
- name: Get workflow run details
|
||||||
|
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
|
||||||
|
id: workflow-run
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const run = await github.rest.actions.getWorkflowRun({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: context.payload.workflow_run.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
conclusion: run.data.conclusion,
|
||||||
|
html_url: run.data.html_url
|
||||||
|
};
|
||||||
|
|
||||||
|
- name: Get completion time
|
||||||
|
id: completion-time
|
||||||
|
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Comment PR - Storybook Started
|
||||||
|
if: steps.pr.outputs.result != 'null' && github.event.action == 'requested'
|
||||||
|
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
|
||||||
|
with:
|
||||||
|
issue-number: ${{ steps.pr.outputs.result }}
|
||||||
|
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
|
||||||
|
comment-author: 'github-actions[bot]'
|
||||||
|
edit-mode: replace
|
||||||
|
body: |
|
||||||
|
<!-- STORYBOOK_BUILD_STATUS -->
|
||||||
|
## 🎨 Storybook Build Status
|
||||||
|
|
||||||
|
<img alt='comfy-loading-gif' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px' style='vertical-align: middle; margin-right: 4px;' /> **Build is starting...**
|
||||||
|
|
||||||
|
⏰ Started at: ${{ steps.completion-time.outputs.time }} UTC
|
||||||
|
|
||||||
|
### 🚀 Building Storybook
|
||||||
|
- 📦 Installing dependencies...
|
||||||
|
- 🔧 Building Storybook components...
|
||||||
|
- 🎨 Running Chromatic visual tests...
|
||||||
|
|
||||||
|
---
|
||||||
|
⏱️ Please wait while the Storybook build is in progress...
|
||||||
|
|
||||||
|
- name: Comment PR - Storybook Complete
|
||||||
|
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
|
||||||
|
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
|
||||||
|
with:
|
||||||
|
issue-number: ${{ steps.pr.outputs.result }}
|
||||||
|
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
|
||||||
|
comment-author: 'github-actions[bot]'
|
||||||
|
edit-mode: replace
|
||||||
|
body: |
|
||||||
|
<!-- STORYBOOK_BUILD_STATUS -->
|
||||||
|
## 🎨 Storybook Build Status
|
||||||
|
|
||||||
|
${{
|
||||||
|
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '✅'
|
||||||
|
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && '⏭️'
|
||||||
|
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && '🚫'
|
||||||
|
|| '❌'
|
||||||
|
}} **${{
|
||||||
|
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && 'Build completed successfully!'
|
||||||
|
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && 'Build skipped.'
|
||||||
|
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && 'Build cancelled.'
|
||||||
|
|| 'Build failed!'
|
||||||
|
}}**
|
||||||
|
|
||||||
|
⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC
|
||||||
|
|
||||||
|
### 🔗 Links
|
||||||
|
- [📊 View Workflow Run](${{ fromJSON(steps.workflow-run.outputs.result).html_url }})
|
||||||
|
|
||||||
|
---
|
||||||
|
${{
|
||||||
|
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '🎉 Your Storybook is ready for review!'
|
||||||
|
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && 'ℹ️ Chromatic was skipped for this PR.'
|
||||||
|
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && 'ℹ️ The Chromatic run was cancelled.'
|
||||||
|
|| '⚠️ Please check the workflow logs for error details.'
|
||||||
|
}}
|
||||||
7
.github/workflows/test-browser-exp.yaml
vendored
@@ -11,6 +11,13 @@ jobs:
|
|||||||
if: github.event.label.name == 'New Browser Test Expectations'
|
if: github.event.label.name == 'New Browser Test Expectations'
|
||||||
steps:
|
steps:
|
||||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
|
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
|
||||||
|
- name: Cache Playwright browsers
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cache/ms-playwright
|
||||||
|
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
playwright-browsers-${{ runner.os }}-
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: npx playwright install chromium --with-deps
|
run: npx playwright install chromium --with-deps
|
||||||
working-directory: ComfyUI_frontend
|
working-directory: ComfyUI_frontend
|
||||||
|
|||||||
391
.github/workflows/test-ui.yaml
vendored
@@ -7,15 +7,12 @@ on:
|
|||||||
branches-ignore:
|
branches-ignore:
|
||||||
[wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*]
|
[wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*]
|
||||||
|
|
||||||
env:
|
|
||||||
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup:
|
setup:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||||
sanitized-branch: ${{ steps.branch-info.outputs.sanitized }}
|
playwright-version: ${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout ComfyUI
|
- name: Checkout ComfyUI
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -48,30 +45,6 @@ jobs:
|
|||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml'
|
cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Get current time
|
|
||||||
id: current-time
|
|
||||||
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Comment PR - Tests Started
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
continue-on-error: true
|
|
||||||
uses: edumserrano/find-create-or-update-comment@v3
|
|
||||||
with:
|
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
|
||||||
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
|
|
||||||
comment-author: 'github-actions[bot]'
|
|
||||||
edit-mode: append
|
|
||||||
body: |
|
|
||||||
<!-- PLAYWRIGHT_TEST_STATUS -->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<img alt='comfy-loading-gif' src="https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
|
|
||||||
<bold>[${{ steps.current-time.outputs.time }} UTC] Preparing browser tests across multiple browsers...</bold>
|
|
||||||
|
|
||||||
---
|
|
||||||
*This comment will be updated when tests complete*
|
|
||||||
|
|
||||||
- name: Cache tool outputs
|
- name: Cache tool outputs
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -94,13 +67,12 @@ jobs:
|
|||||||
id: cache-key
|
id: cache-key
|
||||||
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
|
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Generate sanitized branch name
|
- name: Playwright Version
|
||||||
id: branch-info
|
id: playwright-version
|
||||||
run: |
|
run: |
|
||||||
# Get branch name and sanitize it for Cloudflare branch names
|
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --json | jq --raw-output '.[0].devDependencies["@playwright/test"].version')
|
||||||
BRANCH_NAME="${{ github.head_ref || github.ref_name }}"
|
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT
|
||||||
SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g')
|
working-directory: ComfyUI_frontend
|
||||||
echo "sanitized=${SANITIZED_BRANCH}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Save cache
|
- name: Save cache
|
||||||
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
|
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
|
||||||
@@ -110,17 +82,17 @@ jobs:
|
|||||||
ComfyUI_frontend
|
ComfyUI_frontend
|
||||||
key: comfyui-setup-${{ steps.cache-key.outputs.key }}
|
key: comfyui-setup-${{ steps.cache-key.outputs.key }}
|
||||||
|
|
||||||
playwright-tests:
|
# Sharded chromium tests
|
||||||
|
playwright-tests-chromium-sharded:
|
||||||
needs: setup
|
needs: setup
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
|
||||||
issues: write
|
|
||||||
contents: read
|
contents: read
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
browser: [chromium, chromium-2x, chromium-0.5x, mobile-chrome]
|
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||||
|
shardTotal: [8]
|
||||||
steps:
|
steps:
|
||||||
- name: Wait for cache propagation
|
- name: Wait for cache propagation
|
||||||
run: sleep 10
|
run: sleep 10
|
||||||
@@ -144,32 +116,85 @@ jobs:
|
|||||||
python-version: '3.10'
|
python-version: '3.10'
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
|
|
||||||
- name: Get current time
|
- name: Install requirements
|
||||||
id: current-time
|
|
||||||
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Set project name
|
|
||||||
id: project-name
|
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ matrix.browser }}" = "chromium-0.5x" ]; then
|
python -m pip install --upgrade pip
|
||||||
echo "name=comfyui-playwright-chromium-0-5x" >> $GITHUB_OUTPUT
|
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
||||||
else
|
pip install -r requirements.txt
|
||||||
echo "name=comfyui-playwright-${{ matrix.browser }}" >> $GITHUB_OUTPUT
|
pip install wait-for-it
|
||||||
fi
|
working-directory: ComfyUI
|
||||||
echo "branch=${{ needs.setup.outputs.sanitized-branch }}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Comment PR - Browser Test Started
|
|
||||||
if: github.event_name == 'pull_request'
|
- name: Cache Playwright Browsers
|
||||||
continue-on-error: true
|
uses: actions/cache@v4
|
||||||
uses: edumserrano/find-create-or-update-comment@v3
|
id: cache-playwright-browsers
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
path: '~/.cache/ms-playwright'
|
||||||
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
|
key: '${{ runner.os }}-playwright-browsers-${{ needs.setup.outputs.playwright-version }}'
|
||||||
comment-author: 'github-actions[bot]'
|
|
||||||
edit-mode: append
|
- name: Install Playwright Browsers
|
||||||
body: |
|
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
|
||||||
<img alt='comfy-loading-gif' src="https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
|
run: pnpm exec playwright install chromium --with-deps
|
||||||
<bold>${{ matrix.browser }}</bold>: Running tests...
|
working-directory: ComfyUI_frontend
|
||||||
|
|
||||||
|
- name: Install Playwright Browsers (operating system dependencies)
|
||||||
|
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
|
||||||
|
run: pnpm exec playwright install-deps
|
||||||
|
working-directory: ComfyUI_frontend
|
||||||
|
|
||||||
|
- name: Start ComfyUI server
|
||||||
|
run: |
|
||||||
|
python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist &
|
||||||
|
wait-for-it --service 127.0.0.1:8188 -t 600
|
||||||
|
working-directory: ComfyUI
|
||||||
|
|
||||||
|
- name: Run Playwright tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||||
|
id: playwright
|
||||||
|
run: npx playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob
|
||||||
|
working-directory: ComfyUI_frontend
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_BLOB_OUTPUT_DIR: ../blob-report
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
with:
|
||||||
|
name: blob-report-chromium-${{ matrix.shardIndex }}
|
||||||
|
path: blob-report/
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
playwright-tests:
|
||||||
|
# Ideally, each shard runs test in 6 minutes, but allow up to 15 minutes
|
||||||
|
timeout-minutes: 15
|
||||||
|
needs: setup
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
browser: [chromium-2x, chromium-0.5x, mobile-chrome]
|
||||||
|
steps:
|
||||||
|
- name: Wait for cache propagation
|
||||||
|
run: sleep 10
|
||||||
|
|
||||||
|
- name: Restore cached setup
|
||||||
|
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
|
||||||
|
with:
|
||||||
|
fail-on-cache-miss: true
|
||||||
|
path: |
|
||||||
|
ComfyUI
|
||||||
|
ComfyUI_frontend
|
||||||
|
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
cache: 'pip'
|
||||||
|
|
||||||
- name: Install requirements
|
- name: Install requirements
|
||||||
run: |
|
run: |
|
||||||
@@ -179,215 +204,83 @@ jobs:
|
|||||||
pip install wait-for-it
|
pip install wait-for-it
|
||||||
working-directory: ComfyUI
|
working-directory: ComfyUI
|
||||||
|
|
||||||
|
- name: Cache Playwright Browsers
|
||||||
|
uses: actions/cache@v4
|
||||||
|
id: cache-playwright-browsers
|
||||||
|
with:
|
||||||
|
path: '~/.cache/ms-playwright'
|
||||||
|
key: '${{ runner.os }}-playwright-browsers-${{ needs.setup.outputs.playwright-version }}'
|
||||||
|
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
|
||||||
|
run: pnpm exec playwright install chromium --with-deps
|
||||||
|
working-directory: ComfyUI_frontend
|
||||||
|
|
||||||
|
- name: Install Playwright Browsers (operating system dependencies)
|
||||||
|
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
|
||||||
|
run: pnpm exec playwright install-deps
|
||||||
|
working-directory: ComfyUI_frontend
|
||||||
|
|
||||||
- name: Start ComfyUI server
|
- name: Start ComfyUI server
|
||||||
run: |
|
run: |
|
||||||
python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist &
|
python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist &
|
||||||
wait-for-it --service 127.0.0.1:8188 -t 600
|
wait-for-it --service 127.0.0.1:8188 -t 600
|
||||||
working-directory: ComfyUI
|
working-directory: ComfyUI
|
||||||
|
|
||||||
- name: Cache Playwright browsers
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.cache/ms-playwright
|
|
||||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-${{ matrix.browser }}
|
|
||||||
restore-keys: |
|
|
||||||
playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-
|
|
||||||
playwright-browsers-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
|
||||||
run: npx playwright install chromium --with-deps
|
|
||||||
working-directory: ComfyUI_frontend
|
|
||||||
|
|
||||||
- name: Install Wrangler
|
|
||||||
run: pnpm install -g wrangler
|
|
||||||
|
|
||||||
- name: Run Playwright tests (${{ matrix.browser }})
|
- name: Run Playwright tests (${{ matrix.browser }})
|
||||||
id: playwright
|
id: playwright
|
||||||
run: npx playwright test --project=${{ matrix.browser }} --reporter=html
|
run: npx playwright test --project=${{ matrix.browser }} --reporter=html
|
||||||
working-directory: ComfyUI_frontend
|
working-directory: ComfyUI_frontend
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
if: always() # note: use always() to allow results to be upload/report even tests failed.
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: playwright-report-${{ matrix.browser }}
|
name: playwright-report-${{ matrix.browser }}
|
||||||
path: ComfyUI_frontend/playwright-report/
|
path: ComfyUI_frontend/playwright-report/
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
- name: Deploy to Cloudflare Pages (${{ matrix.browser }})
|
# Merge sharded test reports
|
||||||
id: cloudflare-deploy
|
merge-reports:
|
||||||
if: always()
|
needs: [playwright-tests-chromium-sharded]
|
||||||
continue-on-error: true
|
|
||||||
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 ComfyUI_frontend/playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }}; then
|
|
||||||
SUCCESS=true
|
|
||||||
echo "Deployment successful on attempt $RETRY_COUNT"
|
|
||||||
else
|
|
||||||
echo "Deployment failed on attempt $RETRY_COUNT"
|
|
||||||
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
|
|
||||||
echo "Retrying in 10 seconds..."
|
|
||||||
sleep 10
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ $SUCCESS = false ]; then
|
|
||||||
echo "All deployment attempts failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
|
|
||||||
- name: Save deployment info for summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
mkdir -p deployment-info
|
|
||||||
# Use step conclusion to determine test result
|
|
||||||
if [ "${{ steps.playwright.conclusion }}" = "success" ]; then
|
|
||||||
EXIT_CODE="0"
|
|
||||||
else
|
|
||||||
EXIT_CODE="1"
|
|
||||||
fi
|
|
||||||
DEPLOYMENT_URL="${{ steps.cloudflare-deploy.outputs.deployment-url || steps.cloudflare-deploy.outputs.url || format('https://{0}.{1}.pages.dev', steps.project-name.outputs.branch, steps.project-name.outputs.name) }}"
|
|
||||||
echo "${{ matrix.browser }}|${EXIT_CODE}|${DEPLOYMENT_URL}" > deployment-info/${{ matrix.browser }}.txt
|
|
||||||
|
|
||||||
- name: Upload deployment info
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: deployment-info-${{ matrix.browser }}
|
|
||||||
path: deployment-info/
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
- name: Get completion time
|
|
||||||
id: completion-time
|
|
||||||
if: always()
|
|
||||||
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Comment PR - Browser Test Complete
|
|
||||||
if: always() && github.event_name == 'pull_request'
|
|
||||||
continue-on-error: true
|
|
||||||
uses: edumserrano/find-create-or-update-comment@v3
|
|
||||||
with:
|
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
|
||||||
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
|
|
||||||
comment-author: 'github-actions[bot]'
|
|
||||||
edit-mode: append
|
|
||||||
body: |
|
|
||||||
${{ steps.playwright.conclusion == 'success' && '✅' || '❌' }} **${{ matrix.browser }}**: ${{ steps.playwright.conclusion == 'success' && 'Tests passed!' || 'Tests failed!' }} [View Report](${{ steps.cloudflare-deploy.outputs.deployment-url || format('https://{0}.{1}.pages.dev', steps.project-name.outputs.branch, steps.project-name.outputs.name) }})
|
|
||||||
|
|
||||||
comment-summary:
|
|
||||||
needs: playwright-tests
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: always() && github.event_name == 'pull_request'
|
if: ${{ !cancelled() }}
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download all deployment info
|
- 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
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
pattern: deployment-info-*
|
path: ComfyUI_frontend/all-blob-reports
|
||||||
|
pattern: blob-report-chromium-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
path: deployment-info
|
|
||||||
|
|
||||||
- name: Get completion time
|
- name: Merge into HTML Report
|
||||||
id: completion-time
|
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
||||||
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
|
working-directory: ComfyUI_frontend
|
||||||
|
|
||||||
- name: Generate comment body
|
- name: Upload HTML report
|
||||||
id: comment-body
|
uses: actions/upload-artifact@v4
|
||||||
run: |
|
|
||||||
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > 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)
|
|
||||||
|
|
||||||
if [ "$exit_code" = "0" ]; then
|
|
||||||
status="✅"
|
|
||||||
else
|
|
||||||
status="❌"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "- $status **$browser**: [View Report]($url)" >> comment.md
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "" >> comment.md
|
|
||||||
echo "---" >> comment.md
|
|
||||||
if [ "$ALL_PASSED" = "true" ]; then
|
|
||||||
echo "🎉 Your tests are passing across all browsers!" >> comment.md
|
|
||||||
else
|
|
||||||
echo "⚠️ Please check the test reports for details on failures." >> comment.md
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Comment PR - Tests Complete
|
|
||||||
continue-on-error: true
|
|
||||||
uses: edumserrano/find-create-or-update-comment@v3
|
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
name: playwright-report-chromium
|
||||||
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
|
path: ComfyUI_frontend/playwright-report/
|
||||||
comment-author: 'github-actions[bot]'
|
retention-days: 30
|
||||||
edit-mode: replace
|
|
||||||
body-path: comment.md
|
|
||||||
|
|
||||||
- name: Check test results and fail if needed
|
|
||||||
run: |
|
|
||||||
# Check if all tests passed and fail the job if not
|
|
||||||
ALL_PASSED=true
|
|
||||||
for file in deployment-info/*.txt; do
|
|
||||||
if [ -f "$file" ]; then
|
|
||||||
info=$(cat "$file")
|
|
||||||
exit_code=$(echo "$info" | cut -d'|' -f2)
|
|
||||||
if [ "$exit_code" != "0" ]; then
|
|
||||||
ALL_PASSED=false
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$ALL_PASSED" = "false" ]; then
|
|
||||||
echo "❌ Tests failed in one or more browsers. Failing the CI job."
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "✅ All tests passed across all browsers!"
|
|
||||||
fi
|
|
||||||
|
|||||||
4
.github/workflows/update-electron-types.yaml
vendored
@@ -35,12 +35,12 @@ jobs:
|
|||||||
electron-types-tools-cache-${{ runner.os }}-
|
electron-types-tools-cache-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Update electron types
|
- name: Update electron types
|
||||||
run: pnpm install @comfyorg/comfyui-electron-types@latest
|
run: pnpm install --workspace-root @comfyorg/comfyui-electron-types@latest
|
||||||
|
|
||||||
- name: Get new version
|
- name: Get new version
|
||||||
id: get-version
|
id: get-version
|
||||||
run: |
|
run: |
|
||||||
NEW_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('./pnpm-lock.yaml')).packages['node_modules/@comfyorg/comfyui-electron-types'].version)")
|
NEW_VERSION=$(pnpm list @comfyorg/comfyui-electron-types --json --depth=0 | jq -r '.[0].version')
|
||||||
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
|
|||||||
2
.github/workflows/update-manager-types.yaml
vendored
@@ -121,4 +121,4 @@ jobs:
|
|||||||
labels: Manager
|
labels: Manager
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
add-paths: |
|
add-paths: |
|
||||||
src/types/generatedManagerTypes.ts
|
src/types/generatedManagerTypes.ts
|
||||||
12
.mcp.json
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"playwright": {
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "@executeautomation/playwright-mcp-server"]
|
|
||||||
},
|
|
||||||
"context7": {
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "@upstash/context7-mcp"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,17 +4,26 @@
|
|||||||
transition: background-color 0.3s ease, color 0.3s ease;
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light theme default */
|
/* Light theme default - with explicit color to override media queries */
|
||||||
body {
|
body:not(.dark-theme) {
|
||||||
background-color: #ffffff;
|
background-color: #fff !important;
|
||||||
color: #1a1a1a;
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override browser dark mode preference for light theme */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body:not(.dark-theme) {
|
||||||
|
color: #000 !important;
|
||||||
|
--fg-color: #000 !important;
|
||||||
|
--bg-color: #fff !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme styles */
|
/* Dark theme styles */
|
||||||
body.dark-theme,
|
body.dark-theme,
|
||||||
.dark-theme body {
|
.dark-theme body {
|
||||||
background-color: #0a0a0a;
|
background-color: #202020;
|
||||||
color: #e5e5e5;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure Storybook canvas follows theme */
|
/* Ensure Storybook canvas follows theme */
|
||||||
@@ -24,11 +33,33 @@
|
|||||||
|
|
||||||
.dark-theme .sb-show-main,
|
.dark-theme .sb-show-main,
|
||||||
.dark-theme .docs-story {
|
.dark-theme .docs-story {
|
||||||
background-color: #0a0a0a !important;
|
background-color: #202020 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix for Storybook controls panel in dark mode */
|
/* CSS Variables for theme consistency */
|
||||||
.dark-theme .docblock-argstable-body {
|
body:not(.dark-theme) {
|
||||||
color: #e5e5e5;
|
--fg-color: #000;
|
||||||
|
--bg-color: #fff;
|
||||||
|
--content-bg: #e0e0e0;
|
||||||
|
--content-fg: #000;
|
||||||
|
--content-hover-bg: #adadad;
|
||||||
|
--content-hover-fg: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme {
|
||||||
|
--fg-color: #fff;
|
||||||
|
--bg-color: #202020;
|
||||||
|
--content-bg: #4e4e4e;
|
||||||
|
--content-fg: #fff;
|
||||||
|
--content-hover-bg: #222;
|
||||||
|
--content-hover-fg: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override Storybook's problematic & selector styles */
|
||||||
|
/* Reset only the specific properties that Storybook injects */
|
||||||
|
#storybook-root li+li,
|
||||||
|
#storybook-docs li+li {
|
||||||
|
margin: inherit;
|
||||||
|
padding: inherit;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
44
.vscode/tailwind.json
vendored
@@ -2,12 +2,32 @@
|
|||||||
"version": 1.1,
|
"version": 1.1,
|
||||||
"atDirectives": [
|
"atDirectives": [
|
||||||
{
|
{
|
||||||
"name": "@tailwind",
|
"name": "@import",
|
||||||
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
|
"description": "Use the `@import` directive to inline CSS files, including Tailwind itself, into your stylesheet.",
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"name": "Tailwind Documentation",
|
"name": "Tailwind Documentation",
|
||||||
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
|
"url": "https://tailwindcss.com/docs/functions-and-directives#import"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@theme",
|
||||||
|
"description": "Use the `@theme` directive to define custom design tokens like fonts, colors, and breakpoints.",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#theme"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@layer",
|
||||||
|
"description": "Use the `@layer` directive inside `@theme` to organize custom styles into different layers like `base`, `components`, and `utilities`.",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Tailwind Documentation",
|
||||||
|
"url": "https://tailwindcss.com/docs/functions-and-directives#layer"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -22,32 +42,32 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "@responsive",
|
"name": "@config",
|
||||||
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
|
"description": "Use the `@config` directive to load a legacy JavaScript-based Tailwind configuration file.",
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"name": "Tailwind Documentation",
|
"name": "Tailwind Documentation",
|
||||||
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
|
"url": "https://tailwindcss.com/docs/functions-and-directives#config"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "@screen",
|
"name": "@reference",
|
||||||
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
|
"description": "Use the `@reference` directive to import theme variables, custom utilities, and custom variants from other files without duplicating CSS.",
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"name": "Tailwind Documentation",
|
"name": "Tailwind Documentation",
|
||||||
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
|
"url": "https://tailwindcss.com/docs/functions-and-directives#reference"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "@variants",
|
"name": "@plugin",
|
||||||
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
|
"description": "Use the `@plugin` directive to load a legacy JavaScript-based Tailwind plugin.",
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"name": "Tailwind Documentation",
|
"name": "Tailwind Documentation",
|
||||||
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
|
"url": "https://tailwindcss.com/docs/functions-and-directives#plugin"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
41
CLAUDE.md
@@ -82,6 +82,44 @@ When referencing Comfy-Org repos:
|
|||||||
2. Use GitHub API for branches/PRs/metadata
|
2. Use GitHub API for branches/PRs/metadata
|
||||||
3. Curl GitHub website if needed
|
3. Curl GitHub website if needed
|
||||||
|
|
||||||
|
## Settings and Feature Flags Quick Reference
|
||||||
|
|
||||||
|
### Settings Usage
|
||||||
|
```typescript
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
const value = settingStore.get('Comfy.SomeSetting') // Get setting
|
||||||
|
await settingStore.set('Comfy.SomeSetting', newValue) // Update setting
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic Defaults
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: 'Comfy.Example.Setting',
|
||||||
|
defaultValue: () => window.innerWidth < 1024 ? 'small' : 'large' // Runtime context
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version-Based Defaults
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: 'Comfy.Example.Feature',
|
||||||
|
defaultValue: 'legacy',
|
||||||
|
defaultsByInstallVersion: { '1.25.0': 'enhanced' } // Gradual rollout
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Feature Flags
|
||||||
|
```typescript
|
||||||
|
if (api.serverSupportsFeature('feature_name')) { // Check capability
|
||||||
|
// Use enhanced feature
|
||||||
|
}
|
||||||
|
const value = api.getServerFeature('config_name', defaultValue) // Get config
|
||||||
|
```
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
- Settings system: `docs/SETTINGS.md`
|
||||||
|
- Feature flags system: `docs/FEATURE_FLAGS.md`
|
||||||
|
|
||||||
## Common Pitfalls
|
## Common Pitfalls
|
||||||
|
|
||||||
- NEVER use `any` type - use proper TypeScript types
|
- NEVER use `any` type - use proper TypeScript types
|
||||||
@@ -89,3 +127,6 @@ When referencing Comfy-Org repos:
|
|||||||
- NEVER use `--no-verify` flag when committing
|
- NEVER use `--no-verify` flag when committing
|
||||||
- NEVER delete or disable tests to make them pass
|
- NEVER delete or disable tests to make them pass
|
||||||
- NEVER circumvent quality checks
|
- NEVER circumvent quality checks
|
||||||
|
- NEVER use `dark:` prefix - always use `dark-theme:` for dark mode styles, for example: `dark-theme:text-white dark-theme:bg-black`
|
||||||
|
- NEVER use `:class="[]"` to merge class names - always use `import { cn } from '@/utils/tailwindUtil'`, for example: `<div :class="cn('bg-red-500', { 'bg-blue-500': condition })" />`
|
||||||
|
|
||||||
|
|||||||
@@ -14,4 +14,4 @@
|
|||||||
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs
|
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs
|
||||||
|
|
||||||
# Mask Editor extension
|
# Mask Editor extension
|
||||||
/src/extensions/core/maskeditor.ts @trsommer @Comfy-Org/comfy_frontend_devs
|
/src/extensions/core/maskeditor.ts @brucew4yn3rp @trsommer @Comfy-Org/comfy_frontend_devs
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
|
|||||||
### Prerequisites & Technology Stack
|
### Prerequisites & Technology Stack
|
||||||
|
|
||||||
- **Required Software**:
|
- **Required Software**:
|
||||||
- Node.js (v16 or later; v24 strongly recommended) and pnpm
|
- Node.js (v18 or later to build; v24 for vite dev server) and pnpm
|
||||||
- Git for version control
|
- Git for version control
|
||||||
- A running ComfyUI backend instance
|
- A running ComfyUI backend instance
|
||||||
|
|
||||||
|
|||||||
@@ -453,6 +453,32 @@ export class ComfyPage {
|
|||||||
await workflowsTab.close()
|
await workflowsTab.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach a screenshot to the test report.
|
||||||
|
* By default, screenshots are only taken in non-CI environments.
|
||||||
|
* @param name - Name for the screenshot attachment
|
||||||
|
* @param options - Optional configuration
|
||||||
|
* @param options.runInCI - Whether to take screenshot in CI (default: false)
|
||||||
|
* @param options.fullPage - Whether to capture full page (default: false)
|
||||||
|
*/
|
||||||
|
async attachScreenshot(
|
||||||
|
name: string,
|
||||||
|
options: { runInCI?: boolean; fullPage?: boolean } = {}
|
||||||
|
) {
|
||||||
|
const { runInCI = false, fullPage = false } = options
|
||||||
|
|
||||||
|
// Skip in CI unless explicitly requested
|
||||||
|
if (process.env.CI && !runInCI) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const testInfo = comfyPageFixture.info()
|
||||||
|
await testInfo.attach(name, {
|
||||||
|
body: await this.page.screenshot({ fullPage }),
|
||||||
|
contentType: 'image/png'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async resetView() {
|
async resetView() {
|
||||||
if (await this.resetViewButton.isVisible()) {
|
if (await this.resetViewButton.isVisible()) {
|
||||||
await this.resetViewButton.click()
|
await this.resetViewButton.click()
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { test as base } from '@playwright/test'
|
import { Page, test as base } from '@playwright/test'
|
||||||
import { Page } from 'playwright'
|
|
||||||
|
|
||||||
export class UserSelectPage {
|
export class UserSelectPage {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { Locator, Page } from '@playwright/test'
|
import { Locator, Page, expect } from '@playwright/test'
|
||||||
|
|
||||||
export class Topbar {
|
export class Topbar {
|
||||||
constructor(public readonly page: Page) {}
|
private readonly menuLocator: Locator
|
||||||
|
private readonly menuTrigger: Locator
|
||||||
|
|
||||||
|
constructor(public readonly page: Page) {
|
||||||
|
this.menuLocator = page.locator('.comfy-command-menu')
|
||||||
|
this.menuTrigger = page.locator('.comfyui-logo-wrapper')
|
||||||
|
}
|
||||||
|
|
||||||
async getTabNames(): Promise<string[]> {
|
async getTabNames(): Promise<string[]> {
|
||||||
return await this.page
|
return await this.page
|
||||||
@@ -15,10 +21,33 @@ export class Topbar {
|
|||||||
.innerText()
|
.innerText()
|
||||||
}
|
}
|
||||||
|
|
||||||
getMenuItem(itemLabel: string): Locator {
|
/**
|
||||||
|
* Get a menu item by its label, optionally within a specific parent container
|
||||||
|
*/
|
||||||
|
getMenuItem(itemLabel: string, parent?: Locator): Locator {
|
||||||
|
if (parent) {
|
||||||
|
return parent.locator(`.p-tieredmenu-item:has-text("${itemLabel}")`)
|
||||||
|
}
|
||||||
|
|
||||||
return this.page.locator(`.p-menubar-item-label:text-is("${itemLabel}")`)
|
return this.page.locator(`.p-menubar-item-label:text-is("${itemLabel}")`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the visible submenu (last visible submenu in case of nested menus)
|
||||||
|
*/
|
||||||
|
getVisibleSubmenu(): Locator {
|
||||||
|
return this.page.locator('.p-tieredmenu-submenu:visible').last()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a menu item has an active checkmark
|
||||||
|
*/
|
||||||
|
async isMenuItemActive(menuItem: Locator): Promise<boolean> {
|
||||||
|
const checkmark = menuItem.locator('.pi-check')
|
||||||
|
const classes = await checkmark.getAttribute('class')
|
||||||
|
return classes ? !classes.includes('invisible') : false
|
||||||
|
}
|
||||||
|
|
||||||
getWorkflowTab(tabName: string): Locator {
|
getWorkflowTab(tabName: string): Locator {
|
||||||
return this.page
|
return this.page
|
||||||
.locator(`.workflow-tabs .workflow-label:has-text("${tabName}")`)
|
.locator(`.workflow-tabs .workflow-label:has-text("${tabName}")`)
|
||||||
@@ -66,10 +95,50 @@ export class Topbar {
|
|||||||
|
|
||||||
async openTopbarMenu() {
|
async openTopbarMenu() {
|
||||||
await this.page.waitForTimeout(1000)
|
await this.page.waitForTimeout(1000)
|
||||||
await this.page.locator('.comfyui-logo-wrapper').click()
|
await this.menuTrigger.click()
|
||||||
const menu = this.page.locator('.comfy-command-menu')
|
await this.menuLocator.waitFor({ state: 'visible' })
|
||||||
await menu.waitFor({ state: 'visible' })
|
return this.menuLocator
|
||||||
return menu
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the topbar menu by clicking outside
|
||||||
|
*/
|
||||||
|
async closeTopbarMenu() {
|
||||||
|
await this.page.locator('body').click({ position: { x: 10, y: 10 } })
|
||||||
|
await expect(this.menuLocator).not.toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to a submenu by hovering over a menu item
|
||||||
|
*/
|
||||||
|
async openSubmenu(menuItemLabel: string): Promise<Locator> {
|
||||||
|
const menuItem = this.getMenuItem(menuItemLabel)
|
||||||
|
await menuItem.hover()
|
||||||
|
const submenu = this.getVisibleSubmenu()
|
||||||
|
await submenu.waitFor({ state: 'visible' })
|
||||||
|
return submenu
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get theme menu items and interact with theme switching
|
||||||
|
*/
|
||||||
|
async getThemeMenuItems() {
|
||||||
|
const themeSubmenu = await this.openSubmenu('Theme')
|
||||||
|
return {
|
||||||
|
submenu: themeSubmenu,
|
||||||
|
darkTheme: this.getMenuItem('Dark (Default)', themeSubmenu),
|
||||||
|
lightTheme: this.getMenuItem('Light', themeSubmenu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a specific theme
|
||||||
|
*/
|
||||||
|
async switchTheme(theme: 'dark' | 'light') {
|
||||||
|
const { darkTheme, lightTheme } = await this.getThemeMenuItems()
|
||||||
|
const themeItem = theme === 'dark' ? darkTheme : lightTheme
|
||||||
|
const themeLabel = themeItem.locator('.p-menubar-item-label')
|
||||||
|
await themeLabel.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
async triggerTopbarCommand(path: string[]) {
|
async triggerTopbarCommand(path: string[]) {
|
||||||
@@ -79,9 +148,7 @@ export class Topbar {
|
|||||||
|
|
||||||
const menu = await this.openTopbarMenu()
|
const menu = await this.openTopbarMenu()
|
||||||
const tabName = path[0]
|
const tabName = path[0]
|
||||||
const topLevelMenuItem = this.page.locator(
|
const topLevelMenuItem = this.getMenuItem(tabName)
|
||||||
`.p-menubar-item-label:text-is("${tabName}")`
|
|
||||||
)
|
|
||||||
const topLevelMenu = menu
|
const topLevelMenu = menu
|
||||||
.locator('.p-tieredmenu-item')
|
.locator('.p-tieredmenu-item')
|
||||||
.filter({ has: topLevelMenuItem })
|
.filter({ has: topLevelMenuItem })
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import type { Request, Route } from '@playwright/test'
|
||||||
import _ from 'es-toolkit/compat'
|
import _ from 'es-toolkit/compat'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import type { Request, Route } from 'playwright'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
131
browser_tests/fixtures/utils/vueNodeFixtures.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import type { Locator, Page } from '@playwright/test'
|
||||||
|
|
||||||
|
import type { NodeReference } from './litegraphUtils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VueNodeFixture provides Vue-specific testing utilities for interacting with
|
||||||
|
* Vue node components. It bridges the gap between litegraph node references
|
||||||
|
* and Vue UI components.
|
||||||
|
*/
|
||||||
|
export class VueNodeFixture {
|
||||||
|
constructor(
|
||||||
|
private readonly nodeRef: NodeReference,
|
||||||
|
private readonly page: Page
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the node's header element using data-testid
|
||||||
|
*/
|
||||||
|
async getHeader(): Promise<Locator> {
|
||||||
|
const nodeId = this.nodeRef.id
|
||||||
|
return this.page.locator(`[data-testid="node-header-${nodeId}"]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the node's title element
|
||||||
|
*/
|
||||||
|
async getTitleElement(): Promise<Locator> {
|
||||||
|
const header = await this.getHeader()
|
||||||
|
return header.locator('[data-testid="node-title"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current title text
|
||||||
|
*/
|
||||||
|
async getTitle(): Promise<string> {
|
||||||
|
const titleElement = await this.getTitleElement()
|
||||||
|
return (await titleElement.textContent()) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new title by double-clicking and entering text
|
||||||
|
*/
|
||||||
|
async setTitle(newTitle: string): Promise<void> {
|
||||||
|
const titleElement = await this.getTitleElement()
|
||||||
|
await titleElement.dblclick()
|
||||||
|
|
||||||
|
const input = (await this.getHeader()).locator(
|
||||||
|
'[data-testid="node-title-input"]'
|
||||||
|
)
|
||||||
|
await input.fill(newTitle)
|
||||||
|
await input.press('Enter')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel title editing
|
||||||
|
*/
|
||||||
|
async cancelTitleEdit(): Promise<void> {
|
||||||
|
const titleElement = await this.getTitleElement()
|
||||||
|
await titleElement.dblclick()
|
||||||
|
|
||||||
|
const input = (await this.getHeader()).locator(
|
||||||
|
'[data-testid="node-title-input"]'
|
||||||
|
)
|
||||||
|
await input.press('Escape')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the title is currently being edited
|
||||||
|
*/
|
||||||
|
async isEditingTitle(): Promise<boolean> {
|
||||||
|
const header = await this.getHeader()
|
||||||
|
const input = header.locator('[data-testid="node-title-input"]')
|
||||||
|
return await input.isVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the collapse/expand button
|
||||||
|
*/
|
||||||
|
async getCollapseButton(): Promise<Locator> {
|
||||||
|
const header = await this.getHeader()
|
||||||
|
return header.locator('[data-testid="node-collapse-button"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the node's collapsed state
|
||||||
|
*/
|
||||||
|
async toggleCollapse(): Promise<void> {
|
||||||
|
const button = await this.getCollapseButton()
|
||||||
|
await button.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the collapse icon element
|
||||||
|
*/
|
||||||
|
async getCollapseIcon(): Promise<Locator> {
|
||||||
|
const button = await this.getCollapseButton()
|
||||||
|
return button.locator('i')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the collapse icon's CSS classes
|
||||||
|
*/
|
||||||
|
async getCollapseIconClass(): Promise<string> {
|
||||||
|
const icon = await this.getCollapseIcon()
|
||||||
|
return (await icon.getAttribute('class')) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the collapse button is visible
|
||||||
|
*/
|
||||||
|
async isCollapseButtonVisible(): Promise<boolean> {
|
||||||
|
const button = await this.getCollapseButton()
|
||||||
|
return await button.isVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the node's body/content element
|
||||||
|
*/
|
||||||
|
async getBody(): Promise<Locator> {
|
||||||
|
const nodeId = this.nodeRef.id
|
||||||
|
return this.page.locator(`[data-testid="node-body-${nodeId}"]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the node body is visible (not collapsed)
|
||||||
|
*/
|
||||||
|
async isBodyVisible(): Promise<boolean> {
|
||||||
|
const body = await this.getBody()
|
||||||
|
return await body.isVisible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
@@ -59,18 +59,6 @@ test.describe('Execution error', () => {
|
|||||||
const executionError = comfyPage.page.locator('.comfy-error-report')
|
const executionError = comfyPage.page.locator('.comfy-error-report')
|
||||||
await expect(executionError).toBeVisible()
|
await expect(executionError).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Can display Issue Report form', async ({ comfyPage }) => {
|
|
||||||
await comfyPage.loadWorkflow('nodes/execution_error')
|
|
||||||
await comfyPage.queueButton.click()
|
|
||||||
await comfyPage.nextFrame()
|
|
||||||
|
|
||||||
await comfyPage.page.getByLabel('Help Fix This').click()
|
|
||||||
const issueReportForm = comfyPage.page.getByText(
|
|
||||||
'Submit Error Report (Optional)'
|
|
||||||
)
|
|
||||||
await expect(issueReportForm).toBeVisible()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('Missing models warning', () => {
|
test.describe('Missing models warning', () => {
|
||||||
@@ -303,37 +291,16 @@ test.describe('Settings', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('Feedback dialog', () => {
|
test.describe('Support', () => {
|
||||||
test('Should open from topmenu help command', async ({ comfyPage }) => {
|
test('Should open external zendesk link', async ({ comfyPage }) => {
|
||||||
// Open feedback dialog from top menu
|
|
||||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
|
const pagePromise = comfyPage.page.context().waitForEvent('page')
|
||||||
|
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support'])
|
||||||
|
const newPage = await pagePromise
|
||||||
|
|
||||||
// Verify feedback dialog content is visible
|
await newPage.waitForLoadState('networkidle')
|
||||||
const feedbackHeader = comfyPage.page.getByRole('heading', {
|
await expect(newPage).toHaveURL(/.*support\.comfy\.org.*/)
|
||||||
name: 'Feedback'
|
await newPage.close()
|
||||||
})
|
|
||||||
await expect(feedbackHeader).toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Should close when close button clicked', async ({ comfyPage }) => {
|
|
||||||
// Open feedback dialog
|
|
||||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
|
||||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
|
|
||||||
|
|
||||||
const feedbackHeader = comfyPage.page.getByRole('heading', {
|
|
||||||
name: 'Feedback'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Close feedback dialog
|
|
||||||
await comfyPage.page
|
|
||||||
.getByLabel('', { exact: true })
|
|
||||||
.getByLabel('Close')
|
|
||||||
.click()
|
|
||||||
await feedbackHeader.waitFor({ state: 'hidden' })
|
|
||||||
|
|
||||||
// Verify dialog is closed
|
|
||||||
await expect(feedbackHeader).not.toBeVisible()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |