diff --git a/.github/workflows/weekly-comfyui-release.yaml b/.github/workflows/weekly-comfyui-release.yaml new file mode 100644 index 0000000000..41e6a4271e --- /dev/null +++ b/.github/workflows/weekly-comfyui-release.yaml @@ -0,0 +1,270 @@ +# Automated weekly workflow to bump ComfyUI frontend RC releases +name: "Weekly ComfyUI Release" + +on: + # Schedule for Monday at 12:00 PM PST (20:00 UTC) + schedule: + - cron: '0 20 * * 1' + + # Allow manual triggering + workflow_dispatch: + inputs: + comfyui_fork: + description: 'ComfyUI fork to use for PR (e.g., Comfy-Org/ComfyUI)' + required: false + default: 'Comfy-Org/ComfyUI' + type: string + +jobs: + resolve-version: + runs-on: ubuntu-latest + outputs: + current_version: ${{ steps.resolve.outputs.current_version }} + target_version: ${{ steps.resolve.outputs.target_version }} + target_minor: ${{ steps.resolve.outputs.target_minor }} + target_branch: ${{ steps.resolve.outputs.target_branch }} + needs_release: ${{ steps.resolve.outputs.needs_release }} + diff_url: ${{ steps.resolve.outputs.diff_url }} + latest_patch_tag: ${{ steps.resolve.outputs.latest_patch_tag }} + + steps: + - name: Checkout ComfyUI_frontend + uses: actions/checkout@v5 + with: + fetch-depth: 0 + path: frontend + + - name: Checkout ComfyUI (sparse) + uses: actions/checkout@v5 + with: + repository: comfyanonymous/ComfyUI + sparse-checkout: | + requirements.txt + path: comfyui + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Install dependencies + working-directory: frontend + run: pnpm install --frozen-lockfile + + - name: Resolve release information + id: resolve + working-directory: frontend + run: | + set -euo pipefail + + # Run the resolver script + if ! RESULT=$(tsx scripts/cicd/resolve-comfyui-release.ts ../comfyui .); then + echo "Failed to resolve release information" + exit 1 + fi + + echo "Resolver output:" + echo "$RESULT" + + # Validate JSON output + if ! echo "$RESULT" | jq empty 2>/dev/null; then + echo "Invalid JSON output from resolver" + exit 1 + fi + + # Parse JSON output and set outputs + echo "current_version=$(echo "$RESULT" | jq -r '.current_version')" >> $GITHUB_OUTPUT + echo "target_version=$(echo "$RESULT" | jq -r '.target_version')" >> $GITHUB_OUTPUT + echo "target_minor=$(echo "$RESULT" | jq -r '.target_minor')" >> $GITHUB_OUTPUT + echo "target_branch=$(echo "$RESULT" | jq -r '.target_branch')" >> $GITHUB_OUTPUT + echo "needs_release=$(echo "$RESULT" | jq -r '.needs_release')" >> $GITHUB_OUTPUT + echo "diff_url=$(echo "$RESULT" | jq -r '.diff_url')" >> $GITHUB_OUTPUT + echo "latest_patch_tag=$(echo "$RESULT" | jq -r '.latest_patch_tag')" >> $GITHUB_OUTPUT + + - name: Summary + run: | + echo "## Release Information" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- Current version: ${{ steps.resolve.outputs.current_version }}" >> $GITHUB_STEP_SUMMARY + echo "- Target version: ${{ steps.resolve.outputs.target_version }}" >> $GITHUB_STEP_SUMMARY + echo "- Target branch: ${{ steps.resolve.outputs.target_branch }}" >> $GITHUB_STEP_SUMMARY + echo "- Needs release: ${{ steps.resolve.outputs.needs_release }}" >> $GITHUB_STEP_SUMMARY + echo "- Diff: [${{ steps.resolve.outputs.current_version }}...${{ steps.resolve.outputs.target_version }}](${{ steps.resolve.outputs.diff_url }})" >> $GITHUB_STEP_SUMMARY + + trigger-release-if-needed: + needs: resolve-version + if: needs.resolve-version.outputs.needs_release == 'true' + runs-on: ubuntu-latest + + steps: + - name: Trigger release workflow + env: + GH_TOKEN: ${{ secrets.PR_GH_TOKEN }} + run: | + set -euo pipefail + + echo "Triggering release workflow for branch ${{ needs.resolve-version.outputs.target_branch }}" + + # Trigger the release-version-bump workflow + if ! gh workflow run release-version-bump.yaml \ + --repo Comfy-Org/ComfyUI_frontend \ + --ref main \ + --field version_type=patch \ + --field branch=${{ needs.resolve-version.outputs.target_branch }}; then + echo "Failed to trigger release workflow" + exit 1 + fi + + echo "Release workflow triggered successfully for ${{ needs.resolve-version.outputs.target_branch }}" + + - name: Summary + run: | + echo "## Release Workflow Triggered" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- Branch: ${{ needs.resolve-version.outputs.target_branch }}" >> $GITHUB_STEP_SUMMARY + echo "- Target version: ${{ needs.resolve-version.outputs.target_version }}" >> $GITHUB_STEP_SUMMARY + echo "- [View workflow runs](https://github.com/Comfy-Org/ComfyUI_frontend/actions/workflows/release-version-bump.yaml)" >> $GITHUB_STEP_SUMMARY + + create-comfyui-pr: + needs: [resolve-version, trigger-release-if-needed] + if: always() && needs.resolve-version.result == 'success' + runs-on: ubuntu-latest + + steps: + - name: Checkout ComfyUI fork + uses: actions/checkout@v5 + with: + repository: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }} + token: ${{ secrets.PR_GH_TOKEN }} + path: comfyui + + - name: Update requirements.txt + working-directory: comfyui + run: | + set -euo pipefail + + TARGET_VERSION="${{ needs.resolve-version.outputs.target_version }}" + echo "Updating comfyui-frontend-package to ${TARGET_VERSION}" + + # Update the comfyui-frontend-package version (POSIX-compatible) + sed -i.bak "s/comfyui-frontend-package==[0-9.][0-9.]*/comfyui-frontend-package==${TARGET_VERSION}/" requirements.txt + rm requirements.txt.bak + + # Verify the change was made + if ! grep -q "comfyui-frontend-package==${TARGET_VERSION}" requirements.txt; then + echo "Failed to update requirements.txt" + exit 1 + fi + + echo "Updated requirements.txt:" + grep comfyui-frontend-package requirements.txt + + - name: Build PR description + id: pr-body + run: | + BODY=$(cat <<'EOF' + Bumps frontend to ${{ needs.resolve-version.outputs.target_version }} + + Test quickly: + + ```bash + python main.py --front-end-version Comfy-Org/ComfyUI_frontend@${{ needs.resolve-version.outputs.target_version }} + ``` + + - Diff: [v${{ needs.resolve-version.outputs.current_version }}...v${{ needs.resolve-version.outputs.target_version }}](${{ needs.resolve-version.outputs.diff_url }}) + - PyPI: https://pypi.org/project/comfyui-frontend-package/${{ needs.resolve-version.outputs.target_version }}/ + - npm: https://www.npmjs.com/package/@comfyorg/comfyui-frontend-types/v/${{ needs.resolve-version.outputs.target_version }} + EOF + ) + + # Add release PR note if release was triggered + if [ "${{ needs.resolve-version.outputs.needs_release }}" = "true" ]; then + RELEASE_NOTE="⚠️ **Release PR must be merged first** - check [release workflow runs](https://github.com/Comfy-Org/ComfyUI_frontend/actions/workflows/release-version-bump.yaml)" + BODY=$''"${RELEASE_NOTE}"$'\n\n'"${BODY}" + fi + + # Save to file for later use + printf '%s\n' "$BODY" > pr-body.txt + cat pr-body.txt + + - name: Create PR to ComfyUI + working-directory: comfyui + env: + GH_TOKEN: ${{ secrets.PR_GH_TOKEN }} + COMFYUI_FORK: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }} + run: | + set -euo pipefail + + # Extract fork owner from repository name + FORK_OWNER=$(echo "$COMFYUI_FORK" | cut -d'/' -f1) + + echo "Creating PR from ${COMFYUI_FORK} to comfyanonymous/ComfyUI" + + # Configure git + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Create/update branch (reuse same branch name each week) + BRANCH="automation/comfyui-frontend-bump" + git checkout -B "$BRANCH" + git add requirements.txt + + if ! git diff --cached --quiet; then + git commit -m "Bump comfyui-frontend-package to ${{ needs.resolve-version.outputs.target_version }}" + else + echo "No changes to commit" + exit 0 + fi + + # Force push to fork (overwrites previous week's branch) + # Note: This intentionally destroys branch history to maintain a single PR + # Any review comments or manual commits will need to be re-applied + if ! git push -f origin "$BRANCH"; then + echo "Failed to push branch to fork" + exit 1 + fi + + # Create draft PR from fork to upstream + PR_BODY=$(cat ../pr-body.txt) + + # Try to create PR, ignore error if it already exists + if ! gh pr create \ + --repo comfyanonymous/ComfyUI \ + --head "${FORK_OWNER}:${BRANCH}" \ + --base master \ + --title "Bump comfyui-frontend-package to ${{ needs.resolve-version.outputs.target_version }}" \ + --body "$PR_BODY" \ + --draft 2>&1; then + + # Check if PR already exists + set +e + EXISTING_PR=$(gh pr list --repo comfyanonymous/ComfyUI --head "${FORK_OWNER}:${BRANCH}" --json number --jq '.[0].number' 2>&1) + PR_LIST_EXIT=$? + set -e + + if [ $PR_LIST_EXIT -ne 0 ]; then + echo "Failed to check for existing PR: $EXISTING_PR" + exit 1 + fi + + if [ -n "$EXISTING_PR" ] && [ "$EXISTING_PR" != "null" ]; then + echo "PR already exists (#${EXISTING_PR}), updating branch will update the PR" + else + echo "Failed to create PR and no existing PR found" + exit 1 + fi + fi + + - name: Summary + run: | + echo "## ComfyUI PR Created" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Draft PR created in comfyanonymous/ComfyUI" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### PR Body:" >> $GITHUB_STEP_SUMMARY + cat pr-body.txt >> $GITHUB_STEP_SUMMARY diff --git a/knip.config.ts b/knip.config.ts index a77574f97b..f2907a266f 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -27,7 +27,7 @@ const config: KnipConfig = { project: ['src/**/*.{js,ts}'] } }, - ignoreBinaries: ['python3'], + ignoreBinaries: ['python3', 'gh'], ignoreDependencies: [ // Weird importmap things '@iconify/json', diff --git a/scripts/cicd/resolve-comfyui-release.ts b/scripts/cicd/resolve-comfyui-release.ts new file mode 100755 index 0000000000..b1d4c11562 --- /dev/null +++ b/scripts/cicd/resolve-comfyui-release.ts @@ -0,0 +1,254 @@ +#!/usr/bin/env tsx +import { execSync } from 'child_process' +import fs from 'fs' +import path from 'path' + +interface ReleaseInfo { + current_version: string + target_minor: number + target_version: string + target_branch: string + needs_release: boolean + latest_patch_tag: string | null + branch_head_sha: string | null + tag_commit_sha: string | null + diff_url: string + release_pr_url: string | null +} + +/** + * Execute a command and return stdout + */ +function exec(command: string, cwd?: string): string { + try { + return execSync(command, { + cwd, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'] + }).trim() + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + const cwdInfo = cwd ? ` in directory: ${cwd}` : '' + console.error( + `Command failed: ${command}${cwdInfo}\nError: ${errorMessage}` + ) + return '' + } +} + +/** + * Parse version from requirements.txt + * Handles formats: comfyui-frontend-package==1.2.3, comfyui-frontend-package>=1.2.3, etc. + */ +function parseRequirementsVersion(requirementsPath: string): string | null { + if (!fs.existsSync(requirementsPath)) { + console.error(`Requirements file not found: ${requirementsPath}`) + return null + } + + const content = fs.readFileSync(requirementsPath, 'utf-8') + const match = content.match( + /comfyui-frontend-package\s*(?:==|>=|<=|~=|>|<)\s*([0-9]+\.[0-9]+\.[0-9]+)/ + ) + + if (!match) { + console.error( + 'Could not find comfyui-frontend-package version in requirements.txt' + ) + return null + } + + return match[1] +} + +/** + * Validate semantic version string + */ +function isValidSemver(version: string): boolean { + if (!version || typeof version !== 'string') { + return false + } + + const parts = version.split('.') + if (parts.length !== 3) { + return false + } + + return parts.every((part) => { + const num = Number(part) + return Number.isFinite(num) && num >= 0 && String(num) === part + }) +} + +/** + * Get the latest patch tag for a given minor version + */ +function getLatestPatchTag(repoPath: string, minor: number): string | null { + // Fetch all tags + exec('git fetch --tags', repoPath) + + // Use git's native version sorting to get the latest tag + const latestTag = exec( + `git tag -l 'v1.${minor}.*' --sort=-version:refname | head -n 1`, + repoPath + ) + + if (!latestTag) { + return null + } + + // Validate the tag is a valid semver (vX.Y.Z format) + const validTagRegex = /^v\d+\.\d+\.\d+$/ + if (!validTagRegex.test(latestTag)) { + console.error( + `Latest tag for minor version ${minor} is not valid semver: ${latestTag}` + ) + return null + } + + return latestTag +} + +/** + * Resolve the ComfyUI release information + */ +function resolveRelease( + comfyuiRepoPath: string, + frontendRepoPath: string +): ReleaseInfo | null { + // Parse current version from ComfyUI requirements.txt + const requirementsPath = path.join(comfyuiRepoPath, 'requirements.txt') + const currentVersion = parseRequirementsVersion(requirementsPath) + + if (!currentVersion) { + return null + } + + // Validate version format + if (!isValidSemver(currentVersion)) { + console.error( + `Invalid semantic version format: ${currentVersion}. Expected format: X.Y.Z` + ) + return null + } + + const [major, currentMinor, patch] = currentVersion.split('.').map(Number) + + // Calculate target minor version (next minor) + const targetMinor = currentMinor + 1 + const targetBranch = `core/1.${targetMinor}` + + // Check if target branch exists in frontend repo + exec('git fetch origin', frontendRepoPath) + const branchExists = exec( + `git rev-parse --verify origin/${targetBranch}`, + frontendRepoPath + ) + + if (!branchExists) { + console.error( + `Target branch ${targetBranch} does not exist in frontend repo` + ) + return null + } + + // Get latest patch tag for target minor + const latestPatchTag = getLatestPatchTag(frontendRepoPath, targetMinor) + + let needsRelease = false + let branchHeadSha: string | null = null + let tagCommitSha: string | null = null + let targetVersion = currentVersion + + if (latestPatchTag) { + // Get commit SHA for the tag + tagCommitSha = exec(`git rev-list -n 1 ${latestPatchTag}`, frontendRepoPath) + + // Get commit SHA for branch head + branchHeadSha = exec( + `git rev-parse origin/${targetBranch}`, + frontendRepoPath + ) + + // Check if there are commits between tag and branch head + const commitsBetween = exec( + `git rev-list ${latestPatchTag}..origin/${targetBranch} --count`, + frontendRepoPath + ) + + const commitCount = parseInt(commitsBetween, 10) + needsRelease = !isNaN(commitCount) && commitCount > 0 + + // Parse existing patch number and increment if needed + const tagVersion = latestPatchTag.replace('v', '') + + // Validate tag version format + if (!isValidSemver(tagVersion)) { + console.error( + `Invalid tag version format: ${tagVersion}. Expected format: X.Y.Z` + ) + return null + } + + const [, , existingPatch] = tagVersion.split('.').map(Number) + + // Validate existingPatch is a valid number + if (!Number.isFinite(existingPatch) || existingPatch < 0) { + console.error(`Invalid patch number in tag: ${existingPatch}`) + return null + } + + if (needsRelease) { + targetVersion = `1.${targetMinor}.${existingPatch + 1}` + } else { + targetVersion = tagVersion + } + } else { + // No tags exist for this minor version, need to create v1.{targetMinor}.0 + needsRelease = true + targetVersion = `1.${targetMinor}.0` + branchHeadSha = exec( + `git rev-parse origin/${targetBranch}`, + frontendRepoPath + ) + } + + const diffUrl = `https://github.com/Comfy-Org/ComfyUI_frontend/compare/v${currentVersion}...v${targetVersion}` + + return { + current_version: currentVersion, + target_minor: targetMinor, + target_version: targetVersion, + target_branch: targetBranch, + needs_release: needsRelease, + latest_patch_tag: latestPatchTag, + branch_head_sha: branchHeadSha, + tag_commit_sha: tagCommitSha, + diff_url: diffUrl, + release_pr_url: null // Will be populated by workflow if release is triggered + } +} + +// Main execution +const comfyuiRepoPath = process.argv[2] +const frontendRepoPath = process.argv[3] || process.cwd() + +if (!comfyuiRepoPath) { + console.error( + 'Usage: resolve-comfyui-release.ts [frontend-repo-path]' + ) + process.exit(1) +} + +const releaseInfo = resolveRelease(comfyuiRepoPath, frontendRepoPath) + +if (!releaseInfo) { + console.error('Failed to resolve release information') + process.exit(1) +} + +// Output as JSON for GitHub Actions +// eslint-disable-next-line no-console +console.log(JSON.stringify(releaseInfo, null, 2)) + +export { resolveRelease }