Skip to content

Commit e8d8af4

Browse files
authored
chore: auto label discussions (#5259)
1 parent 63eb85a commit e8d8af4

10 files changed

+334
-0
lines changed

.github/actions/trivy-triage/Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.PHONEY: test
2+
test: helpers.js helpers.test.js
3+
node --test helpers.test.js
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: 'trivy-discussion-triage'
2+
description: 'automatic triage of Trivy discussions'
3+
inputs:
4+
discussion_num:
5+
description: 'Discussion number to triage'
6+
required: false
7+
runs:
8+
using: "composite"
9+
steps:
10+
- name: Conditionally label discussions based on category and content
11+
env:
12+
GH_TOKEN: ${{ github.token }}
13+
uses: actions/github-script@v6
14+
with:
15+
script: |
16+
const {detectDiscussionLabels, fetchDiscussion, labelDiscussion } = require('${{ github.action_path }}/helpers.js');
17+
const config = require('${{ github.action_path }}/config.json');
18+
discussionNum = parseInt(${{ inputs.discussion_num }});
19+
let discussion;
20+
if (discussionNum > 0) {
21+
discussion = (await fetchDiscussion(github, context.repo.owner, context.repo.repo, discussionNum)).repository.discussion;
22+
} else {
23+
discussion = context.payload.discussion;
24+
}
25+
const labels = detectDiscussionLabels(discussion, config.discussionLabels);
26+
if (labels.length > 0) {
27+
console.log(`Adding labels ${labels} to discussion ${discussion.node_id}`);
28+
labelDiscussion(github, discussion.node_id, labels);
29+
}
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"discussionLabels": {
3+
"Container Image":"LA_kwDOCsUTCM75TTQU",
4+
"Filesystem":"LA_kwDOCsUTCM75TTQX",
5+
"Git Repository":"LA_kwDOCsUTCM75TTQk",
6+
"Virtual Machine Image":"LA_kwDOCsUTCM8AAAABMpz1bw",
7+
"Kubernetes":"LA_kwDOCsUTCM75TTQv",
8+
"AWS":"LA_kwDOCsUTCM8AAAABMpz1aA",
9+
"Vulnerability":"LA_kwDOCsUTCM75TTPa",
10+
"Misconfiguration":"LA_kwDOCsUTCM75TTP8",
11+
"License":"LA_kwDOCsUTCM77ztRR",
12+
"Secret":"LA_kwDOCsUTCM75TTQL"
13+
}
14+
}
+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
module.exports = {
2+
detectDiscussionLabels: (discussion, configDiscussionLabels) => {
3+
res = [];
4+
const discussionId = discussion.id;
5+
const category = discussion.category.name;
6+
const body = discussion.body;
7+
if (category !== "Ideas") {
8+
consolt.log("skipping discussion with category ${category} and body ${body}");
9+
}
10+
const scannerPattern = /### Scanner\n\n(.+)/;
11+
const scannerFound = body.match(scannerPattern);
12+
if (scannerFound && scannerFound.length > 1) {
13+
res.push(configDiscussionLabels[scannerFound[1]]);
14+
}
15+
const targetPattern = /### Target\n\n(.+)/;
16+
const targetFound = body.match(targetPattern);
17+
if (targetFound && targetFound.length > 1) {
18+
res.push(configDiscussionLabels[targetFound[1]]);
19+
}
20+
return res;
21+
},
22+
fetchDiscussion: async (github, owner, repo, discussionNum) => {
23+
const query = `query Discussion ($owner: String!, $repo: String!, $discussion_num: Int!){
24+
repository(name: $repo, owner: $owner) {
25+
discussion(number: $discussion_num) {
26+
number,
27+
id,
28+
body,
29+
category {
30+
id,
31+
name
32+
},
33+
labels(first: 100) {
34+
edges {
35+
node {
36+
id,
37+
name
38+
}
39+
}
40+
}
41+
}
42+
}
43+
}`;
44+
const vars = {
45+
owner: owner,
46+
repo: repo,
47+
discussion_num: discussionNum
48+
};
49+
return github.graphql(query, vars);
50+
},
51+
labelDiscussion: async (github, discussionId, labelIds) => {
52+
const query = `mutation AddLabels($labelId: ID!, $labelableId:ID!) {
53+
addLabelsToLabelable(
54+
input: {labelIds: [$labelId], labelableId: $labelableId}
55+
) {
56+
clientMutationId
57+
}
58+
}`;
59+
// TODO: add all labels in one call
60+
labelIds.forEach((labelId) => {
61+
const vars = {
62+
labelId: labelId,
63+
labelableId: discussionId
64+
};
65+
github.graphql(query, vars);
66+
});
67+
}
68+
};
69+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
const assert = require('node:assert/strict');
2+
const { describe, it } = require('node:test');
3+
const {detectDiscussionLabels} = require('./helpers.js');
4+
5+
const configDiscussionLabels = {
6+
"Container Image":"ContainerImageLabel",
7+
"Filesystem":"FilesystemLabel",
8+
"Vulnerability":"VulnerabilityLabel",
9+
"Misconfiguration":"MisconfigurationLabel",
10+
};
11+
12+
describe('trivy-triage', async function() {
13+
describe('detectDiscussionLabels', async function() {
14+
it('detect scanner label', async function() {
15+
const discussion = {
16+
body: 'hello hello\nbla bla.\n### Scanner\n\nVulnerability\n### Target\n\nContainer Image\nbye bye.',
17+
category: {
18+
name: 'Ideas'
19+
}
20+
};
21+
const labels = detectDiscussionLabels(discussion, configDiscussionLabels);
22+
assert(labels.includes('VulnerabilityLabel'));
23+
});
24+
it('detect target label', async function() {
25+
const discussion = {
26+
body: 'hello hello\nbla bla.\n### Scanner\n\nVulnerability\n### Target\n\nContainer Image\nbye bye.',
27+
category: {
28+
name: 'Ideas'
29+
}
30+
};
31+
const labels = detectDiscussionLabels(discussion, configDiscussionLabels);
32+
assert(labels.includes('ContainerImageLabel'));
33+
});
34+
it('detect label when it is first', async function() {
35+
const discussion = {
36+
body: '### Scanner\n\nVulnerability\n### Target\n\nContainer Image\nbye bye.',
37+
category: {
38+
name: 'Ideas'
39+
}
40+
};
41+
const labels = detectDiscussionLabels(discussion, configDiscussionLabels);
42+
assert(labels.includes('ContainerImageLabel'));
43+
});
44+
it('detect label when it is last', async function() {
45+
const discussion = {
46+
body: '### Scanner\n\nVulnerability\n### Target\n\nContainer Image',
47+
category: {
48+
name: 'Ideas'
49+
}
50+
};
51+
const labels = detectDiscussionLabels(discussion, configDiscussionLabels);
52+
assert(labels.includes('ContainerImageLabel'));
53+
});
54+
it('detect scanner and target labels', async function() {
55+
const discussion = {
56+
body: 'hello hello\nbla bla.\n### Scanner\n\nVulnerability\n### Target\n\nContainer Image\nbye bye.',
57+
category: {
58+
name: 'Ideas'
59+
}
60+
};
61+
const labels = detectDiscussionLabels(discussion, configDiscussionLabels);
62+
assert(labels.includes('ContainerImageLabel'));
63+
assert(labels.includes('VulnerabilityLabel'));
64+
});
65+
it('not detect other labels', async function() {
66+
const discussion = {
67+
body: 'hello hello\nbla bla.\n### Scanner\n\nVulnerability\n### Target\n\nContainer Image\nbye bye.',
68+
category: {
69+
name: 'Ideas'
70+
}
71+
};
72+
const labels = detectDiscussionLabels(discussion, configDiscussionLabels);
73+
assert(!labels.includes('FilesystemLabel'));
74+
assert(!labels.includes('MisconfigurationLabel'));
75+
});
76+
});
77+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"active_lock_reason": null,
3+
"answer_chosen_at": null,
4+
"answer_chosen_by": null,
5+
"answer_html_url": null,
6+
"author_association": "OWNER",
7+
"body": "### Description\n\nlfdjs lfkdj dflsakjfd ';djk \r\nfadfd \r\nasdlkf \r\na;df \r\ndfsal;kfd ;akjl\n\n### Target\n\nContainer Image\n\n### Scanner\n\nMisconfiguration",
8+
"category": {
9+
"created_at": "2023-07-02T10:14:46.000+03:00",
10+
"description": "Share ideas for new features",
11+
"emoji": ":bulb:",
12+
"id": 39743708,
13+
"is_answerable": false,
14+
"name": "Ideas",
15+
"node_id": "DIC_kwDOE0GiPM4CXnDc",
16+
"repository_id": 323068476,
17+
"slug": "ideas",
18+
"updated_at": "2023-07-02T10:14:46.000+03:00"
19+
},
20+
"comments": 0,
21+
"created_at": "2023-09-11T08:40:11Z",
22+
"html_url": "https://github.com/itaysk/testactions/discussions/9",
23+
"id": 5614504,
24+
"locked": false,
25+
"node_id": "D_kwDOE0GiPM4AVauo",
26+
"number": 9,
27+
"reactions": {
28+
"+1": 0,
29+
"-1": 0,
30+
"confused": 0,
31+
"eyes": 0,
32+
"heart": 0,
33+
"hooray": 0,
34+
"laugh": 0,
35+
"rocket": 0,
36+
"total_count": 0,
37+
"url": "https://api.github.com/repos/itaysk/testactions/discussions/9/reactions"
38+
},
39+
"repository_url": "https://api.github.com/repos/itaysk/testactions",
40+
"state": "open",
41+
"state_reason": null,
42+
"timeline_url": "https://api.github.com/repos/itaysk/testactions/discussions/9/timeline",
43+
"title": "Title title",
44+
"updated_at": "2023-09-11T08:40:11Z",
45+
"user": {
46+
"avatar_url": "https://avatars.githubusercontent.com/u/1161307?v=4",
47+
"events_url": "https://api.github.com/users/itaysk/events{/privacy}",
48+
"followers_url": "https://api.github.com/users/itaysk/followers",
49+
"following_url": "https://api.github.com/users/itaysk/following{/other_user}",
50+
"gists_url": "https://api.github.com/users/itaysk/gists{/gist_id}",
51+
"gravatar_id": "",
52+
"html_url": "https://github.com/itaysk",
53+
"id": 1161307,
54+
"login": "itaysk",
55+
"node_id": "MDQ6VXNlcjExNjEzMDc=",
56+
"organizations_url": "https://api.github.com/users/itaysk/orgs",
57+
"received_events_url": "https://api.github.com/users/itaysk/received_events",
58+
"repos_url": "https://api.github.com/users/itaysk/repos",
59+
"site_admin": false,
60+
"starred_url": "https://api.github.com/users/itaysk/starred{/owner}{/repo}",
61+
"subscriptions_url": "https://api.github.com/users/itaysk/subscriptions",
62+
"type": "User",
63+
"url": "https://api.github.com/users/itaysk"
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#! /bin/bash
2+
# fetch discussion by discussion number
3+
# requires authenticated gh cli, assumes repo but current git repository
4+
# args:
5+
# $1: discussion number, e.g 123, required
6+
7+
discussion_num="$1"
8+
gh api graphql -F discussion_num="$discussion_num" -F repo="{repo}" -F owner="{owner}" -f query='
9+
query Discussion ($owner: String!, $repo: String!, $discussion_num: Int!){
10+
repository(name: $repo, owner: $owner) {
11+
discussion(number: $discussion_num) {
12+
number,
13+
id,
14+
body,
15+
category {
16+
id,
17+
name
18+
},
19+
labels(first: 100) {
20+
edges {
21+
node {
22+
id,
23+
name
24+
}
25+
}
26+
}
27+
}
28+
}
29+
}'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#! /bin/bash
2+
# fetch labels and their IDs
3+
# requires authenticated gh cli, assumes repo but current git repository
4+
5+
gh api graphql -F repo="{repo}" -F owner="{owner}" -f query='
6+
query GetLabelIds($owner: String!, $repo: String!) {
7+
repository(name: $repo, owner: $owner) {
8+
id
9+
labels(first: 100) {
10+
nodes {
11+
id
12+
name
13+
}
14+
}
15+
}
16+
}'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#! /bin/bash
2+
# add a label to a discussion
3+
# requires authenticated gh cli, assumes repo but current git repository
4+
# args:
5+
# $1: discussion ID (not number!), e.g DIC_kwDOE0GiPM4CXnDc, required
6+
# $2: label ID, e.g. MDU6TGFiZWwzNjIzNjY0MjQ=, required
7+
discussion_id="$1"
8+
label_id="$2"
9+
gh api graphql -F labelableId="$discussion_id" -F labelId="$label_id" -F repo="{repo}" -F owner="{owner}" -f query='
10+
mutation AddLabels($labelId: ID!, $labelableId:ID!) {
11+
addLabelsToLabelable(
12+
input: {labelIds: [$labelId], labelableId: $labelableId}
13+
) {
14+
clientMutationId
15+
}
16+
}'

.github/trivy-triage.yaml

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: Triage Discussion
2+
on:
3+
discussion:
4+
types: [created]
5+
workflow_dispatch:
6+
inputs:
7+
discussion_num:
8+
required: true
9+
jobs:
10+
label:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: ./.github/actions/trivy-triage
15+
with:
16+
discussion_num: ${{ github.event.inputs.discussion_num }}

0 commit comments

Comments
 (0)