mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-10 10:12:45 +00:00
[MIRROR] Updates internal GBP script (#11428)
Co-authored-by: Selis <12716288+ItsSelis@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
83cda4d9e2
commit
9235a9f403
251
tools/pull_request_hooks/autoLabel.js
Normal file
251
tools/pull_request_hooks/autoLabel.js
Normal file
@@ -0,0 +1,251 @@
|
||||
import * as autoLabelConfig from './autoLabelConfig.js';
|
||||
|
||||
/**
|
||||
* Precompute a lowercase keyword → changelog label map
|
||||
*/
|
||||
const keywordToClLabel = (() => {
|
||||
const map = {};
|
||||
for (const [label, { keywords }] of Object.entries(
|
||||
autoLabelConfig.changelog_labels,
|
||||
)) {
|
||||
for (const keyword of keywords) {
|
||||
map[keyword.toLowerCase()] = label;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Precompute title keyword Sets per label for O(1) lookup
|
||||
*/
|
||||
const titleKeywordSets = (() => {
|
||||
const map = {};
|
||||
for (const [label, { keywords }] of Object.entries(
|
||||
autoLabelConfig.title_labels,
|
||||
)) {
|
||||
map[label] = new Set(keywords.map((k) => k.toLowerCase()));
|
||||
}
|
||||
return map;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Precompute filepaths Sets per label for O(1) lookup
|
||||
*/
|
||||
const fileLabelFilepathSets = (() => {
|
||||
const map = {};
|
||||
for (const [
|
||||
label,
|
||||
{ filepaths = [], file_extensions = [], add_only },
|
||||
] of Object.entries(autoLabelConfig.file_labels)) {
|
||||
map[label] = {
|
||||
filepaths: new Set(filepaths),
|
||||
file_extensions: new Set(file_extensions),
|
||||
add_only,
|
||||
};
|
||||
}
|
||||
return map;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Checks the body (primarily the changelog) for labels to add
|
||||
*/
|
||||
function check_body_for_labels(body) {
|
||||
const labels_to_add = [];
|
||||
|
||||
// detect "fixes #1234" or "resolves #1234" in body
|
||||
const fix_regex = /\b(?:fix(?:es|ed)?|resolve[sd]?)\s*#\d+\b/gim;
|
||||
if (fix_regex.test(body)) {
|
||||
labels_to_add.push('Fix');
|
||||
}
|
||||
|
||||
const lines = body.split('\n');
|
||||
let inChangelog = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith(':cl:')) {
|
||||
inChangelog = true;
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('/:cl:')) break;
|
||||
if (!inChangelog) continue;
|
||||
|
||||
// see if the first segment of the line is one of the keywords
|
||||
const keyword = line.split(':')[0]?.toLowerCase();
|
||||
const found_label = keywordToClLabel[keyword];
|
||||
if (!found_label) continue;
|
||||
|
||||
// don't add a billion tags if they forgot to clear all the default ones
|
||||
const line_text = line.split(':')[1]?.trim();
|
||||
const { default_text, alt_default_text } =
|
||||
autoLabelConfig.changelog_labels[found_label];
|
||||
|
||||
if (line_text !== default_text && line_text !== alt_default_text) {
|
||||
labels_to_add.push(found_label);
|
||||
}
|
||||
}
|
||||
|
||||
return labels_to_add;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the title for labels to add (O(1) keyword lookup)
|
||||
*/
|
||||
function check_title_for_labels(title) {
|
||||
const title_lower = title.toLowerCase();
|
||||
const labels_to_add = [];
|
||||
|
||||
for (const [label, keywordSet] of Object.entries(titleKeywordSets)) {
|
||||
for (const keyword of keywordSet) {
|
||||
if (title_lower.includes(keyword)) {
|
||||
labels_to_add.push(label);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return labels_to_add;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks changed files for labels to add/remove (O(1) filepath lookup)
|
||||
*/
|
||||
async function check_diff_files_for_labels(github, context) {
|
||||
const labels_to_add = [];
|
||||
const labels_to_remove = [];
|
||||
|
||||
try {
|
||||
// Use github.paginate to fetch all files (up to ~3000 max)
|
||||
const allFiles = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
per_page: 100, // max per request
|
||||
});
|
||||
|
||||
if (!allFiles?.length) {
|
||||
console.error('No files returned in pagination.');
|
||||
return { labels_to_add, labels_to_remove };
|
||||
}
|
||||
|
||||
// Set of changed filenames for quick lookup
|
||||
const changedFiles = new Set(allFiles.map((f) => f.filename));
|
||||
|
||||
for (const [
|
||||
label,
|
||||
{ filepaths = new Set(), file_extensions = new Set(), add_only },
|
||||
] of Object.entries(fileLabelFilepathSets)) {
|
||||
let found = false;
|
||||
|
||||
// Filepath-based matching
|
||||
for (const filename of changedFiles) {
|
||||
for (const path of filepaths) {
|
||||
if (filename.includes(path)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
|
||||
// File extension-based matching
|
||||
if (!found && file_extensions.size) {
|
||||
for (const filename of changedFiles) {
|
||||
for (const ext of file_extensions) {
|
||||
if (filename.endsWith(ext)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
labels_to_add.push(label);
|
||||
} else if (!add_only) {
|
||||
labels_to_remove.push(label);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching paginated files:', error);
|
||||
}
|
||||
|
||||
return { labels_to_add, labels_to_remove };
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to get the updated label set
|
||||
*/
|
||||
export async function get_updated_label_set({ github, context }) {
|
||||
const { action, pull_request } = context.payload;
|
||||
const {
|
||||
body = '',
|
||||
diff_url,
|
||||
labels = [],
|
||||
mergeable,
|
||||
title = '',
|
||||
} = pull_request;
|
||||
|
||||
const updated_labels = new Set(labels.map((l) => l.name));
|
||||
|
||||
// Always check file diffs
|
||||
if (diff_url) {
|
||||
const { labels_to_add, labels_to_remove } =
|
||||
await check_diff_files_for_labels(github, context);
|
||||
labels_to_add.forEach((label) => updated_labels.add(label));
|
||||
labels_to_remove.forEach((label) => updated_labels.delete(label));
|
||||
}
|
||||
|
||||
// Check body/title only when PR is opened, not on sync
|
||||
if (action === 'opened') {
|
||||
if (title)
|
||||
check_title_for_labels(title).forEach((label) =>
|
||||
updated_labels.add(label),
|
||||
);
|
||||
if (body)
|
||||
check_body_for_labels(body).forEach((label) => updated_labels.add(label));
|
||||
}
|
||||
|
||||
// Always remove Test Merge Candidate
|
||||
updated_labels.delete('Test Merge Candidate');
|
||||
|
||||
// Handle 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];
|
||||
}
|
||||
126
tools/pull_request_hooks/autoLabelConfig.js
Normal file
126
tools/pull_request_hooks/autoLabelConfig.js
Normal file
@@ -0,0 +1,126 @@
|
||||
// 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/'],
|
||||
file_extensions: ['.dmm'],
|
||||
},
|
||||
Tools: {
|
||||
filepaths: ['tools/'],
|
||||
},
|
||||
'Config Update': {
|
||||
filepaths: ['config/', 'code/controllers/configuration/entries/'],
|
||||
add_only: true,
|
||||
},
|
||||
Sprites: {
|
||||
filepaths: ['icons/'],
|
||||
file_extensions: ['.dmi'],
|
||||
add_only: true,
|
||||
},
|
||||
Sound: {
|
||||
filepaths: ['sound/'],
|
||||
file_extensions: ['.ogg'],
|
||||
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