tracer4b b526677126 Questbook Update + Buildscript Improvements (#681)
[EXPAND]
[[messages]]
messageTitle = "QB Update for GT 2.8 (#681)"
messageBody = """
[QB]
[DETAILS]
details = ["Fixes many Quest Book issues", "Updates QB with changes in GT 2.8"]
[DETAILS]
"""

[[messages]]
messageTitle = "Buildscript Refactor (#681)"
messageBody = """
[INTERNAL]
[DETAILS]
details = ["**Important: Buildscript has changed from `npx gulp...` or `gulp...` to `npm run gulp...`**!", "Moves to Node 16 Package Management + Typescript Strict Mode", "New Port QB, Check QB and Fix QB Tasks"]
[DETAILS]
"""
[EXPAND]


Co-authored-by: Integer Limit <103940576+IntegerLimit@users.noreply.github.com>
Co-authored-by: Ghzdude <44148655+ghzdude@users.noreply.github.com>
Co-authored-by: SparkedTheorem <162088357+SparkedTheorem@users.noreply.github.com>
2024-05-14 21:57:00 +10:00

317 lines
9.3 KiB
TypeScript

import ChangelogData from "./changelogData.ts";
import { categories, defaultIndentation } from "./definitions.ts";
import { Category, ChangelogMessage, Commit } from "#types/changelogTypes.ts";
import { repoLink } from "./definitions.ts";
import { Octokit } from "@octokit/rest";
import { getIssueURL, getNewestIssueURLs } from "#utils/util.ts";
let data: ChangelogData;
let octokit: Octokit;
export default async function pushAll(inputData: ChangelogData): Promise<void> {
pushTitle(inputData);
await pushChangelog(inputData);
}
export async function pushSetup(): Promise<void> {
octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
// Save Issue/PR Info to Cache
await getNewestIssueURLs(octokit);
}
export function pushTitle(inputData: ChangelogData): void {
data = inputData;
// Push the titles.
// Center Align is replaced by the correct center align style in the respective deployments.
// Must be triple bracketed, to make mustache not html escape it.
if (data.releaseType === "Cutting Edge Build") {
const date = new Date().toLocaleDateString("en-us", {
year: "numeric",
month: "short",
day: "numeric",
hour12: true,
hour: "numeric",
minute: "numeric",
timeZoneName: "short",
});
// noinspection HtmlDeprecatedAttribute
data.builder.push(
`<h1 align="center">${data.releaseType} (${date})</h1>`,
"",
);
} else {
// noinspection HtmlUnknownAttribute
data.builder.push(
`<h1 {{{ CENTER_ALIGN }}}>${data.releaseType} ${data.to}</h1>`,
"",
);
data.builder.push("{{{ CF_REDIRECT }}}", "");
}
}
export async function pushChangelog(inputData: ChangelogData): Promise<void> {
data = inputData;
data.builder.push(`# Changes Since ${data.since}`, "");
// Push Sections of Changelog
for (const category of categories) {
await pushCategory(category);
}
// Push the commit log
if (data.commitList.length > 0) {
sortCommitList(data.commitList, (commit) => commit);
data.builder.push("## Commits");
data.commitList.forEach((commit) => {
data.builder.push(formatCommit(commit));
});
} else {
// No Commit List = No Changes
data.builder.push("");
data.builder.push("**There haven't been any changes.**");
}
// Push link
data.builder.push(
"",
`**Full Changelog**: [\`${data.since}...${data.to}\`](${repoLink}compare/${data.since}...${data.to})`,
);
}
export function pushSeperator(inputData: ChangelogData): void {
data = inputData;
data.builder.push("", "<hr>", "");
}
/**
* Pushes a given category to the builders.
*/
async function pushCategory(category: Category) {
const categoryLog: string[] = [];
let hasValues = false;
// Push All Sub Categories
for (const subCategory of category.subCategories) {
// Loop through key list instead of map to produce correct order
const list = category.changelogSection?.get(subCategory);
if (list && list.length != 0) {
// Push Key Name (only pushes if Key Name is not "")
if (subCategory.keyName) {
categoryLog.push(`### ${subCategory.keyName}:`);
}
// Sort Log
sortCommitList(
list,
(message) => message.commitObject,
(a, b) => a.commitMessage.localeCompare(b.commitMessage),
);
// Push Log
for (const changelogMessage of list) {
categoryLog.push(await formatChangelogMessage(changelogMessage));
// Push Sub Messages
if (changelogMessage.subChangelogMessages) {
for (const subMessage of changelogMessage.subChangelogMessages)
categoryLog.push(await formatChangelogMessage(subMessage, true));
}
}
categoryLog.push("");
hasValues = true;
}
}
await transformAllIssueURLs(categoryLog);
if (hasValues) {
// Push Title
data.builder.push(`## ${category.categoryName}:`);
// Push previously made log
data.builder.push(...categoryLog);
}
}
/**
* Sorts a list that contains commit data
* @param list A list of type T that contains commit data
* @param transform A function to turn each element of type T into an element of type Commit
* @param backup A backup sort, to call when either element does not have a commit object, or when the commit objects' times are the same. Optional, if not set, will just return 0 (equal) or will compare commit messages.
*/
function sortCommitList<T>(
list: T[],
transform: (obj: T) => Commit | undefined,
backup?: (a: T, b: T) => number,
) {
list.sort((a, b): number => {
const commitA = transform(a);
const commitB = transform(b);
if (!commitA || !commitB) {
// If either commit is undefined
if (backup) return backup(a, b);
return 0;
}
const dateA = new Date(commitA.date);
const dateB = new Date(commitB.date);
// This is reversed, so higher priorities go on top
if (commitB.priority !== commitA.priority)
return (commitB.priority ?? 0) - (commitA.priority ?? 0);
// This is reversed, so the newest commits go on top
if (dateB.getTime() - dateA.getTime() !== 0)
return dateB.getTime() - dateA.getTime();
if (backup) return backup(a, b);
return commitA.message.localeCompare(commitB.message);
});
}
/**
* Sorts a commits list so that newest commits are on the bottom.
* @param list The commit list.
*/
export function sortCommitListReverse(list: Commit[]): void {
list.sort((a, b) => {
const dateA = new Date(a.date);
const dateB = new Date(b.date);
// This is reversed, so higher priorities go on top
if (b.priority !== a.priority) return (b.priority ?? 0) - (a.priority ?? 0); // Priority is still highest first
if (dateA.getTime() - dateB.getTime() !== 0)
return dateA.getTime() - dateB.getTime();
return a.message.localeCompare(b.message);
});
}
/**
* Formats a Changelog Message
* @param changelogMessage The message to format.
* @param subMessage Whether this message is a subMessage (used in details). Set to true to make it a subMessage (different parsing). Defaults to false.
* @return string Formatted Changelog Message
*/
async function formatChangelogMessage(
changelogMessage: ChangelogMessage,
subMessage = false,
): Promise<string> {
const indentation =
changelogMessage.indentation == undefined
? defaultIndentation
: changelogMessage.indentation;
const message = changelogMessage.commitMessage.trim();
if (changelogMessage.specialFormatting)
return changelogMessage.specialFormatting.formatting(
message,
subMessage,
indentation,
changelogMessage.specialFormatting.storage,
);
if (changelogMessage.commitObject && !subMessage) {
if (data.combineList.has(changelogMessage.commitObject.hash)) {
const commits =
data.combineList.get(changelogMessage.commitObject.hash) ?? [];
commits.push(changelogMessage.commitObject);
// Sort original array so newest commits appear at the end instead of start of commit string
sortCommitListReverse(commits);
const formattedCommits: string[] = [];
const authors: string[] = [];
const authorEmails: Set<string> = new Set<string>();
const processedSHAs: Set<string> = new Set<string>();
commits.forEach((commit) => {
if (processedSHAs.has(commit.hash)) return;
if (
!authors.includes(commit.author_name) &&
!authorEmails.has(commit.author_email)
) {
authors.push(commit.author_name);
authorEmails.add(commit.author_email);
}
formattedCommits.push(
`[\`${commit.hash.substring(0, 7)}\`](${repoLink}commit/${commit.hash})`,
);
processedSHAs.add(commit.hash);
});
authors.sort();
return `${indentation}* ${message} - **${authors.join("**, **")}** (${formattedCommits.join(", ")})`;
}
const commit = changelogMessage.commitObject;
const shortSHA = commit.hash.substring(0, 7);
const author = commit.author_name;
return `${indentation}* ${message} - **${author}** ([\`${shortSHA}\`](${repoLink}commit/${commit.hash}))`;
}
return `${indentation}* ${message}`;
}
/**
* Returns a formatted commit
*/
function formatCommit(commit: Commit): string {
const date = new Date(commit.date).toLocaleDateString("en-us", {
year: "numeric",
month: "short",
day: "numeric",
});
const formattedCommit = `${commit.message} - **${commit.author_name}** (${date})`;
const shortSHA = commit.hash.substring(0, 7);
return `* [\`${shortSHA}\`](${repoLink}commit/${commit.hash}): ${formattedCommit}`;
}
/**
* Transforms PR/Issue Tags in all strings of the generated changelog.
* @param changelog The list to transform all PR/Issue Tags of.
*/
async function transformAllIssueURLs(changelog: string[]) {
const promises: Promise<string>[] = [];
for (let i = 0; i < changelog.length; i++) {
const categoryFormatted = changelog[i];
// Transform PR and/or Issue tags into a link.
promises.push(
transformTags(categoryFormatted).then(
(categoryTransformed) => (changelog[i] = categoryTransformed),
),
);
}
// Apply all Link Changes
await Promise.all(promises);
}
/**
* Transforms PR/Issue Tags into Links.
*/
async function transformTags(message: string): Promise<string> {
const promises: Promise<string>[] = [];
if (message.search(/#\d+/) !== -1) {
const matched = message.match(/#\d+/g) ?? [];
for (const match of matched) {
// Extract digits
const digitsMatch = match.match(/\d+/);
if (!digitsMatch) continue;
const digits = Number.parseInt(digitsMatch[0]);
// Get PR/Issue Info (PRs are listed in the Issue API Endpoint)
promises.push(
getIssueURL(digits, octokit).then((url) =>
message.replace(match, `[#${digits}](${url})`),
),
);
}
}
// Resolve all Issue URL Replacements
await Promise.all(promises);
return message;
}