Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions .github/workflows/create-tech-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
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 link', 'project-link', 'project link', 'github-url', 'GitHub URL', 'Project link'
],
ddLink: [
'Due diligence link', 'dd-link', 'due diligence link', 'Due diligence', 'Due diligence link'
],
projectContact: [
'Project contact', 'project-contact', 'project contact', 'Project contact information', 'Project Security Contacts', 'Project contact information'
],
additionalInfo: [
'Additional information', 'additional-information', 'additional information', 'Additional Information', 'Additional information'
]
};

const LABELS_TECH_REVIEW = [
'needs-triage', 'kind/initiative', 'review/tech', 'sub/project-review'
];

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
});
}
}

function extractFormField(body, fieldKey) {
if (!body) return null;
const labels = FIELD_LABELS[fieldKey] || [fieldKey];
for (const label of labels) {
const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const pattern = new RegExp(
`###\\s+${escapedLabel}[^\\n]*\\n\\n([\\s\\S]*?)(?=\\n###|$)`,
'i'
);
const match = body.match(pattern);
if (match && match[1] && match[1].trim().length > 0) {
return match[1].trim();
}
}
return null;
}

function normalize(str) {
return (str || '').trim().toLowerCase();
}

// --- Main Logic ---
const issue = context.payload.issue;
const issueNumber = issue.number;
const issueBody = issue.body;

// Extract fields
const projectName = extractFormField(issueBody, 'name');
const projectLink = extractFormField(issueBody, 'projectLink');
const ddLink = extractFormField(issueBody, 'ddLink');
const projectContact = extractFormField(issueBody, 'projectContact');
const additionalInfo = extractFormField(issueBody, 'additionalInfo');

// Validate required fields
if (!projectName) {
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) {
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
if (await hasExistingComment(issueNumber, COMMENT_MARKERS.techReviewCreated)) {
console.log('ℹ️ Tech review issue already created for this issue.');
return;
}

// Check for existing tech review issues for this project
let existingIssue = null;
try {
const allIssues = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'all',
labels: 'review/tech',
per_page: 100
});

existingIssue = allIssues.find(item => {
if (item.number === issueNumber) return false;
if (!item.title.includes('[Tech Review]:')) return false;
const existingProjectName = extractFormField(item.body, 'name');
if (normalize(existingProjectName) === projectNameLower) return true;
// Fallback: check title for exact word match
const titleLower = item.title.toLowerCase();
const projectNameRegex = new RegExp(`\\b${projectNameLower.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
return projectNameRegex.test(titleLower);
});

if (existingIssue) {
await commentOnce(issueNumber, COMMENT_MARKERS.techReviewExists,
`A tech review issue already exists for this project: [#${existingIssue.number} - ${existingIssue.title}](${existingIssue.html_url})`);
return;
}
} 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
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
});

await commentOnce(issueNumber, COMMENT_MARKERS.techReviewCreated,
`✅ Created tech review issue: [#${newIssue.data.number} - ${newIssue.data.title}](${newIssue.data.html_url})`);
} catch (error) {
await commentOnce(issueNumber, COMMENT_MARKERS.createTechReviewFailed,
`❌ Failed to create tech review issue: ${error.message}`);
throw error;
}