|
| 1 | +# ===================================================================================== |
| 2 | +# Copilot SWE Agent PR Handler |
| 3 | +# ===================================================================================== |
| 4 | +# This workflow automatically handles pull requests created by the GitHub Copilot |
| 5 | +# SWE Agent (https://github.com/apps/copilot-swe-agent). |
| 6 | +# |
| 7 | +# It performs two key actions: |
| 8 | +# 1. Marks draft PRs as "ready for review" |
| 9 | +# 2. Approves pending workflow runs for the PR branch |
| 10 | +# |
| 11 | +# This is necessary because: |
| 12 | +# - PRs from first-time contributors (including bots) require manual approval |
| 13 | +# to run workflows for security reasons |
| 14 | +# - The Copilot agent creates draft PRs that need to be marked as ready |
| 15 | +# ===================================================================================== |
| 16 | + |
| 17 | +name: Copilot PR Handler |
| 18 | + |
| 19 | +on: |
| 20 | + # Use pull_request_target to get write permissions for PRs from forks/bots |
| 21 | + # This is safe here because we're only performing administrative actions, |
| 22 | + # not checking out or running code from the PR |
| 23 | + pull_request_target: |
| 24 | + types: [opened, synchronize, reopened] |
| 25 | + branches: |
| 26 | + - master |
| 27 | + |
| 28 | + # Allow manual triggering for testing and debugging |
| 29 | + workflow_dispatch: |
| 30 | + inputs: |
| 31 | + pr_number: |
| 32 | + description: 'PR number to process (for manual testing)' |
| 33 | + required: false |
| 34 | + type: string |
| 35 | + debug_mode: |
| 36 | + description: 'Enable verbose debug logging' |
| 37 | + required: false |
| 38 | + default: 'false' |
| 39 | + type: boolean |
| 40 | + |
| 41 | +# Minimal permissions required for this workflow |
| 42 | +# - actions: write - Required to approve workflow runs |
| 43 | +# - pull-requests: write - Required to mark PRs as ready for review |
| 44 | +# - contents: read - Required to access repository content |
| 45 | +permissions: |
| 46 | + actions: write |
| 47 | + pull-requests: write |
| 48 | + contents: read |
| 49 | + |
| 50 | +jobs: |
| 51 | + handle-copilot-pr: |
| 52 | + name: Handle Copilot PR |
| 53 | + runs-on: ubuntu-24.04 |
| 54 | + # Only run for the meshery/meshery repository |
| 55 | + # Only run for PRs from the Copilot SWE agent (copilot[bot]) |
| 56 | + if: | |
| 57 | + github.repository == 'meshery/meshery' && |
| 58 | + ( |
| 59 | + github.event_name == 'workflow_dispatch' || |
| 60 | + github.event.pull_request.user.login == 'copilot[bot]' |
| 61 | + ) |
| 62 | + |
| 63 | + steps: |
| 64 | + # ------------------------------------------------------------------------- |
| 65 | + # Step 1: Introspect and log all relevant context for debugging |
| 66 | + # ------------------------------------------------------------------------- |
| 67 | + - name: 🔍 Introspect Inputs and Context |
| 68 | + run: | |
| 69 | + echo "::group::Workflow Context" |
| 70 | + echo "Event Name: ${{ github.event_name }}" |
| 71 | + echo "Actor: ${{ github.actor }}" |
| 72 | + echo "Repository: ${{ github.repository }}" |
| 73 | + echo "::endgroup::" |
| 74 | + |
| 75 | + echo "::group::Pull Request Information" |
| 76 | + echo "PR Number: ${{ github.event.pull_request.number || inputs.pr_number || 'N/A' }}" |
| 77 | + echo "PR Author: ${{ github.event.pull_request.user.login || 'N/A' }}" |
| 78 | + echo "PR Draft Status: ${{ github.event.pull_request.draft || 'N/A' }}" |
| 79 | + echo "PR Head SHA: ${{ github.event.pull_request.head.sha || 'N/A' }}" |
| 80 | + echo "PR Head Ref: ${{ github.event.pull_request.head.ref || 'N/A' }}" |
| 81 | + echo "::endgroup::" |
| 82 | + |
| 83 | + echo "::group::Debug Settings" |
| 84 | + echo "Debug Mode: ${{ inputs.debug_mode || 'false' }}" |
| 85 | + echo "::endgroup::" |
| 86 | + |
| 87 | + # ------------------------------------------------------------------------- |
| 88 | + # Step 2: Mark PR as ready for review if it's in draft state |
| 89 | + # ------------------------------------------------------------------------- |
| 90 | + - name: 📝 Mark PR as Ready for Review |
| 91 | + uses: actions/github-script@v7 |
| 92 | + with: |
| 93 | + github-token: ${{ secrets.GH_ACCESS_TOKEN }} |
| 94 | + script: | |
| 95 | + const prNumber = context.payload.pull_request?.number || parseInt('${{ inputs.pr_number }}') || null; |
| 96 | + |
| 97 | + if (!prNumber) { |
| 98 | + core.info('No PR number available, skipping ready for review step'); |
| 99 | + return; |
| 100 | + } |
| 101 | + |
| 102 | + core.info(`Processing PR #${prNumber}`); |
| 103 | + |
| 104 | + try { |
| 105 | + // Get PR details to check if it's a draft |
| 106 | + const { data: pr } = await github.rest.pulls.get({ |
| 107 | + owner: context.repo.owner, |
| 108 | + repo: context.repo.repo, |
| 109 | + pull_number: prNumber |
| 110 | + }); |
| 111 | + |
| 112 | + core.info(`PR #${prNumber} draft status: ${pr.draft}`); |
| 113 | + |
| 114 | + if (pr.draft) { |
| 115 | + core.info(`Marking PR #${prNumber} as ready for review...`); |
| 116 | + |
| 117 | + // Use GraphQL API to mark as ready for review |
| 118 | + // The REST API doesn't support this operation |
| 119 | + await github.graphql(` |
| 120 | + mutation($pullRequestId: ID!) { |
| 121 | + markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) { |
| 122 | + pullRequest { |
| 123 | + isDraft |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + `, { |
| 128 | + pullRequestId: pr.node_id |
| 129 | + }); |
| 130 | + |
| 131 | + core.info(`✅ PR #${prNumber} has been marked as ready for review`); |
| 132 | + } else { |
| 133 | + core.info(`PR #${prNumber} is already marked as ready for review`); |
| 134 | + } |
| 135 | + } catch (error) { |
| 136 | + core.warning(`Failed to mark PR as ready for review: ${error.message}`); |
| 137 | + // Don't fail the workflow, continue to next step |
| 138 | + } |
| 139 | + |
| 140 | + # ------------------------------------------------------------------------- |
| 141 | + # Step 3: Approve pending workflow runs for this PR |
| 142 | + # ------------------------------------------------------------------------- |
| 143 | + - name: ✅ Approve Pending Workflow Runs |
| 144 | + uses: actions/github-script@v7 |
| 145 | + with: |
| 146 | + github-token: ${{ secrets.GH_ACCESS_TOKEN }} |
| 147 | + script: | |
| 148 | + const prNumber = context.payload.pull_request?.number || parseInt('${{ inputs.pr_number }}') || null; |
| 149 | + const headSha = context.payload.pull_request?.head?.sha || null; |
| 150 | + const headRef = context.payload.pull_request?.head?.ref || null; |
| 151 | + |
| 152 | + if (!headRef && !headSha) { |
| 153 | + core.info('No head ref or SHA available, skipping workflow approval step'); |
| 154 | + return; |
| 155 | + } |
| 156 | + |
| 157 | + core.info(`Looking for pending workflow runs for PR #${prNumber || 'N/A'}`); |
| 158 | + core.info(`Head SHA: ${headSha || 'N/A'}, Head Ref: ${headRef || 'N/A'}`); |
| 159 | + |
| 160 | + try { |
| 161 | + // List workflow runs that are pending approval |
| 162 | + // These are runs with status 'action_required' (waiting for approval) |
| 163 | + const { data: runs } = await github.rest.actions.listWorkflowRunsForRepo({ |
| 164 | + owner: context.repo.owner, |
| 165 | + repo: context.repo.repo, |
| 166 | + status: 'action_required', |
| 167 | + per_page: 100 |
| 168 | + }); |
| 169 | + |
| 170 | + core.info(`Found ${runs.total_count} workflow run(s) awaiting approval`); |
| 171 | + |
| 172 | + // Filter runs for this PR's branch/SHA |
| 173 | + const pendingRuns = runs.workflow_runs.filter(run => { |
| 174 | + const matchesSha = headSha && run.head_sha === headSha; |
| 175 | + const matchesRef = headRef && run.head_branch === headRef; |
| 176 | + return matchesSha || matchesRef; |
| 177 | + }); |
| 178 | + |
| 179 | + core.info(`Found ${pendingRuns.length} pending run(s) for this PR`); |
| 180 | + |
| 181 | + // Approve each pending run |
| 182 | + for (const run of pendingRuns) { |
| 183 | + core.info(`Approving workflow run: ${run.name} (ID: ${run.id})`); |
| 184 | + |
| 185 | + try { |
| 186 | + await github.rest.actions.approveWorkflowRun({ |
| 187 | + owner: context.repo.owner, |
| 188 | + repo: context.repo.repo, |
| 189 | + run_id: run.id |
| 190 | + }); |
| 191 | + core.info(`✅ Approved workflow run: ${run.name} (ID: ${run.id})`); |
| 192 | + } catch (approvalError) { |
| 193 | + core.warning(`Failed to approve run ${run.id}: ${approvalError.message}`); |
| 194 | + } |
| 195 | + } |
| 196 | + |
| 197 | + if (pendingRuns.length === 0) { |
| 198 | + core.info('No pending workflow runs found for this PR'); |
| 199 | + } |
| 200 | + } catch (error) { |
| 201 | + core.warning(`Failed to approve workflow runs: ${error.message}`); |
| 202 | + // Don't fail the workflow |
| 203 | + } |
| 204 | + |
| 205 | + # ------------------------------------------------------------------------- |
| 206 | + # Step 4: Post status comment on the PR |
| 207 | + # ------------------------------------------------------------------------- |
| 208 | + - name: 📢 Post Status Comment |
| 209 | + if: always() |
| 210 | + uses: actions/github-script@v7 |
| 211 | + with: |
| 212 | + github-token: ${{ secrets.GH_ACCESS_TOKEN }} |
| 213 | + script: | |
| 214 | + const prNumber = context.payload.pull_request?.number || parseInt('${{ inputs.pr_number }}') || null; |
| 215 | + |
| 216 | + if (!prNumber) { |
| 217 | + core.info('No PR number available, skipping status comment'); |
| 218 | + return; |
| 219 | + } |
| 220 | + |
| 221 | + const jobStatus = '${{ job.status }}'; |
| 222 | + const statusEmoji = jobStatus === 'success' ? '✅' : jobStatus === 'failure' ? '❌' : '⚠️'; |
| 223 | + |
| 224 | + // Only comment on success to avoid noise |
| 225 | + if (jobStatus === 'success') { |
| 226 | + const body = `### ${statusEmoji} Copilot PR Handler |
| 227 | + |
| 228 | + This pull request from GitHub Copilot has been automatically processed: |
| 229 | + - ✅ Marked as ready for review (if it was a draft) |
| 230 | + - ✅ Approved pending workflow runs |
| 231 | + |
| 232 | + The CI checks should now run automatically.`; |
| 233 | + |
| 234 | + try { |
| 235 | + await github.rest.issues.createComment({ |
| 236 | + owner: context.repo.owner, |
| 237 | + repo: context.repo.repo, |
| 238 | + issue_number: prNumber, |
| 239 | + body: body |
| 240 | + }); |
| 241 | + core.info(`Posted status comment on PR #${prNumber}`); |
| 242 | + } catch (error) { |
| 243 | + core.warning(`Failed to post status comment: ${error.message}`); |
| 244 | + } |
| 245 | + } |
0 commit comments