Skip to content

Commit

Permalink
ci(asana): adds automation for task workflows and cross commenting be…
Browse files Browse the repository at this point in the history
…tween github and asana
  • Loading branch information
nanpx committed Oct 26, 2021
1 parent 5402143 commit 94d8aca
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 0 deletions.
85 changes: 85 additions & 0 deletions .github/workflows/asana.yml
@@ -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
24 changes: 24 additions & 0 deletions .github/workflows/scripts/asana/complete-tasks-comment.js
@@ -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 .github/workflows/scripts/asana/complete-tasks-complete.js
@@ -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 .github/workflows/scripts/asana/pull-request-comment-on-asana.js
@@ -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 .github/workflows/scripts/asana/pull-request-comment-on-pr.js
@@ -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);
});
})();
40 changes: 40 additions & 0 deletions .github/workflows/scripts/asana/pull-request-move-task.js
@@ -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);
});
})();
98 changes: 98 additions & 0 deletions .github/workflows/scripts/util/asana.js
@@ -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,
};
})();

0 comments on commit 94d8aca

Please sign in to comment.