Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ci(asana): adds automation for task workflows and cross commenting be…
…tween github and asana
- Loading branch information
Showing
7 changed files
with
367 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
--- | ||
name: Asana | ||
|
||
on: | ||
pull_request: | ||
branches: | ||
- main | ||
push: | ||
branches: | ||
- main | ||
|
||
jobs: | ||
pull-request: | ||
if: ${{ 'pull_request' == github.event_name }} | ||
name: Pull Request Routine | ||
runs-on: ubuntu-latest | ||
env: | ||
ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} | ||
GH_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} | ||
GIT_START: origin/${{ github.base_ref }} | ||
GIT_END: origin/${{ github.head_ref }} | ||
MY_GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} | ||
MY_GITHUB_USER: ${{ secrets.GH_USER_NAME }} | ||
NODE_AUTH_TOKEN: ${{ secrets.GH_NPM_READ_TOKEN }} | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v2 | ||
with: | ||
fetch-depth: 0 | ||
ssh-key: ${{ secrets.GH_SSH_PRIVATE_KEY }} | ||
|
||
- name: Install Node 14.x | ||
uses: actions/setup-node@v2 | ||
with: | ||
node-version: 14.x | ||
registry-url: https://npm.pkg.github.com | ||
scope: '@simplesenseio' | ||
|
||
- name: Comment on Tasks | ||
if: ${{ 'opened' == github.event.action }} | ||
id: comment-on-asana | ||
run: node "${GITHUB_WORKSPACE}/.github/workflows/scripts/asana/pull-request-comment-on-asana.js" | ||
shell: bash | ||
|
||
- name: Comment on PR | ||
id: comment-on-pr | ||
run: node "${GITHUB_WORKSPACE}/.github/workflows/scripts/asana/pull-request-comment-on-pr.js" | ||
shell: bash | ||
|
||
- name: Move Task Section | ||
id: move-task | ||
run: node "${GITHUB_WORKSPACE}/.github/workflows/scripts/asana/pull-request-move-task.js" | ||
shell: bash | ||
|
||
complete-tasks: | ||
if: ${{ 'push' == github.event_name }} | ||
name: Complete Tasks | ||
runs-on: ubuntu-latest | ||
env: | ||
ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} | ||
GIT_START: ${{ github.event.before }} | ||
GIT_END: ${{ github.event.after }} | ||
NODE_AUTH_TOKEN: ${{ secrets.GH_NPM_READ_TOKEN }} | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v2 | ||
with: | ||
fetch-depth: 0 | ||
|
||
- name: Install Node 14.x | ||
uses: actions/setup-node@v2 | ||
with: | ||
node-version: 14.x | ||
registry-url: https://npm.pkg.github.com | ||
scope: '@simplesenseio' | ||
|
||
- name: Comment on Tasks | ||
id: comment | ||
run: node "${GITHUB_WORKSPACE}/.github/workflows/scripts/asana/complete-tasks-comment.js" | ||
shell: bash | ||
|
||
- name: Complete Tasks | ||
id: complete | ||
run: node "${GITHUB_WORKSPACE}/.github/workflows/scripts/asana/complete-tasks-complete.js" | ||
shell: bash |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
#!/usr/bin/env node | ||
|
||
(() => { | ||
'use strict'; | ||
|
||
const { | ||
getTaskListFromRange, | ||
gitCommitComment, | ||
} = require('../util/asana'); | ||
|
||
async function main() { | ||
const tasks = await getTaskListFromRange(); | ||
|
||
return Promise.all(tasks.map(({ commit, shortCommit, taskIds }) => { | ||
return Promise.all(taskIds.map((taskId) => gitCommitComment(taskId, commit, shortCommit))); | ||
})); | ||
} | ||
|
||
main() | ||
.catch((err) => { | ||
console.log(err); | ||
process.exit(1); | ||
}); | ||
})(); |
24 changes: 24 additions & 0 deletions
24
.github/workflows/scripts/asana/complete-tasks-complete.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
#!/usr/bin/env node | ||
|
||
(() => { | ||
'use strict'; | ||
|
||
const { | ||
getTaskListFromRange, | ||
completeTask, | ||
} = require('../util/asana'); | ||
|
||
async function main() { | ||
const tasks = await getTaskListFromRange(); | ||
|
||
return Promise.all(tasks.map(({ taskIds }) => { | ||
return Promise.all(taskIds.map(completeTask)); | ||
})); | ||
} | ||
|
||
main() | ||
.catch((err) => { | ||
console.log(err); | ||
process.exit(1); | ||
}); | ||
})(); |
27 changes: 27 additions & 0 deletions
27
.github/workflows/scripts/asana/pull-request-comment-on-asana.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
#!/usr/bin/env node | ||
|
||
(() => { | ||
'use strict'; | ||
|
||
const { | ||
getTaskListFromRange, | ||
gitPrComment, | ||
} = require('../util/asana'); | ||
|
||
async function main() { | ||
const tasks = await getTaskListFromRange(); | ||
const ids = []; | ||
|
||
for (const { taskIds } of tasks) { | ||
ids.push(...taskIds); | ||
} | ||
|
||
return Promise.all([...new Set(ids)].map(gitPrComment)); | ||
} | ||
|
||
main() | ||
.catch((err) => { | ||
console.log(err); | ||
process.exit(1); | ||
}); | ||
})(); |
69 changes: 69 additions & 0 deletions
69
.github/workflows/scripts/asana/pull-request-comment-on-pr.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
#!/usr/bin/env node | ||
|
||
(() => { | ||
'use strict'; | ||
|
||
const { | ||
getTaskListFromRange, | ||
} = require('../util/asana'); | ||
const { | ||
get, | ||
patch, | ||
post, | ||
} = require('../util/github-request'); | ||
|
||
const { | ||
GITHUB_REPOSITORY, | ||
MY_GITHUB_USER, | ||
MY_GITHUB_PR_NUMBER, | ||
} = process.env; | ||
|
||
const MD_TITLE = '**Asana Task(s)**'; | ||
const REQUEST_BASE_PATH = `/repos/${ GITHUB_REPOSITORY }/issues/${ MY_GITHUB_PR_NUMBER }/comments`; | ||
|
||
async function getCommentIfExists() { | ||
const response = await get(REQUEST_BASE_PATH); | ||
|
||
return (response.find(({ body, user: { login } = {}} = {}) => (String(body).startsWith(MD_TITLE) && MY_GITHUB_USER === login)) || {}); | ||
} | ||
|
||
async function comment(id = null, body = '') { | ||
if (null === id) return post(REQUEST_BASE_PATH, { data: { body }}); | ||
return patch(`/repos/${ GITHUB_REPOSITORY }/issues/comments/${ id }`, { data: { body }}); | ||
} | ||
|
||
async function generateBody() { | ||
const tasks = await getTaskListFromRange(); | ||
const ids = []; | ||
|
||
for (const { taskIds } of tasks) { | ||
ids.push(...taskIds); | ||
} | ||
|
||
if (!ids.length) return null; | ||
|
||
const list = []; | ||
|
||
for (const id of [...new Set(ids)]) { | ||
list.push(`* [${ id }](https://app.asana.com/0/0/${ id })`); | ||
} | ||
|
||
return `${ MD_TITLE }\n\n${ list.join('\n') }`; | ||
} | ||
|
||
async function main() { | ||
const body = await generateBody(); | ||
|
||
if (null === body) return; | ||
|
||
const { id = null } = await getCommentIfExists(); | ||
|
||
await comment(id, body); | ||
} | ||
|
||
main() | ||
.catch((err) => { | ||
console.log(err); | ||
process.exit(1); | ||
}); | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
#!/usr/bin/env node | ||
|
||
(() => { | ||
'use strict'; | ||
|
||
const { | ||
getSections, | ||
getTask, | ||
getTaskListFromRange, | ||
changeTaskSection, | ||
} = require('../util/asana'); | ||
|
||
async function main() { | ||
const tasks = await getTaskListFromRange(); | ||
const ids = []; | ||
|
||
for (const { taskIds } of tasks) { | ||
ids.push(...taskIds); | ||
} | ||
|
||
return Promise.all([...new Set(ids)].map(async(taskId) => { | ||
const task = await getTask(taskId); | ||
|
||
return Promise.all(task.projects.map(async({ gid: projectId, name: projectName }) => { | ||
// test agains Dev-Sprint-12-34 | ||
if (!(/^dev-sprint-\d{2}-\d{2}$/iu).test(projectName)) return; | ||
const { gid: sectionId = null } = (await getSections(projectId)).find(({ name }) => name.toLowerCase() === 'in review') || {}; | ||
|
||
if (null === sectionId) return; | ||
changeTaskSection(taskId, projectId, sectionId); | ||
})); | ||
})); | ||
} | ||
|
||
main() | ||
.catch((err) => { | ||
console.log(err); | ||
process.exit(1); | ||
}); | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
(() => { | ||
'use strict'; | ||
|
||
const exec = require('util').promisify(require('child_process').exec); | ||
|
||
const { | ||
ASANA_ACCESS_TOKEN, | ||
GIT_START, | ||
GIT_END, | ||
GITHUB_REPOSITORY, | ||
MY_GITHUB_PR_NUMBER, | ||
} = process.env; | ||
|
||
function getTasksFromCommit(msg) { | ||
// match against si-12345 | ||
const matches = msg.match((/si-\d+/giu)); | ||
|
||
if (null === matches) return []; | ||
return matches.map((issue) => issue.split('-')[1]); | ||
} | ||
|
||
async function curlRequest(method, path, payload = null) { | ||
const { stdout } = await exec(`curl -X ${ method } https://app.asana.com/api/1.0/${ path } -H 'Accept: application/json' -H 'Authorization: Bearer ${ ASANA_ACCESS_TOKEN }' ${ null === payload ? '' : `-H 'Content-Type: application/json' -d '${ JSON.stringify({ data: payload }).trim() }'` }`.trim()); | ||
|
||
const data = JSON.parse(stdout.trim()); | ||
|
||
if (Array.isArray(data.errors)) { | ||
console.log(method, path, data); | ||
throw new Error(data.errors[0].message); | ||
} | ||
return data.data; | ||
} | ||
|
||
async function getTask(id) { | ||
return curlRequest('GET', `tasks/${ id }`); | ||
} | ||
|
||
async function getSections(id) { | ||
return curlRequest('GET', `projects/${ id }/sections`); | ||
} | ||
|
||
async function changeTaskSection(task, project, section) { | ||
return curlRequest('POST', `tasks/${ task }/addProject`, { project, section }); | ||
} | ||
|
||
async function completeTask(taskId) { | ||
return curlRequest('PUT', `tasks/${ taskId }`, { completed: true }); | ||
} | ||
|
||
async function gitCommitComment(taskId, commit, shortCommit) { | ||
// eslint-disable-next-line camelcase | ||
return curlRequest('POST', `tasks/${ taskId }/stories`, { html_text: `<body>Completed in <a href="https://github.com/${ GITHUB_REPOSITORY }/commit/${ commit }">${ shortCommit }</a>.</body>` }); | ||
} | ||
|
||
async function gitPrComment(taskId) { | ||
// eslint-disable-next-line camelcase | ||
return curlRequest('POST', `tasks/${ taskId }/stories`, { html_text: `<body>Referenced in <a href="https://github.com/${ GITHUB_REPOSITORY }/pull/${ MY_GITHUB_PR_NUMBER }">PR#${ MY_GITHUB_PR_NUMBER }</a>.</body>` }); | ||
} | ||
|
||
async function getTaskListFromRange() { | ||
const { stdout } = await exec(`git log --pretty=format:'{%n "commit": "%H",%n "shortCommit": "%h",%n "subject": "%s",%n "body": "%b"%n},' ${ GIT_START }...${ GIT_END }`); | ||
|
||
return JSON.parse(`[${ stdout | ||
.trim() | ||
.slice(0, -1) | ||
.replace((/\n/g), ' ') | ||
}]`).map(({ | ||
commit, | ||
shortCommit, | ||
subject, | ||
body, | ||
}) => { | ||
const taskIds = [ | ||
...new Set([ | ||
...getTasksFromCommit(String(subject).trim()), | ||
...getTasksFromCommit(String(body).trim()), | ||
]), | ||
]; | ||
|
||
return { | ||
commit, | ||
shortCommit, | ||
taskIds, | ||
}; | ||
}) | ||
.filter(({ taskIds }) => taskIds.length); | ||
} | ||
|
||
module.exports = { | ||
changeTaskSection, | ||
completeTask, | ||
getSections, | ||
getTask, | ||
getTaskListFromRange, | ||
gitCommitComment, | ||
gitPrComment, | ||
}; | ||
})(); |