mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-27 15:17:01 +01:00
Moves PR Labeling from webhook processor to Github actions (#89190)
## About The Pull Request Strips auto-labeling behavior from the webhook processor to an action. All that remains in the webhook processor is ingame PR announcements, "changelog validation" (which is either broken or we have disabled), and handling for "request review" (which we have disabled) Keywords have been maintained 1:1, unless I missed something or accidentally shoved something where it shouldn't be I wanted to link this to the changelog parser but that seems like a slightly larger project so I'll just throw this up as-is Note: I'm not very experienced in writing actions so review with scrutiny ## Why Actions are a lot easier to maintain and set up for downstreams Adding new labels should now be like, 10x easier since all you need to do is slap it in the config file Webhook processor is also kinda old an breaks semi-frequently ### (Tested)  
This commit is contained in:
@@ -1,10 +1,34 @@
|
||||
name: GBP
|
||||
name: Label and GBP
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed, opened]
|
||||
types: [closed, opened, synchronize]
|
||||
jobs:
|
||||
# labeler must run before gbp because gbp calculates itself based on labels
|
||||
labeler:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'opened' || github.event.action == 'synchronize'
|
||||
permissions:
|
||||
pull-requests: write # to apply labels
|
||||
issues: write # to apply labels
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Run Auto Labeler
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { get_updated_label_set } = await import('${{ github.workspace }}/tools/pull_request_hooks/autoLabel.js');
|
||||
const new_labels = await get_updated_label_set({ github, context });
|
||||
github.rest.issues.setLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: new_labels,
|
||||
});
|
||||
console.log(`Labels updated: ${new_labels}`);
|
||||
gbp:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'opened' || github.event.action == 'closed'
|
||||
steps:
|
||||
- name: "Check for ACTION_ENABLER secret and pass true to output if it exists to be checked by later steps"
|
||||
id: value_holder
|
||||
|
||||
@@ -200,97 +200,6 @@ function get_labels($payload){
|
||||
return $existing;
|
||||
}
|
||||
|
||||
function check_tag_and_replace($payload, $title_tag, $label, &$array_to_add_label_to){
|
||||
$title = $payload['pull_request']['title'];
|
||||
if(stripos($title, $title_tag) !== FALSE){
|
||||
$array_to_add_label_to[] = $label;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function set_labels($payload, $labels, $remove) {
|
||||
global $repoAutoTaggerWhitelist;
|
||||
if(!in_array($payload['repository']['name'], $repoAutoTaggerWhitelist)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existing = get_labels($payload);
|
||||
$tags = array();
|
||||
|
||||
$tags = array_merge($labels, $existing);
|
||||
$tags = array_unique($tags);
|
||||
if($remove) {
|
||||
$tags = array_diff($tags, $remove);
|
||||
}
|
||||
|
||||
$final = array();
|
||||
foreach($tags as $t)
|
||||
$final[] = $t;
|
||||
|
||||
$url = $payload['pull_request']['issue_url'] . '/labels';
|
||||
echo github_apisend($url, 'PUT', $final);
|
||||
}
|
||||
|
||||
//rip bs-12
|
||||
function tag_pr($payload, $opened) {
|
||||
//get the mergeable state
|
||||
$url = $payload['pull_request']['url'];
|
||||
$new_pull_request_payload = json_decode(github_apisend($url), TRUE);
|
||||
if (isset($new_pull_request_payload['id']))
|
||||
$payload['pull_request'] = $new_pull_request_payload;
|
||||
if($payload['pull_request']['mergeable'] == null) {
|
||||
//STILL not ready. Give it a bit, then try one more time
|
||||
sleep(10);
|
||||
$new_pull_request_payload = json_decode(github_apisend($url), TRUE);
|
||||
if (isset($new_pull_request_payload['id']))
|
||||
$payload['pull_request'] = $new_pull_request_payload;
|
||||
}
|
||||
|
||||
$tags = array();
|
||||
$title = $payload['pull_request']['title'];
|
||||
if($opened) { //you only have one shot on these ones so as to not annoy maintainers
|
||||
$tags = checkchangelog($payload);
|
||||
|
||||
if(strpos(strtolower($title), 'logs') !== FALSE || strpos(strtolower($title), 'logging') !== FALSE)
|
||||
$tags[] = 'Logging';
|
||||
if(strpos(strtolower($title), 'refactor') !== FALSE)
|
||||
$tags[] = 'Refactor';
|
||||
if(strpos(strtolower($title), 'revert') !== FALSE)
|
||||
$tags[] = 'Revert';
|
||||
if(strpos(strtolower($title), 'removes') !== FALSE)
|
||||
$tags[] = 'Removal';
|
||||
if(strpos(strtolower($title), 'unit test') !== FALSE)
|
||||
$tags[] = 'Unit Tests';
|
||||
}
|
||||
|
||||
$remove = array('Test Merge Candidate');
|
||||
|
||||
$mergeable = $payload['pull_request']['mergeable'];
|
||||
if($mergeable === TRUE) //only look for the false value
|
||||
$remove[] = 'Merge Conflict';
|
||||
else if ($mergeable === FALSE)
|
||||
$tags[] = 'Merge Conflict';
|
||||
|
||||
$treetags = array('_maps' => 'Map Edit', 'tools' => 'Tools', 'SQL' => 'SQL', '.github' => 'GitHub');
|
||||
$addonlytags = array('icons' => 'Sprites', 'sound' => 'Sound', 'config' => 'Config Update', 'code/controllers/configuration/entries' => 'Config Update', 'tgui' => 'UI');
|
||||
foreach($treetags as $tree => $tag)
|
||||
if(has_tree_been_edited($payload, $tree))
|
||||
$tags[] = $tag;
|
||||
else
|
||||
$remove[] = $tag;
|
||||
foreach($addonlytags as $tree => $tag)
|
||||
if(has_tree_been_edited($payload, $tree))
|
||||
$tags[] = $tag;
|
||||
|
||||
check_tag_and_replace($payload, '[dnm]', 'Do Not Merge', $tags);
|
||||
check_tag_and_replace($payload, '[no gbp]', 'GBP: No Update', $tags);
|
||||
check_tag_and_replace($payload, '[april fools]', 'April Fools', $tags);
|
||||
check_tag_and_replace($payload, '[tm only]', 'Test Merge Only', $tags);
|
||||
|
||||
return array($tags, $remove);
|
||||
}
|
||||
|
||||
function remove_ready_for_review($payload, $labels = null){
|
||||
if($labels == null)
|
||||
$labels = get_labels($payload);
|
||||
@@ -357,16 +266,12 @@ function handle_pr($payload) {
|
||||
$validated = validate_user($payload);
|
||||
switch ($payload["action"]) {
|
||||
case 'opened':
|
||||
list($labels, $remove) = tag_pr($payload, true);
|
||||
set_labels($payload, $labels, $remove);
|
||||
if($no_changelog)
|
||||
check_dismiss_changelog_review($payload);
|
||||
break;
|
||||
case 'edited':
|
||||
check_dismiss_changelog_review($payload);
|
||||
case 'synchronize':
|
||||
list($labels, $remove) = tag_pr($payload, false);
|
||||
set_labels($payload, $labels, $remove);
|
||||
return;
|
||||
case 'reopened':
|
||||
$action = $payload['action'];
|
||||
@@ -581,16 +486,6 @@ function create_comment($payload, $comment){
|
||||
github_apisend($payload['pull_request']['comments_url'], 'POST', json_encode(array('body' => $comment)));
|
||||
}
|
||||
|
||||
//returns the payload issue's labels as a flat array
|
||||
function get_pr_labels_array($payload){
|
||||
$url = $payload['pull_request']['issue_url'] . '/labels';
|
||||
$issue = json_decode(github_apisend($url), true);
|
||||
$result = array();
|
||||
foreach($issue as $l)
|
||||
$result[] = $l['name'];
|
||||
return $result;
|
||||
}
|
||||
|
||||
function is_maintainer($payload, $author){
|
||||
global $maintainer_team_id;
|
||||
$repo_is_org = $payload['pull_request']['base']['repo']['owner']['type'] == 'Organization';
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import * as autoLabelConfig from './autoLabelConfig.js';
|
||||
|
||||
function keyword_to_cl_label() {
|
||||
const keyword_to_cl_label = {};
|
||||
for (let label in autoLabelConfig.changelog_labels) {
|
||||
for (let keyword of autoLabelConfig.changelog_labels[label].keywords) {
|
||||
keyword_to_cl_label[keyword] = label;
|
||||
}
|
||||
}
|
||||
return keyword_to_cl_label;
|
||||
}
|
||||
|
||||
// Checks the body (primarily the changelog) for labels to add
|
||||
function check_body_for_labels(body) {
|
||||
const labels_to_add = [];
|
||||
|
||||
// if the body contains a github "fixes #1234" line, add the Fix tag
|
||||
const fix_regex = new RegExp(`(fix[des]*|resolve[sd]*)\s*#\d+`, 'gmi');
|
||||
if (fix_regex.test(body)) {
|
||||
labels_to_add.push('Fix');
|
||||
}
|
||||
|
||||
const keywords = keyword_to_cl_label();
|
||||
|
||||
let found_cl = false;
|
||||
for (let line of body.split('\n')) {
|
||||
if(line.startsWith(':cl:')) {
|
||||
found_cl = true;
|
||||
continue;
|
||||
} else if(line.startsWith('/:cl:')) {
|
||||
break;
|
||||
} else if(!found_cl) {
|
||||
continue;
|
||||
}
|
||||
// see if the first segment of the line is one of the keywords
|
||||
const found_label = keywords[line.split(':')[0]?.toLowerCase()];
|
||||
if (found_label) {
|
||||
// don't add a billion tags if they forgot to clear all the default ones
|
||||
const line_text = line.split(':')[1].trim();
|
||||
const cl_label = autoLabelConfig.changelog_labels[found_label];
|
||||
if (line_text !== cl_label.default_text && line_text !== cl_label.alt_default_text) {
|
||||
labels_to_add.push(found_label);
|
||||
}
|
||||
}
|
||||
}
|
||||
return labels_to_add;
|
||||
}
|
||||
|
||||
// Checks the title for labels to add
|
||||
function check_title_for_labels(title) {
|
||||
const labels_to_add = [];
|
||||
const title_lower = title.toLowerCase();
|
||||
for (let label in autoLabelConfig.title_labels) {
|
||||
let found = false;
|
||||
for (let keyword of autoLabelConfig.title_labels[label].keywords) {
|
||||
if (title_lower.includes(keyword)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
labels_to_add.push(label);
|
||||
}
|
||||
}
|
||||
return labels_to_add;
|
||||
}
|
||||
|
||||
function check_diff_line_for_element(diff, element) {
|
||||
const tag_re = new RegExp(`^diff --git a/${element}/`);
|
||||
return tag_re.test(diff);
|
||||
}
|
||||
|
||||
// Checks the file diff for labels to add or remove
|
||||
async function check_diff_for_labels(diff_url) {
|
||||
const labels_to_add = [];
|
||||
const labels_to_remove = [];
|
||||
try {
|
||||
const diff = await fetch(diff_url);
|
||||
if (diff.ok) {
|
||||
const diff_txt = await diff.text();
|
||||
for (let label in autoLabelConfig.file_labels) {
|
||||
let found = false;
|
||||
const { filepaths, add_only } = autoLabelConfig.file_labels[label];
|
||||
for (let filepath of filepaths) {
|
||||
if(check_diff_line_for_element(diff_txt, filepath)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
labels_to_add.push(label);
|
||||
}
|
||||
else if (!add_only) {
|
||||
labels_to_remove.push(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.error(`Failed to fetch diff: ${diff.status} ${diff.statusText}`);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return { labels_to_add, labels_to_remove };
|
||||
}
|
||||
|
||||
export async function get_updated_label_set({ github, context }) {
|
||||
const {
|
||||
action,
|
||||
pull_request,
|
||||
} = context.payload;
|
||||
const {
|
||||
body = '',
|
||||
diff_url,
|
||||
labels = [],
|
||||
mergeable,
|
||||
title = '',
|
||||
} = pull_request;
|
||||
|
||||
let updated_labels = new Set();
|
||||
for (let label of labels) {
|
||||
updated_labels.add(label.name);
|
||||
}
|
||||
|
||||
// diff is always checked
|
||||
if (diff_url) {
|
||||
const diff_tags = await check_diff_for_labels(diff_url);
|
||||
for (let label of diff_tags.labels_to_add) {
|
||||
updated_labels.add(label);
|
||||
}
|
||||
for (let label of diff_tags.labels_to_remove) {
|
||||
updated_labels.delete(label);
|
||||
}
|
||||
}
|
||||
// body and title are only checked on open, not on sync
|
||||
if(action === 'opened') {
|
||||
if(title) {
|
||||
for (let label of check_title_for_labels(title)) {
|
||||
updated_labels.add(label);
|
||||
}
|
||||
}
|
||||
if (body) {
|
||||
for (let label of check_body_for_labels(body)) {
|
||||
updated_labels.add(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this is always removed on updates
|
||||
updated_labels.delete('Test Merge Candidate');
|
||||
|
||||
// update merge conflict label
|
||||
let merge_conflict = mergeable === false;
|
||||
// null means it was not reported yet
|
||||
// it is not normally included in the payload - a "get" is needed
|
||||
if(mergeable === null){
|
||||
try {
|
||||
let response = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pull_request.number,
|
||||
});
|
||||
// failed to find? still processing? try again in a few seconds
|
||||
if(response.data.mergeable === null){
|
||||
console.log("Awaiting GitHub response for merge status...")
|
||||
await new Promise(r => setTimeout(r, 10000));
|
||||
response = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pull_request.number,
|
||||
});
|
||||
if(response.data.mergeable === null){
|
||||
throw new Error("Merge status not available");
|
||||
}
|
||||
}
|
||||
|
||||
merge_conflict = response.data.mergeable === false;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
if(merge_conflict){
|
||||
updated_labels.add('Merge Conflict');
|
||||
} else {
|
||||
updated_labels.delete('Merge Conflict');
|
||||
}
|
||||
|
||||
// return the labels to the action, which will apply it
|
||||
return [...updated_labels];
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { get_updated_label_set } from "./autoLabel.js";
|
||||
|
||||
const empty_pr = {
|
||||
action: "opened",
|
||||
pull_request: {
|
||||
body: "This PR will have no labels",
|
||||
title: "Pr with no labels",
|
||||
mergeable: true,
|
||||
},
|
||||
};
|
||||
const empty_label_set = await get_updated_label_set({ github: null, context: { payload: empty_pr } });
|
||||
assert.equal(empty_label_set.length, 0, "No labels should be added");
|
||||
|
||||
const cl = `
|
||||
My Awesome PR
|
||||
|
||||
:cl: Awesome Dude
|
||||
add: Adds Awesome Stuff
|
||||
refactor: refactored some code
|
||||
:/cl:
|
||||
`
|
||||
const cl_pr = {
|
||||
action: "opened",
|
||||
pull_request: {
|
||||
body: cl,
|
||||
title: "Awesome PR",
|
||||
mergeable: false,
|
||||
},
|
||||
};
|
||||
const cl_label_set = await get_updated_label_set({ github: null, context: { payload: cl_pr } });
|
||||
assert.ok(cl_label_set.includes("Merge Conflict"), "Merge Conflict label should be added");
|
||||
assert.ok(cl_label_set.includes("Feature"), "Feature label should be added");
|
||||
assert.ok(!cl_label_set.includes("Refactor"), "Refactor label should not be added");
|
||||
|
||||
const title_pr = {
|
||||
action: "opened",
|
||||
pull_request: {
|
||||
title: "Logging is important",
|
||||
mergeable: true,
|
||||
},
|
||||
};
|
||||
const title_label_set = await get_updated_label_set({ github: null, context: { payload: title_pr } });
|
||||
assert.ok(title_label_set.includes("Logging"), "Logging label should be added");
|
||||
@@ -0,0 +1,124 @@
|
||||
|
||||
// File Labels
|
||||
//
|
||||
// Add a label based on if a file is modified in the diff
|
||||
//
|
||||
// You can optionally set add_only to make the label one-way -
|
||||
// if the edit to the file is removed in a later commit,
|
||||
// the label will not be removed
|
||||
export const file_labels = {
|
||||
'GitHub': {
|
||||
filepaths: ['.github'],
|
||||
},
|
||||
'SQL': {
|
||||
filepaths: ['SQL'],
|
||||
},
|
||||
'Map Edit': {
|
||||
filepaths: ['_maps'],
|
||||
},
|
||||
'Tools': {
|
||||
filepaths: ['tools'],
|
||||
},
|
||||
'Config Update': {
|
||||
filepaths: ['config', 'code/controllers/configuration/entries'],
|
||||
add_only: true,
|
||||
},
|
||||
'Sprites': {
|
||||
filepaths: ['icons'],
|
||||
add_only: true,
|
||||
},
|
||||
'Sound': {
|
||||
filepaths: ['sound'],
|
||||
add_only: true,
|
||||
},
|
||||
'UI': {
|
||||
filepaths: ['tgui'],
|
||||
add_only: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Title Labels
|
||||
//
|
||||
// Add a label based on keywords in the title
|
||||
export const title_labels = {
|
||||
'Logging' : {
|
||||
keywords: ['log', 'logging'],
|
||||
},
|
||||
'Removal' : {
|
||||
keywords: ['remove', 'delete'],
|
||||
},
|
||||
'Refactor' : {
|
||||
keywords: ['refactor'],
|
||||
},
|
||||
'Unit Tests' : {
|
||||
keywords: ['unit test'],
|
||||
},
|
||||
'April Fools' : {
|
||||
keywords: ['[april fools]'],
|
||||
},
|
||||
'Do Not Merge' : {
|
||||
keywords: ['[dnm]', '[do not merge]'],
|
||||
},
|
||||
'GBP: No Update' : {
|
||||
keywords: ['[no gbp]'],
|
||||
},
|
||||
'Test Merge Only' : {
|
||||
keywords: ['[tm only]', '[test merge only]'],
|
||||
},
|
||||
}
|
||||
|
||||
// Changelog Labels
|
||||
//
|
||||
// Adds labels based on keywords in the changelog
|
||||
// TODO use the existing changelog parser
|
||||
export const changelog_labels = {
|
||||
'Fix': {
|
||||
default_text: 'fixed a few things',
|
||||
keywords: ['fix', 'fixes', 'bugfix'],
|
||||
},
|
||||
'Quality of Life': {
|
||||
default_text: 'made something easier to use',
|
||||
keywords: ['qol'],
|
||||
},
|
||||
'Sound': {
|
||||
default_text: 'added/modified/removed audio or sound effects',
|
||||
keywords: ['sound'],
|
||||
},
|
||||
'Feature': {
|
||||
default_text: 'Added new mechanics or gameplay changes',
|
||||
alt_default_text: 'Added more things',
|
||||
keywords: ['add', 'adds', 'rscadd'],
|
||||
},
|
||||
'Removal': {
|
||||
default_text: 'Removed old things',
|
||||
keywords: ['del', 'dels', 'rscdel'],
|
||||
},
|
||||
'Sprites': {
|
||||
default_text: 'added/modified/removed some icons or images',
|
||||
keywords: ['image'],
|
||||
},
|
||||
'Grammar and Formatting': {
|
||||
default_text: 'fixed a few typos',
|
||||
keywords: ['typo', 'spellcheck'],
|
||||
},
|
||||
'Balance': {
|
||||
default_text: 'rebalanced something',
|
||||
keywords: ['balance'],
|
||||
},
|
||||
'Code Improvement': {
|
||||
default_text: 'changed some code',
|
||||
keywords: ['code_imp', 'code'],
|
||||
},
|
||||
'Refactor': {
|
||||
default_text: 'refactored some code',
|
||||
keywords: ['refactor'],
|
||||
},
|
||||
'Config Update': {
|
||||
default_text: 'changed some config setting',
|
||||
keywords: ['config'],
|
||||
},
|
||||
'Administration': {
|
||||
default_text: 'messed with admin stuff',
|
||||
keywords: ['admin'],
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user