214 lines
7.3 KiB
JavaScript
214 lines
7.3 KiB
JavaScript
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 requiredHeadings = ['Involved Issue / 该 PR 相关 Issue', 'Example for the Proposed Route(s) / 路由地址示例', 'New RSS Route Checklist / 新 RSS 路由检查表', 'Note / 说明'];
|
|
const routeHeading = 'Example for the Proposed Route(s) / 路由地址示例';
|
|
const requiredHeadingDepth = 2;
|
|
|
|
/** @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}`);
|
|
|
|
const issueFacts = {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: number,
|
|
};
|
|
const prFacts = {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: number,
|
|
};
|
|
let issue;
|
|
try {
|
|
/** @type {Awaited<ReturnType<typeof github.rest.issues.get>>} */
|
|
const response = await github.rest.issues.get({
|
|
...issueFacts,
|
|
});
|
|
issue = response.data;
|
|
} catch (error) {
|
|
core.warning(error);
|
|
throw error;
|
|
}
|
|
|
|
/** @param {string[]} labels */
|
|
const addLabels = async (labels) => {
|
|
try {
|
|
return await github.rest.issues.addLabels({
|
|
...issueFacts,
|
|
labels,
|
|
});
|
|
} catch (error) {
|
|
core.warning(error);
|
|
}
|
|
};
|
|
/** @param {string} labelName */
|
|
const removeLabel = async (labelName = noFound) => {
|
|
try {
|
|
return await github.rest.issues.removeLabel({
|
|
...issueFacts,
|
|
name: labelName,
|
|
});
|
|
} catch (error) {
|
|
core.warning(error);
|
|
}
|
|
};
|
|
/** @param {'open' | 'closed'} state */
|
|
const updatePrState = async (state) => {
|
|
try {
|
|
return await github.rest.pulls.update({
|
|
...prFacts,
|
|
state,
|
|
});
|
|
} catch (error) {
|
|
core.warning(error);
|
|
}
|
|
};
|
|
/** @param {string} body */
|
|
const createComment = async (body) => {
|
|
try {
|
|
return await github.rest.issues.createComment({
|
|
...issueFacts,
|
|
body,
|
|
});
|
|
} catch (error) {
|
|
core.warning(error);
|
|
}
|
|
};
|
|
const createFailedComment = () => {
|
|
const logUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
|
|
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})。`);
|
|
}
|
|
|
|
return createComment(`Route Test failed, please check your comment body. Check [logs](${logUrl}) for more details.
|
|
路由测试失败,请确认评论部分符合格式规范,详情请检查 [日志](${logUrl})。`);
|
|
};
|
|
|
|
if (isPR) {
|
|
if (issue.labels.some((e) => e.name === 'spam')) {
|
|
core.info('PR labeled as spam, skipping');
|
|
return;
|
|
}
|
|
if (issue.state === 'closed') {
|
|
await updatePrState('open');
|
|
}
|
|
if (issue.labels.some((e) => e.name === criticalFailure || e.name === routeTestFailed)) {
|
|
await removeLabel(criticalFailure);
|
|
await removeLabel(routeTestFailed);
|
|
}
|
|
}
|
|
|
|
if (allowedUser.has(sender)) {
|
|
core.info('PR created by a allowed user, passing');
|
|
await removeLabel();
|
|
await addLabels(['auto: ready to merge']);
|
|
return;
|
|
}
|
|
core.debug('PR created by ' + sender);
|
|
|
|
let hasWriteAccess;
|
|
try {
|
|
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
username: sender,
|
|
});
|
|
hasWriteAccess = ['admin', 'maintain', 'write'].includes(data.permission);
|
|
} catch (error) {
|
|
core.warning(error);
|
|
hasWriteAccess = false;
|
|
}
|
|
core.debug(`hasWriteAccess: ${hasWriteAccess}`);
|
|
|
|
/** @type {string[] | undefined} */
|
|
let routes;
|
|
|
|
if (body) {
|
|
const ast = unified().use(remarkParse).parse(body);
|
|
|
|
let searchStart = 0;
|
|
let searchEnd = ast.children.length;
|
|
|
|
if (isPR) {
|
|
/** @param {string} text */
|
|
const findHeading = (text) => ast.children.findIndex((node) => node.type === 'heading' && node.depth === requiredHeadingDepth && node.children?.some((child) => child.type === 'text' && child.value.trim() === text));
|
|
|
|
const missingHeadings = requiredHeadings.filter((text) => findHeading(text) === -1);
|
|
if (missingHeadings.length > 0) {
|
|
searchStart = -1; // skip search
|
|
} else {
|
|
const headingIndex = findHeading(routeHeading);
|
|
core.debug(`headingIndex: ${headingIndex}`);
|
|
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[0] === 'NOROUTE') {
|
|
core.info('PR stated no route, passing');
|
|
await removeLabel();
|
|
await addLabels(['auto: route test bypassed']);
|
|
|
|
return;
|
|
}
|
|
|
|
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 (isPR) {
|
|
await addLabels([noFound]);
|
|
if (!hasWriteAccess) {
|
|
await updatePrState('closed');
|
|
}
|
|
}
|
|
|
|
throw new Error('Please follow the PR rules: failed to detect route');
|
|
}
|