mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-24 05:30:05 +01:00
Redoes the labeler action js backend (#92504)
## About The Pull Request Redoes the labeler script to work for private or restricted repositories (both of which need an auth token for using the REST api). Previous version used `fetch()`, and checked the line diffs which was inefficient and unnecessary. You only really need to be checking the filenames when it comes to the diffs. Now it uses [octokit.rest.pulls.listFiles](https://octokit.github.io/rest.js/v20/#pulls-list-files) and pagination. This version is faster and can support a larger amount of diffs, up to 3000 files changed, and plays nicely with auth tokens for private or restricted repos. Hooray! Tested the various functions of it & things should work same as before. <img width="591" height="171" alt="image" src="https://github.com/user-attachments/assets/15ff75e2-0335-4b18-b6d7-3aefb312d173" /> Also adds support for matching file extensions, so it can find .dmm and .dmis that might be in directories that do not start with `_maps/` or `icons/`. Thanks to @Kocma-san for doing a large part of this and helping figure out what the issue was. ## Changelog Not player-facing
This commit is contained in:
@@ -1,110 +1,177 @@
|
||||
import * as autoLabelConfig from "./autoLabelConfig.js";
|
||||
|
||||
function keyword_to_cl_label() {
|
||||
const keyword_to_cl_label = {};
|
||||
for (const label in autoLabelConfig.changelog_labels) {
|
||||
for (const keyword of autoLabelConfig.changelog_labels[label].keywords) {
|
||||
keyword_to_cl_label[keyword] = label;
|
||||
/**
|
||||
* 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 keyword_to_cl_label;
|
||||
}
|
||||
return map;
|
||||
})();
|
||||
|
||||
// Checks the body (primarily the changelog) for labels to add
|
||||
/**
|
||||
* 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 = [];
|
||||
|
||||
// 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");
|
||||
// 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 keywords = keyword_to_cl_label();
|
||||
const lines = body.split("\n");
|
||||
let inChangelog = false;
|
||||
|
||||
let found_cl = false;
|
||||
for (const line of body.split("\n")) {
|
||||
for (const line of lines) {
|
||||
if (line.startsWith(":cl:")) {
|
||||
found_cl = true;
|
||||
continue;
|
||||
} else if (line.startsWith("/:cl:")) {
|
||||
break;
|
||||
} else if (!found_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 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);
|
||||
}
|
||||
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
|
||||
/**
|
||||
* Checks the title for labels to add (O(1) keyword lookup)
|
||||
*/
|
||||
function check_title_for_labels(title) {
|
||||
const labels_to_add = [];
|
||||
const title_lower = title.toLowerCase();
|
||||
for (const label in autoLabelConfig.title_labels) {
|
||||
let found = false;
|
||||
for (const keyword of autoLabelConfig.title_labels[label].keywords) {
|
||||
const labels_to_add = [];
|
||||
|
||||
for (const [label, keywordSet] of Object.entries(titleKeywordSets)) {
|
||||
for (const keyword of keywordSet) {
|
||||
if (title_lower.includes(keyword)) {
|
||||
found = true;
|
||||
labels_to_add.push(label);
|
||||
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) {
|
||||
/**
|
||||
* 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 {
|
||||
const diff = await fetch(diff_url);
|
||||
if (diff.ok) {
|
||||
const diff_txt = await diff.text();
|
||||
for (const label in autoLabelConfig.file_labels) {
|
||||
let found = false;
|
||||
const { filepaths, add_only } = autoLabelConfig.file_labels[label];
|
||||
for (const filepath of filepaths) {
|
||||
if (check_diff_line_for_element(diff_txt, filepath)) {
|
||||
// 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) {
|
||||
labels_to_add.push(label);
|
||||
} else if (!add_only) {
|
||||
labels_to_remove.push(label);
|
||||
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;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(`Failed to fetch diff: ${diff.status} ${diff.statusText}`);
|
||||
|
||||
if (found) {
|
||||
labels_to_add.push(label);
|
||||
} else if (!add_only) {
|
||||
labels_to_remove.push(label);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} 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 {
|
||||
@@ -115,39 +182,30 @@ export async function get_updated_label_set({ github, context }) {
|
||||
title = "",
|
||||
} = pull_request;
|
||||
|
||||
const updated_labels = new Set();
|
||||
for (const label of labels) {
|
||||
updated_labels.add(label.name);
|
||||
}
|
||||
const updated_labels = new Set(labels.map((l) => l.name));
|
||||
|
||||
// diff is always checked
|
||||
// Always check file diffs
|
||||
if (diff_url) {
|
||||
const diff_tags = await check_diff_for_labels(diff_url);
|
||||
for (const label of diff_tags.labels_to_add) {
|
||||
updated_labels.add(label);
|
||||
}
|
||||
for (const 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 (const label of check_title_for_labels(title)) {
|
||||
updated_labels.add(label);
|
||||
}
|
||||
}
|
||||
if (body) {
|
||||
for (const label of check_body_for_labels(body)) {
|
||||
updated_labels.add(label);
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
// this is always removed on updates
|
||||
// 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");
|
||||
|
||||
// update merge conflict label
|
||||
// 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
|
||||
@@ -159,6 +217,7 @@ export async function get_updated_label_set({ github, context }) {
|
||||
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));
|
||||
@@ -177,6 +236,7 @@ export async function get_updated_label_set({ github, context }) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (merge_conflict) {
|
||||
updated_labels.add("Merge Conflict");
|
||||
} else {
|
||||
|
||||
@@ -7,31 +7,34 @@
|
||||
// the label will not be removed
|
||||
export const file_labels = {
|
||||
GitHub: {
|
||||
filepaths: [".github"],
|
||||
filepaths: [".github/"],
|
||||
},
|
||||
SQL: {
|
||||
filepaths: ["SQL"],
|
||||
filepaths: ["SQL/"],
|
||||
},
|
||||
"Map Edit": {
|
||||
filepaths: ["_maps"],
|
||||
filepaths: ["_maps/"],
|
||||
file_extensions: [".dmm"],
|
||||
},
|
||||
Tools: {
|
||||
filepaths: ["tools"],
|
||||
filepaths: ["tools/"],
|
||||
},
|
||||
"Config Update": {
|
||||
filepaths: ["config", "code/controllers/configuration/entries"],
|
||||
filepaths: ["config/", "code/controllers/configuration/entries/"],
|
||||
add_only: true,
|
||||
},
|
||||
Sprites: {
|
||||
filepaths: ["icons"],
|
||||
filepaths: ["icons/"],
|
||||
file_extensions: [".dmi"],
|
||||
add_only: true,
|
||||
},
|
||||
Sound: {
|
||||
filepaths: ["sound"],
|
||||
filepaths: ["sound/"],
|
||||
file_extensions: [".ogg"],
|
||||
add_only: true,
|
||||
},
|
||||
UI: {
|
||||
filepaths: ["tgui"],
|
||||
filepaths: ["tgui/"],
|
||||
add_only: true,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user