|
1 | 1 | const COMMENT_MARKER = process.env.INTERMEDIATE_COMMENT_MARKER || '<!-- Intermediate Issue Guard -->'; |
2 | 2 | const INTERMEDIATE_LABEL = process.env.INTERMEDIATE_LABEL?.trim() || 'intermediate'; |
3 | | -const GFI_LABEL = process.env.GFI_LABEL?.trim() || 'Good First Issue'; |
| 3 | +const BEGINNER_LABEL = process.env.BEGINNER_LABEL?.trim() || 'beginner'; |
4 | 4 | const EXEMPT_PERMISSION_LEVELS = (process.env.INTERMEDIATE_EXEMPT_PERMISSIONS || 'admin,maintain,write,triage') |
5 | 5 | .split(',') |
6 | 6 | .map((entry) => entry.trim().toLowerCase()) |
7 | 7 | .filter(Boolean); |
8 | 8 | const DRY_RUN = /^true$/i.test(process.env.DRY_RUN || ''); |
| 9 | +const REQUIRED_BEGINNER_ISSUE_COUNT = 0 ; |
| 10 | + |
| 11 | +function isSafeSearchToken(value) { |
| 12 | + return typeof value === 'string' && /^[a-zA-Z0-9._/-]+$/.test(value); |
| 13 | +} |
9 | 14 |
|
10 | 15 | function hasLabel(issue, labelName) { |
11 | 16 | if (!issue?.labels?.length) { |
@@ -51,51 +56,54 @@ async function hasExemptPermission(github, owner, repo, username) { |
51 | 56 | } |
52 | 57 | } |
53 | 58 |
|
54 | | -async function countCompletedGfiIssues(github, owner, repo, username) { |
55 | | - try { |
56 | | - console.log(`Checking closed '${GFI_LABEL}' issues in ${owner}/${repo} for ${username}.`); |
57 | | - const iterator = github.paginate.iterator(github.rest.issues.listForRepo, { |
| 59 | +async function countCompletedBeginnerIssues(github, owner, repo, username) { |
| 60 | + if ( |
| 61 | + !isSafeSearchToken(owner) || |
| 62 | + !isSafeSearchToken(repo) || |
| 63 | + !isSafeSearchToken(username) || |
| 64 | + !isSafeSearchToken(BEGINNER_LABEL) |
| 65 | + ) { |
| 66 | + console.log('Invalid search inputs', { |
58 | 67 | owner, |
59 | 68 | repo, |
60 | | - state: 'closed', |
61 | | - labels: GFI_LABEL, |
62 | | - assignee: username, |
63 | | - sort: 'updated', |
64 | | - direction: 'desc', |
65 | | - per_page: 100, |
| 69 | + username, |
| 70 | + label: BEGINNER_LABEL, |
66 | 71 | }); |
| 72 | + return null; |
| 73 | + } |
67 | 74 |
|
68 | | - const normalizedAssignee = username.toLowerCase(); |
69 | | - let pageCount = 0; |
70 | | - const MAX_PAGES = 8; |
71 | | - |
72 | | - for await (const { data: issues } of iterator) { |
73 | | - pageCount += 1; |
74 | | - if (pageCount > MAX_PAGES) { |
75 | | - console.log(`Reached pagination safety cap (${MAX_PAGES}) while checking GFIs for ${username}.`); |
76 | | - break; |
77 | | - } |
78 | | - |
79 | | - console.log(`Scanning page ${pageCount} of closed '${GFI_LABEL}' issues for ${username} (items: ${issues.length}).`); |
80 | | - const match = issues.find((issue) => { |
81 | | - if (issue.pull_request) { |
82 | | - return false; |
| 75 | + try { |
| 76 | + const searchQuery = [ |
| 77 | + `repo:${owner}/${repo}`, |
| 78 | + `label:${BEGINNER_LABEL}`, |
| 79 | + 'is:issue', |
| 80 | + 'is:closed', |
| 81 | + `assignee:${username}`, |
| 82 | + ].join(' '); |
| 83 | + |
| 84 | + console.log('GraphQL search query:', searchQuery); |
| 85 | + |
| 86 | + const result = await github.graphql( |
| 87 | + ` |
| 88 | + query ($searchQuery: String!) { |
| 89 | + search(type: ISSUE, query: $searchQuery) { |
| 90 | + issueCount |
83 | 91 | } |
| 92 | + } |
| 93 | + `, |
| 94 | + { searchQuery } |
| 95 | + ); |
84 | 96 |
|
85 | | - const assignees = Array.isArray(issue.assignees) ? issue.assignees : []; |
86 | | - return assignees.some((assignee) => assignee?.login?.toLowerCase() === normalizedAssignee); |
87 | | - }); |
| 97 | + const count = result?.search?.issueCount ?? 0; |
88 | 98 |
|
89 | | - if (match) { |
90 | | - console.log(`Found matching GFI issue #${match.number} (${match.html_url || 'no url'}) for ${username}.`); |
91 | | - return 1; |
92 | | - } |
93 | | - } |
| 99 | + console.log(`Completed Beginner issues for ${username}: ${count}`); |
94 | 100 |
|
95 | | - return 0; |
| 101 | + return count; |
96 | 102 | } catch (error) { |
97 | 103 | const message = error instanceof Error ? error.message : String(error); |
98 | | - console.log(`Unable to verify completed GFIs for ${username}: ${message}`); |
| 104 | + console.log( |
| 105 | + `Failed to count Beginner issues for ${username}: ${message}` |
| 106 | + ); |
99 | 107 | return null; |
100 | 108 | } |
101 | 109 | } |
@@ -127,10 +135,10 @@ function buildRejectionComment({ mentee, completedCount }) { |
127 | 135 | Hi @${mentee}! Thanks for your interest in contributing 💡 |
128 | 136 |
|
129 | 137 | This issue is labeled as intermediate, which means it requires a bit more familiarity with the SDK. |
130 | | -Before you can take it on, please complete at least one Good First Issue so we can make sure you have a smooth on-ramp. |
| 138 | +Before you can take it on, please complete at least one Beginner Issue so we can make sure you have a smooth on-ramp. |
131 | 139 |
|
132 | | -You've completed **${completedCount}** Good First Issue${plural} so far. |
133 | | -Once you wrap up your first GFI, feel free to come back and we’ll gladly help you get rolling here!`; |
| 140 | +You've completed **${completedCount}** Beginner Issue${plural} so far. |
| 141 | +Once you wrap up your first Beginner issue, feel free to come back and we’ll gladly help you get rolling here!`; |
134 | 142 | } |
135 | 143 |
|
136 | 144 | module.exports = async ({ github, context }) => { |
@@ -164,30 +172,34 @@ module.exports = async ({ github, context }) => { |
164 | 172 | return; |
165 | 173 | } |
166 | 174 |
|
167 | | - const completedCount = await countCompletedGfiIssues(github, owner, repo, mentee); |
| 175 | + const completedCount = await countCompletedBeginnerIssues(github, owner, repo, mentee); |
168 | 176 |
|
169 | 177 | if (completedCount === null) { |
170 | | - return console.log(`Skipping guard for @${mentee} on issue #${issue.number} due to API error when verifying GFIs.`); |
| 178 | + return console.log(`Skipping guard for @${mentee} on issue #${issue.number} due to API error when verifying Beginner issues.`); |
| 179 | + } |
| 180 | + |
| 181 | + if(REQUIRED_BEGINNER_ISSUE_COUNT === 0){ |
| 182 | + return console.log(`Skipping guard for @${mentee} on issue #${issue.number}: beginner requirement temporarily disabled`) |
171 | 183 | } |
172 | 184 |
|
173 | | - if (completedCount >= 1) { |
174 | | - console.log(`✅ ${mentee} has completed ${completedCount} GFI(s). Assignment allowed.`); |
| 185 | + if (completedCount >= REQUIRED_BEGINNER_ISSUE_COUNT) { |
| 186 | + console.log(`✅ ${mentee} has completed ${completedCount} Beginner issues. Assignment allowed.`); |
175 | 187 | return; |
176 | 188 | } |
177 | 189 |
|
178 | | - console.log(`❌ ${mentee} has completed ${completedCount} GFI(s). Assignment not allowed; proceeding with removal and comment.`); |
| 190 | + console.log(`❌ ${mentee} has completed ${completedCount} Beginner issues. Assignment not allowed; proceeding with removal and comment.`); |
179 | 191 |
|
180 | 192 | try { |
181 | 193 | if (DRY_RUN) { |
182 | | - console.log(`[dry-run] Would remove @${mentee} from issue #${issue.number} due to missing GFI completion.`); |
| 194 | + console.log(`[dry-run] Would remove @${mentee} from issue #${issue.number} due to missing Beginner issue completion.`); |
183 | 195 | } else { |
184 | 196 | await github.rest.issues.removeAssignees({ |
185 | 197 | owner, |
186 | 198 | repo, |
187 | 199 | issue_number: issue.number, |
188 | 200 | assignees: [mentee], |
189 | 201 | }); |
190 | | - console.log(`Removed @${mentee} from issue #${issue.number} due to missing GFI completion.`); |
| 202 | + console.log(`Removed @${mentee} from issue #${issue.number} due to missing Beginner issue completion.`); |
191 | 203 | } |
192 | 204 | } catch (error) { |
193 | 205 | const message = error instanceof Error ? error.message : String(error); |
|
0 commit comments