diff --git a/.github/workflows/chromatic.yaml b/.github/workflows/chromatic.yaml index 127186d68..bd1d1a441 100644 --- a/.github/workflows/chromatic.yaml +++ b/.github/workflows/chromatic.yaml @@ -1,4 +1,4 @@ -name: 'Chromatic' +name: Storybook and Chromatic CI # - [Automate Chromatic with GitHub Actions â€ĸ Chromatic docs]( https://www.chromatic.com/docs/github-actions/ ) @@ -8,10 +8,97 @@ on: branches: [main] jobs: + # Post starting comment for non-forked PRs + comment-on-pr-start: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false + permissions: + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Post starting comment + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh + ./scripts/cicd/pr-storybook-deploy-and-comment.sh \ + "${{ github.event.pull_request.number }}" \ + "${{ github.head_ref }}" \ + "starting" \ + "$(date -u '+%m/%d/%Y, %I:%M:%S %p')" + + # Build Storybook for all PRs (free Cloudflare deployment) + storybook-build: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + outputs: + conclusion: ${{ steps.job-status.outputs.conclusion }} + workflow-url: ${{ steps.workflow-url.outputs.url }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Cache tool outputs + uses: actions/cache@v4 + with: + path: | + .cache + storybook-static + tsconfig.tsbuildinfo + key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }} + restore-keys: | + storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}- + storybook-cache-${{ runner.os }}- + storybook-tools-cache-${{ runner.os }}- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build Storybook + run: pnpm build-storybook + + - name: Set job status + id: job-status + if: always() + run: | + echo "conclusion=${{ job.status }}" >> $GITHUB_OUTPUT + + - name: Get workflow URL + id: workflow-url + if: always() + run: | + echo "url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_OUTPUT + + - name: Upload Storybook build + if: success() && github.event.pull_request.head.repo.fork == false + uses: actions/upload-artifact@v4 + with: + name: storybook-static + path: storybook-static/ + retention-days: 7 + + # Chromatic deployment only for version-bump-* branches or manual triggers chromatic-deployment: runs-on: ubuntu-latest - # 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' || (github.event_name == 'pull_request' && startsWith(github.head_ref, 'version-bump-')) + outputs: + conclusion: ${{ steps.job-status.outputs.conclusion }} + workflow-url: ${{ steps.workflow-url.outputs.url }} + chromatic-build-url: ${{ steps.chromatic.outputs.buildUrl }} + chromatic-storybook-url: ${{ steps.chromatic.outputs.storybookUrl }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -29,7 +116,6 @@ jobs: node-version: '20' cache: 'pnpm' - - name: Cache tool outputs uses: actions/cache@v4 with: @@ -54,4 +140,92 @@ jobs: buildScriptName: build-storybook autoAcceptChanges: 'main' # Auto-accept changes on main branch exitOnceUploaded: true # Don't wait for UI tests to complete + onlyChanged: true # Only capture changed stories + - name: Set job status + id: job-status + if: always() + run: | + echo "conclusion=${{ job.status }}" >> $GITHUB_OUTPUT + + - name: Get workflow URL + id: workflow-url + if: always() + run: | + echo "url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_OUTPUT + + # Deploy and comment for non-forked PRs only + deploy-and-comment: + needs: [storybook-build] + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && always() + permissions: + pull-requests: write + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download Storybook build + if: needs.storybook-build.outputs.conclusion == 'success' + uses: actions/download-artifact@v4 + with: + name: storybook-static + path: storybook-static + + - name: Make deployment script executable + run: chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh + + - name: Deploy Storybook and comment on PR + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + GITHUB_TOKEN: ${{ github.token }} + WORKFLOW_CONCLUSION: ${{ needs.storybook-build.outputs.conclusion }} + WORKFLOW_URL: ${{ needs.storybook-build.outputs.workflow-url }} + run: | + ./scripts/cicd/pr-storybook-deploy-and-comment.sh \ + "${{ github.event.pull_request.number }}" \ + "${{ github.head_ref }}" \ + "completed" + + # Update comment with Chromatic URLs for version-bump branches + update-comment-with-chromatic: + needs: [chromatic-deployment, deploy-and-comment] + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && startsWith(github.head_ref, 'version-bump-') && needs.chromatic-deployment.outputs.chromatic-build-url != '' + permissions: + pull-requests: write + steps: + - name: Update comment with Chromatic URLs + uses: actions/github-script@v7 + with: + script: | + const buildUrl = '${{ needs.chromatic-deployment.outputs.chromatic-build-url }}'; + const storybookUrl = '${{ needs.chromatic-deployment.outputs.chromatic-storybook-url }}'; + + // Find the existing Storybook comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: ${{ github.event.pull_request.number }} + }); + + const storybookComment = comments.find(comment => + comment.body.includes('') + ); + + if (storybookComment && buildUrl && storybookUrl) { + // Append Chromatic info to existing comment + const updatedBody = storybookComment.body.replace( + /---\n(.*)$/s, + `---\n### 🎨 Chromatic Visual Tests\n- 📊 [View Chromatic Build](${buildUrl})\n- 📚 [View Chromatic Storybook](${storybookUrl})\n\n$1` + ); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: storybookComment.id, + body: updatedBody + }); + } diff --git a/.github/workflows/pr-storybook-comment.yaml b/.github/workflows/pr-storybook-comment.yaml deleted file mode 100644 index 53691d826..000000000 --- a/.github/workflows/pr-storybook-comment.yaml +++ /dev/null @@ -1,126 +0,0 @@ -name: PR Storybook Comment - -on: - workflow_run: - workflows: ['Chromatic'] - types: [requested, completed] - -jobs: - comment-storybook: - runs-on: ubuntu-latest - if: >- - github.repository == 'Comfy-Org/ComfyUI_frontend' - && github.event.workflow_run.event == 'pull_request' - && startsWith(github.event.workflow_run.head_branch, 'version-bump-') - permissions: - pull-requests: write - actions: read - steps: - - name: Get PR number - id: pr - uses: actions/github-script@v7 - with: - script: | - const { data: pullRequests } = await github.rest.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`, - }); - - if (pullRequests.length === 0) { - console.log('No open PR found for this branch'); - return null; - } - - return pullRequests[0].number; - - - name: Log when no PR found - if: steps.pr.outputs.result == 'null' - run: | - echo "âš ī¸ No open PR found for branch: ${{ github.event.workflow_run.head_branch }}" - echo "Workflow run ID: ${{ github.event.workflow_run.id }}" - echo "Repository: ${{ github.event.workflow_run.repository.full_name }}" - echo "Event: ${{ github.event.workflow_run.event }}" - - - name: Get workflow run details - if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' - id: workflow-run - uses: actions/github-script@v7 - with: - script: | - const run = await github.rest.actions.getWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.payload.workflow_run.id, - }); - - return { - conclusion: run.data.conclusion, - html_url: run.data.html_url - }; - - - name: Get completion time - id: completion-time - run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT - - - name: Comment PR - Storybook Started - if: steps.pr.outputs.result != 'null' && github.event.action == 'requested' - uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0 - with: - issue-number: ${{ steps.pr.outputs.result }} - body-includes: '' - comment-author: 'github-actions[bot]' - edit-mode: replace - body: | - - ## 🎨 Storybook Build Status - - comfy-loading-gif **Build is starting...** - - ⏰ Started at: ${{ steps.completion-time.outputs.time }} UTC - - ### 🚀 Building Storybook - - đŸ“Ļ Installing dependencies... - - 🔧 Building Storybook components... - - 🎨 Running Chromatic visual tests... - - --- - âąī¸ Please wait while the Storybook build is in progress... - - - name: Comment PR - Storybook Complete - if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' - uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0 - with: - issue-number: ${{ steps.pr.outputs.result }} - body-includes: '' - comment-author: 'github-actions[bot]' - edit-mode: replace - body: | - - ## 🎨 Storybook Build Status - - ${{ - fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '✅' - || fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && 'â­ī¸' - || fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && 'đŸšĢ' - || '❌' - }} **${{ - fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && 'Build completed successfully!' - || fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && 'Build skipped.' - || fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && 'Build cancelled.' - || 'Build failed!' - }}** - - ⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC - - ### 🔗 Links - - [📊 View Workflow Run](${{ fromJSON(steps.workflow-run.outputs.result).html_url }}) - - --- - ${{ - fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '🎉 Your Storybook is ready for review!' - || fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && 'â„šī¸ Chromatic was skipped for this PR.' - || fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && 'â„šī¸ The Chromatic run was cancelled.' - || 'âš ī¸ Please check the workflow logs for error details.' - }} diff --git a/.github/workflows/pr-storybook-deploy.yaml b/.github/workflows/pr-storybook-deploy.yaml new file mode 100644 index 000000000..226c000dd --- /dev/null +++ b/.github/workflows/pr-storybook-deploy.yaml @@ -0,0 +1,90 @@ +name: PR Storybook Deploy (Forks) + +on: + workflow_run: + workflows: ['Storybook and Chromatic CI'] + types: [requested, completed] + +env: + DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p' + +jobs: + deploy-and-comment-forked-pr: + runs-on: ubuntu-latest + if: | + github.repository == 'Comfy-Org/ComfyUI_frontend' && + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.head_repository != null && + github.event.workflow_run.repository != null && + github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name + permissions: + pull-requests: write + actions: read + steps: + - name: Log workflow trigger info + run: | + echo "Repository: ${{ github.repository }}" + echo "Event: ${{ github.event.workflow_run.event }}" + echo "Head repo: ${{ github.event.workflow_run.head_repository.full_name || 'null' }}" + echo "Base repo: ${{ github.event.workflow_run.repository.full_name || 'null' }}" + echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}" + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Get PR Number + id: pr + uses: actions/github-script@v7 + with: + script: | + const { data: prs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + }); + + const pr = prs.find(p => p.head.sha === context.payload.workflow_run.head_sha); + + if (!pr) { + console.log('No PR found for SHA:', context.payload.workflow_run.head_sha); + return null; + } + + console.log(`Found PR #${pr.number} from fork: ${context.payload.workflow_run.head_repository.full_name}`); + return pr.number; + + - name: Handle Storybook Start + if: steps.pr.outputs.result != 'null' && github.event.action == 'requested' + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh + ./scripts/cicd/pr-storybook-deploy-and-comment.sh \ + "${{ steps.pr.outputs.result }}" \ + "${{ github.event.workflow_run.head_branch }}" \ + "starting" \ + "$(date -u '${{ env.DATE_FORMAT }}')" + + - name: Download and Deploy Storybook + if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' && github.event.workflow_run.conclusion == 'success' + uses: actions/download-artifact@v4 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + name: storybook-static + path: storybook-static + + - name: Handle Storybook Completion + if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + GITHUB_TOKEN: ${{ github.token }} + WORKFLOW_CONCLUSION: ${{ github.event.workflow_run.conclusion }} + WORKFLOW_URL: ${{ github.event.workflow_run.html_url }} + run: | + chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh + ./scripts/cicd/pr-storybook-deploy-and-comment.sh \ + "${{ steps.pr.outputs.result }}" \ + "${{ github.event.workflow_run.head_branch }}" \ + "completed" \ No newline at end of file diff --git a/scripts/cicd/pr-storybook-deploy-and-comment.sh b/scripts/cicd/pr-storybook-deploy-and-comment.sh new file mode 100755 index 000000000..a2a1d37e9 --- /dev/null +++ b/scripts/cicd/pr-storybook-deploy-and-comment.sh @@ -0,0 +1,247 @@ +#!/bin/bash +set -e + +# Deploy Storybook to Cloudflare Pages and comment on PR +# Usage: ./pr-storybook-deploy-and-comment.sh [start_time] + +# Input validation +# Validate PR number is numeric +case "$1" in + ''|*[!0-9]*) + echo "Error: PR_NUMBER must be numeric" >&2 + exit 1 + ;; +esac +PR_NUMBER="$1" + +# Sanitize and validate branch name (allow alphanumeric, dots, dashes, underscores, slashes) +BRANCH_NAME=$(echo "$2" | sed 's/[^a-zA-Z0-9._/-]//g') +if [ -z "$BRANCH_NAME" ]; then + echo "Error: Invalid or empty branch name" >&2 + exit 1 +fi + +# Validate status parameter +STATUS="${3:-completed}" +case "$STATUS" in + starting|completed) ;; + *) + echo "Error: STATUS must be 'starting' or 'completed'" >&2 + exit 1 + ;; +esac + +START_TIME="${4:-$(date -u '+%m/%d/%Y, %I:%M:%S %p')}" + +# Required environment variables +: "${GITHUB_TOKEN:?GITHUB_TOKEN is required}" +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" + +# Cloudflare variables only required for deployment +if [ "$STATUS" = "completed" ]; then + : "${CLOUDFLARE_API_TOKEN:?CLOUDFLARE_API_TOKEN is required for deployment}" + : "${CLOUDFLARE_ACCOUNT_ID:?CLOUDFLARE_ACCOUNT_ID is required for deployment}" +fi + +# Configuration +COMMENT_MARKER="" + +# Install wrangler if not available (output to stderr for debugging) +if ! command -v wrangler > /dev/null 2>&1; then + echo "Installing wrangler v4..." >&2 + npm install -g wrangler@^4.0.0 >&2 || { + echo "Failed to install wrangler" >&2 + echo "failed" + return + } +fi + +# Deploy Storybook report, WARN: ensure inputs are sanitized before calling this function +deploy_storybook() { + dir="$1" + branch="$2" + + [ ! -d "$dir" ] && echo "failed" && return + + project="comfy-storybook" + + echo "Deploying Storybook to project $project on branch $branch..." >&2 + + # Try deployment up to 3 times + i=1 + while [ $i -le 3 ]; do + echo "Deployment attempt $i of 3..." >&2 + # Branch is already sanitized, use it directly + if output=$(wrangler pages deploy "$dir" \ + --project-name="$project" \ + --branch="$branch" 2>&1); then + + # Extract URL from output (improved regex for valid URL characters) + url=$(echo "$output" | grep -oE 'https://[a-zA-Z0-9.-]+\.pages\.dev\S*' | head -1) + result="${url:-https://${branch}.${project}.pages.dev}" + echo "Success! URL: $result" >&2 + echo "$result" # Only this goes to stdout for capture + return + else + echo "Deployment failed on attempt $i: $output" >&2 + fi + [ $i -lt 3 ] && sleep 10 + i=$((i + 1)) + done + + echo "failed" +} + +# Post or update GitHub comment +post_comment() { + body="$1" + temp_file=$(mktemp) + echo "$body" > "$temp_file" + + if command -v gh > /dev/null 2>&1; then + # Find existing comment ID + existing=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ + --jq ".[] | select(.body | contains(\"$COMMENT_MARKER\")) | .id" | head -1) + + if [ -n "$existing" ]; then + # Update specific comment by ID + gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$existing" \ + --field body="$(cat "$temp_file")" + else + gh pr comment "$PR_NUMBER" --body-file "$temp_file" + fi + else + echo "GitHub CLI not available, outputting comment:" + cat "$temp_file" + fi + + rm -f "$temp_file" +} + +# Main execution +if [ "$STATUS" = "starting" ]; then + # Check if this is a version-bump branch + IS_VERSION_BUMP="false" + if echo "$BRANCH_NAME" | grep -q "^version-bump-"; then + IS_VERSION_BUMP="true" + fi + + # Post starting comment with appropriate message + if [ "$IS_VERSION_BUMP" = "true" ]; then + comment=$(cat < **Build is starting...** + +⏰ Started at: $START_TIME UTC + +### 🚀 Building Storybook +- đŸ“Ļ Installing dependencies... +- 🔧 Building Storybook components... +- 🎨 Running Chromatic visual tests... + +--- +âąī¸ Please wait while the Storybook build is in progress... +EOF +) + else + comment=$(cat < **Build is starting...** + +⏰ Started at: $START_TIME UTC + +### 🚀 Building Storybook +- đŸ“Ļ Installing dependencies... +- 🔧 Building Storybook components... +- 🌐 Preparing deployment to Cloudflare Pages... + +--- +âąī¸ Please wait while the Storybook build is in progress... +EOF +) + fi + post_comment "$comment" + +elif [ "$STATUS" = "completed" ]; then + # Deploy and post completion comment + # Convert branch name to Cloudflare-compatible format (lowercase, only alphanumeric and dashes) + cloudflare_branch=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | \ + sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') + + echo "Looking for Storybook build in: $(pwd)/storybook-static" + + # Deploy Storybook if build exists + deployment_url="Not deployed" + if [ -d "storybook-static" ]; then + echo "Found Storybook build, deploying..." + url=$(deploy_storybook "storybook-static" "$cloudflare_branch") + if [ "$url" != "failed" ] && [ -n "$url" ]; then + deployment_url="[View Storybook]($url)" + else + deployment_url="Deployment failed" + fi + else + echo "Storybook build not found at storybook-static" + fi + + # Get workflow conclusion from environment or default to success + WORKFLOW_CONCLUSION="${WORKFLOW_CONCLUSION:-success}" + WORKFLOW_URL="${WORKFLOW_URL:-}" + + # Generate completion comment based on conclusion + if [ "$WORKFLOW_CONCLUSION" = "success" ]; then + status_icon="✅" + status_text="Build completed successfully!" + footer_text="🎉 Your Storybook is ready for review!" + elif [ "$WORKFLOW_CONCLUSION" = "skipped" ]; then + status_icon="â­ī¸" + status_text="Build skipped." + footer_text="â„šī¸ Chromatic was skipped for this PR." + elif [ "$WORKFLOW_CONCLUSION" = "cancelled" ]; then + status_icon="đŸšĢ" + status_text="Build cancelled." + footer_text="â„šī¸ The Chromatic run was cancelled." + else + status_icon="❌" + status_text="Build failed!" + footer_text="âš ī¸ Please check the workflow logs for error details." + fi + + comment="$COMMENT_MARKER +## 🎨 Storybook Build Status + +$status_icon **$status_text** + +⏰ Completed at: $(date -u '+%m/%d/%Y, %I:%M:%S %p') UTC + +### 🔗 Links +- [📊 View Workflow Run]($WORKFLOW_URL)" + + # Add deployment status + if [ "$deployment_url" != "Not deployed" ]; then + if [ "$deployment_url" = "Deployment failed" ]; then + comment="$comment +- ❌ Storybook deployment failed" + elif [ "$WORKFLOW_CONCLUSION" = "success" ]; then + comment="$comment +- 🎨 $deployment_url" + else + comment="$comment +- âš ī¸ Build failed - $deployment_url" + fi + elif [ "$WORKFLOW_CONCLUSION" != "success" ]; then + comment="$comment +- â­ī¸ Storybook deployment skipped (build did not succeed)" + fi + + comment="$comment + +--- +$footer_text" + + post_comment "$comment" +fi \ No newline at end of file