🤖 Claude Code: Update all examples to modern runtime and ESM #6
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Claude Code | |
| on: | |
| # Manual/programmatic trigger | |
| workflow_dispatch: | |
| inputs: | |
| trigger_text: | |
| description: 'Text to trigger Claude with (optional)' | |
| required: true | |
| type: string | |
| default: '@claude' | |
| actor: | |
| description: 'Actor to trigger Claude' | |
| required: true | |
| type: string | |
| default: 'david' | |
| # PR related events | |
| pull_request_target: | |
| types: [opened, synchronize, reopened] | |
| pull_request_review_comment: | |
| types: [created] | |
| pull_request_review: | |
| types: [submitted] | |
| # Issue related events (added) | |
| issues: | |
| types: [opened, assigned] | |
| issue_comment: | |
| types: [created] | |
| # Concurrency control (one run per Issue/PR) | |
| concurrency: | |
| group: claude-${{ github.repository }}-${{ github.event.number || github.run_id }} | |
| cancel-in-progress: false | |
| jobs: | |
| setup: | |
| # Security-focused conditional execution (full support for Issues, PRs, and manual dispatch) | |
| if: | | |
| ( | |
| github.event_name == 'workflow_dispatch' && | |
| github.event.inputs.trigger_text != '' && | |
| github.event.inputs.actor != '' | |
| ) || | |
| ( | |
| github.event_name == 'pull_request_target' && | |
| ( | |
| github.event.pull_request.head.repo.full_name == github.repository || | |
| contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.pull_request.author_association) | |
| ) && | |
| contains(github.event.pull_request.body, '@claude') | |
| ) || | |
| ( | |
| github.event_name == 'issue_comment' && | |
| ( | |
| github.event.sender.login == github.repository_owner || | |
| contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.comment.author_association) | |
| ) && | |
| contains(github.event.comment.body, '@claude') | |
| ) || | |
| ( | |
| github.event_name == 'issues' && | |
| ( | |
| github.event.sender.login == github.repository_owner || | |
| contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.issue.author_association) | |
| ) && | |
| ( | |
| contains(github.event.issue.body, '@claude') || | |
| contains(github.event.issue.title, '@claude') | |
| ) | |
| ) || | |
| ( | |
| github.event_name == 'pull_request_review_comment' && | |
| ( | |
| github.event.sender.login == github.repository_owner || | |
| contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.comment.author_association) | |
| ) && | |
| contains(github.event.comment.body, '@claude') | |
| ) || | |
| ( | |
| github.event_name == 'pull_request_review' && | |
| ( | |
| github.event.sender.login == github.repository_owner || | |
| contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.review.author_association) | |
| ) && | |
| contains(github.event.review.body, '@claude') | |
| ) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| permissions: | |
| # 📁 Content management (highest permissions) | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| discussions: write | |
| # 🔧 Development & CI/CD management | |
| actions: write | |
| checks: write | |
| statuses: write | |
| pages: write | |
| deployments: write | |
| # 📦 Package & security management | |
| packages: write | |
| security-events: write | |
| # 🎯 Project management | |
| repository-projects: write | |
| # 🆔 Authentication & token management | |
| id-token: write | |
| # Outputs | |
| outputs: | |
| should-continue: ${{ steps.should-continue.outputs.should-continue }} | |
| issue-number: ${{ steps.context-info.outputs.issue-number }} | |
| pr-number: ${{ steps.context-info.outputs.pr-number }} | |
| head-ref: ${{ steps.context-info.outputs.head-ref }} | |
| base-ref: ${{ steps.context-info.outputs.base-ref }} | |
| head-sha: ${{ steps.context-info.outputs.head-sha }} | |
| is-pr: ${{ steps.context-info.outputs.is-pr }} | |
| trigger-text: ${{ steps.context-info.outputs.trigger-text }} | |
| has-linked-pr: ${{ steps.context-info.outputs.has-linked-pr }} | |
| status-comment-id: ${{ steps.find_comment.outputs.comment-id || steps.create_comment.outputs.comment-id }} | |
| ###################### | |
| # Setup steps | |
| ###################### | |
| steps: | |
| # Verify user permissions from dispatch | |
| - name: Check dispatch permissions | |
| id: check_membership | |
| if: github.event_name == 'workflow_dispatch' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const allowedUsersInput = '${{ secrets.ALLOWED_USERS }}' | |
| // Skip if no allowed users configured | |
| if (!allowedUsersInput || allowedUsersInput.trim() === '') { | |
| console.log('No allowed users configured in secrets, allowing all') | |
| core.setOutput('is_member', true) | |
| return | |
| } | |
| // Parse the allowed users (comma-separated) | |
| const allowedUsers = allowedUsersInput.split(',').map(u => u.trim()).filter(u => u) | |
| if (allowedUsers.length === 0) { | |
| console.log('Empty allowed users list, allowing all') | |
| core.setOutput('is_member', true) | |
| return | |
| } | |
| let actor | |
| if (context.eventName === 'issue_comment') { | |
| actor = context.payload.comment.user.login | |
| } else if (context.eventName === 'pull_request_review_comment') { | |
| actor = context.payload.comment.user.login | |
| } else if (context.eventName === 'pull_request_review') { | |
| actor = context.payload.review.user.login | |
| } else if (context.eventName === 'issues') { | |
| actor = context.payload.issue.user.login | |
| } | |
| console.log(`Checking permissions for user: ${actor}`) | |
| console.log(`Allowed users: ${allowedUsers.join(', ')}`) | |
| if (allowedUsers.includes(actor)) { | |
| console.log(`User ${actor} is in the allowed list`) | |
| core.setOutput('is_member', true) | |
| return | |
| } | |
| // Get organization name from context | |
| const orgName = context.repo.owner | |
| console.log(`Organization: ${orgName}`) | |
| // Fallback: Check if user has repository permissions | |
| try { | |
| const collaboration = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| username: actor | |
| }) | |
| const permission = collaboration.data.permission | |
| console.log(`User ${actor} has permission level: ${permission}`) | |
| // Allow if user has push access or higher (write, maintain, admin) | |
| const allowed = ['write', 'maintain', 'admin'].includes(permission) | |
| if (allowed) { | |
| core.setOutput('is_member', true) | |
| } else { | |
| core.setFailed(`User ${actor} does not have permission to run this workflow`) | |
| return | |
| } | |
| } catch (error) { | |
| console.log(`Error checking permissions: ${error.message}`) | |
| // Final fallback: Check if user is a public member of the organization | |
| try { | |
| const membership = await github.rest.orgs.getMembershipForUser({ | |
| org: orgName, | |
| username: actor | |
| }) | |
| const allowed = membership.data.state === 'active' | |
| if (allowed) { | |
| core.setOutput('is_member', true) | |
| } else { | |
| core.setFailed(`User ${actor} does not have permission to run this workflow`) | |
| return | |
| } | |
| } catch (membershipError) { | |
| console.log(`Error checking organization membership: ${membershipError.message}`) | |
| core.setFailed(`User ${actor} does not have permission to run this workflow`) | |
| return | |
| } | |
| } | |
| # Collect information from the event | |
| - name: Get Context Information | |
| id: context-info | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| let issueNumber, prNumber, headRef, baseRef, headSha | |
| let triggerText = '' | |
| let hasLinkedPR = false | |
| let isPR = false | |
| if (context.eventName === 'workflow_dispatch') { | |
| console.log('Workflow dispatch event') | |
| console.log(context.payload) | |
| triggerText = context.payload.inputs.trigger_text | |
| // Automatically add @claude to the trigger text if it's not already there | |
| if (!triggerText.includes('@claude')) { | |
| triggerText = '@claude ' + triggerText | |
| } | |
| console.log(`Workflow dispatch #${issueNumber}: ${triggerText}`) | |
| } else if (context.eventName === 'pull_request_target') { | |
| // When a PR is created or updated | |
| isPR = true | |
| issueNumber = context.payload.pull_request.number | |
| prNumber = context.payload.pull_request.number | |
| headRef = context.payload.pull_request.head.ref | |
| baseRef = context.payload.pull_request.base.ref | |
| headSha = context.payload.pull_request.head.sha | |
| triggerText = context.payload.pull_request.body | |
| console.log(`PR #${prNumber}: ${baseRef} <- ${headRef} (${headSha})`) | |
| } else if (context.eventName === 'issues') { | |
| // When an Issue is created or assigned | |
| isPR = false | |
| issueNumber = context.payload.issue.number | |
| triggerText = `${context.payload.issue.title} ${context.payload.issue.body}` | |
| console.log(`Issue #${issueNumber} created`) | |
| } else if (context.eventName === 'issue_comment') { | |
| // Issue/PR comment | |
| issueNumber = context.payload.issue.number | |
| triggerText = context.payload.comment.body | |
| if (context.payload.issue.pull_request) { | |
| // Comment on a PR | |
| isPR = true | |
| try { | |
| const pr = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: issueNumber | |
| }) | |
| prNumber = issueNumber | |
| headRef = pr.data.head.ref | |
| baseRef = pr.data.base.ref | |
| headSha = pr.data.head.sha | |
| console.log(`PR Comment #${prNumber}: ${baseRef} <- ${headRef}`) | |
| } catch (error) { | |
| console.error('Error fetching PR info:', error) | |
| // In case of error, treat as a regular Issue | |
| isPR = false | |
| } | |
| } else { | |
| // Regular Issue comment - check for existing linked PRs | |
| isPR = false | |
| try { | |
| // Get timeline events to find linked pull requests | |
| const { data: timeline } = await github.rest.issues.listEventsForTimeline({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| per_page: 100, | |
| headers: { | |
| accept: 'application/vnd.github.mockingbird-preview+json' | |
| } | |
| }) | |
| console.log(`Timeline: ${JSON.stringify(timeline, null, 2)}`) | |
| const linkedPRs = timeline | |
| // filter out event.event is not cross-referenced | |
| .filter(event => event.event === 'cross-referenced') | |
| // filter out event.source?.issue?.pull_request is null | |
| .filter(event => event.source?.issue?.pull_request?.url) | |
| // return url and pr name, and the issue number and the body and the actor | |
| .map(event => ({ | |
| issueNumber: event.source?.issue?.number, | |
| actor: event.actor?.login, | |
| url: event.source?.issue?.pull_request?.url, | |
| title: event.source?.issue?.title, | |
| body: event.source?.issue?.body, | |
| })) | |
| hasLinkedPR = linkedPRs.length > 0 | |
| console.log(`Linked PRs:`, linkedPRs) | |
| console.log(`Issue Comment #${issueNumber}, already has linked PR: ${hasLinkedPR}`) | |
| } catch (error) { | |
| console.error('Error checking for linked PRs:', error) | |
| } | |
| } | |
| } else if (context.eventName === 'pull_request_review_comment' || context.eventName === 'pull_request_review') { | |
| // PR review related | |
| isPR = true | |
| issueNumber = context.payload.pull_request.number | |
| prNumber = context.payload.pull_request.number | |
| headRef = context.payload.pull_request.head.ref | |
| baseRef = context.payload.pull_request.base.ref | |
| headSha = context.payload.pull_request.head.sha | |
| if (context.eventName === 'pull_request_review_comment') { | |
| triggerText = context.payload.comment.body | |
| } else { | |
| triggerText = context.payload.review.body | |
| } | |
| console.log(`PR Review #${prNumber}: ${baseRef} <- ${headRef}`) | |
| } | |
| // Set outputs | |
| core.setOutput('issue-number', issueNumber) | |
| core.setOutput('pr-number', prNumber || '') | |
| core.setOutput('head-ref', headRef || '') | |
| core.setOutput('base-ref', baseRef || '') | |
| core.setOutput('head-sha', headSha || '') | |
| core.setOutput('is-pr', isPR) | |
| core.setOutput('trigger-text', triggerText) | |
| core.setOutput('has-linked-pr', hasLinkedPR) | |
| console.log(`Final Context:`) | |
| console.log(`Event: ${context.eventName}`) | |
| console.log(`Issue #${issueNumber}`) | |
| console.log(`isPR: ${isPR}`) | |
| console.log(`Trigger Text: ${triggerText}`) | |
| console.log(`Already has linked PR: ${hasLinkedPR}`) | |
| - name: Validate Environment | |
| run: | | |
| echo "🔍 Runtime Environment Information" | |
| echo "==================================" | |
| echo "Event: ${{ github.event_name }}" | |
| echo "Actor: ${{ github.actor }}" | |
| echo "Repository: ${{ github.repository }}" | |
| echo "Issue Number: ${{ steps.context-info.outputs.issue-number }}" | |
| echo "Is PR: ${{ steps.context-info.outputs.is-pr }}" | |
| echo "PR Number: ${{ steps.context-info.outputs.pr-number }}" | |
| echo "Head Ref: ${{ steps.context-info.outputs.head-ref }}" | |
| echo "Base Ref: ${{ steps.context-info.outputs.base-ref }}" | |
| echo "Head SHA: ${{ steps.context-info.outputs.head-sha }}" | |
| echo "Has Linked PR: ${{ steps.context-info.outputs.has-linked-pr }}" | |
| echo "==================================" | |
| # Check for secrets | |
| if [ -z "${{ secrets.CLAUDE_CREDS_API_KEY }}" ]; then | |
| echo "::error::CLAUDE_CREDS_API_KEY is not set" | |
| exit 1 | |
| fi | |
| if [ -z "${{ secrets.CLAUDE_CREDS_API }}" ]; then | |
| echo "::error::CLAUDE_CREDS_API is not set" | |
| exit 1 | |
| fi | |
| echo "✅ Environment validation complete" | |
| - name: Exit early if Issue already has linked PR | |
| id: should-continue | |
| run: | | |
| IS_PR="${{ steps.context-info.outputs.is-pr }}" | |
| HAS_LINKED_PR="${{ steps.context-info.outputs.has-linked-pr }}" | |
| if [[ "$IS_PR" == "false" && "$HAS_LINKED_PR" == "true" ]]; then | |
| echo "Issue already has linked PR. Will skip remaining steps." | |
| echo "should-continue=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "No linked PRs found or this is a PR. Continuing." | |
| echo "should-continue=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Debug issue number | |
| if: github.event_name != 'workflow_dispatch' | |
| run: echo "The issue number is ${{ steps.context-info.outputs.issue-number }}" | |
| # Only add comment if it doesn't exist | |
| - name: Find existing status comment | |
| if: steps.should-continue.outputs.should-continue == 'true' && github.event_name != 'workflow_dispatch' | |
| uses: peter-evans/find-comment@v3 | |
| id: find_comment # We'll check the output of this step | |
| with: | |
| issue-number: ${{ steps.context-info.outputs.issue-number }} | |
| comment-author: 'github-actions[bot]' | |
| body-includes: '<!-- claude-run-status -->' | |
| - name: Create initial "in-progress" comment if it doesn't exist | |
| # This step ONLY runs if the 'find-comment' step found nothing | |
| if: steps.should-continue.outputs.should-continue == 'true' && steps.find_comment.outputs.comment-id == '' && github.event_name != 'workflow_dispatch' | |
| uses: peter-evans/create-or-update-comment@v4 | |
| id: create_comment | |
| with: | |
| issue-number: ${{ steps.context-info.outputs.issue-number }} | |
| body: | | |
| Claude Code is running... ⏳ | |
| <!-- claude-run-status --> | |
| ######################################################### | |
| # Claude Code | |
| ######################################################### | |
| claude: | |
| needs: setup | |
| # Security-focused conditional execution (full support for Issues and PRs) | |
| if: needs.setup.outputs.should-continue == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| # 📁 Content management (highest permissions) | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| discussions: write | |
| # 🔧 Development & CI/CD management | |
| actions: write | |
| checks: write | |
| statuses: write | |
| pages: write | |
| deployments: write | |
| # 📦 Package & security management | |
| packages: write | |
| security-events: write | |
| # 🎯 Project management | |
| repository-projects: write | |
| # 🆔 Authentication & token management | |
| id-token: write | |
| steps: | |
| - name: Checkout Repository | |
| uses: actions/checkout@v4 | |
| with: | |
| # Checkout the feature branch for PRs, or the default branch for Issues | |
| ref: ${{ needs.setup.outputs.head-sha || github.ref }} | |
| fetch-depth: ${{ needs.setup.outputs.is-pr == 'true' && 0 || 1 }} | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Validate Environment | |
| run: | | |
| echo "🔍 Runtime Environment Information" | |
| echo "==================================" | |
| echo "Event: ${{ github.event_name }}" | |
| echo "Actor: ${{ github.actor }}" | |
| echo "Repository: ${{ github.repository }}" | |
| echo "Issue Number: ${{ needs.setup.outputs.issue-number }}" | |
| echo "Is PR: ${{ needs.setup.outputs.is-pr }}" | |
| echo "PR Number: ${{ needs.setup.outputs.pr-number }}" | |
| echo "Head Ref: ${{ needs.setup.outputs.head-ref }}" | |
| echo "Base Ref: ${{ needs.setup.outputs.base-ref }}" | |
| echo "Head SHA: ${{ needs.setup.outputs.head-sha }}" | |
| echo "Has Linked PR: ${{ needs.setup.outputs.has-linked-pr }}" | |
| echo "Trigger Text: ${{ needs.setup.outputs.trigger-text }}" | |
| echo "==================================" | |
| - name: Fetch Base Branch (PR only) | |
| if: needs.setup.outputs.is-pr == 'true' && needs.setup.outputs.base-ref | |
| run: | | |
| echo "📥 Fetching base branch: ${{ needs.setup.outputs.base-ref }}" | |
| git fetch origin ${{ needs.setup.outputs.base-ref }}:${{ needs.setup.outputs.base-ref }} | |
| echo "📋 Changed files:" | |
| git diff --name-only origin/${{ needs.setup.outputs.base-ref }}..HEAD || echo "Failed to get diff" | |
| echo "📊 Change statistics:" | |
| git diff --stat origin/${{ needs.setup.outputs.base-ref }}..HEAD || echo "Failed to get stats" | |
| - name: Get Project Information | |
| id: project-info | |
| run: | | |
| echo "📁 Collecting project information" | |
| # Determine project type | |
| project_type="unknown" | |
| framework="" | |
| if [ -f "package.json" ]; then | |
| project_type="node" | |
| echo "📦 Node.js project detected" | |
| # Detect framework | |
| if grep -q "next" package.json; then | |
| framework="Next.js" | |
| elif grep -q "react" package.json; then | |
| framework="React" | |
| elif grep -q "vue" package.json; then | |
| framework="Vue.js" | |
| elif grep -q "angular" package.json; then | |
| framework="Angular" | |
| elif grep -q "express" package.json; then | |
| framework="Express" | |
| fi | |
| elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then | |
| project_type="python" | |
| framework="Python" | |
| echo "🐍 Python project detected" | |
| elif [ -f "Cargo.toml" ]; then | |
| project_type="rust" | |
| framework="Rust" | |
| echo "🦀 Rust project detected" | |
| elif [ -f "go.mod" ]; then | |
| project_type="go" | |
| framework="Go" | |
| echo "🐹 Go project detected" | |
| elif [ -f "pom.xml" ] || [ -f "build.gradle" ]; then | |
| project_type="java" | |
| framework="Java" | |
| echo "☕ Java project detected" | |
| fi | |
| echo "project-type=$project_type" >> $GITHUB_OUTPUT | |
| echo "framework=$framework" >> $GITHUB_OUTPUT | |
| # Estimate number of files | |
| total_files=$(find . -type f \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" -o -name "*.py" -o -name "*.rs" -o -name "*.go" -o -name "*.java" \) | wc -l) | |
| echo "total-files=$total_files" >> $GITHUB_OUTPUT | |
| echo "📊 Project summary: $framework ($total_files files)" | |
| - name: Setup Env | |
| id: setup-env | |
| uses: DavidWells/actions/get-claude-tokens@master | |
| with: | |
| api-key: ${{ secrets.CLAUDE_CREDS_API_KEY }} | |
| api-endpoint: ${{ secrets.CLAUDE_CREDS_API }} | |
| # - name: Run Claude PR Action | |
| # uses: davidwells/claude-code-action@main | |
| # with: | |
| # use_oauth: true | |
| # claude_access_token: ${{ steps.setup-env.outputs.access-token }} | |
| # claude_refresh_token: ${{ steps.setup-env.outputs.refresh-token }} | |
| # claude_expires_at: ${{ steps.setup-env.outputs.expires-at }} | |
| # model: ${{ steps.setup-env.outputs.model || 'claude-sonnet-4-20250514' }} | |
| # allowed_tools: ${{ steps.setup-env.outputs.allowed_tools || 'Bash,Edit,Read,Write,Glob,Grep,LS,MultiEdit,NotebookRead,NotebookEdit' }} | |
| # timeout_minutes: "60" | |
| - name: Run Claude Code | |
| id: claude | |
| uses: anthropics/claude-code-action@main | |
| timeout-minutes: 20 | |
| continue-on-error: true | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| model: ${{ steps.setup-env.outputs.model || 'claude-sonnet-4-20250514' }} | |
| # If the workflow is triggered by a workflow_dispatch event, use the agent mode, otherwise use the tag mode | |
| mode: ${{ github.event_name == 'workflow_dispatch' && 'agent' || 'tag' }} | |
| # direct prompt from workflow_dispatch event | |
| direct_prompt: ${{ github.event_name == 'workflow_dispatch' && needs.setup.outputs.trigger-text || '' }} | |
| # This is an optional setting that allows Claude to read CI results on PRs | |
| additional_permissions: | | |
| actions: read | |
| # claude_access_token: ${{ secrets.CLAUDE_ACCESS_TOKEN }} | |
| # claude_refresh_token: ${{ secrets.CLAUDE_REFRESH_TOKEN }} | |
| # claude_expires_at: ${{ secrets.CLAUDE_EXPIRES_AT }} | |
| # GITHUB ACTIONS (Maximum Freedom): | |
| allowed_tools: | | |
| Edit,View,Replace,Write,Create, | |
| BatchTool,GlobTool,GrepTool,NotebookEditCell, | |
| Bash(git:*),Bash(npm:*),Bash(yarn:*),Bash(python:*), | |
| Bash(docker:*),Bash(make:*),Bash(cargo:*),Bash(go:*), | |
| Bash(ls:*),Bash(cat:*),Bash(echo:*),Bash(curl:*), | |
| mcp__* | |
| disallowed_tools: | | |
| Bash(sudo:*), | |
| Bash(rm -rf /) | |
| env: | |
| # Pass context information to Claude Code | |
| GITHUB_CONTEXT_TYPE: ${{ needs.setup.outputs.is-pr == 'true' && 'PR' || 'ISSUE' }} | |
| ISSUE_NUMBER: ${{ needs.setup.outputs.issue-number }} | |
| PR_NUMBER: ${{ needs.setup.outputs.pr-number }} | |
| BASE_BRANCH: ${{ needs.setup.outputs.base-ref }} | |
| HEAD_BRANCH: ${{ needs.setup.outputs.head-ref }} | |
| HEAD_SHA: ${{ needs.setup.outputs.head-sha }} | |
| GITHUB_EVENT_NAME: ${{ github.event_name }} | |
| TRIGGER_TEXT: ${{ needs.setup.outputs.trigger-text }} | |
| PROJECT_TYPE: ${{ steps.project-info.outputs.project-type }} | |
| PROJECT_FRAMEWORK: ${{ steps.project-info.outputs.framework }} | |
| TOTAL_FILES: ${{ steps.project-info.outputs.total-files }} | |
| GITHUB_ACTOR: ${{ github.actor }} | |
| REPOSITORY_NAME: ${{ github.repository }} | |
| # 🔑 Enhanced permission information | |
| CLAUDE_PERMISSIONS_LEVEL: "ENHANCED" | |
| REPO_ADMIN_MODE: "true" | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # 📊 Repository information | |
| REPOSITORY_OWNER: ${{ github.repository_owner }} | |
| REPOSITORY_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| REPOSITORY_PRIVATE: ${{ github.event.repository.private }} | |
| REPOSITORY_FORK: ${{ github.event.repository.fork }} | |
| # 🎯 Execution context | |
| WORKFLOW_RUN_ID: ${{ github.run_id }} | |
| WORKFLOW_RUN_NUMBER: ${{ github.run_number }} | |
| COMMIT_SHA: ${{ github.sha }} | |
| REF_NAME: ${{ github.ref_name }} | |
| # 🔧 Available feature flags | |
| CAN_CREATE_RELEASES: "true" | |
| CAN_MANAGE_LABELS: "true" | |
| CAN_MANAGE_MILESTONES: "true" | |
| CAN_MANAGE_PROJECTS: "true" | |
| CAN_MANAGE_WIKI: "true" | |
| CAN_MANAGE_PAGES: "true" | |
| CAN_MANAGE_DEPLOYMENTS: "true" | |
| CAN_MANAGE_SECURITY: "true" | |
| CAN_MANAGE_PACKAGES: "true" | |
| CAN_MANAGE_ACTIONS: "true" | |
| - name: Run Advanced Repository Management | |
| id: advanced-management | |
| if: steps.claude.outcome == 'success' && needs.setup.outputs.issue-number | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const issueNumber = ${{ needs.setup.outputs.issue-number }}; | |
| const isPR = '${{ needs.setup.outputs.is-pr }}' === 'true'; | |
| const triggerText = (${{ toJSON(needs.setup.outputs.trigger-text) }} || '').toLowerCase(); | |
| const framework = '${{ steps.project-info.outputs.framework }}'; | |
| const hashSymbol = String.fromCharCode(35); | |
| console.log('🚀 Starting advanced repository management...'); | |
| const managementResults = { | |
| labels: [], | |
| milestones: [], | |
| projects: [], | |
| releases: [], | |
| security: [], | |
| wiki: [], | |
| pages: [], | |
| actions: [] | |
| }; | |
| try { | |
| // 1. 🏷️ Intelligent Label Management | |
| console.log('📋 Running automatic label management...'); | |
| // Automatically create necessary labels | |
| const requiredLabels = [ | |
| { name: 'claude-code', color: '7B68EE', description: 'Items created or modified by Claude Code' }, | |
| { name: 'auto-generated', color: '00D084', description: 'Automatically generated content' }, | |
| { name: 'security-fix', color: 'FF4444', description: 'Security-related fixes' }, | |
| { name: 'performance', color: 'FFA500', description: 'Performance improvements' }, | |
| { name: 'technical-debt', color: '8B4513', description: 'Resolving technical debt' }, | |
| { name: 'documentation', color: '0366D6', description: 'Documentation related' }, | |
| { name: 'ci-cd', color: '28A745', description: 'CI/CD improvements' } | |
| ]; | |
| for (const label of requiredLabels) { | |
| try { | |
| await github.rest.issues.createLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: label.name, | |
| color: label.color, | |
| description: label.description | |
| }); | |
| managementResults.labels.push(`✅ Created: ${label.name}`); | |
| } catch (error) { | |
| if (error.status === 422) { | |
| managementResults.labels.push(`📋 Exists: ${label.name}`); | |
| } else { | |
| managementResults.labels.push(`❌ Error: ${label.name} - ${error.message}`); | |
| } | |
| } | |
| } | |
| // Automatically apply relevant labels | |
| const autoLabels = ['claude-code', 'auto-generated']; | |
| if (triggerText.includes('security')) { | |
| autoLabels.push('security-fix'); | |
| } | |
| if (triggerText.includes('performance')) { | |
| autoLabels.push('performance'); | |
| } | |
| if (triggerText.includes('technical debt')) { | |
| autoLabels.push('technical-debt'); | |
| } | |
| if (triggerText.includes('document')) { | |
| autoLabels.push('documentation'); | |
| } | |
| if (triggerText.includes('ci') || triggerText.includes('cd') || triggerText.includes('deploy')) { | |
| autoLabels.push('ci-cd'); | |
| } | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| labels: autoLabels | |
| }); | |
| managementResults.labels.push(`🏷️ Applied: ${autoLabels.join(', ')}`); | |
| // 2. 🎯 Automatic Milestone Management | |
| console.log('🎯 Running milestone management...'); | |
| try { | |
| // Create a milestone for the current year and month | |
| const now = new Date(); | |
| const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; | |
| const currentMilestone = `${now.getFullYear()}-${monthNames[now.getMonth()]}`; | |
| try { | |
| const milestone = await github.rest.issues.createMilestone({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: currentMilestone, | |
| description: `Tasks and improvements for ${currentMilestone}`, | |
| due_on: new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString() | |
| }); | |
| managementResults.milestones.push(`✅ Created: ${currentMilestone}`); | |
| } catch (error) { | |
| if (error.status === 422) { | |
| managementResults.milestones.push(`📅 Exists: ${currentMilestone}`); | |
| } else { | |
| managementResults.milestones.push(`❌ Error: ${error.message}`); | |
| } | |
| } | |
| } catch (error) { | |
| managementResults.milestones.push(`❌ Milestone management error: ${error.message}`); | |
| } | |
| // 3. 📊 Project Board Management | |
| console.log('📊 Running project management...'); | |
| try { | |
| // Get projects for the repository | |
| const projects = await github.rest.projects.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo | |
| }); | |
| if (projects.data.length > 0) { | |
| const project = projects.data[0]; | |
| managementResults.projects.push(`📊 Project detected: ${project.name}`); | |
| // Add a card to the To Do column | |
| const columns = await github.rest.projects.listColumns({ | |
| project_id: project.id | |
| }); | |
| const todoColumn = columns.data.find(col => | |
| col.name.toLowerCase().includes('todo') || | |
| col.name.toLowerCase().includes('backlog') | |
| ); | |
| if (todoColumn) { | |
| await github.rest.projects.createCard({ | |
| column_id: todoColumn.id, | |
| content_id: context.payload.issue.id, // Use issue ID for content_id | |
| content_type: 'Issue' | |
| }); | |
| managementResults.projects.push(`📋 Card added: ${project.name}`); | |
| } | |
| } else { | |
| managementResults.projects.push(`ℹ️ Project board not found`); | |
| } | |
| } catch (error) { | |
| managementResults.projects.push(`❌ Project management error: ${error.message}`); | |
| } | |
| // 4. 🔒 Security Alert Handling | |
| console.log('🔒 Running security check...'); | |
| try { | |
| if (triggerText.includes('security') || triggerText.includes('vulnerability')) { | |
| // Check for security alerts | |
| try { | |
| const vulnerabilities = await github.rest.secretScanning.listAlertsForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open' | |
| }); | |
| managementResults.security.push(`🔍 Open security alerts: ${vulnerabilities.data.length}`); | |
| if (vulnerabilities.data.length > 0) { | |
| managementResults.security.push(`⚠️ Security alert confirmation required`); | |
| } | |
| } catch (error) { | |
| managementResults.security.push(`ℹ️ Security alert check: Access restricted or feature disabled`); | |
| } | |
| } else { | |
| managementResults.security.push(`ℹ️ Security check: Skipped`); | |
| } | |
| } catch (error) { | |
| managementResults.security.push(`❌ Security check error: ${error.message}`); | |
| } | |
| // 5. 📚 Automatic Wiki Update | |
| console.log('📚 Running Wiki management...'); | |
| try { | |
| if (triggerText.includes('wiki') || triggerText.includes('document')) { | |
| // Check if Wiki page exists | |
| try { | |
| const repoInfo = await github.rest.repos.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo | |
| }); | |
| if (repoInfo.data.has_wiki) { | |
| managementResults.wiki.push(`📚 Wiki enabled: Updatable`); | |
| // Actual Wiki update is performed by Claude Code | |
| } else { | |
| managementResults.wiki.push(`📚 Wiki disabled: Needs to be enabled in settings`); | |
| } | |
| } catch (error) { | |
| managementResults.wiki.push(`❌ Wiki check error: ${error.message}`); | |
| } | |
| } else { | |
| managementResults.wiki.push(`ℹ️ Wiki update: Skipped`); | |
| } | |
| } catch (error) { | |
| managementResults.wiki.push(`❌ Wiki management error: ${error.message}`); | |
| } | |
| // 6. 🌐 GitHub Pages Management | |
| console.log('🌐 Running GitHub Pages management...'); | |
| try { | |
| if (triggerText.includes('pages') || triggerText.includes('deploy') || triggerText.includes('site')) { | |
| try { | |
| const pages = await github.rest.repos.getPages({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo | |
| }); | |
| managementResults.pages.push(`🌐 Pages enabled: ${pages.data.html_url}`); | |
| // Trigger a Pages build | |
| await github.rest.repos.requestPagesBuild({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo | |
| }); | |
| managementResults.pages.push(`🔄 Triggered Pages rebuild`); | |
| } catch (error) { | |
| if (error.status === 404) { | |
| managementResults.pages.push(`🌐 Pages disabled: Needs to be enabled in settings`); | |
| } else { | |
| managementResults.pages.push(`❌ Pages management error: ${error.message}`); | |
| } | |
| } | |
| } else { | |
| managementResults.pages.push(`ℹ️ Pages management: Skipped`); | |
| } | |
| } catch (error) { | |
| managementResults.pages.push(`❌ Pages management error: ${error.message}`); | |
| } | |
| // 7. ⚙️ Actions Workflow Management | |
| console.log('⚙️ Running Actions management...'); | |
| try { | |
| if (triggerText.includes('workflow') || triggerText.includes('action') || triggerText.includes('ci') || triggerText.includes('cd')) { | |
| const workflows = await github.rest.actions.listRepoWorkflows({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo | |
| }); | |
| managementResults.actions.push(`⚙️ Number of workflows: ${workflows.data.total_count}`); | |
| // Check for disabled workflows | |
| const disabledWorkflows = workflows.data.workflows.filter(w => w.state === 'disabled_manually'); | |
| if (disabledWorkflows.length > 0) { | |
| managementResults.actions.push(`⚠️ Disabled workflows: ${disabledWorkflows.length}`); | |
| } | |
| } else { | |
| managementResults.actions.push(`ℹ️ Actions management: Skipped`); | |
| } | |
| } catch (error) { | |
| managementResults.actions.push(`❌ Actions management error: ${error.message}`); | |
| } | |
| console.log('✅ Advanced repository management complete'); | |
| // Save results to output | |
| core.setOutput('management-results', JSON.stringify(managementResults)); | |
| core.setOutput('management-success', 'true'); | |
| } catch (error) { | |
| console.error('❌ Advanced repository management error:', error); | |
| core.setOutput('management-error', error.message); | |
| core.setOutput('management-success', 'false'); | |
| } | |
| - name: Check for Changes and Prepare for PR | |
| id: check-changes | |
| if: steps.claude.outcome == 'success' && needs.setup.outputs.is-pr == 'false' && steps.claude.outputs.branch_name | |
| run: | | |
| set -e # Exit immediately if a command exits with a non-zero status. | |
| BRANCH_NAME="${{ steps.claude.outputs.branch_name }}" | |
| DEFAULT_BRANCH="origin/${{ github.event.repository.default_branch }}" | |
| echo "--- 1. Checking if remote branch '${BRANCH_NAME}' exists ---" | |
| # Use `git ls-remote` to check for the branch's existence. It exits with 0 if it exists, 2 if not. | |
| if ! git ls-remote --exit-code --heads origin "${BRANCH_NAME}" >/dev/null 2>&1; then | |
| echo "✅ Remote branch '${BRANCH_NAME}' not found. This indicates no code changes were committed." | |
| echo "has-changes=false" >> $GITHUB_OUTPUT | |
| echo "branch-exists=false" >> $GITHUB_OUTPUT | |
| # Exit successfully as this is an expected outcome. | |
| exit 0 | |
| fi | |
| echo "✅ Remote branch found. Proceeding with original fetch and reset logic." | |
| echo "branch-exists=true" >> $GITHUB_OUTPUT | |
| echo "--- 2. DEBUG: Initial Git State ---" | |
| echo "Current branch: $(git rev-parse --abbrev-ref HEAD)" | |
| echo "Current commit: $(git log -1 --pretty=%h)" | |
| echo "Workspace status:" | |
| git status -s | |
| echo "-----------------------------------" | |
| echo "🚀 3. Fetching the specific branch pushed by Claude: '${BRANCH_NAME}'..." | |
| git fetch origin "+refs/heads/${BRANCH_NAME}:refs/remotes/origin/${BRANCH_NAME}" | |
| echo "--- 4. DEBUG: After Fetch ---" | |
| echo "Remote commit for '${BRANCH_NAME}' is: $(git log origin/${BRANCH_NAME} -1 --pretty=%h)" | |
| echo "-----------------------------" | |
| echo "🔄 5. Forcibly resetting local branch to match the fetched remote state..." | |
| git checkout "${BRANCH_NAME}" | |
| git reset --hard "origin/${BRANCH_NAME}" | |
| echo "--- 6. DEBUG: After Resetting Local Branch ---" | |
| echo "Current branch is now: $(git rev-parse --abbrev-ref HEAD)" | |
| echo "Current commit is now: $(git log -1 --pretty=%h)" | |
| echo "Workspace status is now:" | |
| git status -s | |
| echo "---------------------------------------------" | |
| BRANCH_RANGE="${DEFAULT_BRANCH}...${BRANCH_NAME}" | |
| echo "🔍 7. Checking for changes in range: ${BRANCH_RANGE}..." | |
| # Use the exit code of 'git diff --quiet' to check for changes. | |
| if git diff --quiet $BRANCH_RANGE; then | |
| echo "✅ No changes detected between branches. Setting has-changes=false." | |
| echo "has-changes=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "📝 Changes detected. Setting has-changes=true." | |
| echo "has-changes=true" >> $GITHUB_OUTPUT | |
| echo "---" | |
| echo "📄 Changed files (compared to default branch):" | |
| git diff --name-only $BRANCH_RANGE | |
| echo "---" | |
| echo "📊 Change statistics:" | |
| git diff --stat $BRANCH_RANGE | |
| fi | |
| ######################################################### | |
| # IF we have changes, create or update a pull request | |
| ######################################################### | |
| - name: Create or Update Pull Request | |
| id: auto-pr | |
| # The 'if' condition is now correctly chained. | |
| if: | | |
| steps.claude.outcome == 'success' | |
| && needs.setup.outputs.is-pr == 'false' | |
| && steps.claude.outputs.branch_name | |
| && steps.check-changes.outputs.has-changes == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const issueNumber = ${{ needs.setup.outputs.issue-number }} | |
| const branchName = '${{ steps.claude.outputs.branch_name }}' | |
| const defaultBranch = '${{ github.event.repository.default_branch }}' | |
| const owner = context.repo.owner | |
| const repo = context.repo.repo | |
| try { | |
| // 1. Check for an existing PR for this branch | |
| console.log(`Checking for existing PRs for branch: ${branchName}`) | |
| let existingPr = null | |
| try { | |
| const { data: pulls } = await github.rest.pulls.list({ | |
| owner, | |
| repo, | |
| head: `${owner}:${branchName}`, | |
| state: 'open', | |
| per_page: 1 | |
| }) | |
| existingPr = pulls.length > 0 ? pulls[0] : null | |
| } catch (error) { | |
| existingPr = null | |
| } | |
| let pr | |
| if (existingPr) { | |
| // 2. If PR exists, use it | |
| pr = existingPr | |
| console.log(`✅ Found existing PR: #${pr.number}. No new PR will be created.`) | |
| // Optional: Post a comment to the existing PR to notify of the update | |
| const updateComment = `🔄 **Claude Code has updated this branch** with new changes.\n\n[View the latest workflow run](https://github.com/${owner}/${repo}/actions/runs/${{ github.run_id }})` | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: pr.number, | |
| body: updateComment, | |
| }) | |
| } else { | |
| // 3. If no PR exists, create one | |
| console.log(`No existing PR found. Attempting to create a new PR for branch: ${branchName}`) | |
| const { data: issue } = await github.rest.issues.get({ | |
| owner, | |
| repo, | |
| issue_number: issueNumber | |
| }) | |
| // Get the last issue comment by claude on issueNumber | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner, | |
| repo, | |
| issue_number: issueNumber | |
| }) | |
| const claudeComment = comments.find(c => c.user.login === 'claude[bot]' && c.body.includes('Claude')) | |
| const claudeCommentBody = claudeComment ? claudeComment.body.replace(/\[Create PR ➔\]\([^)]+\)/, '') : '' | |
| const prTitle = `🤖 Claude Code: ${issue.title}` | |
| const prBody = `## 🤖 Automated fix by Claude Code | |
| **Related Issue:** #${issueNumber} | |
| **Executed by:** @${{ github.actor }} | |
| --- | |
| ${claudeCommentBody} | |
| **If additional fixes are needed:** Mention \`@ claude\` in a comment on this PR. | |
| *resolves #${issueNumber}* | |
| --- | |
| *Automated PR by [Claude Code Action - Run #${{ github.run_id }}](https://github.com/${owner}/${repo}/actions/runs/${{ github.run_id }})* | |
| ` | |
| const { data: newPr } = await github.rest.pulls.create({ | |
| owner, | |
| repo, | |
| title: prTitle, | |
| head: branchName, | |
| base: defaultBranch, | |
| body: prBody, | |
| draft: false | |
| }) | |
| pr = newPr | |
| console.log(`🎉 PR created successfully: #${pr.number}`) | |
| // Add first comment to the PR for sticky comment | |
| const stickyComment = ` | |
| **ℹ️ Action Run:** https://github.com/${owner}/${repo}/actions/runs/${{ github.run_id }} | |
| <!-- claude-run-status --> | |
| ` | |
| /* Add sticky comment to the PR for other data */ | |
| await github.rest.issues.createComment({ | |
| issue_number: pr.number, | |
| owner, | |
| repo, | |
| body: stickyComment, | |
| author: 'github-actions[bot]' | |
| }) | |
| } | |
| // 4. Set outputs with the PR info (whether new or existing) | |
| core.setOutput('pr-number', pr.number) | |
| core.setOutput('pr-url', pr.html_url) | |
| core.setOutput('branch-name', branchName) | |
| // Ping our webhook - PR creation successful | |
| try { | |
| const webhookUrl = '${{ secrets.CLAUDE_CODE_NOTIFICATIONS_URL }}'; | |
| const webhookToken = '${{ secrets.CLAUDE_CODE_NOTIFICATIONS_KEY }}'; | |
| const jobId = `claude-${context.repo.owner}-${context.repo.repo}-${issueNumber}-${Date.now()}`; | |
| const actionUrl = `https://github.com/${owner}/${repo}/issues/${issueNumber}`; | |
| const webhookData = { | |
| jobId: jobId, | |
| status: 'completed', | |
| repository: `${context.repo.owner}/${context.repo.repo}`, | |
| url: actionUrl, | |
| branch: branchName || context.payload.repository?.default_branch || 'main', | |
| commit: '${{ github.sha }}', | |
| results: { | |
| prCreated: true, | |
| prNumber: pr.number, | |
| prUrl: pr.html_url, | |
| issueNumber: issueNumber, | |
| actor: '${{ github.actor }}', | |
| event: '${{ github.event_name }}', | |
| framework: '${{ steps.project-info.outputs.framework }}', | |
| totalFiles: '${{ steps.project-info.outputs.total-files }}', | |
| hasChanges: '${{ steps.check-changes.outputs.has-changes }}', | |
| managementSuccess: '${{ steps.advanced-management.outputs.management-success }}' | |
| } | |
| }; | |
| console.log('🔔 Sending webhook notification (PR created)...'); | |
| console.log('Webhook URL:', webhookUrl); | |
| console.log('Job ID:', jobId); | |
| const response = await fetch(`${webhookUrl}/job-complete`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${webhookToken}` | |
| }, | |
| body: JSON.stringify(webhookData) | |
| }); | |
| if (response.ok) { | |
| console.log('✅ Webhook notification sent successfully'); | |
| } else { | |
| console.warn(`⚠️ Webhook notification failed: ${response.status} ${response.statusText}`); | |
| } | |
| } catch (error) { | |
| console.warn('⚠️ Webhook notification error:', error.message); | |
| } | |
| } catch (error) { | |
| console.error('❌ PR creation/update error:', error) | |
| core.setOutput('error', error.message) | |
| const failureComment = `❌ **Automatic PR creation failed**\n\n**Error:** \`${error.message}\`\n\n**Solution**: A branch named \`${branchName}\` was created with the changes. Please create a PR from it manually or rerun the job.\n**Details**: [Actions run log](${{ github.server_url }}/${owner}/${repo}/actions/runs/${{ github.run_id }})` | |
| await github.rest.issues.createComment({ | |
| issue_number: issueNumber, | |
| owner, | |
| repo, | |
| body: failureComment | |
| }) | |
| // Ping our webhook - PR creation failed | |
| try { | |
| const webhookUrl = '${{ secrets.CLAUDE_CODE_NOTIFICATIONS_URL }}'; | |
| const webhookToken = '${{ secrets.CLAUDE_CODE_NOTIFICATIONS_KEY }}'; | |
| const jobId = `claude-${context.repo.owner}-${context.repo.repo}-${issueNumber}-${Date.now()}`; | |
| const actionUrl = `https://github.com/${owner}/${repo}/issues/${issueNumber}`; | |
| const webhookData = { | |
| jobId: jobId, | |
| status: 'failed', | |
| repository: `${context.repo.owner}/${context.repo.repo}`, | |
| url: actionUrl, | |
| branch: branchName || context.payload.repository?.default_branch || 'main', | |
| commit: '${{ github.sha }}', | |
| results: { | |
| prCreated: false, | |
| prNumber: null, | |
| prUrl: null, | |
| issueNumber: issueNumber, | |
| actor: '${{ github.actor }}', | |
| event: '${{ github.event_name }}', | |
| framework: '${{ steps.project-info.outputs.framework }}', | |
| totalFiles: '${{ steps.project-info.outputs.total-files }}', | |
| hasChanges: '${{ steps.check-changes.outputs.has-changes }}', | |
| managementSuccess: '${{ steps.advanced-management.outputs.management-success }}', | |
| error: error.message | |
| } | |
| }; | |
| console.log('🔔 Sending webhook notification (PR creation failed)...'); | |
| console.log('Webhook URL:', webhookUrl); | |
| console.log('Job ID:', jobId); | |
| const response = await fetch(`${webhookUrl}/job-complete`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${webhookToken}` | |
| }, | |
| body: JSON.stringify(webhookData) | |
| }); | |
| if (response.ok) { | |
| console.log('✅ Webhook notification sent successfully'); | |
| } else { | |
| console.warn(`⚠️ Webhook notification failed: ${response.status} ${response.statusText}`); | |
| } | |
| } catch (webhookError) { | |
| console.warn('⚠️ Webhook notification error:', webhookError.message); | |
| } | |
| } | |
| - name: Notify on Success | |
| if: steps.claude.outcome == 'success' && needs.setup.outputs.issue-number | |
| id: generate_success_comment | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const isPR = '${{ needs.setup.outputs.is-pr }}' === 'true'; | |
| const contextType = isPR ? 'Pull Request' : 'Issue'; | |
| const eventName = '${{ github.event_name }}'; | |
| const framework = '${{ steps.project-info.outputs.framework }}' || 'Unknown'; | |
| const totalFiles = '${{ steps.project-info.outputs.total-files }}' || '0'; | |
| const hasChanges = '${{ steps.check-changes.outputs.has-changes }}' === 'true'; | |
| const autoPrNumber = '${{ steps.auto-pr.outputs.pr-number }}'; | |
| const autoPrUrl = '${{ steps.auto-pr.outputs.pr-url }}'; | |
| const branchName = '${{ steps.auto-pr.outputs.branch-name }}'; | |
| const managementSuccess = '${{ steps.advanced-management.outputs.management-success }}' === 'true'; | |
| const managementResults = '${{ steps.advanced-management.outputs.management-results }}'; | |
| const owner = context.repo.owner | |
| const repo = context.repo.repo | |
| const eventIcons = { | |
| 'pull_request_target': '🔀', | |
| 'issue_comment': '💬', | |
| 'issues': '📋', | |
| 'pull_request_review_comment': '📝', | |
| 'pull_request_review': '👀' | |
| }; | |
| const icon = eventIcons[eventName] || '🤖'; | |
| const hashSymbol = '#'; | |
| let message = `${icon} **Claude Code execution complete** ✅\n\n`; | |
| // Result of automatic PR creation | |
| let prMessage = ''; | |
| if (!isPR && hasChanges && autoPrNumber) { | |
| message += `🎉 **Automatic PR created successfully!**\n`; | |
| message += `- PR: [${hashSymbol}${autoPrNumber}](${autoPrUrl})\n`; | |
| message += `- Branch: \`${branchName}\`\n`; | |
| message += `- Next step: Review PR → Merge\n\n`; | |
| prMessage = `https://github.com/${owner}/${repo}/pull/${autoPrNumber}/files` | |
| } else if (!isPR && !hasChanges) { | |
| message += `ℹ️ **Analysis only** (no code changes)\n\n`; | |
| } | |
| // Execution info (compact version) | |
| message += `**📊 Execution Info:** ${contextType} ${hashSymbol}${${{ needs.setup.outputs.issue-number }}} | ${framework} (${totalFiles} files) | @${{ github.actor }}\n`; | |
| if (isPR) { | |
| message += `**🌿 Branch:** \`${{ needs.setup.outputs.head-ref }}\` → \`${{ needs.setup.outputs.base-ref }}\`\n`; | |
| } | |
| message += `**ℹ️ Action Run:** https://github.com/${owner}/${repo}/actions/runs/${{ github.run_id }}\n` | |
| if (prMessage) { | |
| message += `**📝 PR Changes:** [https://github.com/${owner}/${repo}/pull/${autoPrNumber}/files](${prMessage})\n` | |
| } | |
| // Repository management results (summary) | |
| if (managementSuccess && managementResults) { | |
| try { | |
| const results = JSON.parse(managementResults); | |
| const sections = ['labels', 'milestones', 'projects', 'security', 'wiki', 'pages', 'actions']; | |
| const hasResults = sections.some(s => results[s] && results[s].length > 0); | |
| if (hasResults) { | |
| // Check if there are actually any results to show | |
| let hasDisplayableResults = false; | |
| const displayableResults = []; | |
| sections.forEach(section => { | |
| if (results[section] && results[section].length > 0) { | |
| const summary = results[section].filter(r => r.includes('✅') || r.includes('⚠️')).slice(0, 2); | |
| if (summary.length > 0) { | |
| hasDisplayableResults = true; | |
| displayableResults.push(...summary.map(r => `- ${r}`)); | |
| } | |
| } | |
| }); | |
| if (hasDisplayableResults) { | |
| message += `\n**🚀 Automated management executed:**\n`; | |
| message += displayableResults.join('\n') + '\n'; | |
| } | |
| } | |
| } catch (error) { | |
| // Do not display in case of error | |
| } | |
| } | |
| // Future action links here | |
| // message += `\n---\n`; | |
| message += `\n<!-- claude-run-status -->`; | |
| // Set as output steps.generate_success_comment.outputs.result | |
| // return message; | |
| core.setOutput('comment-body', message) | |
| - name: Notify on Failure | |
| if: steps.claude.outcome == 'failure' && needs.setup.outputs.issue-number | |
| id: generate_error_comment | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const isPR = '${{ needs.setup.outputs.is-pr }}' === 'true'; | |
| const contextType = isPR ? 'Pull Request' : 'Issue'; | |
| const managementError = '${{ steps.advanced-management.outputs.management-error }}'; | |
| const hashSymbol = '#'; | |
| let message = `❌ **Claude Code execution failed**\n\n`; | |
| message += `An error occurred while processing ${contextType} ${hashSymbol}${{ needs.setup.outputs.issue-number }}.\n\n`; | |
| // Error info (compact version) | |
| message += `**📊 Error Info:** ${contextType} | \`${{ github.event_name }}\` | @${{ github.actor }}\n`; | |
| message += `**🔗 Detailed Log:** [Actions run log](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\n\n`; | |
| if (managementError) { | |
| message += `⚠️ **Repository Management Error:** \`${managementError}\`\n\n`; | |
| } | |
| // Main causes and solutions (concise list) | |
| message += `**🤔 Possible Causes and Solutions:**\n\n`; | |
| message += `**1. Temporary Issue** (most common)\n`; | |
| message += `- Temporary limitations of the Claude API\n`; | |
| message += `- → **Solution**: Rerun with \`claude\` after 5 minutes\n\n`; | |
| message += `**2. Timeout** (15-minute limit)\n`; | |
| message += `- The task may be too complex\n`; | |
| message += `- → **Solution**: Break down the task with more specific instructions\n\n`; | |
| message += `**3. Configuration Issue**\n`; | |
| message += `- Expired or misconfigured tokens\n`; | |
| message += `- → **Solution**: Check in Settings → Secrets → Actions\n`; | |
| message += ` - \`CLAUDE_ACCESS_TOKEN\`\n`; | |
| message += ` - \`CLAUDE_REFRESH_TOKEN\`\n`; | |
| message += ` - \`CLAUDE_EXPIRES_AT\`\n\n`; | |
| // Concise rerun guide | |
| message += `**💡 Rerun Tips:**\n`; | |
| message += `- Be specific: \`claude review src/components/Button.tsx\`\n`; | |
| message += `- Step by step: Start with one file\n`; | |
| message += `- Be clear: State the expected outcome\n\n`; | |
| message += `---\n`; | |
| message += `🔄 **Rerun**: Please try again with \`claude [specific instructions]\`\n`; | |
| message += `📞 **Support**: If the problem persists, please contact an administrator`; | |
| message += `\n<!-- claude-run-status -->`; | |
| // Set as output steps.generate_error_comment.outputs.result | |
| // return message; | |
| core.setOutput('comment-body', message) | |
| # Update the sticky comment with the success or error message | |
| - name: Post or Update Sticky Comment | |
| if: | | |
| always() | |
| && (steps.generate_success_comment.outputs.comment-body || steps.generate_error_comment.outputs.comment-body) | |
| && needs.setup.outputs.status-comment-id | |
| uses: peter-evans/create-or-update-comment@v4 | |
| with: | |
| issue-number: ${{ needs.setup.outputs.issue-number }} | |
| # Pass the ID found in the previous step to ensure we UPDATE | |
| comment-id: ${{ needs.setup.outputs.status-comment-id }} | |
| # The body comes from your script | |
| body: | | |
| ${{ steps.generate_success_comment.outputs.comment-body || steps.generate_error_comment.outputs.comment-body }} | |
| # Use 'replace' to overwrite the old "in-progress" message | |
| edit-mode: replace | |
| - name: Output Execution Log | |
| if: always() | |
| run: | | |
| echo "📊 ===== Execution Summary =====" | |
| echo "Status: ${{ steps.claude.outcome }}" | |
| echo "Context Type: ${{ needs.setup.outputs.is-pr == 'true' && 'PR' || 'Issue' }}" | |
| echo "Issue/PR: '#${{ needs.setup.outputs.issue-number }}'" | |
| echo "Branch: ${{ needs.setup.outputs.head-ref || 'default' }}" | |
| echo "Actor: ${{ github.actor }}" | |
| echo "Event: ${{ github.event_name }}" | |
| echo "Project: ${{ steps.project-info.outputs.framework || 'Unknown' }}" | |
| echo "Files: ${{ steps.project-info.outputs.total-files || '0' }}" | |
| echo "Duration: ${{ steps.claude.outputs.duration || 'N/A' }}" | |
| echo "" | |
| echo "🔧 === Auto PR Creation Result ===" | |
| echo "Has Changes: ${{ steps.check-changes.outputs.has-changes || 'N/A' }}" | |
| echo "Auto PR Number: ${{ steps.auto-pr.outputs.pr-number || 'N/A' }}" | |
| echo "Auto PR URL: ${{ steps.auto-pr.outputs.pr-url || 'N/A' }}" | |
| echo "Branch Name: ${{ steps.auto-pr.outputs.branch-name || 'N/A' }}" | |
| echo "Auto PR Error: ${{ steps.auto-pr.outputs.error || 'None' }}" | |
| echo "" | |
| echo "🚀 === Advanced Repository Management Result ===" | |
| echo "Management Success: ${{ steps.advanced-management.outputs.management-success || 'N/A' }}" | |
| echo "Management Error: ${{ steps.advanced-management.outputs.management-error || 'None' }}" | |
| echo "Management Results Available: ${{ steps.advanced-management.outputs.management-results && 'Yes' || 'No' }}" | |
| echo "" | |
| echo "Timestamp: $(date -u)" | |
| echo "==============================" |