Skip to content

Commit 2826f69

Browse files
authored
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 <lee.calcote@layer5.io>
1 parent 334abad commit 2826f69

File tree

1 file changed

+245
-0
lines changed

1 file changed

+245
-0
lines changed
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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

Comments
 (0)