mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-07-26 18:36:23 +09:00
Merge branch 'explosivegaming:main' into aperx
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@expcluster/lib_commands",
|
"name": "@expcluster/lib_commands",
|
||||||
"version": "0.1.0",
|
"version": "6.5.0",
|
||||||
"description": "Clusterio plugin providing a Lua command processing library.",
|
"description": "Clusterio plugin providing a Lua command processing library.",
|
||||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@expcluster/permission-groups",
|
"name": "@expcluster/permission-groups",
|
||||||
"version": "0.1.0",
|
"version": "6.5.0",
|
||||||
"description": "Clusterio plugin providing syncing of permission groups",
|
"description": "Clusterio plugin providing syncing of permission groups",
|
||||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@expcluster/lib_gui",
|
"name": "@expcluster/lib_gui",
|
||||||
"version": "0.1.0",
|
"version": "6.5.0",
|
||||||
"description": "Clusterio plugin providing a Lua GUI definition library.",
|
"description": "Clusterio plugin providing a Lua GUI definition library.",
|
||||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@expcluster/legacy",
|
"name": "@expcluster/legacy",
|
||||||
"version": "0.1.0",
|
"version": "6.5.0",
|
||||||
"description": "Clusterio plugin implementing the legacy v6 scenario updated for factorio 2.0",
|
"description": "Clusterio plugin implementing the legacy v6 scenario updated for factorio 2.0",
|
||||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expcluster/lib_commands": "workspace:^",
|
"@expcluster/lib_commands": "workspace:^",
|
||||||
"@expcluster/lib_util": "workspace:^",
|
"@expcluster/lib_util": "workspace:^",
|
||||||
|
"@expcluster/lib_gui": "workspace:^",
|
||||||
"@sinclair/typebox": "catalog:"
|
"@sinclair/typebox": "catalog:"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@expcluster/scenario",
|
"name": "@expcluster/scenario",
|
||||||
"version": "0.1.0",
|
"version": "6.5.0",
|
||||||
"description": "Clusterio plugin implementing the Explosive Gaming scenario.",
|
"description": "Clusterio plugin implementing the Explosive Gaming scenario.",
|
||||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expcluster/lib_commands": "workspace:^",
|
"@expcluster/lib_commands": "workspace:^",
|
||||||
"@expcluster/lib_util": "workspace:^",
|
"@expcluster/lib_util": "workspace:^",
|
||||||
|
"@expcluster/lib_gui": "workspace:^",
|
||||||
"@sinclair/typebox": "catalog:"
|
"@sinclair/typebox": "catalog:"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@expcluster/server-ups",
|
"name": "@expcluster/server-ups",
|
||||||
"version": "0.1.0",
|
"version": "6.5.0",
|
||||||
"description": "Clusterio plugin providing in game server ups counter",
|
"description": "Clusterio plugin providing in game server ups counter",
|
||||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -25,9 +25,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sinclair/typebox": "catalog:",
|
"@sinclair/typebox": "catalog:",
|
||||||
|
"@expcluster/lib_commands": "workspace:^",
|
||||||
"@expcluster/lib_util": "workspace:^",
|
"@expcluster/lib_util": "workspace:^",
|
||||||
"@expcluster/lib_gui": "workspace:^",
|
"@expcluster/lib_gui": "workspace:^"
|
||||||
"@expcluster/lib_commands": "workspace:^"
|
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@expcluster/lib_util",
|
"name": "@expcluster/lib_util",
|
||||||
"version": "0.1.0",
|
"version": "6.5.0",
|
||||||
"description": "Clusterio plugin providing Lua libraries and other utilities.",
|
"description": "Clusterio plugin providing Lua libraries and other utilities.",
|
||||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
export async function githubFetch(
|
||||||
|
path: string,
|
||||||
|
query: Record<string, string>= {},
|
||||||
|
init: RequestInit & { headers?: Record<string, string> } = {},
|
||||||
|
) {
|
||||||
|
const url = new URL("https://api.github.com/");
|
||||||
|
url.pathname = path;
|
||||||
|
for (const [name, value] of Object.entries(query)) {
|
||||||
|
url.searchParams.set(name, value);
|
||||||
|
}
|
||||||
|
init = {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
...init.headers as Record<string, string> ?? {},
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (process.env.GH_TOKEN) {
|
||||||
|
init.headers!["Authorization"] = `Bearer ${process.env.GH_TOKEN}`;
|
||||||
|
}
|
||||||
|
console.log("Fetching", url.href);
|
||||||
|
const response = await fetch(url, init);
|
||||||
|
if (!response.ok) {
|
||||||
|
console.log(response.headers);
|
||||||
|
throw new Error(`GitHub replied with ${response.status} ${response.statusText}: ${await response.text()}`);
|
||||||
|
}
|
||||||
|
if (!response.headers.get("Content-Type")?.startsWith("application/json")) {
|
||||||
|
throw new Error(
|
||||||
|
`GitHub replied with ${response.status} ${response.statusText}: ${response.headers.get("Content-Type")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function githubFetchJson<T>(
|
||||||
|
path: string,
|
||||||
|
query: Record<string, string> = {},
|
||||||
|
init: RequestInit & { headers?: Record<string, string> } = {},
|
||||||
|
): Promise<T> {
|
||||||
|
const response = await githubFetch(path, query, init);
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function githubFetchJsonPaginated<T>(
|
||||||
|
path: string,
|
||||||
|
query: Record<string, string> = {},
|
||||||
|
init: RequestInit & { headers?: Record<string, string> } = {},
|
||||||
|
shouldContinue: (result: T[]) => boolean = () => true,
|
||||||
|
): Promise<T[]> {
|
||||||
|
const data: T[] = [];
|
||||||
|
let pagesRemaining = true;
|
||||||
|
query = {
|
||||||
|
...query,
|
||||||
|
per_page: "100",
|
||||||
|
};
|
||||||
|
while (pagesRemaining) {
|
||||||
|
const response = await githubFetch(path, query, init);
|
||||||
|
const responseItems = await response.json();
|
||||||
|
data.push(...responseItems);
|
||||||
|
const linkHeader = response.headers.get("Link");
|
||||||
|
pagesRemaining = linkHeader?.includes(`rel="next"`) ?? false;
|
||||||
|
if (pagesRemaining) {
|
||||||
|
pagesRemaining = shouldContinue(responseItems);
|
||||||
|
}
|
||||||
|
if (pagesRemaining) {
|
||||||
|
query = {
|
||||||
|
...query,
|
||||||
|
page: String((query.page ? (Number(query.page)) : 1) + 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal types that include only what is used and useful for our scripts. See
|
||||||
|
// https://docs.github.com/en/rest for complete reference.
|
||||||
|
export interface PullRequest {
|
||||||
|
number: number,
|
||||||
|
url: string,
|
||||||
|
html_url: string,
|
||||||
|
state: string,
|
||||||
|
title: string,
|
||||||
|
body: string | null,
|
||||||
|
created_at: string,
|
||||||
|
updated_at: string,
|
||||||
|
closed_at: string | null,
|
||||||
|
merged_at: string | null,
|
||||||
|
merge_commit_sha: string | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Issue {
|
||||||
|
number: number,
|
||||||
|
title: string,
|
||||||
|
url: string,
|
||||||
|
html_url: string,
|
||||||
|
state: string,
|
||||||
|
body?: string | null,
|
||||||
|
user: {
|
||||||
|
login: string,
|
||||||
|
url: string,
|
||||||
|
html_url: string,
|
||||||
|
}
|
||||||
|
pull_request?: {
|
||||||
|
url: string,
|
||||||
|
html_url: string,
|
||||||
|
merged_at?: string | null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Release {
|
||||||
|
tag_name: string,
|
||||||
|
target_commitish: string,
|
||||||
|
name: string | null,
|
||||||
|
body: string | null,
|
||||||
|
created_at: string,
|
||||||
|
published_at: string | null,
|
||||||
|
updated_at?: string | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Commit {
|
||||||
|
sha: string,
|
||||||
|
commit: {
|
||||||
|
message: string,
|
||||||
|
},
|
||||||
|
author: null | {} | {
|
||||||
|
name?: null | string,
|
||||||
|
login: string,
|
||||||
|
type: string,
|
||||||
|
},
|
||||||
|
parents: {
|
||||||
|
sha: string,
|
||||||
|
}[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Reference {
|
||||||
|
ref: string,
|
||||||
|
object: {
|
||||||
|
type: string,
|
||||||
|
sha: string,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
import {
|
||||||
|
type Release,
|
||||||
|
type Issue,
|
||||||
|
githubFetchJson,
|
||||||
|
githubFetchJsonPaginated,
|
||||||
|
} from "./github_api.mts";
|
||||||
|
|
||||||
|
type Changelog = Record<string, string[]>;
|
||||||
|
|
||||||
|
const repository = "explosivegaming/ExpCluster";
|
||||||
|
const branch = "main";
|
||||||
|
|
||||||
|
const changelogSections = [
|
||||||
|
"Major Features",
|
||||||
|
"Features",
|
||||||
|
"Fixes",
|
||||||
|
"Changes",
|
||||||
|
"Breaking Changes",
|
||||||
|
"Meta",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Generic result codes
|
||||||
|
const success = 0;
|
||||||
|
const failure = 1;
|
||||||
|
|
||||||
|
type ParserState = {
|
||||||
|
pos: number,
|
||||||
|
lines: string[],
|
||||||
|
};
|
||||||
|
|
||||||
|
function createParser(lines: string[]): ParserState {
|
||||||
|
return { pos: 0, lines };
|
||||||
|
}
|
||||||
|
|
||||||
|
function atEnd(parser: ParserState) {
|
||||||
|
return parser.pos >= parser.lines.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentLine(parser: ParserState) {
|
||||||
|
if (atEnd(parser)) {
|
||||||
|
throw new Error("Attempt to access line past end");
|
||||||
|
}
|
||||||
|
return parser.lines[parser.pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
function skipToLine(
|
||||||
|
parser: ParserState,
|
||||||
|
pattern: RegExp
|
||||||
|
) {
|
||||||
|
for (; parser.pos < parser.lines.length; parser.pos++) {
|
||||||
|
if (pattern.test(currentLine(parser))) {
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
function skipEmptyLines(parser: ParserState) {
|
||||||
|
while (parser.pos < parser.lines.length && /^ *$/.test(currentLine(parser))) {
|
||||||
|
parser.pos += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPullRequestChangelog(
|
||||||
|
parser: ParserState,
|
||||||
|
):
|
||||||
|
| [typeof success, string[]]
|
||||||
|
| [typeof failure, undefined, string]
|
||||||
|
{
|
||||||
|
const headerFail = skipToLine(parser, /^###? Changelog *$/);
|
||||||
|
if (headerFail) {
|
||||||
|
return [failure, , "Header not found"];
|
||||||
|
}
|
||||||
|
parser.pos += 1;
|
||||||
|
skipEmptyLines(parser);
|
||||||
|
if (!atEnd(parser) && /^None/i.test(currentLine(parser))) {
|
||||||
|
parser.pos += 1;
|
||||||
|
return [success, []];
|
||||||
|
}
|
||||||
|
const blockStartFail = skipToLine(parser, /```/);
|
||||||
|
if (blockStartFail) {
|
||||||
|
return [failure, , "Code block not found after header"];
|
||||||
|
}
|
||||||
|
parser.pos += 1
|
||||||
|
const blockStartPos = parser.pos;
|
||||||
|
const blockEndFail = skipToLine(parser, /```/);
|
||||||
|
if (blockEndFail) {
|
||||||
|
return [failure, , "End of code block not found after header"];
|
||||||
|
}
|
||||||
|
return [success, parser.lines.slice(blockStartPos, parser.pos)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseChangelog(parser: ParserState, pr: Issue, issues: Issue[]): [Changelog, string[]] {
|
||||||
|
let section: string | undefined;
|
||||||
|
let changelog: Changelog = Object.create(null);
|
||||||
|
let warnings: string[] = [];
|
||||||
|
while (!atEnd(parser)) {
|
||||||
|
const line = currentLine(parser);
|
||||||
|
parser.pos += 1;
|
||||||
|
if (line.trim() === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.startsWith("###")) {
|
||||||
|
section = line.slice(4).trim();
|
||||||
|
if (!changelogSections.includes(section)) {
|
||||||
|
warnings.push(
|
||||||
|
`Changelog section ${section} on line ${parser.pos} is not one of ${changelogSections.join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
changelog[section] = changelog[section] ?? [];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!section) {
|
||||||
|
warnings.push(`Unexpected content "${line}" outside of a changelog section on line ${parser.pos}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.startsWith("- ")) {
|
||||||
|
const entry = line
|
||||||
|
.slice(2)
|
||||||
|
.trim()
|
||||||
|
.replace(new RegExp(`\\[#(\\d+)\\]\\(https://github.com/${repository}/(issues|pull)/\\d+\\)`, "g"), "#$1");
|
||||||
|
let hasRef = false
|
||||||
|
for (const ref of entry.matchAll(/#(\d+)/g)) {
|
||||||
|
hasRef = true;
|
||||||
|
const id = Number.parseInt(ref[1], 10);
|
||||||
|
const refIssue = issues.find(issue => issue.number === id);
|
||||||
|
if (!refIssue) {
|
||||||
|
warnings.push(
|
||||||
|
`Issue reference ${ref[0]} on line ${parser.pos} does not exist ` +
|
||||||
|
"or has not been updated since the last release"
|
||||||
|
);
|
||||||
|
} else if (refIssue.pull_request && refIssue.number !== pr.number) {
|
||||||
|
warnings.push(
|
||||||
|
`Pull request reference ${ref[0]} on line ${parser.pos} is to a ` +
|
||||||
|
"different Pull request than this one."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasRef) {
|
||||||
|
warnings.push(`Changelog entry on line ${parser.pos} does not reference an issue`);
|
||||||
|
}
|
||||||
|
changelog[section].push(entry);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
warnings.push(`Unexpected content "${line}" on line ${parser.pos}`);
|
||||||
|
}
|
||||||
|
return [changelog, warnings];
|
||||||
|
}
|
||||||
|
|
||||||
|
function changelogFromPullRequests(pullRequests: Issue[], issues: Issue[]) {
|
||||||
|
const mergedChangelog: Changelog = Object.create(null);
|
||||||
|
for (const pr of pullRequests) {
|
||||||
|
if (!pr.body) {
|
||||||
|
console.error(`Failed to extract changelog in ${pr.html_url}: No body`)
|
||||||
|
console.log(pr.title)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const [
|
||||||
|
changelogFail,
|
||||||
|
changelogLines,
|
||||||
|
changelogFailReason
|
||||||
|
] = extractPullRequestChangelog(createParser(pr.body!.split(/\r?\n/)));
|
||||||
|
if (changelogFail) {
|
||||||
|
console.error(`Failed to extract changelog in ${pr.html_url}: ${changelogFailReason}`)
|
||||||
|
console.log(pr.title)
|
||||||
|
console.log(pr.body)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const [changelog, warnings] = parseChangelog(createParser(changelogLines), pr, issues);
|
||||||
|
for (const warning of warnings) {
|
||||||
|
console.warn(`Warning in changelog for ${pr.html_url}: ${warning}`)
|
||||||
|
}
|
||||||
|
for (const [section, items] of Object.entries(changelog)) {
|
||||||
|
mergedChangelog[section] = [...mergedChangelog[section] ?? [], ...items];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mergedChangelog;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLastRelease() {
|
||||||
|
// Assumes releases are ordered by their creation or tag time in descending order.
|
||||||
|
const releases = await githubFetchJson<Release[]>(`/repos/${repository}/releases`);
|
||||||
|
const latest = releases[0]
|
||||||
|
if (!latest) {
|
||||||
|
throw new Error(`Unable to find latest release on ${branch}`);
|
||||||
|
}
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchIssuesUpdatedSince(since: string) {
|
||||||
|
return await githubFetchJsonPaginated<Issue>(
|
||||||
|
`/repos/${repository}/issues`,
|
||||||
|
{ state: "all", since, sort: "updated", direction: "asc" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function printMarkdown(
|
||||||
|
changelog: Changelog,
|
||||||
|
issues: Issue[],
|
||||||
|
users: Issue["user"][],
|
||||||
|
refText: (issue: Issue) => string,
|
||||||
|
userText: (user: Issue["user"]) => string,
|
||||||
|
) {
|
||||||
|
const sections = new Set([...changelogSections, ...Object.keys(changelog)]);
|
||||||
|
for (const section of sections) {
|
||||||
|
if (section in changelog) {
|
||||||
|
console.log(`### ${section}\n`);
|
||||||
|
const entries = changelog[section];
|
||||||
|
console.log(entries.map(text => (
|
||||||
|
`- ${text.replaceAll(/#(\d+)/g, (ref, idAsText) => {
|
||||||
|
const id = Number.parseInt(idAsText, 10);
|
||||||
|
if (!Number.isFinite(id)) {
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
const issue = issues.find(issue => issue.number === id);
|
||||||
|
if (!issue) {
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
return refText(issue);
|
||||||
|
})}`
|
||||||
|
)).join("\n"));
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Many thanks to the following for contributing to this release:`);
|
||||||
|
console.log(users.map(userText).join("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const lastRelease = await fetchLastRelease();
|
||||||
|
const issues = await fetchIssuesUpdatedSince(lastRelease.created_at);
|
||||||
|
|
||||||
|
const pullRequests = issues.filter(issue => (
|
||||||
|
issue.pull_request
|
||||||
|
&& issue.pull_request.merged_at
|
||||||
|
&& issue.pull_request.merged_at > lastRelease.created_at
|
||||||
|
));
|
||||||
|
|
||||||
|
const changelog = changelogFromPullRequests(pullRequests, issues);
|
||||||
|
const users = [...new Map(pullRequests.map(issue => [issue.user.login, issue.user])).values()]
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
console.log("=== Github Release Markdown ===");
|
||||||
|
printMarkdown(changelog, issues, users,
|
||||||
|
issue => `${repository}#${issue.number}`,
|
||||||
|
user => `@${user.login}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
console.log("=== Discord / Changelog Markdown ===");
|
||||||
|
printMarkdown(changelog, issues, users,
|
||||||
|
issue => `[#${issue.number}](<${issue.html_url}>)`,
|
||||||
|
user => `[@${user.login}](<${user.html_url}>)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
main().catch(console.error);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user