Flaky integration tests (SQLite): background goroutines not cleaned up in the global ratelimiter #129
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Issue Validation | |
| on: | |
| issues: | |
| types: | |
| - opened | |
| - edited | |
| - labeled | |
| - unlabeled | |
| - reopened | |
| permissions: | |
| issues: write | |
| jobs: | |
| validate-issue: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Validate issue template requirements | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require("fs"); | |
| const path = require("path"); | |
| const issue = context.payload.issue; | |
| const issueNumber = issue.number; | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const TYPE_LABELS = ["bug", "feature", "improvement", "refractoring"]; | |
| const BUG_LABEL = "bug"; | |
| const ENHANCEMENT_LABELS = ["feature", "improvement", "refractoring"]; | |
| const NEEDS_INFO_LABEL = "needs-info"; | |
| const FALLBACK_BUG_REQUIRED = [ | |
| "Description", | |
| "Steps to Reproduce / How to Trigger", | |
| "Expected Behavior", | |
| "Actual Behavior", | |
| "Logs / Screenshots", | |
| "Environment", | |
| ]; | |
| const FALLBACK_ENHANCEMENT_REQUIRED = [ | |
| "Description", | |
| "Is this a breaking change?", | |
| "Scope of the feature (server, specific client, all clients)", | |
| ]; | |
| const normalize = (title) => | |
| title | |
| .toLowerCase() | |
| .replace(/[^\w\s]+/g, " ") | |
| .replace(/\s+/g, " ") | |
| .trim(); | |
| const labelNames = (issue.labels || []) | |
| .map((label) => (typeof label === "string" ? label : label.name)) | |
| .filter(Boolean) | |
| .map((label) => label.toLowerCase()); | |
| let hasTypeLabel = TYPE_LABELS.some((label) => labelNames.includes(label)); | |
| let isBug = labelNames.includes(BUG_LABEL); | |
| let isEnhancement = ENHANCEMENT_LABELS.some((label) => labelNames.includes(label)); | |
| let missingLabel = !hasTypeLabel; | |
| // Detect issue type from template usage and auto-apply label | |
| const detectIssueType = (body) => { | |
| if (!body) return null; | |
| const normalizedBody = body.toLowerCase(); | |
| // Check for bug template sections | |
| if (normalizedBody.includes("steps to reproduce") || | |
| normalizedBody.includes("expected behavior") || | |
| normalizedBody.includes("actual behavior")) { | |
| return "bug"; | |
| } | |
| // Check for feature template sections | |
| if (normalizedBody.includes("breaking change") || | |
| normalizedBody.includes("scope of the feature")) { | |
| return "feature"; | |
| } | |
| return null; | |
| }; | |
| const detectedType = detectIssueType(issue.body); | |
| // Auto-apply type label if missing but detected | |
| if (missingLabel && detectedType) { | |
| await github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number: issueNumber, | |
| labels: [detectedType], | |
| }); | |
| // Refresh label check | |
| labelNames.push(detectedType); | |
| hasTypeLabel = TYPE_LABELS.some((label) => labelNames.includes(label)); | |
| isBug = labelNames.includes(BUG_LABEL); | |
| isEnhancement = ENHANCEMENT_LABELS.some((label) => labelNames.includes(label)); | |
| missingLabel = !hasTypeLabel; | |
| } | |
| const readTemplateHeadings = (fileName) => { | |
| try { | |
| const templatePath = path.join( | |
| process.env.GITHUB_WORKSPACE || process.cwd(), | |
| ".github", | |
| "ISSUE_TEMPLATE", | |
| fileName | |
| ); | |
| const contents = fs.readFileSync(templatePath, "utf8"); | |
| const headings = []; | |
| for (const line of contents.split(/\r?\n/)) { | |
| const match = line.match(/^###\s+(.+?)\s*$/); | |
| if (match?.[1]) { | |
| headings.push(match[1].trim()); | |
| } | |
| } | |
| return headings.length > 0 ? headings : null; | |
| } catch { | |
| return null; | |
| } | |
| }; | |
| const BUG_REQUIRED = | |
| readTemplateHeadings("bug_report.md") || FALLBACK_BUG_REQUIRED; | |
| const ENHANCEMENT_REQUIRED = | |
| readTemplateHeadings("feature_request.md") || | |
| FALLBACK_ENHANCEMENT_REQUIRED; | |
| const requiredSections = isBug | |
| ? BUG_REQUIRED | |
| : isEnhancement | |
| ? ENHANCEMENT_REQUIRED | |
| : []; | |
| const parseSections = (body) => { | |
| const sections = new Map(); | |
| if (!body) { | |
| return sections; | |
| } | |
| const lines = body.split(/\r?\n/); | |
| let currentKey = null; | |
| let buffer = []; | |
| const flush = () => { | |
| if (currentKey) { | |
| sections.set(currentKey, buffer.join("\n").trim()); | |
| } | |
| buffer = []; | |
| }; | |
| for (const line of lines) { | |
| const trimmed = line.trim(); | |
| const headingMatch = trimmed.match(/^#{1,6}\s*(.+?)\s*$/); | |
| const boldMatch = trimmed.match(/^\*\*(.+?)\*\*$/); | |
| const colonMatch = trimmed.match(/^(.+?):\s*$/); | |
| const title = headingMatch?.[1] || boldMatch?.[1] || colonMatch?.[1]; | |
| if (title) { | |
| flush(); | |
| currentKey = normalize(title); | |
| continue; | |
| } | |
| if (currentKey) { | |
| buffer.push(line); | |
| } | |
| } | |
| flush(); | |
| return sections; | |
| }; | |
| const hasMeaningfulContent = (content) => { | |
| if (!content) { | |
| return false; | |
| } | |
| const cleaned = content | |
| .replace(/<!--[\s\S]*?-->/g, "") | |
| .replace(/[\s*-]+/g, " ") | |
| .trim() | |
| .toLowerCase(); | |
| if (!cleaned) { | |
| return false; | |
| } | |
| return !["n/a", "na", "none", "tbd", "todo"].includes(cleaned); | |
| }; | |
| const sections = parseSections(issue.body || ""); | |
| const missingSections = requiredSections.filter((section) => { | |
| const content = sections.get(normalize(section)); | |
| return !hasMeaningfulContent(content); | |
| }); | |
| const ensureNeedsInfoLabelExists = async () => { | |
| try { | |
| await github.rest.issues.getLabel({ | |
| owner, | |
| repo, | |
| name: NEEDS_INFO_LABEL, | |
| }); | |
| } catch (error) { | |
| if (error.status !== 404) { | |
| throw error; | |
| } | |
| await github.rest.issues.createLabel({ | |
| owner, | |
| repo, | |
| name: NEEDS_INFO_LABEL, | |
| color: "f9d0c4", | |
| description: "Needs additional information from the reporter", | |
| }); | |
| } | |
| }; | |
| const findExistingBotComment = async () => { | |
| const comments = await github.paginate( | |
| github.rest.issues.listComments, | |
| { owner, repo, issue_number: issueNumber, per_page: 100 } | |
| ); | |
| return comments.find( | |
| (comment) => | |
| comment.user?.type === "Bot" && | |
| comment.body?.includes("<!-- issue-template-check -->") | |
| ); | |
| }; | |
| const updateOrCreateComment = async (body) => { | |
| const existing = await findExistingBotComment(); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner, | |
| repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| return; | |
| } | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: issueNumber, | |
| body, | |
| }); | |
| }; | |
| const deleteExistingComment = async () => { | |
| const existing = await findExistingBotComment(); | |
| if (!existing) { | |
| return; | |
| } | |
| await github.rest.issues.deleteComment({ | |
| owner, | |
| repo, | |
| comment_id: existing.id, | |
| }); | |
| }; | |
| if (missingLabel || missingSections.length > 0) { | |
| await ensureNeedsInfoLabelExists(); | |
| if (!labelNames.includes(NEEDS_INFO_LABEL)) { | |
| await github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number: issueNumber, | |
| labels: [NEEDS_INFO_LABEL], | |
| }); | |
| } | |
| const templateLines = [ | |
| "Issue templates:", | |
| `- Bug report: https://github.com/${owner}/${repo}/issues/new?template=bug_report.md`, | |
| `- Feature request (use for feature, improvement, refractoring): https://github.com/${owner}/${repo}/issues/new?template=feature_request.md`, | |
| ].join("\n"); | |
| const missingLabelLine = missingLabel | |
| ? `- Add one label from: ${TYPE_LABELS.join(", ")}` | |
| : null; | |
| const missingSectionsLines = | |
| missingSections.length > 0 | |
| ? [ | |
| "Missing required sections:", | |
| ...missingSections.map((section) => `- ${section}`), | |
| ].join("\n") | |
| : null; | |
| const messageParts = [ | |
| "<!-- issue-template-check -->", | |
| "Thanks for opening this issue. To help us triage it, please provide the missing details:", | |
| missingLabelLine, | |
| missingSectionsLines, | |
| templateLines, | |
| ].filter(Boolean); | |
| await updateOrCreateComment(messageParts.join("\n")); | |
| return; | |
| } | |
| if (labelNames.includes(NEEDS_INFO_LABEL)) { | |
| await github.rest.issues.removeLabel({ | |
| owner, | |
| repo, | |
| issue_number: issueNumber, | |
| name: NEEDS_INFO_LABEL, | |
| }); | |
| } | |
| await deleteExistingComment(); |