[Incubation] Higress Incubation Application #59
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: Create Governance Review Issue | |
| on: | |
| issues: | |
| types: [labeled] | |
| jobs: | |
| create-gov-review: | |
| # Only spawn a Governance Review companion issue when 'review/governance' is | |
| # applied to something that is NOT itself a Governance Review request. | |
| # Without this guard, opening an issue from | |
| # .github/ISSUE_TEMPLATE/gov-review.yml (which applies 'review/governance' | |
| # on creation) would cause this workflow to create a duplicate | |
| # [Gov. Review]: issue — the same failure mode as #2246 / #2247 for tech | |
| # reviews. | |
| if: >- | |
| github.event.label.name == 'review/governance' && | |
| !startsWith(github.event.issue.title, '[Gov. Review]') && | |
| !startsWith(github.event.issue.title, '[Gov Review]') && | |
| !startsWith(github.event.issue.title, '[Governance Review]') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| contents: read | |
| steps: | |
| - name: Extract issue information and create governance review | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| // --- Constants --- | |
| const FIELD_LABELS = { | |
| name: [ | |
| 'Project name', 'Name', 'name', 'project name', 'project-name', 'subproject-name', 'Project Name', 'Subproject Name' | |
| ], | |
| projectLink: [ | |
| 'Project Repo(s)', 'Project Repo', 'Project link', 'project-link', 'project link', 'github-url', 'GitHub URL', 'Project link', 'Project Site' | |
| ], | |
| ddLink: [ | |
| 'Due diligence link', 'dd-link', 'due diligence link', 'Due diligence', 'Due diligence link' | |
| ], | |
| projectContact: [ | |
| 'Project points of contacts', 'Project points of contact', 'Project contact', 'project-contact', 'project contact', 'Project contact information', 'Project Security Contacts', 'Communication' | |
| ], | |
| additionalInfo: [ | |
| 'Additional information', 'additional-information', 'additional information', 'Additional Information', 'Additional information' | |
| ] | |
| }; | |
| // Mirrors labels declared in .github/ISSUE_TEMPLATE/gov-review.yml | |
| const LABELS_GOV_REVIEW = [ | |
| 'needs-triage', 'kind/review', 'review/governance', 'sub/project-reviews' | |
| ]; | |
| const COMMENT_MARKERS = { | |
| govReviewCreated: 'Created governance review issue:', | |
| govReviewExists: 'governance review issue already exists', | |
| missingProjectName: 'Could not extract project name', | |
| missingProjectLink: 'Could not extract project link', | |
| createGovReviewFailed: 'Failed to create governance review issue' | |
| }; | |
| // --- Helper Functions --- | |
| async function hasExistingComment(issueNumber, marker) { | |
| try { | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| }); | |
| return comments.some(comment => | |
| comment.user.type === 'Bot' && | |
| comment.body.includes(marker) | |
| ); | |
| } catch (error) { | |
| console.log('⚠️ Error checking for existing comments:', error.message); | |
| return false; | |
| } | |
| } | |
| async function commentOnce(issueNumber, marker, body) { | |
| if (!(await hasExistingComment(issueNumber, marker))) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| body | |
| }); | |
| } | |
| } | |
| async function commentExistingGovReview(issueNumber, existingIssue) { | |
| await commentOnce( | |
| issueNumber, | |
| COMMENT_MARKERS.govReviewExists, | |
| `Governance review issue: [#${existingIssue.number} - ${existingIssue.title}](${existingIssue.html_url})` | |
| ); | |
| } | |
| function extractFormField(body, fieldKey) { | |
| if (!body) return null; | |
| const labels = FIELD_LABELS[fieldKey] || [fieldKey]; | |
| // First try GitHub template format: ### Field Label\n\nvalue | |
| for (const label of labels) { | |
| const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| const templatePattern = new RegExp( | |
| `###\\s+${escapedLabel}[^\\n]*\\n\\n([\\s\\S]*?)(?=\\n###|$)`, | |
| 'i' | |
| ); | |
| const templateMatch = body.match(templatePattern); | |
| if (templateMatch && templateMatch[1] && templateMatch[1].trim().length > 0) { | |
| return templateMatch[1].trim(); | |
| } | |
| } | |
| // Fallback: Try plain text format: Field Label: value | |
| for (const label of labels) { | |
| const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| // Match "Field Label:" or "Field Label" followed by colon and value | |
| const plainPattern = new RegExp( | |
| `^${escapedLabel}\\s*:?\\s+(.+)$`, | |
| 'im' | |
| ); | |
| const plainMatch = body.match(plainPattern); | |
| if (plainMatch && plainMatch[1] && plainMatch[1].trim().length > 0) { | |
| // Extract value, stopping at newline or end | |
| const value = plainMatch[1].trim().split(/\n/)[0].trim(); | |
| if (value.length > 0) { | |
| return value; | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| function extractProjectNameFallback(issueTitle, issueBody) { | |
| // Handle DD app titles like: | |
| // [Graduation] KubeVirt Graduation Application | |
| // [Incubation] HAMi Incubation Application | |
| if (issueTitle) { | |
| const titlePattern = /^\s*\[(?:Gov\.?\s*Review|Governance\s*Review|Graduation|Incubation|Sandbox)\]\s*(.+?)\s*(?:Governance\s*Review|Gov\.?\s*Review|Graduation\s*Application|Incubation\s*Application|Sandbox\s*Application)?\s*$/i; | |
| const titleMatch = issueTitle.match(titlePattern); | |
| if (titleMatch && titleMatch[1] && titleMatch[1].trim().length > 0) { | |
| return titleMatch[1].trim(); | |
| } | |
| } | |
| // Handle markdown heading bodies like: | |
| // # KubeVirt Graduation Application | |
| if (issueBody) { | |
| const headingPattern = /^#\s+(.+?)\s+(?:Graduation|Incubation|Sandbox)\s+Application\s*$/im; | |
| const headingMatch = issueBody.match(headingPattern); | |
| if (headingMatch && headingMatch[1] && headingMatch[1].trim().length > 0) { | |
| return headingMatch[1].trim(); | |
| } | |
| } | |
| return null; | |
| } | |
| function normalize(str) { | |
| return (str || '').trim().toLowerCase(); | |
| } | |
| function extractGovReviewProjectNameFromTitle(title) { | |
| if (!title) return null; | |
| // Accept "[Gov. Review]:", "[Gov Review]:" and "[Governance Review]:" | |
| const match = title.match(/^\s*\[(?:Gov\.?\s*Review|Governance\s*Review)\]:\s*(.+?)\s*$/i); | |
| return match && match[1] ? match[1].trim() : null; | |
| } | |
| function escapeForSearchQuery(value) { | |
| return (value || '').replace(/"/g, '\\"').trim(); | |
| } | |
| // --- Main Logic --- | |
| const issue = context.payload.issue; | |
| const issueNumber = issue.number; | |
| const issueTitle = issue.title; | |
| const issueBody = issue.body; | |
| console.log(`🔍 Processing issue #${issueNumber}`); | |
| console.log(`📄 Issue body length: ${issueBody ? issueBody.length : 0}`); | |
| // Extract fields | |
| const projectName = extractFormField(issueBody, 'name') || extractProjectNameFallback(issueTitle, issueBody); | |
| const projectLink = extractFormField(issueBody, 'projectLink'); | |
| const ddLink = extractFormField(issueBody, 'ddLink'); | |
| const projectContact = extractFormField(issueBody, 'projectContact'); | |
| const additionalInfo = extractFormField(issueBody, 'additionalInfo'); | |
| console.log(`📋 Extracted fields: | |
| projectName: ${projectName || 'NOT FOUND'} | |
| projectLink: ${projectLink || 'NOT FOUND'} | |
| ddLink: ${ddLink || 'NOT FOUND'} | |
| projectContact: ${projectContact || 'NOT FOUND'} | |
| additionalInfo: ${additionalInfo ? 'FOUND' : 'NOT FOUND'} | |
| `); | |
| // Validate required fields | |
| if (!projectName) { | |
| console.log('❌ Missing project name - commenting and exiting'); | |
| await commentOnce(issueNumber, COMMENT_MARKERS.missingProjectName, | |
| `❌ Could not extract project name from issue body. Please ensure the issue was created from a template with a "Project name" field.`); | |
| return; | |
| } | |
| if (!projectLink) { | |
| console.log('❌ Missing project link - commenting and exiting'); | |
| await commentOnce(issueNumber, COMMENT_MARKERS.missingProjectLink, | |
| `❌ Could not extract project link from issue body. Please ensure the issue contains a "Project link" or "GitHub URL" field.`); | |
| return; | |
| } | |
| const normalizedProjectName = projectName.trim(); | |
| const projectNameLower = normalize(projectName); | |
| // Check if governance review already created for this issue | |
| const hasExistingCommentCheck = await hasExistingComment(issueNumber, COMMENT_MARKERS.govReviewCreated); | |
| console.log(`🔍 Checked for existing comment: ${hasExistingCommentCheck}`); | |
| if (hasExistingCommentCheck) { | |
| console.log('ℹ️ Governance review issue already created for this issue - exiting'); | |
| return; | |
| } | |
| // Safety check #1: GitHub search for existing OPEN governance review issue by project | |
| let existingIssue = null; | |
| try { | |
| const searchQuery = [ | |
| `repo:${context.repo.owner}/${context.repo.repo}`, | |
| 'is:issue', | |
| 'is:open', | |
| 'in:title', | |
| '"[Gov. Review]:"', | |
| `"${escapeForSearchQuery(normalizedProjectName)}"` | |
| ].join(' '); | |
| const searchResults = await github.paginate(github.rest.search.issuesAndPullRequests, { | |
| q: searchQuery, | |
| per_page: 100 | |
| }); | |
| existingIssue = searchResults.find(item => { | |
| if (item.number === issueNumber) return false; | |
| if (item.pull_request) return false; | |
| const projectNameFromGovReviewTitle = extractGovReviewProjectNameFromTitle(item.title); | |
| return normalize(projectNameFromGovReviewTitle) === projectNameLower; | |
| }); | |
| if (existingIssue) { | |
| console.log(`ℹ️ Found existing governance review issue via search #${existingIssue.number} - commenting and exiting`); | |
| await commentExistingGovReview(issueNumber, existingIssue); | |
| return; | |
| } | |
| } catch (error) { | |
| console.log('⚠️ Error checking for existing issues via search:', error.message); | |
| } | |
| // Safety check #2: scan open issues as fallback | |
| try { | |
| const openIssues = await github.paginate(github.rest.issues.listForRepo, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| per_page: 100 | |
| }); | |
| existingIssue = openIssues.find(item => { | |
| if (item.number === issueNumber) return false; | |
| if (item.pull_request) return false; | |
| const projectNameFromGovReviewTitle = extractGovReviewProjectNameFromTitle(item.title); | |
| if (!projectNameFromGovReviewTitle) return false; | |
| if (normalize(projectNameFromGovReviewTitle) === projectNameLower) return true; | |
| const existingProjectNameFromBody = extractFormField(item.body, 'name'); | |
| return normalize(existingProjectNameFromBody) === projectNameLower; | |
| }); | |
| if (existingIssue) { | |
| console.log(`ℹ️ Found existing governance review issue #${existingIssue.number} - commenting and exiting`); | |
| await commentExistingGovReview(issueNumber, existingIssue); | |
| return; | |
| } else { | |
| console.log('✅ No existing governance review issue found - proceeding to create'); | |
| } | |
| } catch (error) { | |
| console.log('⚠️ Error checking for existing issues:', error.message); | |
| } | |
| // Informational: find prior CLOSED governance review issues for | |
| // the same project. These do NOT block creation (a new review is | |
| // expected e.g. when a project moves Incubation -> Graduation), but | |
| // they are linked in the new issue for reviewer context. | |
| let priorClosedIssues = []; | |
| try { | |
| const closedSearchQuery = [ | |
| `repo:${context.repo.owner}/${context.repo.repo}`, | |
| 'is:issue', | |
| 'is:closed', | |
| 'in:title', | |
| '"[Gov. Review]:"', | |
| `"${escapeForSearchQuery(normalizedProjectName)}"` | |
| ].join(' '); | |
| const closedResults = await github.paginate(github.rest.search.issuesAndPullRequests, { | |
| q: closedSearchQuery, | |
| per_page: 100 | |
| }); | |
| priorClosedIssues = closedResults.filter(item => { | |
| if (item.pull_request) return false; | |
| const nameFromTitle = extractGovReviewProjectNameFromTitle(item.title); | |
| return normalize(nameFromTitle) === projectNameLower; | |
| }); | |
| if (priorClosedIssues.length > 0) { | |
| console.log(`ℹ️ Found ${priorClosedIssues.length} prior closed governance review issue(s): ${priorClosedIssues.map(i => `#${i.number}`).join(', ')}`); | |
| } else { | |
| console.log('ℹ️ No prior closed governance review issues found'); | |
| } | |
| } catch (error) { | |
| console.log('⚠️ Error searching for prior closed governance reviews:', error.message); | |
| } | |
| // Build governance review issue body | |
| let issueBodyContent = `### Project name\n\n${normalizedProjectName}\n\n`; | |
| issueBodyContent += `### Project link\n\n${projectLink}\n\n`; | |
| issueBodyContent += `### Due diligence link\n\n${ddLink || ''}\n\n`; | |
| issueBodyContent += `### Project contact information\n\n${projectContact || 'To be provided'}\n\n`; | |
| issueBodyContent += `### Additional information\n\n${additionalInfo || ''}\n\n`; | |
| if (priorClosedIssues.length > 0) { | |
| const priorList = priorClosedIssues | |
| .map(i => `- [#${i.number} - ${i.title}](${i.html_url})${i.closed_at ? ` (closed ${i.closed_at.split('T')[0]})` : ''}`) | |
| .join('\n'); | |
| issueBodyContent += `### Prior governance reviews\n\n${priorList}\n\n`; | |
| } | |
| const originalIssueUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/issues/${issueNumber}`; | |
| issueBodyContent += `---\n\n_This issue was automatically created from [issue #${issueNumber}](${originalIssueUrl})_`; | |
| // Create governance review issue | |
| console.log(`🚀 Attempting to create governance review issue for: ${normalizedProjectName}`); | |
| try { | |
| const newIssue = await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: `[Gov. Review]: ${normalizedProjectName}`, | |
| body: issueBodyContent, | |
| labels: LABELS_GOV_REVIEW | |
| }); | |
| console.log(`✅ Successfully created issue #${newIssue.data.number}`); | |
| await commentOnce(issueNumber, COMMENT_MARKERS.govReviewCreated, | |
| `✅ Created governance review issue: [#${newIssue.data.number} - ${newIssue.data.title}](${newIssue.data.html_url})`); | |
| } catch (error) { | |
| console.error(`❌ Error creating issue: ${error.message}`); | |
| console.error(`❌ Error stack: ${error.stack}`); | |
| await commentOnce(issueNumber, COMMENT_MARKERS.createGovReviewFailed, | |
| `❌ Failed to create governance review issue: ${error.message}`); | |
| throw error; | |
| } |