-
-
Save AntoineLemaire/cb5968cf01e1e3b3a11e4cb9eee8ecc5 to your computer and use it in GitHub Desktop.
Daily Scrum Helper with integration with Jira, Github and Google Calendar API
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
// In Google Drive, create a new document of type Google Script. If it does not exist, use "Connect more apps" to enable Google Scripts | |
// Copy-paste this script into the content of the Google Script | |
// This script sends you at each execution an email with Github commits of last day, Jira modified tickets of last day, Google Calendar events of last day, and Google Calendar events of current day | |
// For Jira, just put your username and password. Use https://id.atlassian.com to get them, change password if necessary | |
// For Calendar access, in Google Script, go to Resources > Advanced Google Services and enable "Calendar API" | |
// For Github, it's complicated... you have to follow this tutorial: https://www.benlcollins.com/apps-script/oauth-github | |
// For Slack : Go to https://api.slack.com/legacy/custom-integrations/legacy-tokens, log in with your slack account, and refresh the page. You'll be able to get a Legacy Token. (This will end on May 5th, 2020, we need to move to Slack Apps...) | |
// Last step, make the script a daily cron: go to Edit > Current project's triggers and run "sendDailyReport" or "sendDailyReportSlack" every day between 5am and 6am | |
// You can run manually "sendDailyReportMail" or "sendDailyReportSlack" to test if it's working ;) | |
// Settings | |
var jiraUsername = ''; // Your Jira e-mail | |
var jiraApiToken = ''; // Your Jira API Token | |
var githubAuthorName = ''; // Your Github username | |
var githubAppClientId = ''; // Github client id (cf above, tutorial) | |
var githubAppClientSecret = ''; // Github client secret (cf above, tutorial) | |
var slackAccessToken = ''; // Slack Legacy Token. | |
var slackChannelId = ''; // Slack channel where you want your message be posted (can be found in URL of web app of Slack) | |
// Functions | |
function sendDailyReportMail() { | |
var date = new Date(); | |
// If it's WE | |
if (date.getDay() === 6 || date.getDay() === 0) { | |
// Do nothing | |
return null; | |
} | |
// If it's monday | |
if (date.getDay() === 1) { | |
var yesterdayTextPreprosition = "of friday"; | |
var yesterdayText = "friday"; | |
var yesterdayDate = new Date(date.getTime() - 24 * 3 * 60 * 60 * 1000); | |
} else { | |
var yesterdayTextPreprosition = "of yesterday"; | |
var yesterdayText = "yesterday"; | |
var yesterdayDate = new Date(date.getTime() - 24 * 60 * 60 * 1000); | |
} | |
yesterdayDate.setHours(2, 0, 0); | |
var todayDate = new Date(); | |
todayDate.setHours(2, 0, 0); | |
var tomorrowDate = new Date(date.getTime() + 24 * 60 * 60 * 1000); | |
tomorrowDate.setHours(2, 0, 0); | |
var content = ''; | |
content += '<h2>Github\'s commits ' + yesterdayTextPreprosition + ' :</h2>'; | |
var githubCommits = getGithubCommits(getFormattedDate(yesterdayDate), getFormattedDate(todayDate)); | |
if (githubCommits.length === 0) { | |
content += '<p>Aucun</p>'; | |
} else { | |
content += '<ul>'; | |
for (var i = 0; i < githubCommits.length; i++) { | |
content += '<li>' + githubCommits[i] + '</li>'; | |
} | |
content += '</ul>'; | |
} | |
content += '<h2>JIRA tickets modified ' + yesterdayText + ' :</h2>'; | |
var jiraEvents = getJiraEvents(yesterdayDate.toISOString().split('T')[0]); | |
if (jiraEvents.length === 0) { | |
content += '<p>Aucun</p>'; | |
} else { | |
content += '<ul>'; | |
for (var i = 0; i < jiraEvents.length; i++) { | |
content += '<li>' + jiraEvents[i] + '</li>'; | |
} | |
content += '</ul>'; | |
} | |
// Google Calendar: yesterday events | |
content += '<h2>' + yesterdayText.charAt(0).toUpperCase() + yesterdayText.slice(1) + '`s Events:</h2>'; | |
var yesterdayEvents = getCalendarEvents(yesterdayDate.toISOString(), todayDate.toISOString()); | |
if (yesterdayEvents.length === 0) { | |
content += '<p>Aucun</p>'; | |
} else { | |
content += '<ul>'; | |
for (var i = 0; i < yesterdayEvents.length; i++) { | |
content += '<li>' + yesterdayEvents[i] + '</li>'; | |
} | |
content += '</ul>'; | |
} | |
// Google Calendar: today events | |
content += '<h2>Today\'s Events :</h2>'; | |
var todayEvents = getCalendarEvents(todayDate.toISOString(), tomorrowDate.toISOString()); | |
if (todayEvents.length === 0) { | |
content += '<p>Aucun</p>'; | |
} else { | |
content += '<ul>'; | |
for (var i = 0; i < todayEvents.length; i++) { | |
content += '<li>' + todayEvents[i] + '</li>'; | |
} | |
content += '</ul>'; | |
} | |
if (githubCommits.length > 0 || jiraEvents.length > 0 || yesterdayEvents.length > 0 || todayEvents.length) { | |
// Send mail | |
var dateString = ('0' + date.getDate()).slice(-2) + '/' + ('0' + (date.getMonth() + 1)).slice(-2); | |
var email = Session.getActiveUser().getEmail(); | |
var subject = 'Daily scrum ' + dateString; | |
var body = '<body><h1>Daily scrum ' + dateString + ' :</h1>' + content; | |
GmailApp.sendEmail(email, subject, body, {htmlBody: body}); | |
} | |
} | |
function sendDailyReportSlack() { | |
var url = 'https://slack.com/api/chat.postMessage'; | |
var headers = { | |
'Authorization': Utilities.formatString('Bearer %s', slackAccessToken), | |
'Content-type': 'application/json' | |
}; | |
var data = { | |
'channel': slackChannelId, | |
'text': getSlackContentMessage() | |
}; | |
var response = UrlFetchApp.fetch(url, { | |
'headers': headers, | |
'method': "POST", | |
'muteHttpExceptions': true, | |
'payload' : JSON.stringify(data) | |
}); | |
Logger.log(response); | |
console.log(response); | |
} | |
function getSlackContentMessage() { | |
var date = new Date(); | |
var dateString = ('0' + date.getDate()).slice(-2) + '/' + ('0' + (date.getMonth() + 1)).slice(-2); | |
// If it's WE | |
if (date.getDay() === 6 || date.getDay() === 0) { | |
// Do nothing | |
return null; | |
} | |
// If it's monday | |
if (date.getDay() === 1) { | |
var yesterdayTextPreprosition = "of friday"; | |
var yesterdayText = "friday"; | |
var yesterdayDate = new Date(date.getTime() - 24 * 3 * 60 * 60 * 1000); | |
} else { | |
var yesterdayTextPreprosition = "of yesterday"; | |
var yesterdayText = "yesterday"; | |
var yesterdayDate = new Date(date.getTime() - 24 * 60 * 60 * 1000); | |
} | |
yesterdayDate.setHours(2, 0, 0); | |
var todayDate = new Date(); | |
todayDate.setHours(2, 0, 0); | |
var tomorrowDate = new Date(date.getTime() + 24 * 60 * 60 * 1000); | |
tomorrowDate.setHours(2, 0, 0); | |
var content = '*_Daily Scrum ' + dateString + '_*'; | |
content += '\n*Github\'s commits ' + yesterdayTextPreprosition + ' :*\n'; | |
var githubCommits = getGithubCommits(getFormattedDate(yesterdayDate), getFormattedDate(todayDate)); | |
if (githubCommits.length === 0) { | |
content += 'Aucun\n'; | |
} else { | |
for (var i = 0; i < githubCommits.length; i++) { | |
content += ' • ' + githubCommits[i] + '\n'; | |
} | |
} | |
content += '\n*JIRA tickets modified ' + yesterdayText + ' :*\n'; | |
var jiraEvents = getJiraEvents(yesterdayDate.toISOString().split('T')[0]); | |
if (jiraEvents.length === 0) { | |
content += 'Aucun'; | |
} else { | |
for (var i = 0; i < jiraEvents.length; i++) { | |
content += ' • ' + jiraEvents[i] + '\n'; | |
} | |
} | |
// Google Calendar: yesterday events | |
content += '\n*' + yesterdayText.charAt(0).toUpperCase() + yesterdayText.slice(1) + '`s Events:*\n'; | |
var yesterdayEvents = getCalendarEvents(yesterdayDate.toISOString(), todayDate.toISOString()); | |
if (yesterdayEvents.length === 0) { | |
content += 'Aucun'; | |
} else { | |
for (var i = 0; i < yesterdayEvents.length; i++) { | |
content += ' • ' + yesterdayEvents[i] + '\n'; | |
} | |
} | |
// Google Calendar: today events | |
content += '\n*Today\'s Events:*\n'; | |
var todayEvents = getCalendarEvents(todayDate.toISOString(), tomorrowDate.toISOString()); | |
if (todayEvents.length === 0) { | |
content += 'Aucun'; | |
} else { | |
for (var i = 0; i < todayEvents.length; i++) { | |
content += ' • ' + todayEvents[i] + '\n'; | |
} | |
} | |
if (githubCommits.length > 0 || jiraEvents.length > 0 || yesterdayEvents.length > 0 || todayEvents.length) { | |
return content; | |
} | |
return null; | |
} | |
function parseDate(string) { | |
var parts = string.split('T'); | |
parts[0] = parts[0].replace(/-/g, '/'); | |
parts[1] = parts[1].split('+')[0]; | |
return new Date(parts.join(' ')); | |
} | |
function getCalendarEvents(timeMin, timeMax) { | |
var calendarId = 'primary'; | |
var events = Calendar.Events.list(calendarId, { | |
timeMin: timeMin, | |
timeMax: timeMax, | |
singleEvents: true, | |
orderBy: 'startTime', | |
maxResults: 10 | |
}); | |
console.log(events); | |
var finalEvents = []; | |
if (events.items && events.items.length > 0) { | |
for (var i = 0; i < events.items.length; i++) { | |
var event = events.items[i]; | |
if (!event.start.date) { | |
var startString = Utilities.formatDate(parseDate(event.start.dateTime), 'Europe/Paris', 'HH:mm'); | |
var endString = Utilities.formatDate(parseDate(event.end.dateTime), 'Europe/Paris', 'HH:mm'); | |
console.log('%s (%s)', event.summary, startString); | |
finalEvents.push(Utilities.formatString('%s (%s - %s)', event.summary, startString, endString)); | |
} | |
} | |
} else { | |
console.log('No events found.'); | |
} | |
return finalEvents; | |
} | |
function getJiraEvents(timeMin) { | |
var baseURL = "https://yoopies.atlassian.net/rest/api/2/search"; | |
var encCred = Utilities.base64Encode(jiraUsername + ":" + jiraApiToken); | |
var fetchArgs = { | |
contentType: "application/json", | |
headers: {"Authorization": "Basic " + encCred}, | |
muteHttpExceptions: true | |
}; | |
var issues = []; | |
console.log(timeMin); | |
var jql = "?jql=assignee = currentUser() AND updatedDate %3E= " + timeMin + " ORDER BY updated ASC"; | |
var httpResponse = UrlFetchApp.fetch(baseURL + jql, fetchArgs); | |
if (httpResponse) { | |
var rspns = httpResponse.getResponseCode(); | |
console.log(rspns); | |
switch (rspns) { | |
case 200: | |
var data = JSON.parse(httpResponse.getContentText()); | |
console.log(data); | |
for (var id in data["issues"]) { | |
// Check the data is valid and the Jira fields exist | |
if (data["issues"][id] && data["issues"][id].fields) { | |
var summary = data["issues"][id].fields.summary; | |
var status = data['issues'][id].fields.status.name; | |
var key = data['issues'][id].key; | |
issues.push(Utilities.formatString('%s - %s (%s)', key, summary, status)); | |
} | |
} | |
break; | |
case 404: | |
console.log("Jira 404, no item found"); | |
break; | |
default: | |
console.log("Error: " + httpResponse.getContentText()); | |
break; | |
} | |
} else { | |
console.log("Jira Error, unable to make requests to Jira!"); | |
} | |
return issues; | |
} | |
function getGithubCommits(timeMin, timeMax) { | |
var service = getGithubService(); | |
var commits = []; | |
var maybeAuthorized = service.hasAccess(); | |
if (maybeAuthorized) { | |
console.log("App has access."); | |
var url = "https://api.github.com/search/commits?q=author-date:" + timeMin + ".." + timeMax + "+author:" + githubAuthorName + "&sort=author-date&order=desc"; | |
console.log(url); | |
var accessToken = service.getAccessToken(); | |
var headers = { | |
Authorization: Utilities.formatString('Bearer %s', accessToken), | |
Accept: 'application/vnd.github.cloak-preview' | |
}; | |
console.log(headers); | |
var response = UrlFetchApp.fetch(url, { | |
'headers': headers, | |
'method': "GET", | |
'muteHttpExceptions': true | |
}); | |
var code = response.getResponseCode(); | |
if (code >= 200 && code < 300) { | |
var json = JSON.parse(response.getContentText()); | |
console.log('LIST OF COMMITS'); | |
for (var id in json.items) { | |
console.log(json.items[id]); | |
var message = json.items[id].commit.message; | |
if (message.indexOf("Merge branch") !== 0) { | |
var repository = json.items[id].repository.full_name; | |
var commit = Utilities.formatString('%s : %s', repository, message); | |
if (!commits.includes(commit)) { | |
commits.push(commit); | |
} | |
} | |
} | |
} else if (code == 401 || code == 403) { | |
// Not fully authorized for this action. | |
maybeAuthorized = false; | |
} else { | |
// Handle other response codes by logging them and throwing an | |
// exception. | |
console.log("Backend server error (%s): %s", code.toString(), | |
resp.getContentText("utf-8")); | |
throw ("Backend server error: " + code); | |
} | |
} | |
if (!maybeAuthorized) { | |
// Invoke the authorization flow using the default authorization | |
// prompt card. | |
var authorizationUrl = service.getAuthorizationUrl(); | |
console.log(Utilities.formatString("Open the following URL and re-run the script: %s", | |
authorizationUrl)); | |
console.log(Utilities.formatString("Open the following URL and re-run the script: %s", | |
authorizationUrl)); | |
CardService.newAuthorizationException() | |
.setAuthorizationUrl(authorizationUrl) | |
.setResourceDisplayName("Display name to show to the user") | |
.throwException(); | |
} | |
return commits; | |
} | |
function getGithubService() { | |
return OAuth2.createService('GitHub') | |
.setAuthorizationBaseUrl('https://github.com/login/oauth/authorize') | |
.setTokenUrl('https://github.com/login/oauth/access_token') | |
.setClientId(githubAppClientId) | |
.setClientSecret(githubAppClientSecret) | |
.setCallbackFunction('authCallback') | |
.setPropertyStore(PropertiesService.getUserProperties()) | |
.setScope('user,repo'); | |
} | |
// handle the callback | |
function authCallback(request) { | |
var githubService = getGithubService(); | |
var isAuthorized = githubService.handleCallback(request); | |
if (isAuthorized) { | |
return HtmlService.createHtmlOutput('Success! You can close this tab.'); | |
} else { | |
return HtmlService.createHtmlOutput('Denied. You can close this tab'); | |
} | |
} | |
// Logs the redict URI to register | |
function logRedirectUri() { | |
var service = getGithubService(); | |
console.log(service.getRedirectUri()); | |
} | |
// Format date into YYYY-MM-DDTHH:MM:SS+00:00 | |
function getFormattedDate(date) { | |
return date.getFullYear() + '-' + ('0' + (date.getMonth() + 1)).slice(-2) + '-' + ('0' + date.getDate()).slice(-2) + 'T' + ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2) + ':' + ('0' + date.getSeconds()).slice(-2) + "%2B00:00" | |
} | |
// recreate Array.prototype.includes which does not exist in Google Script | |
if (!Array.prototype.includes) { | |
Object.defineProperty(Array.prototype, 'includes', { | |
value: function(searchElement, fromIndex) { | |
if (this == null) { | |
throw new TypeError('"this" is null or not defined'); | |
} | |
// 1. Let O be ? ToObject(this value). | |
var o = Object(this); | |
// 2. Let len be ? ToLength(? Get(O, "length")). | |
var len = o.length >>> 0; | |
// 3. If len is 0, return false. | |
if (len === 0) { | |
return false; | |
} | |
// 4. Let n be ? ToInteger(fromIndex). | |
// (If fromIndex is undefined, this step produces the value 0.) | |
var n = fromIndex | 0; | |
// 5. If n ≥ 0, then | |
// a. Let k be n. | |
// 6. Else n < 0, | |
// a. Let k be len + n. | |
// b. If k < 0, let k be 0. | |
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); | |
function sameValueZero(x, y) { | |
return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y)); | |
} | |
// 7. Repeat, while k < len | |
while (k < len) { | |
// a. Let elementK be the result of ? Get(O, ! ToString(k)). | |
// b. If SameValueZero(searchElement, elementK) is true, return true. | |
if (sameValueZero(o[k], searchElement)) { | |
return true; | |
} | |
// c. Increase k by 1. | |
k++; | |
} | |
// 8. Return false | |
return false; | |
} | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment