-
-
Notifications
You must be signed in to change notification settings - Fork 887
239 lines (216 loc) · 9.62 KB
/
Copy pathcopilot-issue-fix.yml
File metadata and controls
239 lines (216 loc) · 9.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
---
name: Copilot - Issue Fix
on:
workflow_dispatch:
inputs:
issue_number:
description: "GitHub issue number to assign to Copilot"
required: true
type: string
base_branch:
description: "Base branch for Copilot's generated PR. Defaults to the repository default branch."
required: false
type: string
default: ""
custom_agent:
description: "Repository custom agent to use"
required: false
type: string
default: "canary-bugfix"
custom_instructions:
description: "Additional instructions for Copilot"
required: false
type: string
default: ""
issue_comment:
types:
- created
permissions: {}
jobs:
authorize:
name: Authorize admin trigger
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'issue_comment' && github.event.issue.pull_request == null && startsWith(github.event.comment.body, '/copilot fix')) }}
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
issues: read
outputs:
authorized: ${{ steps.gate.outputs.authorized }}
issue_number: ${{ steps.gate.outputs.issue_number }}
base_branch: ${{ steps.gate.outputs.base_branch }}
custom_agent: ${{ steps.gate.outputs.custom_agent }}
custom_instructions: ${{ steps.gate.outputs.custom_instructions }}
steps:
- name: Check repository admin permission
id: gate
shell: bash
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
ACTOR: ${{ github.actor }}
TRIGGERING_ACTOR: ${{ github.triggering_actor }}
EVENT_NAME: ${{ github.event_name }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
DISPATCH_ISSUE_NUMBER: ${{ inputs.issue_number || '' }}
DISPATCH_BASE_BRANCH: ${{ inputs.base_branch || '' }}
DISPATCH_CUSTOM_AGENT: ${{ inputs.custom_agent || 'canary-bugfix' }}
DISPATCH_CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions || '' }}
COMMENT_BODY: ${{ github.event.comment.body || '' }}
COMMENT_ISSUE_NUMBER: ${{ github.event.issue.number || '' }}
run: |
set -euo pipefail
echo "authorized=false" >> "$GITHUB_OUTPUT"
if [ "$EVENT_NAME" = "issue_comment" ]; then
normalized_comment="$(printf '%s' "$COMMENT_BODY" | tr -d '\r')"
first_line="$(printf '%s\n' "$normalized_comment" | sed -n '/[^[:space:]]/ { s/^[[:space:]]*//; s/[[:space:]]*$//; p; q; }')"
if [ "$first_line" != "/copilot fix" ]; then
echo "Comment does not start with the exact '/copilot fix' command. Skipping Copilot assignment."
exit 0
fi
fi
for user in "$ACTOR" "$TRIGGERING_ACTOR"; do
permission="$(gh api "repos/${REPOSITORY}/collaborators/${user}/permission" --jq '.permission' 2>/dev/null || echo "none")"
if [ "$permission" != "admin" ]; then
echo "Actor @${user} has repository permission '${permission}', not 'admin'. Skipping Copilot assignment."
exit 0
fi
done
if [ "$EVENT_NAME" = "issue_comment" ]; then
issue_number="$COMMENT_ISSUE_NUMBER"
base_branch="$DEFAULT_BRANCH"
custom_agent="canary-bugfix"
custom_instructions="$(printf '%s\n' "$normalized_comment" | awk '
BEGIN { seen_command = 0 }
{
line = $0
trimmed = line
gsub(/^[[:space:]]+|[[:space:]]+$/, "", trimmed)
if (!seen_command && trimmed == "") {
next
}
if (!seen_command) {
seen_command = 1
next
}
print line
}
')"
else
issue_number="$DISPATCH_ISSUE_NUMBER"
base_branch="${DISPATCH_BASE_BRANCH:-$DEFAULT_BRANCH}"
custom_agent="${DISPATCH_CUSTOM_AGENT:-canary-bugfix}"
custom_instructions="$DISPATCH_CUSTOM_INSTRUCTIONS"
fi
if ! [[ "$issue_number" =~ ^[0-9]+$ ]]; then
echo "Invalid issue number: ${issue_number}" >&2
exit 1
fi
if ! [[ "$base_branch" =~ ^[A-Za-z0-9._/-]+$ ]] || [[ "$base_branch" == *..* ]] || [[ "$base_branch" == /* ]] || [[ "$base_branch" == */ ]]; then
echo "Unsafe base branch: ${base_branch}" >&2
exit 1
fi
if ! [[ "$custom_agent" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "Unsafe custom agent name: ${custom_agent}" >&2
exit 1
fi
encoded_base_branch="${base_branch//\//%2F}"
gh api "repos/${REPOSITORY}/branches/${encoded_base_branch}" --jq '.name' >/dev/null
issue_json="$(gh api "repos/${REPOSITORY}/issues/${issue_number}")"
issue_state="$(jq -r '.state' <<< "$issue_json")"
is_pull_request="$(jq -r 'has("pull_request")' <<< "$issue_json")"
if [ "$is_pull_request" = "true" ]; then
echo "Issue #${issue_number} is a pull request. This workflow only handles issues." >&2
exit 1
fi
if [ "$issue_state" != "open" ]; then
echo "Issue #${issue_number} is not open. Skipping Copilot assignment."
exit 0
fi
if [ -z "$custom_instructions" ]; then
custom_instructions="Fix the reported issue with a narrow, reviewable change. Follow the repository Copilot instructions and the ${custom_agent} agent guidance. Add or update focused tests if practical, and explain any validation limits in the pull request."
fi
{
echo "authorized=true"
echo "issue_number=${issue_number}"
echo "base_branch=${base_branch}"
echo "custom_agent=${custom_agent}"
delimiter="CUSTOM_INSTRUCTIONS_$(date +%s%N)"
echo "custom_instructions<<${delimiter}"
printf '%s\n' "$custom_instructions"
echo "${delimiter}"
} >> "$GITHUB_OUTPUT"
assign-copilot:
name: Assign issue to Copilot
needs:
- authorize
if: ${{ needs.authorize.outputs.authorized == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
actions: write
contents: write
issues: write
pull-requests: write
steps:
- name: Re-check admin permission
shell: bash
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
ACTOR: ${{ github.actor }}
TRIGGERING_ACTOR: ${{ github.triggering_actor }}
run: |
set -euo pipefail
for user in "$ACTOR" "$TRIGGERING_ACTOR"; do
permission="$(gh api "repos/${REPOSITORY}/collaborators/${user}/permission" --jq '.permission' 2>/dev/null || echo "none")"
if [ "$permission" != "admin" ]; then
echo "Actor @${user} has repository permission '${permission}', not 'admin'." >&2
exit 1
fi
done
- name: Assign Copilot cloud agent
shell: bash
env:
ASSIGN_TOKEN: ${{ secrets.COPILOT_ASSIGN_TOKEN || github.token }}
COMMENT_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
ISSUE_NUMBER: ${{ needs.authorize.outputs.issue_number }}
BASE_BRANCH: ${{ needs.authorize.outputs.base_branch }}
CUSTOM_AGENT: ${{ needs.authorize.outputs.custom_agent }}
CUSTOM_INSTRUCTIONS: ${{ needs.authorize.outputs.custom_instructions }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
payload_file="$(mktemp)"
jq -n \
--arg repository "$REPOSITORY" \
--arg base_branch "$BASE_BRANCH" \
--arg custom_agent "$CUSTOM_AGENT" \
--arg custom_instructions "$CUSTOM_INSTRUCTIONS" \
'{
assignees: ["copilot-swe-agent[bot]"],
agent_assignment: {
target_repo: $repository,
base_branch: $base_branch,
custom_instructions: $custom_instructions,
custom_agent: $custom_agent,
model: ""
}
}' > "$payload_file"
set +e
assign_output="$(GH_TOKEN="$ASSIGN_TOKEN" gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/${REPOSITORY}/issues/${ISSUE_NUMBER}/assignees" \
--input "$payload_file" 2>&1)"
assign_status=$?
set -e
if [ "$assign_status" -ne 0 ]; then
echo "::error::Copilot assignment API request failed. Verify that COPILOT_ASSIGN_TOKEN is a GitHub user token scoped to this repository with read/write access to Actions, Contents, Issues, and Pull requests."
echo "$assign_output" >&2
GH_TOKEN="$COMMENT_TOKEN" gh issue comment "$ISSUE_NUMBER" --repo "$REPOSITORY" --body "Copilot assignment was authorized, but GitHub rejected the automated assignment from this workflow: ${RUN_URL}. Verify that \`COPILOT_ASSIGN_TOKEN\` is a GitHub user token scoped to this repository with read/write access to Actions, Contents, Issues, and Pull requests." || true
exit "$assign_status"
fi
GH_TOKEN="$COMMENT_TOKEN" gh issue comment "$ISSUE_NUMBER" --repo "$REPOSITORY" --body "Assigned this issue to Copilot cloud agent via admin-gated workflow: ${RUN_URL}."