Skip to content

Commit 5677fb0

Browse files
committed
feat: add support for /stdlib todo slash command
--- type: pre_commit_static_analysis_report description: Results of running static analysis checks when committing changes. report: - task: lint_filenames status: passed - task: lint_editorconfig status: passed - task: lint_markdown status: na - task: lint_package_json status: na - task: lint_repl_help status: na - task: lint_javascript_src status: na - task: lint_javascript_cli status: na - task: lint_javascript_examples status: na - task: lint_javascript_tests status: na - task: lint_javascript_benchmarks status: na - task: lint_python status: na - task: lint_r status: na - task: lint_c_src status: na - task: lint_c_examples status: na - task: lint_c_benchmarks status: na - task: lint_c_tests_fixtures status: na - task: lint_shell status: na - task: lint_typescript_declarations status: passed - task: lint_typescript_tests status: na - task: lint_license_headers status: passed ---
1 parent 544556c commit 5677fb0

1 file changed

Lines changed: 155 additions & 2 deletions

File tree

.github/workflows/slash_commands.yml

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ jobs:
6565
script: |
6666
const commentBody = context.payload.comment.body.trim();
6767
const RE_COMMANDS = /^\/stdlib\s+(help|check-files|update-copyright-years|lint-autofix|merge|rebase|make-commands)$/i;
68-
const isRecognizedCommand = RE_COMMANDS.test( commentBody );
68+
const RE_TODO_COMMAND = /^\/stdlib\s+todo\b/i;
69+
const isRecognizedCommand = RE_COMMANDS.test( commentBody ) || RE_TODO_COMMAND.test( commentBody );
6970
7071
if ( isRecognizedCommand ) {
7172
await github.rest.reactions.createForIssueComment({
@@ -206,6 +207,157 @@ jobs:
206207
STDLIB_BOT_GPG_PRIVATE_KEY: ${{ secrets.STDLIB_BOT_GPG_PRIVATE_KEY }}
207208
STDLIB_BOT_GPG_PASSPHRASE: ${{ secrets.STDLIB_BOT_GPG_PASSPHRASE }}
208209

210+
# Define a job for creating a new todo issue:
211+
todo:
212+
213+
# Define a display name:
214+
name: 'Create a new todo issue'
215+
216+
# Define the type of virtual host machine:
217+
runs-on: ubuntu-latest
218+
219+
# Ensure initial reaction job has completed before running this job:
220+
needs: [ add_initial_reaction ]
221+
222+
# Define the conditions under which the job should run:
223+
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/stdlib todo')
224+
225+
# Define the job's steps:
226+
steps:
227+
# Create a new todo issue:
228+
- name: 'Create a new todo issue'
229+
# Pin action to full length commit SHA
230+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
231+
with:
232+
github-token: ${{ secrets.STDLIB_BOT_PAT_REPO_WRITE }}
233+
script: |
234+
const commentBody = context.payload.comment.body.trim();
235+
236+
// Parse the fenced code block following '/stdlib todo':
237+
const RE_CODE_BLOCK = /```text\s*\{([^}]*)\}\s*\n([\s\S]*?)```/;
238+
const blockMatch = commentBody.match( RE_CODE_BLOCK );
239+
if ( !blockMatch ) {
240+
const lines = commentBody.split( '\n' );
241+
const quote = lines.map( line => `> ${line}` ).join( '\n' );
242+
await github.rest.issues.createComment({
243+
'owner': context.repo.owner,
244+
'repo': context.repo.repo,
245+
'issue_number': context.issue.number,
246+
'body': `${quote}\n\n@${context.payload.comment.user.login}, failed to parse the \`/stdlib todo\` command. Expected a fenced code block with attributes, e.g.,\n\n\`\`\`\n/stdlib todo\n\n\\\`\\\`\\\`text {stdlib=public labels="foo,bar"}\n[TODO]: Issue title\n\nIssue body\n\\\`\\\`\\\`\n\`\`\``
247+
});
248+
core.setFailed( 'No code block found in /stdlib todo comment.' );
249+
return;
250+
}
251+
252+
const attrs = blockMatch[ 1 ];
253+
const content = blockMatch[ 2 ].trim();
254+
255+
// Parse 'stdlib' attribute (values: 'public' or 'private'):
256+
const stdlibAttrMatch = attrs.match( /stdlib=(\w+)/i );
257+
const stdlibTarget = stdlibAttrMatch ? stdlibAttrMatch[ 1 ].toLowerCase() : 'public';
258+
if ( stdlibTarget !== 'public' && stdlibTarget !== 'private' ) {
259+
const lines = commentBody.split( '\n' );
260+
const quote = lines.map( line => `> ${line}` ).join( '\n' );
261+
await github.rest.issues.createComment({
262+
'owner': context.repo.owner,
263+
'repo': context.repo.repo,
264+
'issue_number': context.issue.number,
265+
'body': `${quote}\n\n@${context.payload.comment.user.login}, unrecognized \`stdlib\` attribute value \`${stdlibTarget}\`. Valid values are \`public\` (opens issue on \`stdlib-js/stdlib\`) or \`private\` (opens issue on the internal todo repository).`
266+
});
267+
core.setFailed( `Unrecognized stdlib attribute value: ${stdlibTarget}` );
268+
return;
269+
}
270+
271+
// Parse 'labels' attribute (comma-separated list):
272+
const labelsAttrMatch = attrs.match( /labels="([^"]*)"/i );
273+
const labels = labelsAttrMatch
274+
? labelsAttrMatch[ 1 ].split( ',' ).map( l => l.trim() ).filter( Boolean )
275+
: [];
276+
277+
// Parse the issue title from the '[TODO]:' line:
278+
const contentLines = content.split( '\n' );
279+
const RE_TITLE = /^\[TODO\]:\s*(.+)/i;
280+
const titleMatch = contentLines[ 0 ].match( RE_TITLE );
281+
if ( !titleMatch ) {
282+
const lines = commentBody.split( '\n' );
283+
const quote = lines.map( line => `> ${line}` ).join( '\n' );
284+
await github.rest.issues.createComment({
285+
'owner': context.repo.owner,
286+
'repo': context.repo.repo,
287+
'issue_number': context.issue.number,
288+
'body': `${quote}\n\n@${context.payload.comment.user.login}, failed to parse the todo title. The first line of the code block must be of the form \`[TODO]: Issue title\`.`
289+
});
290+
core.setFailed( 'No [TODO]: line found in the code block.' );
291+
return;
292+
}
293+
294+
const title = titleMatch[ 1 ].trim();
295+
const body = contentLines.slice( 1 ).join( '\n' ).trim();
296+
297+
// Determine the target repository:
298+
const targetOwner = 'stdlib-js';
299+
const targetRepo = ( stdlibTarget === 'private' ) ? 'todo' : 'stdlib';
300+
301+
// For private repo targets, verify the commenter is an org member:
302+
if ( stdlibTarget === 'private' ) {
303+
const commenter = context.payload.comment.user.login;
304+
try {
305+
await github.rest.orgs.checkMembershipForUser({
306+
'org': targetOwner,
307+
'username': commenter
308+
});
309+
} catch ( err ) {
310+
console.log( 'Error checking org membership: %s', err.message );
311+
const lines = commentBody.split( '\n' );
312+
const quote = lines.map( line => `> ${line}` ).join( '\n' );
313+
await github.rest.issues.createComment({
314+
'owner': context.repo.owner,
315+
'repo': context.repo.repo,
316+
'issue_number': context.issue.number,
317+
'body': `${quote}\n\n@${commenter}, you must be a member of the \`${targetOwner}\` organization to create issues on the private todo repository.`
318+
});
319+
core.setFailed( `User ${commenter} is not a member of ${targetOwner}.` );
320+
return;
321+
}
322+
}
323+
324+
// Build issue creation parameters:
325+
const issueParams = {
326+
'owner': targetOwner,
327+
'repo': targetRepo,
328+
'title': title
329+
};
330+
// Build provenance footer:
331+
const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${context.issue.number}`;
332+
const provenance = `\n\n---\n*Created via \`/stdlib todo\` from [${context.repo.owner}/${context.repo.repo}#${context.issue.number}](${prUrl}) by @${context.payload.comment.user.login}.*`;
333+
334+
if ( body ) {
335+
issueParams.body = body + provenance;
336+
} else {
337+
issueParams.body = provenance.trim();
338+
}
339+
if ( labels.length > 0 ) {
340+
issueParams.labels = labels;
341+
}
342+
343+
// Create the issue:
344+
const issue = await github.rest.issues.create( issueParams );
345+
346+
// Check for labels that were silently dropped (do not exist in the target repo):
347+
const appliedLabels = issue.data.labels.map( l => l.name );
348+
const droppedLabels = labels.filter( l => !appliedLabels.includes( l ) );
349+
350+
// Post a confirmation comment:
351+
const confirmBody = droppedLabels.length > 0
352+
? `@${context.payload.comment.user.login}, the following todo issue has been created: ${issue.data.html_url}\n\n> [!WARNING]\n> The following labels were not applied because they do not exist on \`${targetOwner}/${targetRepo}\`: ${droppedLabels.map( l => `\`${l}\`` ).join( ', ' )}.`
353+
: `@${context.payload.comment.user.login}, the following todo issue has been created: ${issue.data.html_url}`;
354+
await github.rest.issues.createComment({
355+
'owner': context.repo.owner,
356+
'repo': context.repo.repo,
357+
'issue_number': context.issue.number,
358+
'body': confirmBody
359+
});
360+
209361
# Define a job for printing a list of available slash commands:
210362
help:
211363

@@ -241,6 +393,7 @@ jobs:
241393
- `/stdlib lint-autofix` - Auto-fix lint errors.
242394
- `/stdlib merge` - Merge changes from develop branch into this PR.
243395
- `/stdlib rebase` - Rebase this PR on top of develop branch.
396+
- `/stdlib todo` - Create a new todo issue.
244397
245398
# GitHub token:
246399
token: ${{ secrets.STDLIB_BOT_PAT_REPO_WRITE }}
@@ -255,7 +408,7 @@ jobs:
255408
runs-on: ubuntu-latest
256409

257410
# Ensure all previous jobs have completed before running this job:
258-
needs: [ add_initial_reaction, check_files, make-commands, update_copyright_years, fix_lint_errors, merge_develop, rebase_develop, help ]
411+
needs: [ add_initial_reaction, check_files, make-commands, update_copyright_years, fix_lint_errors, merge_develop, rebase_develop, help, todo ]
259412

260413
# Define the conditions under which the job should run:
261414
if: |

0 commit comments

Comments
 (0)