const LABEL_NAME = "needs info" const RE_VERSION = /Yazi\s+Version\s*:\s\d+\.\d+\.\d+\s\(/gm const RE_DEPENDENCIES = /Dependencies\s+[/a-z]+\s*:\s/gm const RE_CHECKLIST = /#{3}\s+Checklist\s+(?:^-\s+\[x]\s+.+?(?:\n|\r\n|$)){2}/gm const RE_PR_CHECKLIST = /#{2}\s+Checklist\s+(?:^-\s+\[x]\s+.+?(?:\n|\r\n|$)){2}/gm function pullRequestBody(content) { if (RE_PR_CHECKLIST.test(content)) { return null } return "All required checklist items must be checked in the PR description." } function bugReportBody(creator, content, hash) { if (RE_DEPENDENCIES.test(content) && RE_CHECKLIST.test(content) && new RegExp(` \\(${hash}[a-f0-9]? `).test(content)) { return null } return `Hey @${creator}, thank you for opening the issue to help improve Yazi, appreciate it! I noticed that you did not correctly follow the issue template. Please ensure that: - The bug can still be reproduced on the [newest nightly build](https://yazi-rs.github.io/docs/installation/#binaries). - The environment info (\`ya env\`) is updated for the newest nightly. - The non-optional items in the checklist are checked. Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity. Our maintainers work on Yazi in their free time, this helps them work efficiently, understand your setup quickly, and find a more appropriate solution. Thanks for your understanding! 🙏 ` } function featureRequestBody(creator, content) { if (RE_VERSION.test(content) && RE_DEPENDENCIES.test(content) && RE_CHECKLIST.test(content)) { return null } return `Hey @${creator}, thank you for opening the issue to help improve Yazi, appreciate it! I noticed that you did not correctly follow the issue template. Please ensure that: - The requested feature does not exist in the [newest nightly build](https://yazi-rs.github.io/docs/installation/#binaries). - The environment info (\`ya env\`) is updated for the newest nightly. - The non-optional items in the checklist are checked. Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity. Our maintainers work on Yazi in their free time, this helps them work efficiently, understand your setup quickly, and find a more appropriate solution. Thanks for your understanding! 🙏 ` } function skipValidation(context) { const login = context.payload.issue?.user?.login || context.payload.pull_request?.user?.login const owner = context.payload.repository?.owner?.login || context.repo.owner return !!login && login === owner } module.exports = async ({ github, context, core }) => { async function nightlyHash() { try { const { data: tagRef } = await github.rest.git.getRef({ owner: "sxyazi", repo: "yazi", ref: "tags/nightly" }) return tagRef.object.sha.slice(0, 7) } catch (e) { if (e.status === 404) { core.error("Nightly tag not found") } else { core.error(`Error fetching nightly version: ${e.message}`) } return null } } async function hasLabel(id, label) { try { const { data: labels } = await github.rest.issues.listLabelsOnIssue({ ...context.repo, issue_number: id, }) return labels.some(l => l.name === label) } catch (e) { core.error(`Error checking labels: ${e.message}`) return false } } async function lastLabeledAt(id) { try { const { data: events } = await github.rest.issues.listEvents({ ...context.repo, issue_number: id, per_page: 100, }) const all = events.filter(v => v.event === "labeled" && v.label?.name === LABEL_NAME) return all.at(-1)?.created_at } catch (e) { core.error(`Error getting label timestamp: ${e.message}`) return null } } async function removedLabelManually(id) { try { const { data: events } = await github.rest.issues.listEvents({ ...context.repo, issue_number: id, per_page: 100, }) const all = events.filter(v => v.event === "unlabeled" && v.label?.name === LABEL_NAME) return all.length === 0 ? false : !all.at(-1).actor.login.endsWith("[bot]") } catch (e) { core.error(`Error checking label removal history: ${e.message}`) return false } } async function updateLabels(id, mark, body) { try { const marked = await hasLabel(id, LABEL_NAME) if (!mark && marked) { await github.rest.issues.removeLabel({ ...context.repo, issue_number: id, name: LABEL_NAME, }) await hideOldComments(id) } else if (mark && !marked && !await removedLabelManually(id)) { await github.rest.issues.addLabels({ ...context.repo, issue_number: id, labels: [LABEL_NAME], }) await hideOldComments(id) await github.rest.issues.createComment({ ...context.repo, issue_number: id, body, }) } } catch (e) { core.error(`Error updating labels: ${e.message}`) } } async function hideOldComments(id) { try { const comments = await github.paginate(github.rest.issues.listComments, { ...context.repo, issue_number: id, per_page: 100, }) for (const c of comments) { const byBot = c.user?.login?.endsWith("[bot]") || c.user?.type === "Bot" const contains = c?.body?.includes("or closed after 2 days of inactivity") if (!byBot || !contains || !c.node_id) continue try { await github.graphql( `mutation($subjectId: ID!, $classifier: ReportedContentClassifiers!) { minimizeComment(input: {subjectId: $subjectId, classifier: $classifier}) { minimizedComment { isMinimized } } }`, { subjectId: c.node_id, classifier: "OUTDATED" }, ) } catch (e) { core.error(`Error minimizing comment ${c.id}: ${e.message}`) } } } catch (e) { core.error(`Error listing comments: ${e.message}`) } } async function closeOldIssues() { try { const { data: issues } = await github.rest.issues.listForRepo({ ...context.repo, state: "open", labels: LABEL_NAME, }) const now = new Date() const twoDaysAgo = new Date(now - 2 * 24 * 60 * 60 * 1000) for (const issue of issues) { const markedAt = new Date(await lastLabeledAt(issue.number) || issue.created_at) if (markedAt < twoDaysAgo) { await github.rest.issues.update({ ...context.repo, issue_number: issue.number, state: "closed", state_reason: "not_planned", }) await github.rest.issues.createComment({ ...context.repo, issue_number: issue.number, body: `This issue has been automatically closed because it was marked as \`${LABEL_NAME}\` for more than 2 days without updates. If the problem persists, please file a new issue and complete the issue template so we can capture all the details necessary to investigate further.`, }) } } } catch (e) { core.error(`Error checking old issues: ${e.message}`) } } async function closeUnsupportedIssue(id) { try { await github.rest.issues.update({ ...context.repo, issue_number: id, state: "closed", state_reason: "not_planned", }) await github.rest.issues.createComment({ ...context.repo, issue_number: id, body: `Unsupported issue template. Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.yml) or [Feature Request](https://github.com/sxyazi/yazi/issues/new?template=feature.yml) template should be used.`, }) } catch (e) { core.error(`Error closing unsupported issue: ${e.message}`) } } async function main() { if (context.eventName === "schedule") { await closeOldIssues() return } if (skipValidation(context)) { return } if (context.eventName === "issues") { const id = context.payload.issue.number const content = context.payload.issue.body || "" const creator = context.payload.issue.user.login const hash = await nightlyHash() if (!hash) return if (await hasLabel(id, "bug")) { const body = bugReportBody(creator, content, hash) await updateLabels(id, !!body, body) } else if (await hasLabel(id, "feature")) { const body = featureRequestBody(creator, content) await updateLabels(id, !!body, body) } else if (context.payload.action === "opened") { await closeUnsupportedIssue(id) } } else if (context.eventName === "pull_request" || context.eventName === "pull_request_target") { const content = context.payload.pull_request.body || "" const body = pullRequestBody(content) if (body) { core.setFailed(body) } } } await main() }