Skip to content

Commit 572ac55

Browse files
committed
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 ---
1 parent 2cce346 commit 572ac55

2 files changed

Lines changed: 220 additions & 136 deletions

File tree

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
#/
2+
# @license Apache-2.0
3+
#
4+
# Copyright (c) 2024 The Stdlib Authors.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#/
18+
19+
# Workflow name:
20+
name: create_todo_issue
21+
22+
# Workflow triggers:
23+
on:
24+
# Allow the workflow to be triggered by other workflows:
25+
workflow_call:
26+
# Define the input parameters for the workflow:
27+
inputs:
28+
pull_request_number:
29+
description: 'Pull request number'
30+
required: true
31+
type: number
32+
comment_body:
33+
description: 'Body of the slash command comment'
34+
required: true
35+
type: string
36+
user:
37+
description: 'GitHub login of the commenter'
38+
required: true
39+
type: string
40+
41+
# Define the secrets accessible by the workflow:
42+
secrets:
43+
STDLIB_BOT_GITHUB_TOKEN:
44+
description: 'stdlib-bot GitHub token with permission to create issues and comments'
45+
required: true
46+
47+
# Allow the workflow to be manually triggered:
48+
workflow_dispatch:
49+
inputs:
50+
pull_request_number:
51+
description: 'Pull request number'
52+
required: true
53+
type: number
54+
comment_body:
55+
description: 'Body of the slash command comment'
56+
required: true
57+
type: string
58+
user:
59+
description: 'GitHub login of the commenter'
60+
required: true
61+
type: string
62+
63+
# Workflow jobs:
64+
jobs:
65+
66+
# Define a job for creating a new todo issue:
67+
create_todo_issue:
68+
69+
# Define a display name:
70+
name: 'Create a new todo issue'
71+
72+
# Define the type of virtual host machine:
73+
runs-on: ubuntu-latest
74+
75+
# Define the job's steps:
76+
steps:
77+
# Create a new todo issue:
78+
- name: 'Create a new todo issue'
79+
# Pin action to full length commit SHA
80+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
81+
env:
82+
COMMENT_BODY: ${{ inputs.comment_body }}
83+
COMMENTER: ${{ inputs.user }}
84+
PR_NUMBER: ${{ inputs.pull_request_number }}
85+
with:
86+
github-token: ${{ secrets.STDLIB_BOT_GITHUB_TOKEN }}
87+
script: |
88+
const commentBody = process.env.COMMENT_BODY.trim();
89+
const commenter = process.env.COMMENTER;
90+
const prNumber = parseInt( process.env.PR_NUMBER, 10 );
91+
92+
// Parse the fenced code block following '/stdlib todo':
93+
const RE_CODE_BLOCK = /```text\s*\{([^}]*)\}\s*\n([\s\S]*?)```/;
94+
const blockMatch = commentBody.match( RE_CODE_BLOCK );
95+
if ( !blockMatch ) {
96+
const lines = commentBody.split( '\n' );
97+
const quote = lines.map( line => `> ${line}` ).join( '\n' );
98+
await github.rest.issues.createComment({
99+
'owner': context.repo.owner,
100+
'repo': context.repo.repo,
101+
'issue_number': prNumber,
102+
'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\`\`\``
103+
});
104+
core.setFailed( 'No code block found in /stdlib todo comment.' );
105+
return;
106+
}
107+
108+
const attrs = blockMatch[ 1 ];
109+
const content = blockMatch[ 2 ].trim();
110+
111+
// Parse 'stdlib' attribute (values: 'public' or 'private'):
112+
const stdlibAttrMatch = attrs.match( /stdlib=(\w+)/i );
113+
const stdlibTarget = stdlibAttrMatch ? stdlibAttrMatch[ 1 ].toLowerCase() : 'public';
114+
if ( stdlibTarget !== 'public' && stdlibTarget !== 'private' ) {
115+
const lines = commentBody.split( '\n' );
116+
const quote = lines.map( line => `> ${line}` ).join( '\n' );
117+
await github.rest.issues.createComment({
118+
'owner': context.repo.owner,
119+
'repo': context.repo.repo,
120+
'issue_number': prNumber,
121+
'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).`
122+
});
123+
core.setFailed( `Unrecognized stdlib attribute value: ${stdlibTarget}` );
124+
return;
125+
}
126+
127+
// Parse 'labels' attribute (comma-separated list):
128+
const labelsAttrMatch = attrs.match( /labels="([^"]*)"/i );
129+
const labels = labelsAttrMatch
130+
? labelsAttrMatch[ 1 ].split( ',' ).map( l => l.trim() ).filter( Boolean )
131+
: [];
132+
133+
// Parse the issue title from the '[TODO]:' line:
134+
const contentLines = content.split( '\n' );
135+
const RE_TITLE = /^\[TODO\]:\s*(.+)/i;
136+
const titleMatch = contentLines[ 0 ].match( RE_TITLE );
137+
if ( !titleMatch ) {
138+
const lines = commentBody.split( '\n' );
139+
const quote = lines.map( line => `> ${line}` ).join( '\n' );
140+
await github.rest.issues.createComment({
141+
'owner': context.repo.owner,
142+
'repo': context.repo.repo,
143+
'issue_number': prNumber,
144+
'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\`.`
145+
});
146+
core.setFailed( 'No [TODO]: line found in the code block.' );
147+
return;
148+
}
149+
150+
const title = titleMatch[ 1 ].trim();
151+
const body = contentLines.slice( 1 ).join( '\n' ).trim();
152+
153+
// Determine the target repository:
154+
const targetOwner = 'stdlib-js';
155+
const targetRepo = ( stdlibTarget === 'private' ) ? 'todo' : 'stdlib';
156+
157+
// Verify the commenter is an org member:
158+
try {
159+
await github.rest.orgs.checkMembershipForUser({
160+
'org': targetOwner,
161+
'username': commenter
162+
});
163+
} catch ( err ) {
164+
console.log( 'Error checking org membership: %s', err.message );
165+
const lines = commentBody.split( '\n' );
166+
const quote = lines.map( line => `> ${line}` ).join( '\n' );
167+
await github.rest.issues.createComment({
168+
'owner': context.repo.owner,
169+
'repo': context.repo.repo,
170+
'issue_number': prNumber,
171+
'body': `${quote}\n\n@${commenter}, you must be a member of the \`${targetOwner}\` organization to use the \`/stdlib todo\` command.`
172+
});
173+
core.setFailed( `User ${commenter} is not a member of ${targetOwner}.` );
174+
return;
175+
}
176+
177+
// Build issue creation parameters:
178+
const issueParams = {
179+
'owner': targetOwner,
180+
'repo': targetRepo,
181+
'title': title
182+
};
183+
// Build provenance footer:
184+
const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}`;
185+
const provenance = `\n\n---\n*Created via \`/stdlib todo\` from [${context.repo.owner}/${context.repo.repo}#${prNumber}](${prUrl}) by @${commenter}.*`;
186+
187+
if ( body ) {
188+
issueParams.body = body + provenance;
189+
} else {
190+
issueParams.body = provenance.trim();
191+
}
192+
if ( labels.length > 0 ) {
193+
issueParams.labels = labels;
194+
}
195+
196+
// Create the issue:
197+
const issue = await github.rest.issues.create( issueParams );
198+
199+
// Check for labels that were silently dropped (do not exist in the target repo):
200+
const appliedLabels = issue.data.labels.map( l => l.name );
201+
const droppedLabels = labels.filter( l => !appliedLabels.includes( l ) );
202+
203+
// Post a confirmation comment:
204+
const confirmBody = droppedLabels.length > 0
205+
? `@${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( ', ' )}.`
206+
: `@${commenter}, the following todo issue has been created: ${issue.data.html_url}`;
207+
await github.rest.issues.createComment({
208+
'owner': context.repo.owner,
209+
'repo': context.repo.repo,
210+
'issue_number': prNumber,
211+
'body': confirmBody
212+
});

.github/workflows/slash_commands.yml

Lines changed: 8 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -213,148 +213,20 @@ jobs:
213213
# Define a display name:
214214
name: 'Create a new todo issue'
215215

216-
# Define the type of virtual host machine:
217-
runs-on: ubuntu-latest
218-
219216
# Ensure initial reaction job has completed before running this job:
220217
needs: [ add_initial_reaction ]
221218

222219
# Define the conditions under which the job should run:
223220
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/stdlib todo')
224221

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-
// Verify the commenter is an org member:
302-
const commenter = context.payload.comment.user.login;
303-
try {
304-
await github.rest.orgs.checkMembershipForUser({
305-
'org': targetOwner,
306-
'username': commenter
307-
});
308-
} catch ( err ) {
309-
console.log( 'Error checking org membership: %s', err.message );
310-
const lines = commentBody.split( '\n' );
311-
const quote = lines.map( line => `> ${line}` ).join( '\n' );
312-
await github.rest.issues.createComment({
313-
'owner': context.repo.owner,
314-
'repo': context.repo.repo,
315-
'issue_number': context.issue.number,
316-
'body': `${quote}\n\n@${commenter}, you must be a member of the \`${targetOwner}\` organization to use the \`/stdlib todo\` command.`
317-
});
318-
core.setFailed( `User ${commenter} is not a member of ${targetOwner}.` );
319-
return;
320-
}
321-
322-
// Build issue creation parameters:
323-
const issueParams = {
324-
'owner': targetOwner,
325-
'repo': targetRepo,
326-
'title': title
327-
};
328-
// Build provenance footer:
329-
const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${context.issue.number}`;
330-
const provenance = `\n\n---\n*Created via \`/stdlib todo\` from [${context.repo.owner}/${context.repo.repo}#${context.issue.number}](${prUrl}) by @${commenter}.*`;
331-
332-
if ( body ) {
333-
issueParams.body = body + provenance;
334-
} else {
335-
issueParams.body = provenance.trim();
336-
}
337-
if ( labels.length > 0 ) {
338-
issueParams.labels = labels;
339-
}
340-
341-
// Create the issue:
342-
const issue = await github.rest.issues.create( issueParams );
343-
344-
// Check for labels that were silently dropped (do not exist in the target repo):
345-
const appliedLabels = issue.data.labels.map( l => l.name );
346-
const droppedLabels = labels.filter( l => !appliedLabels.includes( l ) );
347-
348-
// Post a confirmation comment:
349-
const confirmBody = droppedLabels.length > 0
350-
? `@${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( ', ' )}.`
351-
: `@${commenter}, the following todo issue has been created: ${issue.data.html_url}`;
352-
await github.rest.issues.createComment({
353-
'owner': context.repo.owner,
354-
'repo': context.repo.repo,
355-
'issue_number': context.issue.number,
356-
'body': confirmBody
357-
});
222+
# Run reusable workflow:
223+
uses: ./.github/workflows/create_todo_issue.yml
224+
with:
225+
pull_request_number: ${{ github.event.issue.number }}
226+
comment_body: ${{ github.event.comment.body }}
227+
user: ${{ github.event.comment.user.login }}
228+
secrets:
229+
STDLIB_BOT_GITHUB_TOKEN: ${{ secrets.STDLIB_BOT_PAT_REPO_WRITE }}
358230

359231
# Define a job for printing a list of available slash commands:
360232
help:

0 commit comments

Comments
 (0)