Skip to content

Commit ccc71cd

Browse files
committed
Add Copilot triage workflow to auto-classify issues and apply labels
1 parent 7d28222 commit ccc71cd

1 file changed

Lines changed: 138 additions & 0 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
on:
2+
workflow_call:
3+
inputs:
4+
additional_context:
5+
type: 'string'
6+
description: 'Any additional context from the request'
7+
required: false
8+
9+
jobs:
10+
triage:
11+
runs-on: ubuntu-latest
12+
timeout-minutes: 7
13+
outputs:
14+
available_labels: '${{ steps.get_labels.outputs.available_labels }}'
15+
selected_labels: '${{ env.SELECTED_LABELS }}'
16+
permissions:
17+
contents: 'read'
18+
id-token: 'write'
19+
issues: 'read'
20+
pull-requests: 'read'
21+
steps:
22+
- name: 'Get repository labels'
23+
id: get_labels
24+
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' # ratchet:actions/github-script@v7.0.1
25+
with:
26+
# NOTE: we intentionally do not use the given token. The default
27+
# GITHUB_TOKEN provided by the action has enough permissions to read
28+
# the labels.
29+
script: |-
30+
const { data: labels } = await github.rest.issues.listLabelsForRepo({
31+
owner: context.repo.owner,
32+
repo: context.repo.repo,
33+
});
34+
35+
if (!labels || labels.length === 0) {
36+
core.setFailed('There are no issue labels in this repository.')
37+
}
38+
39+
const labelNames = labels.map(label => label.name).sort();
40+
core.setOutput('available_labels', labelNames.join(','));
41+
core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`);
42+
return labelNames;
43+
- uses: austenstone/copilot-cli-actions/.github/actions/copilot@main
44+
if: steps.get_labels.outputs.available_labels != ''
45+
env:
46+
GITHUB_TOKEN: '' # Do NOT pass any auth tokens here since this runs on untrusted inputs
47+
ISSUE_TITLE: '${{ github.event.issue.title }}'
48+
ISSUE_BODY: '${{ github.event.issue.body }}'
49+
AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
50+
with:
51+
github-token: ${{ secrets.PAT }}
52+
prompt: |
53+
## Role
54+
55+
You are an issue triage assistant. Analyze the current GitHub issue and identify the most appropriate existing labels. Use the available tools to gather information; do not ask for information to be provided.
56+
57+
## Guidelines
58+
59+
- Only use labels that are from the list of available labels.
60+
- You can choose multiple labels to apply.
61+
- When generating shell commands, you **MUST NOT** use command substitution with `$(...)`, `<(...)`, or `>(...)`. This is a security measure to prevent unintended command execution.
62+
63+
## Input Data
64+
65+
**Available Labels** (comma-separated):
66+
```
67+
${{ env.AVAILABLE_LABELS }}
68+
```
69+
70+
**Issue Title**:
71+
```
72+
${{ env.ISSUE_TITLE }}
73+
```
74+
75+
**Issue Body**:
76+
```
77+
${{ env.ISSUE_BODY }}
78+
```
79+
80+
**Output File Path**:
81+
```
82+
${{ env.GITHUB_ENV }}
83+
```
84+
85+
## Steps
86+
87+
1. Review the issue title, issue body, and available labels provided above.
88+
89+
2. Based on the issue title and issue body, classify the issue and choose all appropriate labels from the list of available labels.
90+
91+
3. Convert the list of appropriate labels into a comma-separated list (CSV). If there are no appropriate labels, use the empty string.
92+
93+
4. Use the "echo" shell command to append the CSV labels to the output file path provided above:
94+
95+
```
96+
echo "SELECTED_LABELS=[APPROPRIATE_LABELS_AS_CSV]" >> "[filepath_for_env]"
97+
```
98+
99+
for example:
100+
101+
```
102+
echo "SELECTED_LABELS=bug,enhancement" >> "/tmp/runner/env"
103+
```
104+
105+
- name: 'Apply labels'
106+
env:
107+
ISSUE_NUMBER: '${{ github.event.issue.number }}'
108+
AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
109+
SELECTED_LABELS: '${{ env.SELECTED_LABELS }}'
110+
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' # ratchet:actions/github-script@v7.0.1
111+
with:
112+
github-token: '${{ secrets.PAT || secrets.GITHUB_TOKEN || github.token }}'
113+
script: |-
114+
// Parse the available labels
115+
const availableLabels = (process.env.AVAILABLE_LABELS || '').split(',')
116+
.map((label) => label.trim())
117+
.sort()
118+
119+
// Parse the label as a CSV, reject invalid ones - we do this just
120+
// in case someone was able to prompt inject malicious labels.
121+
const selectedLabels = (process.env.SELECTED_LABELS || '').split(',')
122+
.map((label) => label.trim())
123+
.filter((label) => availableLabels.includes(label))
124+
.sort()
125+
126+
// Set the labels
127+
const issueNumber = process.env.ISSUE_NUMBER;
128+
if (selectedLabels && selectedLabels.length > 0) {
129+
await github.rest.issues.setLabels({
130+
owner: context.repo.owner,
131+
repo: context.repo.repo,
132+
issue_number: issueNumber,
133+
labels: selectedLabels,
134+
});
135+
core.info(`Successfully set labels: ${selectedLabels.join(',')}`);
136+
} else {
137+
core.info(`Failed to determine labels to set. There may not be enough information in the issue or pull request.`)
138+
}

0 commit comments

Comments
 (0)