chore: use mdast for route identification script

This commit is contained in:
Tony
2026-03-13 19:57:27 +08:00
parent 346c79e4d0
commit 75cb4a3fb9
5 changed files with 277 additions and 21 deletions
+2
View File
@@ -143,6 +143,8 @@
"zod": "4.3.6"
},
"devDependencies": {
"@actions/core": "3.0.0",
"@actions/github": "9.0.0",
"@bbob/types": "4.3.1",
"@cloudflare/containers": "0.1.1",
"@cloudflare/puppeteer": "1.0.6",
+181
View File
@@ -284,6 +284,12 @@ importers:
specifier: 4.3.6
version: 4.3.6
devDependencies:
'@actions/core':
specifier: 3.0.0
version: 3.0.0
'@actions/github':
specifier: 9.0.0
version: 9.0.0
'@bbob/types':
specifier: 4.3.1
version: 4.3.1
@@ -467,6 +473,24 @@ importers:
packages:
'@actions/core@3.0.0':
resolution: {integrity: sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==}
'@actions/exec@3.0.0':
resolution: {integrity: sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==}
'@actions/github@9.0.0':
resolution: {integrity: sha512-yJ0RoswsAaKcvkmpCE4XxBRiy/whH2SdTBHWzs0gi4wkqTDhXMChjSdqBz/F4AeiDlP28rQqL33iHb+kjAMX6w==}
'@actions/http-client@3.0.2':
resolution: {integrity: sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA==}
'@actions/http-client@4.0.0':
resolution: {integrity: sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==}
'@actions/io@3.0.2':
resolution: {integrity: sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==}
'@asamuzakjp/css-color@4.1.2':
resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==}
@@ -1397,6 +1421,48 @@ packages:
resolution: {integrity: sha512-yLdw1saMUx4lmwMXOk72QK/N0wuw9x0JuOAuAX+q2Z8IEZzB5di6SEKL6a8eNh5cy3RLKdeXaXJFF4NF2LwXDg==}
engines: {node: '>=18'}
'@octokit/auth-token@6.0.0':
resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==}
engines: {node: '>= 20'}
'@octokit/core@7.0.6':
resolution: {integrity: sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==}
engines: {node: '>= 20'}
'@octokit/endpoint@11.0.3':
resolution: {integrity: sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==}
engines: {node: '>= 20'}
'@octokit/graphql@9.0.3':
resolution: {integrity: sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==}
engines: {node: '>= 20'}
'@octokit/openapi-types@27.0.0':
resolution: {integrity: sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==}
'@octokit/plugin-paginate-rest@14.0.0':
resolution: {integrity: sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==}
engines: {node: '>= 20'}
peerDependencies:
'@octokit/core': '>=6'
'@octokit/plugin-rest-endpoint-methods@17.0.0':
resolution: {integrity: sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==}
engines: {node: '>= 20'}
peerDependencies:
'@octokit/core': '>=6'
'@octokit/request-error@7.1.0':
resolution: {integrity: sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==}
engines: {node: '>= 20'}
'@octokit/request@10.0.8':
resolution: {integrity: sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==}
engines: {node: '>= 20'}
'@octokit/types@16.0.0':
resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==}
'@one-ini/wasm@0.1.1':
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
@@ -2963,6 +3029,9 @@ packages:
bcrypt-pbkdf@1.0.2:
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
before-after-hook@4.0.0:
resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==}
bezier-js@6.1.4:
resolution: {integrity: sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==}
@@ -3761,6 +3830,9 @@ packages:
resolution: {integrity: sha512-/S1rINk15hWHN1bbKZg23aBIN+yjWcVKGUuG1cIDCpGoidH46LU8S8Sax6MiCBSqwyjuSD7d90hBYc7TG/5yIQ==}
engines: {node: '>=18.0.0'}
fast-content-type-parse@3.0.0:
resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -4336,6 +4408,9 @@ packages:
json-stringify-safe@5.0.1:
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
json-with-bigint@3.5.7:
resolution: {integrity: sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==}
jsonfile@6.2.0:
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
@@ -5699,6 +5774,10 @@ packages:
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
tunnel@0.0.6:
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
turndown@7.2.2:
resolution: {integrity: sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==}
@@ -5763,6 +5842,10 @@ packages:
undici-types@7.18.2:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
undici@6.24.0:
resolution: {integrity: sha512-lVLNosgqo5EkGqh5XUDhGfsMSoO8K0BAN0TyJLvwNRSl4xWGZlCVYsAIpa/OpA3TvmnM01GWcoKmc3ZWo5wKKA==}
engines: {node: '>=18.17'}
undici@7.18.2:
resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==}
engines: {node: '>=20.18.1'}
@@ -5780,6 +5863,9 @@ packages:
unist-util-stringify-position@4.0.0:
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
universal-user-agent@7.0.3:
resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==}
universalify@0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'}
@@ -6174,6 +6260,37 @@ packages:
snapshots:
'@actions/core@3.0.0':
dependencies:
'@actions/exec': 3.0.0
'@actions/http-client': 4.0.0
'@actions/exec@3.0.0':
dependencies:
'@actions/io': 3.0.2
'@actions/github@9.0.0':
dependencies:
'@actions/http-client': 3.0.2
'@octokit/core': 7.0.6
'@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6)
'@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.6)
'@octokit/request': 10.0.8
'@octokit/request-error': 7.1.0
undici: 6.24.0
'@actions/http-client@3.0.2':
dependencies:
tunnel: 0.0.6
undici: 6.24.0
'@actions/http-client@4.0.0':
dependencies:
tunnel: 0.0.6
undici: 6.24.0
'@actions/io@3.0.2': {}
'@asamuzakjp/css-color@4.1.2':
dependencies:
'@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
@@ -6921,6 +7038,58 @@ snapshots:
'@notionhq/client@5.12.0': {}
'@octokit/auth-token@6.0.0': {}
'@octokit/core@7.0.6':
dependencies:
'@octokit/auth-token': 6.0.0
'@octokit/graphql': 9.0.3
'@octokit/request': 10.0.8
'@octokit/request-error': 7.1.0
'@octokit/types': 16.0.0
before-after-hook: 4.0.0
universal-user-agent: 7.0.3
'@octokit/endpoint@11.0.3':
dependencies:
'@octokit/types': 16.0.0
universal-user-agent: 7.0.3
'@octokit/graphql@9.0.3':
dependencies:
'@octokit/request': 10.0.8
'@octokit/types': 16.0.0
universal-user-agent: 7.0.3
'@octokit/openapi-types@27.0.0': {}
'@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.6)':
dependencies:
'@octokit/core': 7.0.6
'@octokit/types': 16.0.0
'@octokit/plugin-rest-endpoint-methods@17.0.0(@octokit/core@7.0.6)':
dependencies:
'@octokit/core': 7.0.6
'@octokit/types': 16.0.0
'@octokit/request-error@7.1.0':
dependencies:
'@octokit/types': 16.0.0
'@octokit/request@10.0.8':
dependencies:
'@octokit/endpoint': 11.0.3
'@octokit/request-error': 7.1.0
'@octokit/types': 16.0.0
fast-content-type-parse: 3.0.0
json-with-bigint: 3.5.7
universal-user-agent: 7.0.3
'@octokit/types@16.0.0':
dependencies:
'@octokit/openapi-types': 27.0.0
'@one-ini/wasm@0.1.1': {}
'@open-draft/deferred-promise@2.2.0': {}
@@ -8358,6 +8527,8 @@ snapshots:
dependencies:
tweetnacl: 0.14.5
before-after-hook@4.0.0: {}
bezier-js@6.1.4: {}
bidi-js@1.0.3:
@@ -9251,6 +9422,8 @@ snapshots:
oauth-1.0a: 2.2.6
query-string: 9.3.1
fast-content-type-parse@3.0.0: {}
fast-deep-equal@3.1.3: {}
fast-fifo@1.3.2: {}
@@ -9878,6 +10051,8 @@ snapshots:
json-stringify-safe@5.0.1: {}
json-with-bigint@3.5.7: {}
jsonfile@6.2.0:
dependencies:
universalify: 2.0.1
@@ -11558,6 +11733,8 @@ snapshots:
dependencies:
safe-buffer: '@nolyfill/safe-buffer@1.0.44'
tunnel@0.0.6: {}
turndown@7.2.2:
dependencies:
'@mixmark-io/domino': 2.2.0
@@ -11610,6 +11787,8 @@ snapshots:
undici-types@7.18.2: {}
undici@6.24.0: {}
undici@7.18.2: {}
undici@7.24.0: {}
@@ -11632,6 +11811,8 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
universal-user-agent@7.0.3: {}
universalify@0.2.0: {}
universalify@2.0.1: {}
@@ -14,6 +14,11 @@ const route = 'route';
// eslint-disable-next-line unicorn/no-useless-collection-argument
const dndUsernames = new Set([]);
/**
* @param {string} body
* @param {typeof import('@actions/core')} core
* @returns {Promise<string[] | null>}
*/
async function parseBodyRoutes(body, core) {
const ast = await unified().use(remarkParse).parse(body);
@@ -43,6 +48,11 @@ async function parseBodyRoutes(body, core) {
throw new Error('unable to parse the issue body: route does not exist');
}
/**
* @param {string[]} routes
* @param {typeof import('@actions/core')} core
* @returns {Promise<(string[] | undefined)[]>}
*/
async function getMaintainersByRoutes(routes, core) {
const response = await fetch(maintainerURL);
const maintainers = await response.json();
@@ -57,6 +67,10 @@ async function getMaintainersByRoutes(routes, core) {
});
}
/**
* @param {{ github: ReturnType<typeof import('@actions/github').getOctokit>, context: typeof import('@actions/github').context, core: typeof import('@actions/core') }} githubScript
* @returns {Promise<void>}
*/
export default async function callMaintainer({ github, context, core }) {
const body = context.payload.issue.body;
const issueFacts = {
@@ -65,6 +79,7 @@ export default async function callMaintainer({ github, context, core }) {
repo: context.repo.repo,
};
/** @param {string[]} labels */
const addLabels = (labels) =>
github.rest.issues
.addLabels({
@@ -74,6 +89,7 @@ export default async function callMaintainer({ github, context, core }) {
.catch((error) => {
core.warning(error);
});
/** @param {'open' | 'closed'} state */
const updateIssueState = (state) =>
github.rest.issues
.update({
+71 -21
View File
@@ -1,16 +1,25 @@
import remarkParse from 'remark-parse';
import { unified } from 'unified';
const noFound = 'auto: route no found';
const criticalFailure = 'auto: DO NOT merge';
const routeTestFailed = 'auto: not ready to review';
const allowedUser = new Set(['dependabot[bot]', 'pull[bot]']); // dependabot and downstream PR requested by pull[bot]
const requiredHeading = 'Example for the Proposed Route(s) / 路由地址示例';
/** @type {boolean} */
const isPR = Boolean(process.env.PULL_REQUEST);
/**
* @param {{ github: ReturnType<typeof import('@actions/github').getOctokit>, context: typeof import('@actions/github').context, core: typeof import('@actions/core') }} githubScript
* @param {string | null} body
* @param {number} number
* @param {string} sender
* @returns {Promise<string[] | void>}
*/
export default async function identify({ github, context, core }, body, number, sender) {
core.debug(`sender: ${sender}`);
core.debug(`body: ${body}`);
// Remove all HTML comments before performing the match
const bodyNoCmts = body?.replaceAll(/<!--[\S\s]*?-->/g, '');
const m = bodyNoCmts?.match(/```routes\s+([\S\s]*?)```/);
core.debug(`match: ${m}`);
let routes;
const issueFacts = {
owner: context.repo.owner,
@@ -22,6 +31,7 @@ export default async function identify({ github, context, core }, body, number,
repo: context.repo.repo,
pull_number: number,
};
/** @type {Awaited<ReturnType<typeof github.rest.issues.get>>} */
const { data: issue } = await github.rest.issues
.get({
...issueFacts,
@@ -30,6 +40,7 @@ export default async function identify({ github, context, core }, body, number,
core.warning(error);
});
/** @param {string[]} labels */
const addLabels = (labels) =>
github.rest.issues
.addLabels({
@@ -39,6 +50,7 @@ export default async function identify({ github, context, core }, body, number,
.catch((error) => {
core.warning(error);
});
/** @param {string} labelName */
const removeLabel = (labelName = noFound) =>
github.rest.issues
.removeLabel({
@@ -48,6 +60,7 @@ export default async function identify({ github, context, core }, body, number,
.catch((error) => {
core.warning(error);
});
/** @param {'open' | 'closed'} state */
const updatePrState = (state) =>
github.rest.pulls
.update({
@@ -57,6 +70,7 @@ export default async function identify({ github, context, core }, body, number,
.catch((error) => {
core.warning(error);
});
/** @param {string} body */
const createComment = (body) =>
github.rest.issues
.createComment({
@@ -69,7 +83,7 @@ export default async function identify({ github, context, core }, body, number,
const createFailedComment = () => {
const logUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
if (process.env.PULL_REQUEST) {
if (isPR) {
return createComment(`Auto Route Test failed, please check your PR body format and reopen pull request. Check [logs](${logUrl}) for more details.
自动路由测试失败,请确认 PR 正文部分符合格式规范并重新开启,详情请检查 [日志](${logUrl})。`);
}
@@ -78,7 +92,7 @@ export default async function identify({ github, context, core }, body, number,
路由测试失败,请确认评论部分符合格式规范,详情请检查 [日志](${logUrl})。`);
};
if (issue.pull_request) {
if (isPR) {
if (issue.state === 'closed') {
await updatePrState('open');
}
@@ -97,33 +111,69 @@ export default async function identify({ github, context, core }, body, number,
core.debug('PR created by ' + sender);
}
if (m && m[1]) {
routes = m[1].trim().split(/\r?\n/);
/** @type {string[] | undefined} */
let routes;
if (body) {
const ast = unified().use(remarkParse).parse(body);
let searchStart = 0;
let searchEnd = ast.children.length;
if (isPR) {
const headingIndex = ast.children.findIndex((node) => node.type === 'heading' && node.children?.some((child) => child.type === 'text' && child.value.trim() === requiredHeading));
core.debug(`headingIndex: ${headingIndex}`);
if (headingIndex === -1) {
searchStart = -1; // skip search
} else {
searchStart = headingIndex + 1;
const nextHeading = ast.children.findIndex((node, i) => i > headingIndex && node.type === 'heading');
if (nextHeading !== -1) {
searchEnd = nextHeading;
}
}
}
for (let i = searchStart; i >= 0 && i < searchEnd; i++) {
const node = ast.children[i];
if (node.type === 'code' && node.lang === 'routes') {
const code = node.value?.trim();
core.debug(`match: ${code}`);
if (code) {
routes = code.split(/\r?\n/).filter(Boolean);
}
break;
}
}
}
if (routes?.length) {
core.info(`routes detected: ${routes}`);
if (routes.length && routes[0] === 'NOROUTE') {
if (routes[0] === 'NOROUTE') {
core.info('PR stated no route, passing');
await removeLabel();
await addLabels(['auto: route test bypassed']);
return;
} else if (routes.length) {
if (routes.some((e) => e.includes('/:'))) {
await addLabels([noFound]);
return createComment(`Please use actual values in \`routes\` section instead of path parameters.
请在 \`routes\` 部分使用实际值而不是路径参数。`);
}
core.exportVariable('TEST_CONTINUE', true);
await removeLabel();
return routes;
}
if (routes.some((e) => e.includes('/:'))) {
await addLabels([noFound]);
return createComment(`Please use actual values in \`routes\` section instead of path parameters.
请在 \`routes\` 部分使用实际值而不是路径参数。`);
}
core.exportVariable('TEST_CONTINUE', true);
await removeLabel();
return routes;
}
core.warning('Seems like no valid routes can be found. Failing.');
await createFailedComment();
if (process.env.PULL_REQUEST) {
if (isPR) {
await addLabels([noFound]);
await updatePrState('closed');
}
+7
View File
@@ -3,6 +3,13 @@ import jsBeautify from 'js-beautify';
const routeTestFailed = 'auto: not ready to review';
const readyToReview = 'auto: ready to review';
/**
* @param {{ github: ReturnType<typeof import('@actions/github').getOctokit>, context: typeof import('@actions/github').context, core: typeof import('@actions/core') }} githubScript
* @param {string} baseUrl
* @param {string[]} routes
* @param {number} number
* @returns {Promise<void>}
*/
export default async function test({ github, context, core }, baseUrl, routes, number) {
if (routes[0] === 'NOROUTE') {
return;