herdr/.github/workflows/issue-gate.yml

257 lines
11 KiB
YAML

name: Issue Gate
on:
issues:
types: [opened]
permissions:
issues: write
jobs:
check-template:
if: ${{ !github.event.issue.pull_request }}
runs-on: ubuntu-latest
steps:
- name: Close non-template issues
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ secrets.KANGAL_GITHUB_TOKEN }}
script: |
const issue = context.payload.issue;
const author = issue.user.login;
const sender = context.payload.sender?.login ?? author;
const bugConfirmationPattern = /^\s*-\s*\[[xX]\]\s*I confirm this is a reproducible bug, not a feature request, idea, question, contribution proposal, or direction check\.\s*$/m;
const reproductionConfirmationPattern = /^\s*-\s*\[[xX]\]\s*I reproduced this bug on the version and environment reported below using the exact steps provided\.\s*$/m;
const requiredSections = [
'### Is this a reproducible bug?',
'### Current behavior',
'### Expected behavior',
'### Reproduction',
'### Impact',
'### Environment',
];
const maxBodyLength = 8000;
const maxExtraHeadings = 1;
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function extractSection(body, heading) {
const pattern = new RegExp(`^${escapeRegExp(heading)}\\s*$`, 'm');
const match = pattern.exec(body);
if (!match) {
return null;
}
const sectionStart = match.index + match[0].length;
const rest = body.slice(sectionStart);
const headingIndex = requiredSections.indexOf(heading);
const nextTemplateHeading = requiredSections
.slice(headingIndex + 1)
.map((section) => {
const nextPattern = new RegExp(`^${escapeRegExp(section)}\\s*$`, 'm');
const nextMatch = nextPattern.exec(rest);
return nextMatch ? nextMatch.index : -1;
})
.filter((index) => index !== -1)
.sort((left, right) => left - right)[0];
return (nextTemplateHeading === undefined ? rest : rest.slice(0, nextTemplateHeading)).trim();
}
function hasContent(section) {
return typeof section === 'string' && section.trim().length > 0;
}
function hasEnvironmentContent(section) {
if (!hasContent(section)) {
return false;
}
return section.split(/\r?\n/).some((line) => {
const value = line.trim();
return value.length > 0 && !/^(?:-\s*)?[^:]+:\s*$/.test(value);
});
}
function bodyWithoutFencedCodeBlocks(value) {
let inFence = false;
return value
.split(/\r?\n/)
.filter((line) => {
if (/^\s{0,3}(```|~~~)/.test(line)) {
inFence = !inFence;
return false;
}
return !inFence;
})
.join('\n');
}
async function getPermission(username) {
try {
const { data: permissionLevel } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username,
});
return permissionLevel.permission;
} catch {
return null;
}
}
const maintainerPermissions = ['admin', 'maintain', 'write'];
const authorPermission = await getPermission(author);
if (maintainerPermissions.includes(authorPermission)) {
console.log(`${author} is a collaborator with ${authorPermission} access; leaving issue open`);
return;
}
const senderPermission = sender === author ? authorPermission : await getPermission(sender);
if (maintainerPermissions.includes(senderPermission)) {
console.log(`${sender} opened this issue with ${senderPermission} access; leaving issue open`);
return;
}
const body = issue.body || '';
const translationConfirmationPattern = /^\s*-\s*\[[xX]\]\s*I confirm this is a translation issue in the herdr docs, not a bug report, feature request, or question\.\s*$/m;
const translationSections = ['### Page', '### Language', '### What is wrong'];
if (
translationConfirmationPattern.test(body) &&
translationSections.every((section) => body.includes(section))
) {
console.log(`#${issue.number} matches the translation issue template; leaving issue open`);
return;
}
const hasBugConfirmation = bugConfirmationPattern.test(body) && reproductionConfirmationPattern.test(body);
const allowedHeadings = new Set(requiredSections);
const headingBody = bodyWithoutFencedCodeBlocks(body);
const headings = [...headingBody.matchAll(/^\s{0,3}#{1,6}\s+(.+?)\s*$/gm)].map((match) => match[0].trim());
const headingCounts = new Map(
requiredSections.map((section) => [
section,
headings.filter((heading) => heading === section).length,
]),
);
const hasEveryRequiredHeading = requiredSections.every(
(section) => headingCounts.get(section) > 0,
);
const hasBugTemplate = requiredSections.every(
(section) => headingCounts.get(section) === 1,
);
const repeatedTemplateHeadings = requiredSections.filter(
(section) => headingCounts.get(section) > 1,
);
const extraHeadings = headings.filter((heading) => !allowedHeadings.has(heading));
const structuralViolations = [];
if (repeatedTemplateHeadings.length > 0) {
structuralViolations.push('one or more required template headings are repeated');
}
if (extraHeadings.length > maxExtraHeadings) {
structuralViolations.push(`${extraHeadings.length} extra markdown headings were added`);
}
if (body.length > maxBodyLength) {
structuralViolations.push(`the report is ${body.length} characters; the limit is ${maxBodyLength}`);
}
const currentBehavior = extractSection(body, '### Current behavior');
const expectedBehavior = extractSection(body, '### Expected behavior');
const reproduction = extractSection(body, '### Reproduction');
const impact = extractSection(body, '### Impact');
const environment = extractSection(body, '### Environment');
const hasRequiredContent = [
currentBehavior,
expectedBehavior,
reproduction,
impact,
].every(hasContent);
const hasEnvironment = hasEnvironmentContent(environment);
if (hasBugConfirmation && hasEveryRequiredHeading && structuralViolations.length > 0) {
const message = [
`hi @${author}, thanks for opening this.`,
'',
'this report exceeds the bug report structure limits:',
...structuralViolations.map((violation) => `- ${violation}`),
'',
'please keep the report within the required template, under 8,000 characters, and focused on observed behavior, exact reproduction, impact, and environment.',
'',
'closing this so the issue tracker stays limited to concise, actionable bug reports.',
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: message,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'closed',
state_reason: 'not_planned',
});
return;
}
if (hasBugConfirmation && hasBugTemplate && hasRequiredContent && hasEnvironment) {
console.log(`#${issue.number} matches the bug report template`);
return;
}
if (hasBugConfirmation && hasBugTemplate && hasRequiredContent && !hasEnvironment) {
const message = [
`hi @${author}, thanks for opening this.`,
'',
'this bug report has no filled environment details. please edit the environment section with the Herdr version, update channel, operating system, and terminal.',
'',
'shell and relevant config are optional, but they help when they affect the bug.',
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: message,
});
console.log(`#${issue.number} has no environment details; leaving issue open for correction`);
return;
}
const message = [
`hi @${author}, thanks for opening this.`,
'',
'this issue does not appear to use the full bug report template. herdr issues are only for reproducible bugs and maintainer-created or maintainer-converted work items.',
'',
'bug reports must include the reproducible-bug confirmation, current behavior, expected behavior, reproduction, impact, and environment fields with herdr version, update channel, operating system, and terminal.',
'',
'feature requests, ideas, questions, contribution proposals, and direction checks belong in discussions:',
`https://github.com/${context.repo.owner}/${context.repo.repo}/discussions`,
'',
'please read the contributing guidelines before opening issues or prs:',
`https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${context.payload.repository.default_branch}/CONTRIBUTING.md`,
'',
'closing this so the issue tracker stays limited to actionable bug reports.',
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: message,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'closed',
state_reason: 'not_planned',
});