From 5677fb056f8d0f48b27fa1f8151e3a40d1306372 Mon Sep 17 00:00:00 2001 From: Karan Anand Date: Sat, 9 May 2026 11:02:03 -0700 Subject: [PATCH 1/9] 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 --- --- .github/workflows/slash_commands.yml | 157 ++++++++++++++++++++++++++- 1 file changed, 155 insertions(+), 2 deletions(-) diff --git a/.github/workflows/slash_commands.yml b/.github/workflows/slash_commands.yml index 5d27639d747b..27ce14890555 100644 --- a/.github/workflows/slash_commands.yml +++ b/.github/workflows/slash_commands.yml @@ -65,7 +65,8 @@ jobs: script: | const commentBody = context.payload.comment.body.trim(); const RE_COMMANDS = /^\/stdlib\s+(help|check-files|update-copyright-years|lint-autofix|merge|rebase|make-commands)$/i; - const isRecognizedCommand = RE_COMMANDS.test( commentBody ); + const RE_TODO_COMMAND = /^\/stdlib\s+todo\b/i; + const isRecognizedCommand = RE_COMMANDS.test( commentBody ) || RE_TODO_COMMAND.test( commentBody ); if ( isRecognizedCommand ) { await github.rest.reactions.createForIssueComment({ @@ -206,6 +207,157 @@ jobs: STDLIB_BOT_GPG_PRIVATE_KEY: ${{ secrets.STDLIB_BOT_GPG_PRIVATE_KEY }} STDLIB_BOT_GPG_PASSPHRASE: ${{ secrets.STDLIB_BOT_GPG_PASSPHRASE }} + # Define a job for creating a new todo issue: + todo: + + # Define a display name: + name: 'Create a new todo issue' + + # Define the type of virtual host machine: + runs-on: ubuntu-latest + + # Ensure initial reaction job has completed before running this job: + needs: [ add_initial_reaction ] + + # Define the conditions under which the job should run: + if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/stdlib todo') + + # Define the job's steps: + steps: + # Create a new todo issue: + - name: 'Create a new todo issue' + # Pin action to full length commit SHA + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ secrets.STDLIB_BOT_PAT_REPO_WRITE }} + script: | + const commentBody = context.payload.comment.body.trim(); + + // Parse the fenced code block following '/stdlib todo': + const RE_CODE_BLOCK = /```text\s*\{([^}]*)\}\s*\n([\s\S]*?)```/; + const blockMatch = commentBody.match( RE_CODE_BLOCK ); + if ( !blockMatch ) { + const lines = commentBody.split( '\n' ); + const quote = lines.map( line => `> ${line}` ).join( '\n' ); + await github.rest.issues.createComment({ + 'owner': context.repo.owner, + 'repo': context.repo.repo, + 'issue_number': context.issue.number, + '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\`\`\`` + }); + core.setFailed( 'No code block found in /stdlib todo comment.' ); + return; + } + + const attrs = blockMatch[ 1 ]; + const content = blockMatch[ 2 ].trim(); + + // Parse 'stdlib' attribute (values: 'public' or 'private'): + const stdlibAttrMatch = attrs.match( /stdlib=(\w+)/i ); + const stdlibTarget = stdlibAttrMatch ? stdlibAttrMatch[ 1 ].toLowerCase() : 'public'; + if ( stdlibTarget !== 'public' && stdlibTarget !== 'private' ) { + const lines = commentBody.split( '\n' ); + const quote = lines.map( line => `> ${line}` ).join( '\n' ); + await github.rest.issues.createComment({ + 'owner': context.repo.owner, + 'repo': context.repo.repo, + 'issue_number': context.issue.number, + '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).` + }); + core.setFailed( `Unrecognized stdlib attribute value: ${stdlibTarget}` ); + return; + } + + // Parse 'labels' attribute (comma-separated list): + const labelsAttrMatch = attrs.match( /labels="([^"]*)"/i ); + const labels = labelsAttrMatch + ? labelsAttrMatch[ 1 ].split( ',' ).map( l => l.trim() ).filter( Boolean ) + : []; + + // Parse the issue title from the '[TODO]:' line: + const contentLines = content.split( '\n' ); + const RE_TITLE = /^\[TODO\]:\s*(.+)/i; + const titleMatch = contentLines[ 0 ].match( RE_TITLE ); + if ( !titleMatch ) { + const lines = commentBody.split( '\n' ); + const quote = lines.map( line => `> ${line}` ).join( '\n' ); + await github.rest.issues.createComment({ + 'owner': context.repo.owner, + 'repo': context.repo.repo, + 'issue_number': context.issue.number, + '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\`.` + }); + core.setFailed( 'No [TODO]: line found in the code block.' ); + return; + } + + const title = titleMatch[ 1 ].trim(); + const body = contentLines.slice( 1 ).join( '\n' ).trim(); + + // Determine the target repository: + const targetOwner = 'stdlib-js'; + const targetRepo = ( stdlibTarget === 'private' ) ? 'todo' : 'stdlib'; + + // For private repo targets, verify the commenter is an org member: + if ( stdlibTarget === 'private' ) { + const commenter = context.payload.comment.user.login; + try { + await github.rest.orgs.checkMembershipForUser({ + 'org': targetOwner, + 'username': commenter + }); + } catch ( err ) { + console.log( 'Error checking org membership: %s', err.message ); + const lines = commentBody.split( '\n' ); + const quote = lines.map( line => `> ${line}` ).join( '\n' ); + await github.rest.issues.createComment({ + 'owner': context.repo.owner, + 'repo': context.repo.repo, + 'issue_number': context.issue.number, + 'body': `${quote}\n\n@${commenter}, you must be a member of the \`${targetOwner}\` organization to create issues on the private todo repository.` + }); + core.setFailed( `User ${commenter} is not a member of ${targetOwner}.` ); + return; + } + } + + // Build issue creation parameters: + const issueParams = { + 'owner': targetOwner, + 'repo': targetRepo, + 'title': title + }; + // Build provenance footer: + const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${context.issue.number}`; + 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}.*`; + + if ( body ) { + issueParams.body = body + provenance; + } else { + issueParams.body = provenance.trim(); + } + if ( labels.length > 0 ) { + issueParams.labels = labels; + } + + // Create the issue: + const issue = await github.rest.issues.create( issueParams ); + + // Check for labels that were silently dropped (do not exist in the target repo): + const appliedLabels = issue.data.labels.map( l => l.name ); + const droppedLabels = labels.filter( l => !appliedLabels.includes( l ) ); + + // Post a confirmation comment: + const confirmBody = droppedLabels.length > 0 + ? `@${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( ', ' )}.` + : `@${context.payload.comment.user.login}, the following todo issue has been created: ${issue.data.html_url}`; + await github.rest.issues.createComment({ + 'owner': context.repo.owner, + 'repo': context.repo.repo, + 'issue_number': context.issue.number, + 'body': confirmBody + }); + # Define a job for printing a list of available slash commands: help: @@ -241,6 +393,7 @@ jobs: - `/stdlib lint-autofix` - Auto-fix lint errors. - `/stdlib merge` - Merge changes from develop branch into this PR. - `/stdlib rebase` - Rebase this PR on top of develop branch. + - `/stdlib todo` - Create a new todo issue. # GitHub token: token: ${{ secrets.STDLIB_BOT_PAT_REPO_WRITE }} @@ -255,7 +408,7 @@ jobs: runs-on: ubuntu-latest # Ensure all previous jobs have completed before running this job: - needs: [ add_initial_reaction, check_files, make-commands, update_copyright_years, fix_lint_errors, merge_develop, rebase_develop, help ] + needs: [ add_initial_reaction, check_files, make-commands, update_copyright_years, fix_lint_errors, merge_develop, rebase_develop, help, todo ] # Define the conditions under which the job should run: if: | From 2cce34684608c8f12e47c707a93a64ac8f4cb35d Mon Sep 17 00:00:00 2001 From: Karan Anand Date: Wed, 13 May 2026 21:01:49 -0700 Subject: [PATCH 2/9] chore: require org membership --- 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 --- --- .github/workflows/slash_commands.yml | 46 +++++++++++++--------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/.github/workflows/slash_commands.yml b/.github/workflows/slash_commands.yml index 27ce14890555..baaa60db26e3 100644 --- a/.github/workflows/slash_commands.yml +++ b/.github/workflows/slash_commands.yml @@ -298,27 +298,25 @@ jobs: const targetOwner = 'stdlib-js'; const targetRepo = ( stdlibTarget === 'private' ) ? 'todo' : 'stdlib'; - // For private repo targets, verify the commenter is an org member: - if ( stdlibTarget === 'private' ) { - const commenter = context.payload.comment.user.login; - try { - await github.rest.orgs.checkMembershipForUser({ - 'org': targetOwner, - 'username': commenter - }); - } catch ( err ) { - console.log( 'Error checking org membership: %s', err.message ); - const lines = commentBody.split( '\n' ); - const quote = lines.map( line => `> ${line}` ).join( '\n' ); - await github.rest.issues.createComment({ - 'owner': context.repo.owner, - 'repo': context.repo.repo, - 'issue_number': context.issue.number, - 'body': `${quote}\n\n@${commenter}, you must be a member of the \`${targetOwner}\` organization to create issues on the private todo repository.` - }); - core.setFailed( `User ${commenter} is not a member of ${targetOwner}.` ); - return; - } + // Verify the commenter is an org member: + const commenter = context.payload.comment.user.login; + try { + await github.rest.orgs.checkMembershipForUser({ + 'org': targetOwner, + 'username': commenter + }); + } catch ( err ) { + console.log( 'Error checking org membership: %s', err.message ); + const lines = commentBody.split( '\n' ); + const quote = lines.map( line => `> ${line}` ).join( '\n' ); + await github.rest.issues.createComment({ + 'owner': context.repo.owner, + 'repo': context.repo.repo, + 'issue_number': context.issue.number, + 'body': `${quote}\n\n@${commenter}, you must be a member of the \`${targetOwner}\` organization to use the \`/stdlib todo\` command.` + }); + core.setFailed( `User ${commenter} is not a member of ${targetOwner}.` ); + return; } // Build issue creation parameters: @@ -329,7 +327,7 @@ jobs: }; // Build provenance footer: const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${context.issue.number}`; - 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}.*`; + const provenance = `\n\n---\n*Created via \`/stdlib todo\` from [${context.repo.owner}/${context.repo.repo}#${context.issue.number}](${prUrl}) by @${commenter}.*`; if ( body ) { issueParams.body = body + provenance; @@ -349,8 +347,8 @@ jobs: // Post a confirmation comment: const confirmBody = droppedLabels.length > 0 - ? `@${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( ', ' )}.` - : `@${context.payload.comment.user.login}, the following todo issue has been created: ${issue.data.html_url}`; + ? `@${commenter}, 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( ', ' )}.` + : `@${commenter}, the following todo issue has been created: ${issue.data.html_url}`; await github.rest.issues.createComment({ 'owner': context.repo.owner, 'repo': context.repo.repo, From 572ac55489fcc88f3f99394257114526440fa539 Mon Sep 17 00:00:00 2001 From: Karan Anand Date: Thu, 14 May 2026 20:33:28 -0700 Subject: [PATCH 3/9] chore: use a reusable workflow --- 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 --- --- .github/workflows/create_todo_issue.yml | 212 ++++++++++++++++++++++++ .github/workflows/slash_commands.yml | 144 +--------------- 2 files changed, 220 insertions(+), 136 deletions(-) create mode 100644 .github/workflows/create_todo_issue.yml diff --git a/.github/workflows/create_todo_issue.yml b/.github/workflows/create_todo_issue.yml new file mode 100644 index 000000000000..a45820d394a5 --- /dev/null +++ b/.github/workflows/create_todo_issue.yml @@ -0,0 +1,212 @@ +#/ +# @license Apache-2.0 +# +# Copyright (c) 2024 The Stdlib Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#/ + +# Workflow name: +name: create_todo_issue + +# Workflow triggers: +on: + # Allow the workflow to be triggered by other workflows: + workflow_call: + # Define the input parameters for the workflow: + inputs: + pull_request_number: + description: 'Pull request number' + required: true + type: number + comment_body: + description: 'Body of the slash command comment' + required: true + type: string + user: + description: 'GitHub login of the commenter' + required: true + type: string + + # Define the secrets accessible by the workflow: + secrets: + STDLIB_BOT_GITHUB_TOKEN: + description: 'stdlib-bot GitHub token with permission to create issues and comments' + required: true + + # Allow the workflow to be manually triggered: + workflow_dispatch: + inputs: + pull_request_number: + description: 'Pull request number' + required: true + type: number + comment_body: + description: 'Body of the slash command comment' + required: true + type: string + user: + description: 'GitHub login of the commenter' + required: true + type: string + +# Workflow jobs: +jobs: + + # Define a job for creating a new todo issue: + create_todo_issue: + + # Define a display name: + name: 'Create a new todo issue' + + # Define the type of virtual host machine: + runs-on: ubuntu-latest + + # Define the job's steps: + steps: + # Create a new todo issue: + - name: 'Create a new todo issue' + # Pin action to full length commit SHA + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + COMMENT_BODY: ${{ inputs.comment_body }} + COMMENTER: ${{ inputs.user }} + PR_NUMBER: ${{ inputs.pull_request_number }} + with: + github-token: ${{ secrets.STDLIB_BOT_GITHUB_TOKEN }} + script: | + const commentBody = process.env.COMMENT_BODY.trim(); + const commenter = process.env.COMMENTER; + const prNumber = parseInt( process.env.PR_NUMBER, 10 ); + + // Parse the fenced code block following '/stdlib todo': + const RE_CODE_BLOCK = /```text\s*\{([^}]*)\}\s*\n([\s\S]*?)```/; + const blockMatch = commentBody.match( RE_CODE_BLOCK ); + if ( !blockMatch ) { + const lines = commentBody.split( '\n' ); + const quote = lines.map( line => `> ${line}` ).join( '\n' ); + await github.rest.issues.createComment({ + 'owner': context.repo.owner, + 'repo': context.repo.repo, + 'issue_number': prNumber, + 'body': `${quote}\n\n@${commenter}, 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\`\`\`` + }); + core.setFailed( 'No code block found in /stdlib todo comment.' ); + return; + } + + const attrs = blockMatch[ 1 ]; + const content = blockMatch[ 2 ].trim(); + + // Parse 'stdlib' attribute (values: 'public' or 'private'): + const stdlibAttrMatch = attrs.match( /stdlib=(\w+)/i ); + const stdlibTarget = stdlibAttrMatch ? stdlibAttrMatch[ 1 ].toLowerCase() : 'public'; + if ( stdlibTarget !== 'public' && stdlibTarget !== 'private' ) { + const lines = commentBody.split( '\n' ); + const quote = lines.map( line => `> ${line}` ).join( '\n' ); + await github.rest.issues.createComment({ + 'owner': context.repo.owner, + 'repo': context.repo.repo, + 'issue_number': prNumber, + 'body': `${quote}\n\n@${commenter}, unrecognized \`stdlib\` attribute value \`${stdlibTarget}\`. Valid values are \`public\` (opens issue on \`stdlib-js/stdlib\`) or \`private\` (opens issue on the internal todo repository).` + }); + core.setFailed( `Unrecognized stdlib attribute value: ${stdlibTarget}` ); + return; + } + + // Parse 'labels' attribute (comma-separated list): + const labelsAttrMatch = attrs.match( /labels="([^"]*)"/i ); + const labels = labelsAttrMatch + ? labelsAttrMatch[ 1 ].split( ',' ).map( l => l.trim() ).filter( Boolean ) + : []; + + // Parse the issue title from the '[TODO]:' line: + const contentLines = content.split( '\n' ); + const RE_TITLE = /^\[TODO\]:\s*(.+)/i; + const titleMatch = contentLines[ 0 ].match( RE_TITLE ); + if ( !titleMatch ) { + const lines = commentBody.split( '\n' ); + const quote = lines.map( line => `> ${line}` ).join( '\n' ); + await github.rest.issues.createComment({ + 'owner': context.repo.owner, + 'repo': context.repo.repo, + 'issue_number': prNumber, + 'body': `${quote}\n\n@${commenter}, failed to parse the todo title. The first line of the code block must be of the form \`[TODO]: Issue title\`.` + }); + core.setFailed( 'No [TODO]: line found in the code block.' ); + return; + } + + const title = titleMatch[ 1 ].trim(); + const body = contentLines.slice( 1 ).join( '\n' ).trim(); + + // Determine the target repository: + const targetOwner = 'stdlib-js'; + const targetRepo = ( stdlibTarget === 'private' ) ? 'todo' : 'stdlib'; + + // Verify the commenter is an org member: + try { + await github.rest.orgs.checkMembershipForUser({ + 'org': targetOwner, + 'username': commenter + }); + } catch ( err ) { + console.log( 'Error checking org membership: %s', err.message ); + const lines = commentBody.split( '\n' ); + const quote = lines.map( line => `> ${line}` ).join( '\n' ); + await github.rest.issues.createComment({ + 'owner': context.repo.owner, + 'repo': context.repo.repo, + 'issue_number': prNumber, + 'body': `${quote}\n\n@${commenter}, you must be a member of the \`${targetOwner}\` organization to use the \`/stdlib todo\` command.` + }); + core.setFailed( `User ${commenter} is not a member of ${targetOwner}.` ); + return; + } + + // Build issue creation parameters: + const issueParams = { + 'owner': targetOwner, + 'repo': targetRepo, + 'title': title + }; + // Build provenance footer: + const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}`; + const provenance = `\n\n---\n*Created via \`/stdlib todo\` from [${context.repo.owner}/${context.repo.repo}#${prNumber}](${prUrl}) by @${commenter}.*`; + + if ( body ) { + issueParams.body = body + provenance; + } else { + issueParams.body = provenance.trim(); + } + if ( labels.length > 0 ) { + issueParams.labels = labels; + } + + // Create the issue: + const issue = await github.rest.issues.create( issueParams ); + + // Check for labels that were silently dropped (do not exist in the target repo): + const appliedLabels = issue.data.labels.map( l => l.name ); + const droppedLabels = labels.filter( l => !appliedLabels.includes( l ) ); + + // Post a confirmation comment: + const confirmBody = droppedLabels.length > 0 + ? `@${commenter}, 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( ', ' )}.` + : `@${commenter}, the following todo issue has been created: ${issue.data.html_url}`; + await github.rest.issues.createComment({ + 'owner': context.repo.owner, + 'repo': context.repo.repo, + 'issue_number': prNumber, + 'body': confirmBody + }); diff --git a/.github/workflows/slash_commands.yml b/.github/workflows/slash_commands.yml index baaa60db26e3..082fdd17f1d8 100644 --- a/.github/workflows/slash_commands.yml +++ b/.github/workflows/slash_commands.yml @@ -213,148 +213,20 @@ jobs: # Define a display name: name: 'Create a new todo issue' - # Define the type of virtual host machine: - runs-on: ubuntu-latest - # Ensure initial reaction job has completed before running this job: needs: [ add_initial_reaction ] # Define the conditions under which the job should run: if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/stdlib todo') - # Define the job's steps: - steps: - # Create a new todo issue: - - name: 'Create a new todo issue' - # Pin action to full length commit SHA - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{ secrets.STDLIB_BOT_PAT_REPO_WRITE }} - script: | - const commentBody = context.payload.comment.body.trim(); - - // Parse the fenced code block following '/stdlib todo': - const RE_CODE_BLOCK = /```text\s*\{([^}]*)\}\s*\n([\s\S]*?)```/; - const blockMatch = commentBody.match( RE_CODE_BLOCK ); - if ( !blockMatch ) { - const lines = commentBody.split( '\n' ); - const quote = lines.map( line => `> ${line}` ).join( '\n' ); - await github.rest.issues.createComment({ - 'owner': context.repo.owner, - 'repo': context.repo.repo, - 'issue_number': context.issue.number, - '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\`\`\`` - }); - core.setFailed( 'No code block found in /stdlib todo comment.' ); - return; - } - - const attrs = blockMatch[ 1 ]; - const content = blockMatch[ 2 ].trim(); - - // Parse 'stdlib' attribute (values: 'public' or 'private'): - const stdlibAttrMatch = attrs.match( /stdlib=(\w+)/i ); - const stdlibTarget = stdlibAttrMatch ? stdlibAttrMatch[ 1 ].toLowerCase() : 'public'; - if ( stdlibTarget !== 'public' && stdlibTarget !== 'private' ) { - const lines = commentBody.split( '\n' ); - const quote = lines.map( line => `> ${line}` ).join( '\n' ); - await github.rest.issues.createComment({ - 'owner': context.repo.owner, - 'repo': context.repo.repo, - 'issue_number': context.issue.number, - '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).` - }); - core.setFailed( `Unrecognized stdlib attribute value: ${stdlibTarget}` ); - return; - } - - // Parse 'labels' attribute (comma-separated list): - const labelsAttrMatch = attrs.match( /labels="([^"]*)"/i ); - const labels = labelsAttrMatch - ? labelsAttrMatch[ 1 ].split( ',' ).map( l => l.trim() ).filter( Boolean ) - : []; - - // Parse the issue title from the '[TODO]:' line: - const contentLines = content.split( '\n' ); - const RE_TITLE = /^\[TODO\]:\s*(.+)/i; - const titleMatch = contentLines[ 0 ].match( RE_TITLE ); - if ( !titleMatch ) { - const lines = commentBody.split( '\n' ); - const quote = lines.map( line => `> ${line}` ).join( '\n' ); - await github.rest.issues.createComment({ - 'owner': context.repo.owner, - 'repo': context.repo.repo, - 'issue_number': context.issue.number, - '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\`.` - }); - core.setFailed( 'No [TODO]: line found in the code block.' ); - return; - } - - const title = titleMatch[ 1 ].trim(); - const body = contentLines.slice( 1 ).join( '\n' ).trim(); - - // Determine the target repository: - const targetOwner = 'stdlib-js'; - const targetRepo = ( stdlibTarget === 'private' ) ? 'todo' : 'stdlib'; - - // Verify the commenter is an org member: - const commenter = context.payload.comment.user.login; - try { - await github.rest.orgs.checkMembershipForUser({ - 'org': targetOwner, - 'username': commenter - }); - } catch ( err ) { - console.log( 'Error checking org membership: %s', err.message ); - const lines = commentBody.split( '\n' ); - const quote = lines.map( line => `> ${line}` ).join( '\n' ); - await github.rest.issues.createComment({ - 'owner': context.repo.owner, - 'repo': context.repo.repo, - 'issue_number': context.issue.number, - 'body': `${quote}\n\n@${commenter}, you must be a member of the \`${targetOwner}\` organization to use the \`/stdlib todo\` command.` - }); - core.setFailed( `User ${commenter} is not a member of ${targetOwner}.` ); - return; - } - - // Build issue creation parameters: - const issueParams = { - 'owner': targetOwner, - 'repo': targetRepo, - 'title': title - }; - // Build provenance footer: - const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${context.issue.number}`; - const provenance = `\n\n---\n*Created via \`/stdlib todo\` from [${context.repo.owner}/${context.repo.repo}#${context.issue.number}](${prUrl}) by @${commenter}.*`; - - if ( body ) { - issueParams.body = body + provenance; - } else { - issueParams.body = provenance.trim(); - } - if ( labels.length > 0 ) { - issueParams.labels = labels; - } - - // Create the issue: - const issue = await github.rest.issues.create( issueParams ); - - // Check for labels that were silently dropped (do not exist in the target repo): - const appliedLabels = issue.data.labels.map( l => l.name ); - const droppedLabels = labels.filter( l => !appliedLabels.includes( l ) ); - - // Post a confirmation comment: - const confirmBody = droppedLabels.length > 0 - ? `@${commenter}, 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( ', ' )}.` - : `@${commenter}, the following todo issue has been created: ${issue.data.html_url}`; - await github.rest.issues.createComment({ - 'owner': context.repo.owner, - 'repo': context.repo.repo, - 'issue_number': context.issue.number, - 'body': confirmBody - }); + # Run reusable workflow: + uses: ./.github/workflows/create_todo_issue.yml + with: + pull_request_number: ${{ github.event.issue.number }} + comment_body: ${{ github.event.comment.body }} + user: ${{ github.event.comment.user.login }} + secrets: + STDLIB_BOT_GITHUB_TOKEN: ${{ secrets.STDLIB_BOT_PAT_REPO_WRITE }} # Define a job for printing a list of available slash commands: help: From 92f029425518e6355591fd5232d27588bfd4ff35 Mon Sep 17 00:00:00 2001 From: Karan Anand Date: Thu, 14 May 2026 20:46:19 -0700 Subject: [PATCH 4/9] chore: move logic to a script --- 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: passed - task: lint_typescript_declarations status: passed - task: lint_typescript_tests status: na - task: lint_license_headers status: passed --- --- .github/workflows/create_todo_issue.yml | 145 +------- .../workflows/scripts/create_todo_issue/run | 312 ++++++++++++++++++ tsconfig.json | 47 --- 3 files changed, 327 insertions(+), 177 deletions(-) create mode 100755 .github/workflows/scripts/create_todo_issue/run delete mode 100644 tsconfig.json diff --git a/.github/workflows/create_todo_issue.yml b/.github/workflows/create_todo_issue.yml index a45820d394a5..eca7dbca1dbd 100644 --- a/.github/workflows/create_todo_issue.yml +++ b/.github/workflows/create_todo_issue.yml @@ -74,139 +74,24 @@ jobs: # Define the job's steps: steps: + # Checkout the repository: + - name: 'Checkout repository' + # Pin action to full length commit SHA + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # Ensure we have access to the scripts directory: + sparse-checkout: | + .github/workflows/scripts + sparse-checkout-cone-mode: false + timeout-minutes: 10 + # Create a new todo issue: - name: 'Create a new todo issue' - # Pin action to full length commit SHA - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: + GITHUB_TOKEN: ${{ secrets.STDLIB_BOT_GITHUB_TOKEN || secrets.STDLIB_BOT_PAT_REPO_WRITE }} COMMENT_BODY: ${{ inputs.comment_body }} COMMENTER: ${{ inputs.user }} PR_NUMBER: ${{ inputs.pull_request_number }} - with: - github-token: ${{ secrets.STDLIB_BOT_GITHUB_TOKEN }} - script: | - const commentBody = process.env.COMMENT_BODY.trim(); - const commenter = process.env.COMMENTER; - const prNumber = parseInt( process.env.PR_NUMBER, 10 ); - - // Parse the fenced code block following '/stdlib todo': - const RE_CODE_BLOCK = /```text\s*\{([^}]*)\}\s*\n([\s\S]*?)```/; - const blockMatch = commentBody.match( RE_CODE_BLOCK ); - if ( !blockMatch ) { - const lines = commentBody.split( '\n' ); - const quote = lines.map( line => `> ${line}` ).join( '\n' ); - await github.rest.issues.createComment({ - 'owner': context.repo.owner, - 'repo': context.repo.repo, - 'issue_number': prNumber, - 'body': `${quote}\n\n@${commenter}, 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\`\`\`` - }); - core.setFailed( 'No code block found in /stdlib todo comment.' ); - return; - } - - const attrs = blockMatch[ 1 ]; - const content = blockMatch[ 2 ].trim(); - - // Parse 'stdlib' attribute (values: 'public' or 'private'): - const stdlibAttrMatch = attrs.match( /stdlib=(\w+)/i ); - const stdlibTarget = stdlibAttrMatch ? stdlibAttrMatch[ 1 ].toLowerCase() : 'public'; - if ( stdlibTarget !== 'public' && stdlibTarget !== 'private' ) { - const lines = commentBody.split( '\n' ); - const quote = lines.map( line => `> ${line}` ).join( '\n' ); - await github.rest.issues.createComment({ - 'owner': context.repo.owner, - 'repo': context.repo.repo, - 'issue_number': prNumber, - 'body': `${quote}\n\n@${commenter}, unrecognized \`stdlib\` attribute value \`${stdlibTarget}\`. Valid values are \`public\` (opens issue on \`stdlib-js/stdlib\`) or \`private\` (opens issue on the internal todo repository).` - }); - core.setFailed( `Unrecognized stdlib attribute value: ${stdlibTarget}` ); - return; - } - - // Parse 'labels' attribute (comma-separated list): - const labelsAttrMatch = attrs.match( /labels="([^"]*)"/i ); - const labels = labelsAttrMatch - ? labelsAttrMatch[ 1 ].split( ',' ).map( l => l.trim() ).filter( Boolean ) - : []; - - // Parse the issue title from the '[TODO]:' line: - const contentLines = content.split( '\n' ); - const RE_TITLE = /^\[TODO\]:\s*(.+)/i; - const titleMatch = contentLines[ 0 ].match( RE_TITLE ); - if ( !titleMatch ) { - const lines = commentBody.split( '\n' ); - const quote = lines.map( line => `> ${line}` ).join( '\n' ); - await github.rest.issues.createComment({ - 'owner': context.repo.owner, - 'repo': context.repo.repo, - 'issue_number': prNumber, - 'body': `${quote}\n\n@${commenter}, failed to parse the todo title. The first line of the code block must be of the form \`[TODO]: Issue title\`.` - }); - core.setFailed( 'No [TODO]: line found in the code block.' ); - return; - } - - const title = titleMatch[ 1 ].trim(); - const body = contentLines.slice( 1 ).join( '\n' ).trim(); - - // Determine the target repository: - const targetOwner = 'stdlib-js'; - const targetRepo = ( stdlibTarget === 'private' ) ? 'todo' : 'stdlib'; - - // Verify the commenter is an org member: - try { - await github.rest.orgs.checkMembershipForUser({ - 'org': targetOwner, - 'username': commenter - }); - } catch ( err ) { - console.log( 'Error checking org membership: %s', err.message ); - const lines = commentBody.split( '\n' ); - const quote = lines.map( line => `> ${line}` ).join( '\n' ); - await github.rest.issues.createComment({ - 'owner': context.repo.owner, - 'repo': context.repo.repo, - 'issue_number': prNumber, - 'body': `${quote}\n\n@${commenter}, you must be a member of the \`${targetOwner}\` organization to use the \`/stdlib todo\` command.` - }); - core.setFailed( `User ${commenter} is not a member of ${targetOwner}.` ); - return; - } - - // Build issue creation parameters: - const issueParams = { - 'owner': targetOwner, - 'repo': targetRepo, - 'title': title - }; - // Build provenance footer: - const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}`; - const provenance = `\n\n---\n*Created via \`/stdlib todo\` from [${context.repo.owner}/${context.repo.repo}#${prNumber}](${prUrl}) by @${commenter}.*`; - - if ( body ) { - issueParams.body = body + provenance; - } else { - issueParams.body = provenance.trim(); - } - if ( labels.length > 0 ) { - issueParams.labels = labels; - } - - // Create the issue: - const issue = await github.rest.issues.create( issueParams ); - - // Check for labels that were silently dropped (do not exist in the target repo): - const appliedLabels = issue.data.labels.map( l => l.name ); - const droppedLabels = labels.filter( l => !appliedLabels.includes( l ) ); - - // Post a confirmation comment: - const confirmBody = droppedLabels.length > 0 - ? `@${commenter}, 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( ', ' )}.` - : `@${commenter}, the following todo issue has been created: ${issue.data.html_url}`; - await github.rest.issues.createComment({ - 'owner': context.repo.owner, - 'repo': context.repo.repo, - 'issue_number': prNumber, - 'body': confirmBody - }); + run: | + . "$GITHUB_WORKSPACE/.github/workflows/scripts/create_todo_issue/run" + timeout-minutes: 10 diff --git a/.github/workflows/scripts/create_todo_issue/run b/.github/workflows/scripts/create_todo_issue/run new file mode 100755 index 000000000000..edb0fcb9fbaa --- /dev/null +++ b/.github/workflows/scripts/create_todo_issue/run @@ -0,0 +1,312 @@ +#!/usr/bin/env bash +#/ +# @license Apache-2.0 +# +# Copyright (c) 2024 The Stdlib Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#/ + +# Script to create a new todo issue from a /stdlib todo slash command. +# +# Environment variables: +# +# GITHUB_TOKEN GitHub token with permission to create issues and comments. +# COMMENT_BODY Body of the /stdlib todo slash command comment. +# COMMENTER GitHub login of the commenter. +# PR_NUMBER Pull request number. + +# Ensure that the exit status of pipelines is non-zero in the event that at least one of the commands in a pipeline fails: +set -o pipefail + + +# VARIABLES # + +# GitHub API base URL: +github_api_url="https://api.github.com" + +# Repository owner and name: +repo_owner="stdlib-js" +repo_name="stdlib" + +# Convenience variable for a backtick character (avoids quoting issues in strings): +bt='`' + + +# FUNCTIONS # + +# Error handler. +# +# $1 - error status +on_error() { + echo 'ERROR: An error was encountered during execution.' >&2 + exit "$1" +} + +# Prints a success message. +print_success() { + echo 'Success!' >&2 +} + +# Posts a comment on the pull request. +# +# $1 - comment body (plain text; will be JSON-encoded) +post_pr_comment() { + local body="$1" + local payload + payload="{\"body\":$(printf '%s' "${body}" | jq -R -s -c .)}" + curl -s -X POST \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Content-Type: application/json" \ + -d "${payload}" \ + "${github_api_url}/repos/${repo_owner}/${repo_name}/issues/${PR_NUMBER}/comments" \ + > /dev/null +} + + +# MAIN # + +main() { + local tmp_dir + local tmp_comment + local tmp_attrs + local tmp_content + local quote + local attrs + local stdlib_target + local labels_raw + local labels_json + local content + local first_line + local title + local body_raw + local body + local target_owner + local target_repo + local membership_status + local pr_url + local provenance + local issue_body + local issue_payload + local issue_response + local issue_url + local requested_label + local dropped_labels + local dropped_list + local label + + # Validate required inputs: + if [ -z "${PR_NUMBER}" ]; then + echo "ERROR: PR_NUMBER is required." >&2 + on_error 1 + fi + if [ -z "${COMMENTER}" ]; then + echo "ERROR: COMMENTER is required." >&2 + on_error 1 + fi + if [ -z "${COMMENT_BODY}" ]; then + echo "ERROR: COMMENT_BODY is required." >&2 + on_error 1 + fi + + # Create a temporary working directory: + tmp_dir=$(mktemp -d) + tmp_comment="${tmp_dir}/comment.txt" + tmp_attrs="${tmp_dir}/attrs.txt" + tmp_content="${tmp_dir}/content.txt" + + # Write the comment body to a file to safely handle multi-line content: + printf '%s' "${COMMENT_BODY}" > "${tmp_comment}" + + # Build a Markdown quote of the comment body for use in error replies: + quote=$(sed 's/^/> /' "${tmp_comment}") + + # Use awk to extract the fenced code block and its attributes. + # Expected format: ```text {attrs}\n```: + awk -v attrs_file="${tmp_attrs}" -v content_file="${tmp_content}" ' + BEGIN { in_block = 0; first_line = 1 } + !in_block && /^```text[[:space:]]*\{/ { + match($0, /\{[^}]*\}/) + print substr($0, RSTART + 1, RLENGTH - 2) > attrs_file + in_block = 1 + first_line = 1 + next + } + in_block && /^```[[:space:]]*$/ { + in_block = 0 + exit + } + in_block { + if (first_line) { + printf "%s", $0 > content_file + first_line = 0 + } else { + printf "\n%s", $0 >> content_file + } + } + ' "${tmp_comment}" + + # Verify that a code block was found: + if [ ! -f "${tmp_attrs}" ]; then + post_pr_comment "${quote} + +@${COMMENTER}, failed to parse the ${bt}/stdlib todo${bt} command. Expected a fenced code block with attributes, e.g., + +${bt}${bt}${bt} +/stdlib todo + +${bt}${bt}${bt}text {stdlib=public labels=\"foo,bar\"} +[TODO]: Issue title + +Issue body +${bt}${bt}${bt} +${bt}${bt}${bt}" + rm -rf "${tmp_dir}" + on_error 1 + fi + + attrs=$(cat "${tmp_attrs}") + content=$(cat "${tmp_content}" 2>/dev/null || printf '') + + # Parse the 'stdlib' attribute (values: 'public' or 'private'): + stdlib_target=$(printf '%s' "${attrs}" | grep -oiP '(?<=stdlib=)\w+' | tr '[:upper:]' '[:lower:]' || true) + if [ -z "${stdlib_target}" ]; then + stdlib_target="public" + fi + + if [ "${stdlib_target}" != "public" ] && [ "${stdlib_target}" != "private" ]; then + post_pr_comment "${quote} + +@${COMMENTER}, unrecognized ${bt}stdlib${bt} attribute value ${bt}${stdlib_target}${bt}. Valid values are ${bt}public${bt} (opens issue on ${bt}stdlib-js/stdlib${bt}) or ${bt}private${bt} (opens issue on the internal todo repository)." + rm -rf "${tmp_dir}" + on_error 1 + fi + + # Parse the 'labels' attribute (comma-separated list): + labels_raw=$(printf '%s' "${attrs}" | grep -oP '(?<=labels=")[^"]*' || true) + if [ -n "${labels_raw}" ]; then + labels_json=$(printf '%s' "${labels_raw}" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v '^$' | jq -R . | jq -s .) + else + labels_json='[]' + fi + + # Parse the issue title from the '[TODO]: ...' line: + first_line=$(printf '%s' "${content}" | head -n 1) + title=$(printf '%s' "${first_line}" | grep -ioP '(?<=^\[TODO\]:[[:space:]])\S.*' || true) + if [ -z "${title}" ]; then + post_pr_comment "${quote} + +@${COMMENTER}, failed to parse the todo title. The first line of the code block must be of the form ${bt}[TODO]: Issue title${bt}." + rm -rf "${tmp_dir}" + on_error 1 + fi + + # Extract the issue body (lines after the title) and trim leading/trailing blank lines: + body_raw=$(printf '%s' "${content}" | tail -n +2) + body=$(printf '%s' "${body_raw}" | sed '/./,$!d' | tac | sed '/./,$!d' | tac) + + # Determine the target repository: + target_owner="stdlib-js" + if [ "${stdlib_target}" = "private" ]; then + target_repo="todo" + else + target_repo="stdlib" + fi + + # Verify the commenter is a member of the target organization: + membership_status=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "Accept: application/vnd.github.v3+json" \ + "${github_api_url}/orgs/${target_owner}/members/${COMMENTER}") + + if [ "${membership_status}" != "204" ]; then + echo "Error checking org membership: HTTP ${membership_status}" >&2 + post_pr_comment "${quote} + +@${COMMENTER}, you must be a member of the ${bt}${target_owner}${bt} organization to use the ${bt}/stdlib todo${bt} command." + rm -rf "${tmp_dir}" + on_error 1 + fi + + # Build provenance footer: + pr_url="https://github.com/${repo_owner}/${repo_name}/pull/${PR_NUMBER}" + provenance="--- +*Created via ${bt}/stdlib todo${bt} from [${repo_owner}/${repo_name}#${PR_NUMBER}](${pr_url}) by @${COMMENTER}.*" + + # Build the issue body with provenance footer: + if [ -n "${body}" ]; then + issue_body="${body} + +${provenance}" + else + issue_body="${provenance}" + fi + + # Build issue creation JSON payload: + issue_payload=$(jq -n \ + --arg title "${title}" \ + --arg body "${issue_body}" \ + --argjson labels "${labels_json}" \ + '{"title": $title, "body": $body, "labels": $labels}') + + # Create the issue: + issue_response=$(curl -s \ + -X POST \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Content-Type: application/json" \ + -d "${issue_payload}" \ + "${github_api_url}/repos/${target_owner}/${target_repo}/issues") + + issue_url=$(printf '%s' "${issue_response}" | jq -r '.html_url // empty') + if [ -z "${issue_url}" ]; then + echo "ERROR: Failed to create issue. Response: ${issue_response}" >&2 + rm -rf "${tmp_dir}" + on_error 1 + fi + + # Identify labels that were silently dropped (labels that do not exist in the target repo): + dropped_labels=() + while IFS= read -r requested_label; do + if [ -n "${requested_label}" ]; then + if ! printf '%s' "${issue_response}" | jq -e --arg l "${requested_label}" '[.labels[].name] | index($l) != null' > /dev/null 2>&1; then + dropped_labels+=("${requested_label}") + fi + fi + done < <(printf '%s' "${labels_json}" | jq -r '.[]') + + # Post a confirmation comment: + if [ ${#dropped_labels[@]} -gt 0 ]; then + dropped_list="" + for label in "${dropped_labels[@]}"; do + if [ -n "${dropped_list}" ]; then + dropped_list="${dropped_list}, ${bt}${label}${bt}" + else + dropped_list="${bt}${label}${bt}" + fi + done + post_pr_comment "@${COMMENTER}, the following todo issue has been created: ${issue_url} + +> [!WARNING] +> The following labels were not applied because they do not exist on ${bt}${target_owner}/${target_repo}${bt}: ${dropped_list}." + else + post_pr_comment "@${COMMENTER}, the following todo issue has been created: ${issue_url}" + fi + + rm -rf "${tmp_dir}" + print_success + exit 0 +} + +main diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 39d2d41cb5b1..000000000000 --- a/tsconfig.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "allowSyntheticDefaultImports": false, - "allowUnreachableCode": false, - "allowUnusedLabels": false, - "alwaysStrict": true, - "baseUrl": "lib/node_modules", - "checkJs": true, - "forceConsistentCasingInFileNames": true, - "keyofStringsOnly": false, - "lib": [ - "es6" - ], - "module": "commonjs", - "moduleResolution": "node", - "newLine": "lf", - "noEmit": true, - "noFallthroughCasesInSwitch": true, - "noImplicitAny": true, - "noImplicitReturns": false, - "noImplicitThis": true, - "noStrictGenericChecks": false, - "noUnusedLocals": true, - "noUnusedParameters": true, - "paths": {}, - "pretty": true, - "strictBindCallApply": true, - "strictFunctionTypes": true, - "strictNullChecks": true, - "suppressExcessPropertyErrors": false, - "suppressImplicitAnyIndexErrors": false, - "typeRoots": [ "." ], - "types": [] - }, - "include": [ - "docs/types/index.d.ts", - "docs/types/test.ts", - "lib/node_modules/**/docs/types/index.d.ts", - "lib/node_modules/**/docs/types/test.ts", - "lib/node_modules/**/@stdlib/types/index.d.ts", - "lib/node_modules/**/@stdlib/types/test.ts" - ], - "exclude": [ - "node_modules" - ] -} From cdbb42c0803fd8442722f964030c4ff54e1e1cc1 Mon Sep 17 00:00:00 2001 From: Karan Anand Date: Thu, 14 May 2026 20:56:42 -0700 Subject: [PATCH 5/9] chore: restore accidentally deleted tsconfig.json --- 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 --- --- tsconfig.json | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tsconfig.json diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000000..39d2d41cb5b1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,47 @@ +{ + "compilerOptions": { + "allowJs": true, + "allowSyntheticDefaultImports": false, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "alwaysStrict": true, + "baseUrl": "lib/node_modules", + "checkJs": true, + "forceConsistentCasingInFileNames": true, + "keyofStringsOnly": false, + "lib": [ + "es6" + ], + "module": "commonjs", + "moduleResolution": "node", + "newLine": "lf", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": false, + "noImplicitThis": true, + "noStrictGenericChecks": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "paths": {}, + "pretty": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "suppressExcessPropertyErrors": false, + "suppressImplicitAnyIndexErrors": false, + "typeRoots": [ "." ], + "types": [] + }, + "include": [ + "docs/types/index.d.ts", + "docs/types/test.ts", + "lib/node_modules/**/docs/types/index.d.ts", + "lib/node_modules/**/docs/types/test.ts", + "lib/node_modules/**/@stdlib/types/index.d.ts", + "lib/node_modules/**/@stdlib/types/test.ts" + ], + "exclude": [ + "node_modules" + ] +} From bc4dd125dc195feb58d9ca78452eba93ee69b51c Mon Sep 17 00:00:00 2001 From: Karan Anand Date: Thu, 14 May 2026 21:06:08 -0700 Subject: [PATCH 6/9] feat: add support for `/stdlib todo` slash command on commits --- 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: passed - task: lint_typescript_declarations status: passed - task: lint_typescript_tests status: na - task: lint_license_headers status: passed --- --- .github/workflows/create_todo_issue.yml | 21 +++++-- .../workflows/scripts/create_todo_issue/run | 49 +++++++++------ .github/workflows/slash_commands.yml | 62 ++++++++++++++----- 3 files changed, 91 insertions(+), 41 deletions(-) diff --git a/.github/workflows/create_todo_issue.yml b/.github/workflows/create_todo_issue.yml index eca7dbca1dbd..35f9976f1526 100644 --- a/.github/workflows/create_todo_issue.yml +++ b/.github/workflows/create_todo_issue.yml @@ -26,9 +26,13 @@ on: # Define the input parameters for the workflow: inputs: pull_request_number: - description: 'Pull request number' - required: true - type: number + description: 'Pull request number (required when triggered from a PR comment; mutually exclusive with commit_sha)' + required: false + type: string + commit_sha: + description: 'Commit SHA (required when triggered from a commit comment; mutually exclusive with pull_request_number)' + required: false + type: string comment_body: description: 'Body of the slash command comment' required: true @@ -48,9 +52,13 @@ on: workflow_dispatch: inputs: pull_request_number: - description: 'Pull request number' - required: true - type: number + description: 'Pull request number (required when triggered from a PR comment; mutually exclusive with commit_sha)' + required: false + type: string + commit_sha: + description: 'Commit SHA (required when triggered from a commit comment; mutually exclusive with pull_request_number)' + required: false + type: string comment_body: description: 'Body of the slash command comment' required: true @@ -92,6 +100,7 @@ jobs: COMMENT_BODY: ${{ inputs.comment_body }} COMMENTER: ${{ inputs.user }} PR_NUMBER: ${{ inputs.pull_request_number }} + COMMIT_SHA: ${{ inputs.commit_sha }} run: | . "$GITHUB_WORKSPACE/.github/workflows/scripts/create_todo_issue/run" timeout-minutes: 10 diff --git a/.github/workflows/scripts/create_todo_issue/run b/.github/workflows/scripts/create_todo_issue/run index edb0fcb9fbaa..09be33af3cf8 100755 --- a/.github/workflows/scripts/create_todo_issue/run +++ b/.github/workflows/scripts/create_todo_issue/run @@ -1,5 +1,5 @@ #!/usr/bin/env bash -#/ +# # @license Apache-2.0 # # Copyright (c) 2024 The Stdlib Authors. @@ -15,7 +15,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -#/ # Script to create a new todo issue from a /stdlib todo slash command. # @@ -24,7 +23,8 @@ # GITHUB_TOKEN GitHub token with permission to create issues and comments. # COMMENT_BODY Body of the /stdlib todo slash command comment. # COMMENTER GitHub login of the commenter. -# PR_NUMBER Pull request number. +# PR_NUMBER Pull request number (mutually exclusive with COMMIT_SHA). +# COMMIT_SHA Commit SHA (mutually exclusive with PR_NUMBER). # Ensure that the exit status of pipelines is non-zero in the event that at least one of the commands in a pipeline fails: set -o pipefail @@ -58,19 +58,25 @@ print_success() { echo 'Success!' >&2 } -# Posts a comment on the pull request. +# Posts a comment on the source context (pull request or commit). # # $1 - comment body (plain text; will be JSON-encoded) -post_pr_comment() { +post_source_comment() { local body="$1" local payload + local endpoint payload="{\"body\":$(printf '%s' "${body}" | jq -R -s -c .)}" + if [ -n "${PR_NUMBER}" ]; then + endpoint="${github_api_url}/repos/${repo_owner}/${repo_name}/issues/${PR_NUMBER}/comments" + else + endpoint="${github_api_url}/repos/${repo_owner}/${repo_name}/commits/${COMMIT_SHA}/comments" + fi curl -s -X POST \ -H "Authorization: token ${GITHUB_TOKEN}" \ -H "Accept: application/vnd.github.v3+json" \ -H "Content-Type: application/json" \ -d "${payload}" \ - "${github_api_url}/repos/${repo_owner}/${repo_name}/issues/${PR_NUMBER}/comments" \ + "${endpoint}" \ > /dev/null } @@ -95,7 +101,6 @@ main() { local target_owner local target_repo local membership_status - local pr_url local provenance local issue_body local issue_payload @@ -107,8 +112,12 @@ main() { local label # Validate required inputs: - if [ -z "${PR_NUMBER}" ]; then - echo "ERROR: PR_NUMBER is required." >&2 + if [ -z "${PR_NUMBER}" ] && [ -z "${COMMIT_SHA}" ]; then + echo "ERROR: Either PR_NUMBER or COMMIT_SHA is required." >&2 + on_error 1 + fi + if [ -n "${PR_NUMBER}" ] && [ -n "${COMMIT_SHA}" ]; then + echo "ERROR: PR_NUMBER and COMMIT_SHA are mutually exclusive." >&2 on_error 1 fi if [ -z "${COMMENTER}" ]; then @@ -159,7 +168,7 @@ main() { # Verify that a code block was found: if [ ! -f "${tmp_attrs}" ]; then - post_pr_comment "${quote} + post_source_comment "${quote} @${COMMENTER}, failed to parse the ${bt}/stdlib todo${bt} command. Expected a fenced code block with attributes, e.g., @@ -186,7 +195,7 @@ ${bt}${bt}${bt}" fi if [ "${stdlib_target}" != "public" ] && [ "${stdlib_target}" != "private" ]; then - post_pr_comment "${quote} + post_source_comment "${quote} @${COMMENTER}, unrecognized ${bt}stdlib${bt} attribute value ${bt}${stdlib_target}${bt}. Valid values are ${bt}public${bt} (opens issue on ${bt}stdlib-js/stdlib${bt}) or ${bt}private${bt} (opens issue on the internal todo repository)." rm -rf "${tmp_dir}" @@ -205,7 +214,7 @@ ${bt}${bt}${bt}" first_line=$(printf '%s' "${content}" | head -n 1) title=$(printf '%s' "${first_line}" | grep -ioP '(?<=^\[TODO\]:[[:space:]])\S.*' || true) if [ -z "${title}" ]; then - post_pr_comment "${quote} + post_source_comment "${quote} @${COMMENTER}, failed to parse the todo title. The first line of the code block must be of the form ${bt}[TODO]: Issue title${bt}." rm -rf "${tmp_dir}" @@ -232,7 +241,7 @@ ${bt}${bt}${bt}" if [ "${membership_status}" != "204" ]; then echo "Error checking org membership: HTTP ${membership_status}" >&2 - post_pr_comment "${quote} + post_source_comment "${quote} @${COMMENTER}, you must be a member of the ${bt}${target_owner}${bt} organization to use the ${bt}/stdlib todo${bt} command." rm -rf "${tmp_dir}" @@ -240,9 +249,13 @@ ${bt}${bt}${bt}" fi # Build provenance footer: - pr_url="https://github.com/${repo_owner}/${repo_name}/pull/${PR_NUMBER}" - provenance="--- -*Created via ${bt}/stdlib todo${bt} from [${repo_owner}/${repo_name}#${PR_NUMBER}](${pr_url}) by @${COMMENTER}.*" + if [ -n "${PR_NUMBER}" ]; then + provenance="--- +*Created via ${bt}/stdlib todo${bt} from [${repo_owner}/${repo_name}#${PR_NUMBER}](https://github.com/${repo_owner}/${repo_name}/pull/${PR_NUMBER}) by @${COMMENTER}.*" + else + provenance="--- +*Created via ${bt}/stdlib todo${bt} from [${repo_owner}/${repo_name}@${COMMIT_SHA:0:7}](https://github.com/${repo_owner}/${repo_name}/commit/${COMMIT_SHA}) by @${COMMENTER}.*" + fi # Build the issue body with provenance footer: if [ -n "${body}" ]; then @@ -296,12 +309,12 @@ ${provenance}" dropped_list="${bt}${label}${bt}" fi done - post_pr_comment "@${COMMENTER}, the following todo issue has been created: ${issue_url} + post_source_comment "@${COMMENTER}, the following todo issue has been created: ${issue_url} > [!WARNING] > The following labels were not applied because they do not exist on ${bt}${target_owner}/${target_repo}${bt}: ${dropped_list}." else - post_pr_comment "@${COMMENTER}, the following todo issue has been created: ${issue_url}" + post_source_comment "@${COMMENTER}, the following todo issue has been created: ${issue_url}" fi rm -rf "${tmp_dir}" diff --git a/.github/workflows/slash_commands.yml b/.github/workflows/slash_commands.yml index 082fdd17f1d8..8e75b8efea11 100644 --- a/.github/workflows/slash_commands.yml +++ b/.github/workflows/slash_commands.yml @@ -25,6 +25,9 @@ on: types: - created - edited + commit_comment: + types: + - created # Workflow jobs: jobs: @@ -39,12 +42,13 @@ jobs: runs-on: ubuntu-latest # Define the conditions under which the job should run: - if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/stdlib') + if: (github.event.issue.pull_request || github.event_name == 'commit_comment') && startsWith(github.event.comment.body, '/stdlib') # Define the job's steps: steps: - # Add "bot: In progress" label to the issue / PR: + # Add "bot: In progress" label to the issue / PR (only applicable to PR comments): - name: 'Add in-progress label' + if: github.event.issue.pull_request # Pin action to full length commit SHA uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: @@ -63,29 +67,52 @@ jobs: with: github-token: ${{ secrets.STDLIB_BOT_PAT_REPO_WRITE }} script: | + const isCommitComment = context.eventName === 'commit_comment'; const commentBody = context.payload.comment.body.trim(); const RE_COMMANDS = /^\/stdlib\s+(help|check-files|update-copyright-years|lint-autofix|merge|rebase|make-commands)$/i; const RE_TODO_COMMAND = /^\/stdlib\s+todo\b/i; - const isRecognizedCommand = RE_COMMANDS.test( commentBody ) || RE_TODO_COMMAND.test( commentBody ); + + // For commit comments, only the todo command is supported: + const isRecognizedCommand = isCommitComment + ? RE_TODO_COMMAND.test( commentBody ) + : RE_COMMANDS.test( commentBody ) || RE_TODO_COMMAND.test( commentBody ); if ( isRecognizedCommand ) { - await github.rest.reactions.createForIssueComment({ - 'owner': context.repo.owner, - 'repo': context.repo.repo, - 'comment_id': context.payload.comment.id, - 'content': 'eyes' - }); + if ( isCommitComment ) { + await github.rest.reactions.createForCommitComment({ + 'owner': context.repo.owner, + 'repo': context.repo.repo, + 'comment_id': context.payload.comment.id, + 'content': 'eyes' + }); + } else { + await github.rest.reactions.createForIssueComment({ + 'owner': context.repo.owner, + 'repo': context.repo.repo, + 'comment_id': context.payload.comment.id, + 'content': 'eyes' + }); + } } else { // Include the full user comment as a Markdown quote block in response: const lines = commentBody.split( '\n' ); const quote = lines.map( line => `> ${line}` ).join( '\n' ); - await github.rest.issues.createComment({ - 'owner': context.repo.owner, - 'repo': context.repo.repo, - 'issue_number': context.issue.number, - 'body': `${quote}\n\n@${context.payload.comment.user.login}, slash command not recognized. Please use \`/stdlib help\` to view available commands.` - }); + if ( isCommitComment ) { + await github.rest.repos.createCommitComment({ + 'owner': context.repo.owner, + 'repo': context.repo.repo, + 'commit_sha': context.payload.comment.commit_id, + 'body': `${quote}\n\n@${context.payload.comment.user.login}, slash command not recognized. Please use \`/stdlib help\` to view available commands.` + }); + } else { + await github.rest.issues.createComment({ + 'owner': context.repo.owner, + 'repo': context.repo.repo, + 'issue_number': context.issue.number, + 'body': `${quote}\n\n@${context.payload.comment.user.login}, slash command not recognized. Please use \`/stdlib help\` to view available commands.` + }); + } } # Define a job for checking for required files: @@ -217,12 +244,13 @@ jobs: needs: [ add_initial_reaction ] # Define the conditions under which the job should run: - if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/stdlib todo') + if: (github.event.issue.pull_request || github.event_name == 'commit_comment') && startsWith(github.event.comment.body, '/stdlib todo') # Run reusable workflow: uses: ./.github/workflows/create_todo_issue.yml with: - pull_request_number: ${{ github.event.issue.number }} + pull_request_number: ${{ github.event_name != 'commit_comment' && github.event.issue.number || '' }} + commit_sha: ${{ github.event_name == 'commit_comment' && github.event.comment.commit_id || '' }} comment_body: ${{ github.event.comment.body }} user: ${{ github.event.comment.user.login }} secrets: From fd8cfc033e3bd1d307958dc18201e2ac41a2f0f1 Mon Sep 17 00:00:00 2001 From: Karan Anand Date: Thu, 14 May 2026 21:09:37 -0700 Subject: [PATCH 7/9] refactor: use source_type and source_ref inputs instead of two optional fields --- 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: passed - task: lint_typescript_declarations status: passed - task: lint_typescript_tests status: na - task: lint_license_headers status: passed --- --- .github/workflows/create_todo_issue.yml | 28 +++++++++---------- .../workflows/scripts/create_todo_issue/run | 28 +++++++++++-------- .github/workflows/slash_commands.yml | 4 +-- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/.github/workflows/create_todo_issue.yml b/.github/workflows/create_todo_issue.yml index 35f9976f1526..99b294bad860 100644 --- a/.github/workflows/create_todo_issue.yml +++ b/.github/workflows/create_todo_issue.yml @@ -25,13 +25,13 @@ on: workflow_call: # Define the input parameters for the workflow: inputs: - pull_request_number: - description: 'Pull request number (required when triggered from a PR comment; mutually exclusive with commit_sha)' - required: false + source_type: + description: 'Source context type: ''pr'' (pull request comment) or ''commit'' (commit comment)' + required: true type: string - commit_sha: - description: 'Commit SHA (required when triggered from a commit comment; mutually exclusive with pull_request_number)' - required: false + source_ref: + description: 'Pull request number when source_type is ''pr''; commit SHA when source_type is ''commit''' + required: true type: string comment_body: description: 'Body of the slash command comment' @@ -51,13 +51,13 @@ on: # Allow the workflow to be manually triggered: workflow_dispatch: inputs: - pull_request_number: - description: 'Pull request number (required when triggered from a PR comment; mutually exclusive with commit_sha)' - required: false + source_type: + description: 'Source context type: ''pr'' (pull request comment) or ''commit'' (commit comment)' + required: true type: string - commit_sha: - description: 'Commit SHA (required when triggered from a commit comment; mutually exclusive with pull_request_number)' - required: false + source_ref: + description: 'Pull request number when source_type is ''pr''; commit SHA when source_type is ''commit''' + required: true type: string comment_body: description: 'Body of the slash command comment' @@ -99,8 +99,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.STDLIB_BOT_GITHUB_TOKEN || secrets.STDLIB_BOT_PAT_REPO_WRITE }} COMMENT_BODY: ${{ inputs.comment_body }} COMMENTER: ${{ inputs.user }} - PR_NUMBER: ${{ inputs.pull_request_number }} - COMMIT_SHA: ${{ inputs.commit_sha }} + SOURCE_TYPE: ${{ inputs.source_type }} + SOURCE_REF: ${{ inputs.source_ref }} run: | . "$GITHUB_WORKSPACE/.github/workflows/scripts/create_todo_issue/run" timeout-minutes: 10 diff --git a/.github/workflows/scripts/create_todo_issue/run b/.github/workflows/scripts/create_todo_issue/run index 09be33af3cf8..0c95d1110351 100755 --- a/.github/workflows/scripts/create_todo_issue/run +++ b/.github/workflows/scripts/create_todo_issue/run @@ -23,8 +23,8 @@ # GITHUB_TOKEN GitHub token with permission to create issues and comments. # COMMENT_BODY Body of the /stdlib todo slash command comment. # COMMENTER GitHub login of the commenter. -# PR_NUMBER Pull request number (mutually exclusive with COMMIT_SHA). -# COMMIT_SHA Commit SHA (mutually exclusive with PR_NUMBER). +# SOURCE_TYPE Source context type: 'pr' or 'commit'. +# SOURCE_REF Pull request number when SOURCE_TYPE is 'pr'; commit SHA when SOURCE_TYPE is 'commit'. # Ensure that the exit status of pipelines is non-zero in the event that at least one of the commands in a pipeline fails: set -o pipefail @@ -66,10 +66,10 @@ post_source_comment() { local payload local endpoint payload="{\"body\":$(printf '%s' "${body}" | jq -R -s -c .)}" - if [ -n "${PR_NUMBER}" ]; then - endpoint="${github_api_url}/repos/${repo_owner}/${repo_name}/issues/${PR_NUMBER}/comments" + if [ "${SOURCE_TYPE}" = "pr" ]; then + endpoint="${github_api_url}/repos/${repo_owner}/${repo_name}/issues/${SOURCE_REF}/comments" else - endpoint="${github_api_url}/repos/${repo_owner}/${repo_name}/commits/${COMMIT_SHA}/comments" + endpoint="${github_api_url}/repos/${repo_owner}/${repo_name}/commits/${SOURCE_REF}/comments" fi curl -s -X POST \ -H "Authorization: token ${GITHUB_TOKEN}" \ @@ -112,12 +112,16 @@ main() { local label # Validate required inputs: - if [ -z "${PR_NUMBER}" ] && [ -z "${COMMIT_SHA}" ]; then - echo "ERROR: Either PR_NUMBER or COMMIT_SHA is required." >&2 + if [ -z "${SOURCE_TYPE}" ]; then + echo "ERROR: SOURCE_TYPE is required." >&2 on_error 1 fi - if [ -n "${PR_NUMBER}" ] && [ -n "${COMMIT_SHA}" ]; then - echo "ERROR: PR_NUMBER and COMMIT_SHA are mutually exclusive." >&2 + if [ "${SOURCE_TYPE}" != "pr" ] && [ "${SOURCE_TYPE}" != "commit" ]; then + echo "ERROR: SOURCE_TYPE must be 'pr' or 'commit'." >&2 + on_error 1 + fi + if [ -z "${SOURCE_REF}" ]; then + echo "ERROR: SOURCE_REF is required." >&2 on_error 1 fi if [ -z "${COMMENTER}" ]; then @@ -249,12 +253,12 @@ ${bt}${bt}${bt}" fi # Build provenance footer: - if [ -n "${PR_NUMBER}" ]; then + if [ "${SOURCE_TYPE}" = "pr" ]; then provenance="--- -*Created via ${bt}/stdlib todo${bt} from [${repo_owner}/${repo_name}#${PR_NUMBER}](https://github.com/${repo_owner}/${repo_name}/pull/${PR_NUMBER}) by @${COMMENTER}.*" +*Created via ${bt}/stdlib todo${bt} from [${repo_owner}/${repo_name}#${SOURCE_REF}](https://github.com/${repo_owner}/${repo_name}/pull/${SOURCE_REF}) by @${COMMENTER}.*" else provenance="--- -*Created via ${bt}/stdlib todo${bt} from [${repo_owner}/${repo_name}@${COMMIT_SHA:0:7}](https://github.com/${repo_owner}/${repo_name}/commit/${COMMIT_SHA}) by @${COMMENTER}.*" +*Created via ${bt}/stdlib todo${bt} from [${repo_owner}/${repo_name}@${SOURCE_REF:0:7}](https://github.com/${repo_owner}/${repo_name}/commit/${SOURCE_REF}) by @${COMMENTER}.*" fi # Build the issue body with provenance footer: diff --git a/.github/workflows/slash_commands.yml b/.github/workflows/slash_commands.yml index 8e75b8efea11..4e74e75ae271 100644 --- a/.github/workflows/slash_commands.yml +++ b/.github/workflows/slash_commands.yml @@ -249,8 +249,8 @@ jobs: # Run reusable workflow: uses: ./.github/workflows/create_todo_issue.yml with: - pull_request_number: ${{ github.event_name != 'commit_comment' && github.event.issue.number || '' }} - commit_sha: ${{ github.event_name == 'commit_comment' && github.event.comment.commit_id || '' }} + source_type: ${{ github.event_name == 'commit_comment' && 'commit' || 'pr' }} + source_ref: ${{ github.event_name == 'commit_comment' && github.event.comment.commit_id || github.event.issue.number }} comment_body: ${{ github.event.comment.body }} user: ${{ github.event.comment.user.login }} secrets: From d0aaee8c650a2a020340d6c12fd76e886079184e Mon Sep 17 00:00:00 2001 From: Karan Anand Date: Thu, 14 May 2026 21:20:20 -0700 Subject: [PATCH 8/9] fix: error body --- 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: passed - task: lint_typescript_declarations status: passed - task: lint_typescript_tests status: na - task: lint_license_headers status: passed --- --- .github/workflows/scripts/create_todo_issue/run | 9 ++++++--- .github/workflows/slash_commands.yml | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/scripts/create_todo_issue/run b/.github/workflows/scripts/create_todo_issue/run index 0c95d1110351..b637c7778046 100755 --- a/.github/workflows/scripts/create_todo_issue/run +++ b/.github/workflows/scripts/create_todo_issue/run @@ -73,7 +73,7 @@ post_source_comment() { fi curl -s -X POST \ -H "Authorization: token ${GITHUB_TOKEN}" \ - -H "Accept: application/vnd.github.v3+json" \ + -H "Accept: application/vnd.github+json" \ -H "Content-Type: application/json" \ -d "${payload}" \ "${endpoint}" \ @@ -240,7 +240,7 @@ ${bt}${bt}${bt}" # Verify the commenter is a member of the target organization: membership_status=$(curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: token ${GITHUB_TOKEN}" \ - -H "Accept: application/vnd.github.v3+json" \ + -H "Accept: application/vnd.github+json" \ "${github_api_url}/orgs/${target_owner}/members/${COMMENTER}") if [ "${membership_status}" != "204" ]; then @@ -281,7 +281,7 @@ ${provenance}" issue_response=$(curl -s \ -X POST \ -H "Authorization: token ${GITHUB_TOKEN}" \ - -H "Accept: application/vnd.github.v3+json" \ + -H "Accept: application/vnd.github+json" \ -H "Content-Type: application/json" \ -d "${issue_payload}" \ "${github_api_url}/repos/${target_owner}/${target_repo}/issues") @@ -326,4 +326,7 @@ ${provenance}" exit 0 } +# Set an error handler to capture errors and perform any clean-up tasks: +trap 'on_error $?' ERR + main diff --git a/.github/workflows/slash_commands.yml b/.github/workflows/slash_commands.yml index 4e74e75ae271..984da0a91c07 100644 --- a/.github/workflows/slash_commands.yml +++ b/.github/workflows/slash_commands.yml @@ -103,7 +103,7 @@ jobs: 'owner': context.repo.owner, 'repo': context.repo.repo, 'commit_sha': context.payload.comment.commit_id, - 'body': `${quote}\n\n@${context.payload.comment.user.login}, slash command not recognized. Please use \`/stdlib help\` to view available commands.` + 'body': `${quote}\n\n@${context.payload.comment.user.login}, slash command not recognized. On commit comments, only \`/stdlib todo\` is supported.` }); } else { await github.rest.issues.createComment({ From ab4b3edd2a47bde98c8e25b5c30dba55aa443d39 Mon Sep 17 00:00:00 2001 From: Karan Anand Date: Thu, 14 May 2026 21:24:33 -0700 Subject: [PATCH 9/9] chore: restore comment update --- 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 --- --- .github/workflows/slash_commands.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/slash_commands.yml b/.github/workflows/slash_commands.yml index 984da0a91c07..1316ceee4683 100644 --- a/.github/workflows/slash_commands.yml +++ b/.github/workflows/slash_commands.yml @@ -46,7 +46,7 @@ jobs: # Define the job's steps: steps: - # Add "bot: In progress" label to the issue / PR (only applicable to PR comments): + # Add "bot: In progress" label to the issue / PR: - name: 'Add in-progress label' if: github.event.issue.pull_request # Pin action to full length commit SHA