2025-04-14 23:37:08 +08:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
// @ts-check
|
|
|
|
|
/**
|
|
|
|
|
* @file Auto-close script for closing won't add icons.
|
|
|
|
|
*/
|
|
|
|
|
import path from 'node:path';
|
|
|
|
|
import process from 'node:process';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @typedef {object} Rule
|
|
|
|
|
* @property {RegExp[]} patterns - The pattern to match against the issue title.
|
|
|
|
|
* @property {string} reason - The issue numbers to include in the reason text.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @typedef {object} Issue
|
|
|
|
|
* @property {{name: string}[]} labels - Isesue labels.
|
|
|
|
|
* @property {string} state - Issue state, possible values are 'open' and 'closed'.
|
|
|
|
|
* @property {string} title - Issue title.
|
|
|
|
|
* @property {string} body - Issue body.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/** @type {Rule[]} */
|
|
|
|
|
const rules = await import(
|
|
|
|
|
path.join(import.meta.dirname, 'autoclose.rules.js')
|
|
|
|
|
).then((module) => module.default);
|
|
|
|
|
|
|
|
|
|
const {GITHUB_TOKEN, GITHUB_REPOSITORY, ISSUE_NUMBER} = process.env;
|
|
|
|
|
|
|
|
|
|
if ([GITHUB_TOKEN, GITHUB_REPOSITORY, ISSUE_NUMBER].some((v) => !v)) {
|
|
|
|
|
console.error(
|
|
|
|
|
`${Object.entries({GITHUB_TOKEN, GITHUB_REPOSITORY, ISSUE_NUMBER})
|
|
|
|
|
.filter(([, v]) => !v)
|
|
|
|
|
.map(([k]) => k)
|
|
|
|
|
.join(', ')} environment variable(s) must be set.`,
|
|
|
|
|
);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetch data from GitHub API.
|
|
|
|
|
* @param {string} url The URL to fetch.
|
|
|
|
|
* @param {globalThis.RequestInit} options The options to pass to the fetch function.
|
|
|
|
|
* @returns {Promise<any>} - The response data.
|
|
|
|
|
*/
|
|
|
|
|
const githubFetch = async (url, options) => {
|
|
|
|
|
const response = await globalThis.fetch(new URL(url), {
|
|
|
|
|
...options,
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: 'application/vnd.github+json',
|
|
|
|
|
Authorization: `Bearer ${GITHUB_TOKEN}`,
|
|
|
|
|
'X-GitHub-Api-Version': '2022-11-28',
|
|
|
|
|
...options?.headers,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(
|
2025-04-14 17:54:22 +02:00
|
|
|
`Failed to fetch ${url}: ${response.status} (${response.statusText}).`,
|
2025-04-14 23:37:08 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if the issue is a won't add icon issue.
|
|
|
|
|
* GitHub REST API: https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#get-an-issue.
|
|
|
|
|
* @returns {Promise<string | undefined>} - Returns reason if the issue is a won't add icon issue, undefined otherwise.
|
|
|
|
|
*/
|
|
|
|
|
const checkIfCanBeClosed = async () => {
|
|
|
|
|
const url = `https://api.github.com/repos/${GITHUB_REPOSITORY}/issues/${ISSUE_NUMBER}`;
|
|
|
|
|
const response = await githubFetch(url, {method: 'GET'});
|
|
|
|
|
|
|
|
|
|
/** @type {Issue} */
|
|
|
|
|
const json = await response.json();
|
|
|
|
|
const {labels, state, title, body} = json;
|
|
|
|
|
const labelNames = new Set(labels.map((label) => label.name));
|
|
|
|
|
if (state === 'closed') return undefined;
|
|
|
|
|
if (labelNames.has('meta')) return undefined;
|
|
|
|
|
if (labelNames.has('in discussion')) return undefined;
|
|
|
|
|
if (!labelNames.has('new icon')) return undefined;
|
|
|
|
|
const matched = rules.find((rule) =>
|
|
|
|
|
rule.patterns.some((pattern) => {
|
|
|
|
|
const brandNamePattern = new RegExp(
|
|
|
|
|
`### Brand Name\n*${pattern.source.replaceAll('$', '')}\n*###`,
|
|
|
|
|
'i',
|
|
|
|
|
);
|
|
|
|
|
return pattern.test(title) || brandNamePattern.test(body);
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
if (!matched) return undefined;
|
|
|
|
|
return matched.reason;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Close the issue as not planned.
|
|
|
|
|
* GitHub REST API: https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#update-an-issue.
|
|
|
|
|
*/
|
|
|
|
|
const closeAsNotPlanned = async () => {
|
|
|
|
|
const url = `https://api.github.com/repos/${GITHUB_REPOSITORY}/issues/${ISSUE_NUMBER}`;
|
|
|
|
|
await githubFetch(url, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
state: 'closed',
|
|
|
|
|
state_reason: 'not_planned', // eslint-disable-line camelcase
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add labels to the issue.
|
|
|
|
|
* GitHub REST API: https://docs.github.com/en/rest/issues/labels?apiVersion=2022-11-28#add-labels-to-an-issue.
|
|
|
|
|
*/
|
|
|
|
|
const addLabels = async () => {
|
|
|
|
|
const url = `https://api.github.com/repos/${GITHUB_REPOSITORY}/issues/${ISSUE_NUMBER}/labels`;
|
|
|
|
|
await githubFetch(url, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
labels: ['duplicate', "won't add"],
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Post a comment on the issue.
|
|
|
|
|
* GitHub REST API: https://docs.github.com/en/rest/issues/comments?apiVersion=2022-11-28#create-an-issue-comment.
|
|
|
|
|
* @param {string} reason The reason for closing the issue.
|
|
|
|
|
*/
|
|
|
|
|
const commentWithReason = async (reason) => {
|
|
|
|
|
const url = `https://api.github.com/repos/${GITHUB_REPOSITORY}/issues/${ISSUE_NUMBER}/comments`;
|
|
|
|
|
await githubFetch(url, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
body: reason,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const reason = await checkIfCanBeClosed();
|
|
|
|
|
if (reason) {
|
|
|
|
|
await closeAsNotPlanned();
|
|
|
|
|
await addLabels();
|
|
|
|
|
await commentWithReason(reason);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Error) {
|
|
|
|
|
console.error(error.message);
|
|
|
|
|
} else {
|
|
|
|
|
console.error(String(error));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|