From 2826f69aacf81354439a33fc7cfa9b034ea602f7 Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Thu, 4 Dec 2025 16:16:39 -0600 Subject: [PATCH] Add Copilot PR Handler workflow This workflow automates handling of pull requests created by the GitHub Copilot SWE Agent, marking draft PRs as ready for review and approving pending workflow runs. Signed-off-by: Lee Calcote --- .github/workflows/copilot-pr-handler.yml | 245 +++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 .github/workflows/copilot-pr-handler.yml diff --git a/.github/workflows/copilot-pr-handler.yml b/.github/workflows/copilot-pr-handler.yml new file mode 100644 index 000000000..677a519d8 --- /dev/null +++ b/.github/workflows/copilot-pr-handler.yml @@ -0,0 +1,245 @@ +# ===================================================================================== +# Copilot SWE Agent PR Handler +# ===================================================================================== +# This workflow automatically handles pull requests created by the GitHub Copilot +# SWE Agent (https://github.com/apps/copilot-swe-agent). +# +# It performs two key actions: +# 1. Marks draft PRs as "ready for review" +# 2. Approves pending workflow runs for the PR branch +# +# This is necessary because: +# - PRs from first-time contributors (including bots) require manual approval +# to run workflows for security reasons +# - The Copilot agent creates draft PRs that need to be marked as ready +# ===================================================================================== + +name: Copilot PR Handler + +on: + # Use pull_request_target to get write permissions for PRs from forks/bots + # This is safe here because we're only performing administrative actions, + # not checking out or running code from the PR + pull_request_target: + types: [opened, synchronize, reopened] + branches: + - master + + # Allow manual triggering for testing and debugging + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to process (for manual testing)' + required: false + type: string + debug_mode: + description: 'Enable verbose debug logging' + required: false + default: 'false' + type: boolean + +# Minimal permissions required for this workflow +# - actions: write - Required to approve workflow runs +# - pull-requests: write - Required to mark PRs as ready for review +# - contents: read - Required to access repository content +permissions: + actions: write + pull-requests: write + contents: read + +jobs: + handle-copilot-pr: + name: Handle Copilot PR + runs-on: ubuntu-24.04 + # Only run for the meshery/meshery repository + # Only run for PRs from the Copilot SWE agent (copilot[bot]) + if: | + github.repository == 'meshery/meshery' && + ( + github.event_name == 'workflow_dispatch' || + github.event.pull_request.user.login == 'copilot[bot]' + ) + + steps: + # ------------------------------------------------------------------------- + # Step 1: Introspect and log all relevant context for debugging + # ------------------------------------------------------------------------- + - name: 🔍 Introspect Inputs and Context + run: | + echo "::group::Workflow Context" + echo "Event Name: ${{ github.event_name }}" + echo "Actor: ${{ github.actor }}" + echo "Repository: ${{ github.repository }}" + echo "::endgroup::" + + echo "::group::Pull Request Information" + echo "PR Number: ${{ github.event.pull_request.number || inputs.pr_number || 'N/A' }}" + echo "PR Author: ${{ github.event.pull_request.user.login || 'N/A' }}" + echo "PR Draft Status: ${{ github.event.pull_request.draft || 'N/A' }}" + echo "PR Head SHA: ${{ github.event.pull_request.head.sha || 'N/A' }}" + echo "PR Head Ref: ${{ github.event.pull_request.head.ref || 'N/A' }}" + echo "::endgroup::" + + echo "::group::Debug Settings" + echo "Debug Mode: ${{ inputs.debug_mode || 'false' }}" + echo "::endgroup::" + + # ------------------------------------------------------------------------- + # Step 2: Mark PR as ready for review if it's in draft state + # ------------------------------------------------------------------------- + - name: 📝 Mark PR as Ready for Review + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GH_ACCESS_TOKEN }} + script: | + const prNumber = context.payload.pull_request?.number || parseInt('${{ inputs.pr_number }}') || null; + + if (!prNumber) { + core.info('No PR number available, skipping ready for review step'); + return; + } + + core.info(`Processing PR #${prNumber}`); + + try { + // Get PR details to check if it's a draft + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + core.info(`PR #${prNumber} draft status: ${pr.draft}`); + + if (pr.draft) { + core.info(`Marking PR #${prNumber} as ready for review...`); + + // Use GraphQL API to mark as ready for review + // The REST API doesn't support this operation + await github.graphql(` + mutation($pullRequestId: ID!) { + markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) { + pullRequest { + isDraft + } + } + } + `, { + pullRequestId: pr.node_id + }); + + core.info(`✅ PR #${prNumber} has been marked as ready for review`); + } else { + core.info(`PR #${prNumber} is already marked as ready for review`); + } + } catch (error) { + core.warning(`Failed to mark PR as ready for review: ${error.message}`); + // Don't fail the workflow, continue to next step + } + + # ------------------------------------------------------------------------- + # Step 3: Approve pending workflow runs for this PR + # ------------------------------------------------------------------------- + - name: ✅ Approve Pending Workflow Runs + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GH_ACCESS_TOKEN }} + script: | + const prNumber = context.payload.pull_request?.number || parseInt('${{ inputs.pr_number }}') || null; + const headSha = context.payload.pull_request?.head?.sha || null; + const headRef = context.payload.pull_request?.head?.ref || null; + + if (!headRef && !headSha) { + core.info('No head ref or SHA available, skipping workflow approval step'); + return; + } + + core.info(`Looking for pending workflow runs for PR #${prNumber || 'N/A'}`); + core.info(`Head SHA: ${headSha || 'N/A'}, Head Ref: ${headRef || 'N/A'}`); + + try { + // List workflow runs that are pending approval + // These are runs with status 'action_required' (waiting for approval) + const { data: runs } = await github.rest.actions.listWorkflowRunsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + status: 'action_required', + per_page: 100 + }); + + core.info(`Found ${runs.total_count} workflow run(s) awaiting approval`); + + // Filter runs for this PR's branch/SHA + const pendingRuns = runs.workflow_runs.filter(run => { + const matchesSha = headSha && run.head_sha === headSha; + const matchesRef = headRef && run.head_branch === headRef; + return matchesSha || matchesRef; + }); + + core.info(`Found ${pendingRuns.length} pending run(s) for this PR`); + + // Approve each pending run + for (const run of pendingRuns) { + core.info(`Approving workflow run: ${run.name} (ID: ${run.id})`); + + try { + await github.rest.actions.approveWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: run.id + }); + core.info(`✅ Approved workflow run: ${run.name} (ID: ${run.id})`); + } catch (approvalError) { + core.warning(`Failed to approve run ${run.id}: ${approvalError.message}`); + } + } + + if (pendingRuns.length === 0) { + core.info('No pending workflow runs found for this PR'); + } + } catch (error) { + core.warning(`Failed to approve workflow runs: ${error.message}`); + // Don't fail the workflow + } + + # ------------------------------------------------------------------------- + # Step 4: Post status comment on the PR + # ------------------------------------------------------------------------- + - name: 📢 Post Status Comment + if: always() + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GH_ACCESS_TOKEN }} + script: | + const prNumber = context.payload.pull_request?.number || parseInt('${{ inputs.pr_number }}') || null; + + if (!prNumber) { + core.info('No PR number available, skipping status comment'); + return; + } + + const jobStatus = '${{ job.status }}'; + const statusEmoji = jobStatus === 'success' ? '✅' : jobStatus === 'failure' ? '❌' : '⚠️'; + + // Only comment on success to avoid noise + if (jobStatus === 'success') { + const body = `### ${statusEmoji} Copilot PR Handler + + This pull request from GitHub Copilot has been automatically processed: + - ✅ Marked as ready for review (if it was a draft) + - ✅ Approved pending workflow runs + + The CI checks should now run automatically.`; + + try { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: body + }); + core.info(`Posted status comment on PR #${prNumber}`); + } catch (error) { + core.warning(`Failed to post status comment: ${error.message}`); + } + }