From 2d6e7a647e74b5d3ba06df1241125f56ca38bd01 Mon Sep 17 00:00:00 2001 From: Integer Limit <103940576+IntegerLimit@users.noreply.github.com> Date: Wed, 1 Nov 2023 08:26:24 +1100 Subject: [PATCH] Allow for Transforming Issue Tags into Links (#511) [COMBINE] commits = ["ce5272fcf17b617495f05de102cf39dc23aece95"] [COMBINE] --- tools/globals.ts | 6 ++ tools/tasks/changelog/createChangelog.ts | 11 +++- tools/tasks/changelog/generateModChanges.ts | 3 +- tools/tasks/changelog/pusher.ts | 72 ++++++++++++++------- tools/tasks/changelog/specialParser.ts | 29 +++++---- tools/tasks/misc/releaseCommit.ts | 15 ++--- tools/tasks/shared/index.ts | 6 +- tools/util/util.ts | 61 ++++++++++++++++- 8 files changed, 146 insertions(+), 57 deletions(-) diff --git a/tools/globals.ts b/tools/globals.ts index d557a57..641f6f4 100644 --- a/tools/globals.ts +++ b/tools/globals.ts @@ -16,3 +16,9 @@ export const configFolder = upath.join(overridesFolder, "config"); export const configOverridesFolder = upath.join(overridesFolder, "config-overrides"); export const rootDirectory = ".."; export const templatesFolder = "templates"; + +// The Repository Owner (For Issues & PR Tags Transforms in Changelog) +export const repoOwner = "Nomi-CEu"; + +// The Repository Name (For Issues & PR Tags Transforms in Changelog) +export const repoName = "Nomi-CEu"; diff --git a/tools/tasks/changelog/createChangelog.ts b/tools/tasks/changelog/createChangelog.ts index e1c9181..a3868c1 100644 --- a/tools/tasks/changelog/createChangelog.ts +++ b/tools/tasks/changelog/createChangelog.ts @@ -10,6 +10,8 @@ import parse from "./parser"; import { specialParserSetup } from "./specialParser"; import generateModChanges from "./generateModChanges"; import pushAll, { pushChangelog, pushSeperator, pushTitle } from "./pusher"; +import log from "fancy-log"; +import * as util from "util"; /** * Generates a changelog based on environmental variables, and saves it a changelog data class. @@ -24,6 +26,8 @@ async function createChangelog(): Promise { const tags = data.getIterations(); pushTitle(data); for (const tag of tags) { + const iteration = tags.indexOf(tag); + log(`Iteration ${iteration + 1} of Changelog.`); data.setupIteration(tag); categoriesSetup(); specialParserSetup(data); @@ -34,8 +38,8 @@ async function createChangelog(): Promise { await generateModChanges(data); - pushChangelog(data); - if (tags.indexOf(tag) < tags.length - 1) { + await pushChangelog(data); + if (iteration < tags.length - 1) { // More to go pushSeperator(data); data.resetForIteration(); @@ -43,6 +47,7 @@ async function createChangelog(): Promise { } return data; } + log("No Iterations Detected."); categoriesSetup(); specialParserSetup(data); @@ -53,7 +58,7 @@ async function createChangelog(): Promise { await generateModChanges(data); - pushAll(data); + await pushAll(data); return data; } diff --git a/tools/tasks/changelog/generateModChanges.ts b/tools/tasks/changelog/generateModChanges.ts index 2108e3d..70cb4eb 100644 --- a/tools/tasks/changelog/generateModChanges.ts +++ b/tools/tasks/changelog/generateModChanges.ts @@ -8,6 +8,7 @@ import { defaultIndentation, modChangesAllocations, repoLink } from "./definitio import ChangelogData from "./changelogData"; import { SpecialChangelogFormatting } from "../../types/changelogTypes"; import { sortCommitListReverse } from "./pusher"; +import { error } from "fancy-log"; /** * Mod Changes special formatting @@ -161,7 +162,7 @@ function getCommitChange(SHA: string): CommitChange { oldManifest = JSON.parse(getFileAtRevision("manifest.json", `${SHA}^`)) as ModpackManifest; newManifest = JSON.parse(getFileAtRevision("manifest.json", SHA)) as ModpackManifest; } catch (e) { - console.error(dedent` + error(dedent` Failed to parse the manifest.json file at commit ${SHA} or the commit before! Skipping...`); return; diff --git a/tools/tasks/changelog/pusher.ts b/tools/tasks/changelog/pusher.ts index 8dd5879..6daa894 100644 --- a/tools/tasks/changelog/pusher.ts +++ b/tools/tasks/changelog/pusher.ts @@ -2,12 +2,15 @@ import ChangelogData from "./changelogData"; import { categories, defaultIndentation } from "./definitions"; import { Category, ChangelogMessage, Commit } from "../../types/changelogTypes"; import { repoLink } from "./definitions"; +import { Octokit } from "@octokit/rest"; +import { getIssueURL, getNewestIssueURLs } from "../../util/util"; let data: ChangelogData; +let octokit: Octokit; -export default function pushAll(inputData: ChangelogData): void { +export default async function pushAll(inputData: ChangelogData): Promise { pushTitle(inputData); - pushChangelog(inputData); + await pushChangelog(inputData); } export function pushTitle(inputData: ChangelogData): void { @@ -35,15 +38,22 @@ export function pushTitle(inputData: ChangelogData): void { } } -export function pushChangelog(inputData: ChangelogData): void { +export async function pushChangelog(inputData: ChangelogData): Promise { data = inputData; + octokit = new Octokit({ + auth: process.env.GITHUB_TOKEN, + }); + + // Save Issue/PR Info to Cache + await getNewestIssueURLs(octokit); + data.builder.push(`# Changes Since ${data.since}`, ""); // Push Sections of Changelog - categories.forEach((category) => { - pushCategory(category); - }); + for (const category of categories) { + await pushCategory(category); + } // Push the commit log if (data.commitList.length > 0) { @@ -75,12 +85,12 @@ export function pushSeperator(inputData: ChangelogData): void { /** * Pushes a given category to the builders. */ -function pushCategory(category: Category) { +async function pushCategory(category: Category) { const categoryLog: string[] = []; let hasValues = false; // Push All Sub Categories - category.subCategories.forEach((subCategory) => { + 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) { @@ -97,19 +107,18 @@ function pushCategory(category: Category) { ); // Push Log - list.forEach((changelogMessage) => { - categoryLog.push(formatChangelogMessage(changelogMessage)); + for (const changelogMessage of list) { + categoryLog.push(await formatChangelogMessage(changelogMessage)); // Push Sub Messages if (changelogMessage.subChangelogMessages) { - changelogMessage.subChangelogMessages.forEach((subMessage) => { - categoryLog.push(formatChangelogMessage(subMessage, true)); - }); + for (const subMessage of changelogMessage.subChangelogMessages) + categoryLog.push(await formatChangelogMessage(subMessage, true)); } - }); + } categoryLog.push(""); hasValues = true; } - }); + } if (hasValues) { // Push Title data.builder.push(`## ${category.categoryName}:`); @@ -164,22 +173,15 @@ export function sortCommitListReverse(list: Commit[]): void { * @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 */ -function formatChangelogMessage(changelogMessage: ChangelogMessage, subMessage = false): string { +async function formatChangelogMessage(changelogMessage: ChangelogMessage, subMessage = false): Promise { if (changelogMessage.specialFormatting) return changelogMessage.specialFormatting.formatting(changelogMessage, changelogMessage.specialFormatting.storage); const indentation = changelogMessage.indentation == undefined ? defaultIndentation : changelogMessage.indentation; let message = changelogMessage.commitMessage.trim(); - // Transform PR tags into a link. - if (message.match(/\(#\d+\)/g)) { - const matched = message.match(/\(#\d+\)/g); - matched.forEach((match) => { - // Extract digits - const digits = match.match(/\d+/g); - message = message.replace(match, `([#${digits}](${repoLink}pull/${digits}))`); - }); - } + // Transform PR and/or Issue tags into a link. + message = await transformTags(message); if (changelogMessage.commitObject && !subMessage) { if (data.combineList.has(changelogMessage.commitObject.hash)) { @@ -228,3 +230,23 @@ function formatCommit(commit: Commit): string { return `* [\`${shortSHA}\`](${repoLink}commit/${commit.hash}): ${formattedCommit}`; } + +/** + * Transforms PR/Issue Tags into Links. + */ +async function transformTags(message: string): Promise { + if (message.search(/#\d+/) !== -1) { + const matched = message.match(/#\d+/g); + for (const match of matched) { + // Extract digits + const digits = Number.parseInt(match.match(/\d+/)[0]); + + // Get PR/Issue Info (PRs are listed in the Issue API Endpoint) + const url = await getIssueURL(digits, octokit); + if (url) { + message = message.replace(match, `[#${digits}](${url})`); + } + } + } + return message; +} diff --git a/tools/tasks/changelog/specialParser.ts b/tools/tasks/changelog/specialParser.ts index 7fccca8..526f27b 100644 --- a/tools/tasks/changelog/specialParser.ts +++ b/tools/tasks/changelog/specialParser.ts @@ -28,6 +28,7 @@ import { } from "./definitions"; import { findCategories, findSubCategory } from "./parser"; import ChangelogData from "./changelogData"; +import { error } from "fancy-log"; let data: ChangelogData; @@ -46,7 +47,7 @@ export async function parseIgnore(commitBody: string, commitObject: Commit): Pro if (!info) return undefined; if (!info.checks) { - console.error(dedent` + error(dedent` Ignore Info in body: \`\`\` ${commitBody}\`\`\` @@ -58,7 +59,7 @@ export async function parseIgnore(commitBody: string, commitObject: Commit): Pro try { infoKeys = Object.keys(info.checks); } catch (err) { - console.error(dedent` + error(dedent` Could not get the keys in Ignore Info of body: \`\`\` ${commitBody}\`\`\` @@ -73,7 +74,7 @@ export async function parseIgnore(commitBody: string, commitObject: Commit): Pro infoKeys.forEach((key) => { if (ignoreKeys.has(key)) checkResults.push(ignoreChecks[key].call(this, info.checks[key], data)); else { - console.error(dedent` + error(dedent` Ignore Check with key '${key}' in body: \`\`\` ${commitBody}\`\`\` @@ -85,7 +86,7 @@ export async function parseIgnore(commitBody: string, commitObject: Commit): Pro } }); if (checkResults.length === 0) { - console.error(dedent` + error(dedent` No Ignore Checks found in body: \`\`\` ${commitBody}\`\`\` @@ -102,7 +103,7 @@ export async function parseIgnore(commitBody: string, commitObject: Commit): Pro if (info.logic === undefined) logic = defaultIgnoreLogic; else if (Object.keys(ignoreLogics).includes(info.logic)) logic = ignoreLogics[info.logic]; else { - console.error(dedent` + error(dedent` Ignore Logic '${info.logic}' in body: \`\`\` ${commitBody}\`\`\` @@ -290,7 +291,7 @@ async function parseTOML( if (!itemKey) item = parseResult.data as T; else item = parseResult.data[itemKey]; } catch (e) { - console.error(dedent` + error(dedent` Failed parsing TOML in body: \`\`\` ${commitBody}\`\`\` @@ -298,13 +299,13 @@ async function parseTOML( This could be because of invalid syntax.`); if (commitObject.body && commitBody !== commitObject.body) { - console.error(dedent` + error(dedent` Original Body: \`\`\` ${commitObject.body}\`\`\``); } - console.error(`\n${endMessage}\n`); + error(`\n${endMessage}\n`); if (data.isTest) throw e; return undefined; } @@ -335,19 +336,19 @@ async function parseTOMLToList( const endMessage = getEndMessage(delimiter); if (!messages || !Array.isArray(messages) || messages.length === 0) { - console.error(dedent` + error(dedent` List (key: '${listKey}') in body: \`\`\` ${commitBody}\`\`\` of commit object ${commitObject.hash} (${commitObject.message}) is empty, not a list, or does not exist.`); if (commitObject.body && commitBody !== commitObject.body) { - console.error(dedent` + error(dedent` Original Body: \`\`\` ${commitObject.body}\`\`\``); } - console.error(`${endMessage}\n`); + error(`${endMessage}\n`); if (data.isTest) throw new Error("Failed Parsing Message List. See Above."); return; @@ -355,19 +356,19 @@ async function parseTOMLToList( for (let i = 0; i < messages.length; i++) { const item = messages[i]; if (!emptyCheck(item)) { - console.error(dedent` + error(dedent` Missing Requirements for entry ${i + 1} in body: \`\`\` ${commitBody}\`\`\` of commit object ${commitObject.hash} (${commitObject.message}).`); if (commitObject.body && commitBody !== commitObject.body) { - console.error(dedent` + error(dedent` Original Body: \`\`\` ${commitObject.body}\`\`\``); } - console.error(`${endMessage}\n`); + error(`${endMessage}\n`); if (data.isTest) throw new Error("Bad Entry. See Above."); continue; diff --git a/tools/tasks/misc/releaseCommit.ts b/tools/tasks/misc/releaseCommit.ts index c869df1..2d519fe 100644 --- a/tools/tasks/misc/releaseCommit.ts +++ b/tools/tasks/misc/releaseCommit.ts @@ -6,6 +6,7 @@ import gulp from "gulp"; import dedent from "dedent-js"; import { checkEnvironmentalVariables } from "../../util/util"; import sortedStringify from "json-stable-stringify-without-jsonify"; +import log, { error } from "fancy-log"; // This updates all the files, for a release. @@ -30,11 +31,11 @@ export async function check(): Promise { const versionsFilePath: string = upath.join(templatesFolder, "versions.txt"); if (notRelease) { - console.log("Detected that this is not a release commit."); - console.log("Version info will not change, but the files will be updated from the template."); + log("Detected that this is not a release commit."); + log("Version info will not change, but the files will be updated from the template."); await checkNotRelease(versionsFilePath); } else { - console.log("Detected that this is a release commit."); + log("Detected that this is a release commit."); await checkRelease(versionsFilePath); } } @@ -50,9 +51,7 @@ export async function setNotRelease(): Promise { async function checkNotRelease(versionsFilePath: string) { // Check if versions.txt exists if (!fs.existsSync(versionsFilePath)) { - console.error( - `Version.txt does not exist. Creating empty file, and adding ${version} to it. This may be an error.`, - ); + error(`Version.txt does not exist. Creating empty file, and adding ${version} to it. This may be an error.`); // Create Versions.txt, with version await fs.promises.writeFile(versionsFilePath, ` - ${version}`); @@ -62,7 +61,7 @@ async function checkNotRelease(versionsFilePath: string) { // No Duplicate Key if (!versionList.includes(version)) { - console.error(`Version is not in version.txt. Adding ${version} to version.txt. This may be an error.`); + error(`Version is not in version.txt. Adding ${version} to version.txt. This may be an error.`); versionList = ` - ${version}\n${versionList}`; await fs.promises.writeFile(versionsFilePath, versionList); @@ -74,7 +73,7 @@ async function checkNotRelease(versionsFilePath: string) { async function checkRelease(versionsFilePath: string) { // Check if versions.txt exists if (!fs.existsSync(versionsFilePath)) { - console.error("Version.txt does not exist. Creating empty file. This may be an error."); + error("Version.txt does not exist. Creating empty file. This may be an error."); // Create Versions.txt fs.closeSync(fs.openSync(versionsFilePath, "w")); diff --git a/tools/tasks/shared/index.ts b/tools/tasks/shared/index.ts index 4148055..d09297d 100644 --- a/tools/tasks/shared/index.ts +++ b/tools/tasks/shared/index.ts @@ -85,12 +85,12 @@ async function fetchExternalDependencies() { */ async function fetchOrMakeChangelog() { if (isEnvVariableSet("CHANGELOG_URL") && isEnvVariableSet("CHANGELOG_CF_URL")) { - console.log("Using Changelog Files from URL."); + log("Using Changelog Files from URL."); await downloadChangelogs(process.env.CHANGELOG_URL, process.env.CHANGELOG_CF_URL); return; } if (isEnvVariableSet("CHANGELOG_BRANCH")) { - console.log("Using Changelog Files from Branch."); + log("Using Changelog Files from Branch."); const url = "https://raw.githubusercontent.com/Nomi-CEu/Nomi-CEu/{{ branch }}/{{ filename }}"; await downloadChangelogs( mustache.render(url, { branch: process.env.CHANGELOG_BRANCH, filename: "CHANGELOG.md" }), @@ -98,7 +98,7 @@ async function fetchOrMakeChangelog() { ); return; } - console.log("Creating Changelog Files."); + log("Creating Changelog Files."); await createBuildChangelog(); } diff --git a/tools/util/util.ts b/tools/util/util.ts index 997fa57..03f9762 100644 --- a/tools/util/util.ts +++ b/tools/util/util.ts @@ -13,10 +13,11 @@ import { fetchFileInfo, fetchProject, fetchProjectsBulk } from "./curseForgeAPI" import Bluebird from "bluebird"; import { VersionManifest } from "../types/versionManifest"; import { VersionsManifest } from "../types/versionsManifest"; -import log from "fancy-log"; +import log, { error } from "fancy-log"; import { pathspec, SimpleGit, simpleGit } from "simple-git"; import { Commit, ModChangeInfo } from "../types/changelogTypes"; -import { rootDirectory } from "../globals"; +import { repoName, repoOwner, rootDirectory } from "../globals"; +import { Octokit } from "@octokit/rest"; const LIBRARY_REG = /^(.+?):(.+?):(.+?)$/; @@ -238,7 +239,7 @@ export async function getChangelog(since = "HEAD", to = "HEAD", dirs: string[] = const commitList: Commit[] = []; await git.log(options, (err, output) => { if (err) { - console.error(err); + error(err); throw new Error(); } @@ -443,3 +444,57 @@ export function cleanupVersion(version: string): string { const list = version.match(/[\d+.?]+/g); return list[list.length - 1]; } + +const issueURLCache: Map = new Map(); + +/** + * Gets newest updated 100 closed issue/PR URLs of the repo and saves it to the cache. + */ +export async function getNewestIssueURLs(octokit: Octokit): Promise { + if (issueURLCache.size > 0) return; + try { + const issues = await octokit.issues.listForRepo({ + owner: repoOwner, + repo: repoName, + per_page: 100, + state: "closed", + sort: "updated", + }); + if (issues.status !== 200) { + error(`Failed to get all Issue URLs of Repo. Returned Status Code ${issues.status}, expected Status 200.`); + return; + } + issues.data.forEach((issue) => { + if (!issueURLCache.has(issue.number)) issueURLCache.set(issue.number, issue.html_url); + }); + } catch (e) { + error("Failed to get all Issue URLs of Repo. This may be because there are no issues, or because of rate limits."); + } +} + +/** + * Gets the specified Issue URL from the cache, or retrieves it. + */ +export async function getIssueURL(issueNumber: number, octokit: Octokit): Promise { + if (issueURLCache.has(issueNumber)) return issueURLCache.get(issueNumber); + try { + const issueInfo = await octokit.issues.get({ + owner: repoOwner, + repo: repoName, + issue_number: issueNumber, + }); + if (issueInfo.status !== 200) { + error( + `Failed to get the Issue/PR Info for Issue/PR #${issueNumber}. Returned Status Code ${issueInfo.status}, expected Status 200.`, + ); + return ""; + } + log(`No Issue URL Cache for Issue Number ${issueNumber}. Retrieved Specifically.`); + return issueInfo.data.html_url; + } catch (e) { + error( + `Failed to get the Issue/PR Info for Issue/PR #${issueNumber}. This may be because this is not a PR or Issue, or could be because of rate limits.`, + ); + return ""; + } +}