[Initiative]: Vendor-neutral coding harness for CNCF contributors #270
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 Tech Review Issue | |
| on: | |
| issues: | |
| types: [labeled] | |
| jobs: | |
| create-tech-review: | |
| if: github.event.label.name == 'review/tech' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| contents: read | |
| steps: | |
| - name: Extract issue information and create tech 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' | |
| ] | |
| }; | |
| const LABELS_TECH_REVIEW = [ | |
| 'needs-triage', 'kind/initiative', 'review/tech', 'sub/project-reviews' | |
| ]; | |
| const COMMENT_MARKERS = { | |
| techReviewCreated: 'Created tech review issue:', | |
| techReviewExists: 'tech review issue already exists', | |
| missingProjectName: 'Could not extract project name', | |
| missingProjectLink: 'Could not extract project link', | |
| createTechReviewFailed: 'Failed to create tech 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 commentExistingTechReview(issueNumber, existingIssue) { | |
| await commentOnce( | |
| issueNumber, | |
| COMMENT_MARKERS.techReviewExists, | |
| `Tech 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*\[(?:Tech\s*Review|Graduation|Incubation|Sandbox)\]\s*(.+?)\s*(?:Tech\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 extractTechReviewProjectNameFromTitle(title) { | |
| if (!title) return null; | |
| const match = title.match(/^\s*\[Tech\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 tech review already created for this issue | |
| const hasExistingCommentCheck = await hasExistingComment(issueNumber, COMMENT_MARKERS.techReviewCreated); | |
| console.log(`🔍 Checked for existing comment: ${hasExistingCommentCheck}`); | |
| if (hasExistingCommentCheck) { | |
| console.log('ℹ️ Tech review issue already created for this issue - exiting'); | |
| return; | |
| } | |
| // Safety check #1: GitHub search for existing OPEN tech review issue by project | |
| let existingIssue = null; | |
| try { | |
| const searchQuery = [ | |
| `repo:${context.repo.owner}/${context.repo.repo}`, | |
| 'is:issue', | |
| 'is:open', | |
| 'in:title', | |
| '"[Tech 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 projectNameFromTechReviewTitle = extractTechReviewProjectNameFromTitle(item.title); | |
| return normalize(projectNameFromTechReviewTitle) === projectNameLower; | |
| }); | |
| if (existingIssue) { | |
| console.log(`ℹ️ Found existing tech review issue via search #${existingIssue.number} - commenting and exiting`); | |
| await commentExistingTechReview(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 projectNameFromTechReviewTitle = extractTechReviewProjectNameFromTitle(item.title); | |
| if (!projectNameFromTechReviewTitle) return false; | |
| if (normalize(projectNameFromTechReviewTitle) === projectNameLower) return true; | |
| const existingProjectNameFromBody = extractFormField(item.body, 'name'); | |
| return normalize(existingProjectNameFromBody) === projectNameLower; | |
| }); | |
| if (existingIssue) { | |
| console.log(`ℹ️ Found existing tech review issue #${existingIssue.number} - commenting and exiting`); | |
| await commentExistingTechReview(issueNumber, existingIssue); | |
| return; | |
| } else { | |
| console.log('✅ No existing tech review issue found - proceeding to create'); | |
| } | |
| } catch (error) { | |
| console.log('⚠️ Error checking for existing issues:', error.message); | |
| } | |
| // Build tech 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`; | |
| 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 tech review issue | |
| console.log(`🚀 Attempting to create tech review issue for: ${normalizedProjectName}`); | |
| try { | |
| const newIssue = await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: `[Tech Review]: ${normalizedProjectName}`, | |
| body: issueBodyContent, | |
| labels: LABELS_TECH_REVIEW | |
| }); | |
| console.log(`✅ Successfully created issue #${newIssue.data.number}`); | |
| await commentOnce(issueNumber, COMMENT_MARKERS.techReviewCreated, | |
| `✅ Created tech 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.createTechReviewFailed, | |
| `❌ Failed to create tech review issue: ${error.message}`); | |
| throw error; | |
| } |