Currently my team at RealStake is using Asana to manage the task flow. It works quite smooth but there is one issue that we need to move tasks manually for every pull request from Github to sync things up.
In order to solve that bottleneck, I think of using Github webhook to trigger events to a micro server hosted on Zeit then calling to Asana API.
For this simple task, bringing up an Express server may be too much, so I search around and find the inspiration from https://codewithhugo.com/simple-zeit-micro-improve-node/ then decide to give Zeit's Micro a try.
Goals
- pull request created - > add comment to asana -> move task to "in review" section
- pull request merged - > add comment to asana - > move task to "on dev/stg/prod" section
Stack
- use Zeit Now platform for deployment (https://zeit.co)
- use Zeit Micro for server (https://github.com/zeit/micro)
- use Asana library (https://github.com/Asana/node-asana)
To make this work, first I suggest a rule for my team to include Asana task ID into pull request, e.g. pull request name - ref#task_id so I can get the ID correctly.
function match(toMatch) {
let result = toMatch.match(/#(ref)?([0-9]{16})|([0-9]{16})/g);
if (result) {
return result.map(item => item.replace('#', '').replace('ref', ''));
}
}
Then I define simple steps to extract Asana IDs from pull request data and push to a unique array.
// github.js
getAsanaIds: async data => {
let ids = [];
// check title
const title = data['pull_request']['title'];
const matchTitle = match(title);
if (matchTitle) matchTitle.forEach(item => ids.push(item));
// check body
const body = data['pull_request']['body'];
const matchBody = match(body);
if (matchBody) matchBody.forEach(item => ids.push(item));
// check commits
const commits = await getCommits(data);
for (const commit of commits) {
const matchCommit = await match(commit['commit']['message']);
if (matchCommit) matchCommit.forEach(item => ids.push(item));
}
// check comments and review comments
const comments = (await getComments(data)).concat(
await getReviewComments(data),
);
for (const comment of comments) {
const matchComment = await match(comment['body']);
if (matchComment) matchComment.forEach(item => ids.push(item));
}
if (ids.length === 0) throw Error('No Asana task ID found!');
const uniqueIds = [...new Set(ids)];
return uniqueIds;
},
// asana.js
getAsanaTask: async asanaId => {
const task = await client.tasks.findById(asanaId);
if (!task) throw Error('Failed to find Asana task with id: ' + asanaId);
return task;
},
To move Asana tasks to correct collumn, I need to map their names with pull request status.
// github.js
getAsanaSectionId: (asanaSections, data) => {
let section;
if (data.merged === false && data.state === 'open') {
if (data.base === 'develop') section = 'in review';
if (data.base === 'release') section = 'staging ready';
if (data.base === 'master') section = 'production ready';
}
if (data.merged === true && data.state == 'closed') {
if (data.base === 'develop') section = 'on test';
if (data.base === 'release') section = 'on staging';
if (data.base === 'master') section = 'done';
}
for (const item of Object.keys(asanaSections)) {
if (item.toLowerCase().includes(section)) {
return asanaSections[item];
}
}
}
// asana.js
addAsanaTask: async ({ asanaId, projectId, sectionId }) => {
const data = {
project: projectId,
section: sectionId,
};
const result = await client.tasks.addProject(asanaId, data);
if (Object.keys(result).length != 0) {
throw Error("Failed to change Asana task's section!");
}
},
Finally, after moving tasks, I need to add comment to Asana task to update necessary info. for the team members.
// github.js
getPullRequestData: async data => {
let commit_urls = [];
const commits = await getCommits(data);
for (const commit of commits) {
const item = ` ${commit['html_url']} - ${commit['commit']['message']} - ${commit['committer']['login']}`;
commit_urls.push(item);
}
return {
title: "data['pull_request']['title'],"
body: data['pull_request']['body'],
url: data['pull_request']['html_url'],
state: data['pull_request']['state'],
user: {
login: data['pull_request']['user']['login'],
},
head: data['pull_request']['head']['ref'],
base: data['pull_request']['base']['ref'],
merged: data['pull_request']['merged'],
commits: commit_urls,
};
}
// asana.js
addComment: async (asanaId, githubData) => {
const comment = {
text: `Pull Request ${githubData.url} from ${githubData.user.login}
Title: ${githubData.title} - Body: ${githubData.body}
From: ${githubData.head} - To: ${githubData.base} - State: ${githubData.state} - Merged: ${githubData.merged}
Commits: ${githubData.commits}`,
};
const story = await client.tasks.addComment(asanaId, comment);
if (!story)
throw Error(
'Failed to add comment to Asana task with id: ' + asanaId,
);
},
Besides, we are using Slack for communicating so it is useful to notify pull request status via defined channels.
async function notify(githubData) {
const text = `Pull Request ${githubData.url} from ${githubData.user.login}
Title: ${githubData.title} - Body: ${githubData.body}
From: ${githubData.head} - To: ${githubData.base}
State: ${githubData.state} - Merged: ${githubData.merged}`;
try {
const channel =
githubData.base === 'master'
? '#pull-requests-master'
: '#pull-requests';
const res = await bot.chat.meMessage({
token,
channel,
text,
});
if (res.ok) console.log('Notified Slack successfully');
} catch (e) {
console.log('Failed to notify Slack', e);
}
}
And the result is as expected.
The whole code can be found at https://github.com/trannguyenhung011086/sync-asana-github-zeit-micro
From building up this simple workflow, I develop more insight in development tasks like trying to predict more future use cases.
Specifically, at first I only thought of using the exact format ref#task_id but we often forget the format so I must make the regexp pattern more flexible. Another example is where I extract the ids from github pull request. It can be in the title, body, or calling further API call to commit details, comment details, etc.
This is my first time implementing such a task for improving our workflow and the experience is so satisfying :)
Top comments (0)