commit b501efe68bdb0af12c62dd314f4bb9109e146d90 Author: Tim Taubert Date: Sun May 29 19:14:59 2016 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30670b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +npm-debug.log +node_modules/ +*.swp +lib/ diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..9258ff2 --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +web: node lib/index server +worker: node lib/index worker diff --git a/package.json b/package.json new file mode 100644 index 0000000..eb04b55 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "nss-taskcluster", + "version": "1.0.0", + "private": true, + "author": "Tim Taubert ", + "description": "Notifications for NSS on Taskcluster", + "scripts": { + "compile": "babel-compile -p taskcluster src:lib", + "install": "npm run compile" + }, + "repository": { + "type": "git", + "url": "https://github.com/ttaubert/nss-taskcluster.git" + }, + "devDependencies": { + "babel-cli": "^6.0.0", + "babel-compile": "^2.0.0", + "babel-preset-taskcluster": "^2.3.0" + }, + "dependencies": { + "array-unique": "^0.2.1", + "irc-colors": "^1.2.1", + "jerk": "^1.1.23", + "peoplestring-parse": "^2.0.3", + "request-json": "^0.5.6", + "taskcluster-client": "^1.0.1" + } +} diff --git a/src/hg.js b/src/hg.js new file mode 100644 index 0000000..953030e --- /dev/null +++ b/src/hg.js @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import request from "request-json"; +import parse from "peoplestring-parse"; + +const HG_HOST = "https://hg.mozilla.org/"; +const HG_REPO = "projects/nss"; + +async function jsonRequest(url) { + let client = request.createClient(HG_HOST); + + return new Promise((resolve, reject) => { + client.get(url, (err, res, json) => { + if (err) { + reject(err); + } else { + resolve(json); + } + }); + }); +} + +async function shortenRevision(revision) { + async function tryShortenedRevisionHash(length) { + let shortened = revision.slice(0, length); + let json = jsonRequest(HG_REPO + "/json-pushes?changeset=" + shortened); + + if (typeof(json) == "object") { + return shortened; + } + + return tryShortenedRevisionHash(length + 2); + } + + return tryShortenedRevisionHash(12); +}; + +async function fetchChangesets(revision, options = {}) { + let includeHref = options && options.includeHref; + let json = await jsonRequest(HG_REPO + "/json-pushes?full&changeset=" + revision); + + let id = Object.keys(json)[0]; + let changesets = json[id].changesets; + + for (let changeset of changesets) { + changeset.author = parse(changeset.author); + + if (includeHref) { + let short_rev = await shortenRevision(revision); + changeset.href = HG_HOST + HG_REPO + "/rev/" + short_rev; + } + } + + return changesets; +} + +async function fetchBlamelist(revision) { + let changesets = await fetchChangesets(revision); + return changesets.map(changeset => changeset.author); +}; + +module.exports.fetchChangesets = fetchChangesets; +module.exports.fetchBlamelist = fetchBlamelist; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..0a6c76a --- /dev/null +++ b/src/index.js @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import hg from "./hg"; +import irc from "./irc"; +import tcc from "./tcc"; +import colors from "irc-colors"; +import unique from "array-unique"; + +const TASK_INSPECTOR_URL = "https://tools.taskcluster.net/task-inspector/#"; + +const PLATFORMS = { + linux32: "Linux", + linux64: "Linux x64" +}; + +function containsNssRoute(routes) { + return routes.some(r => /^tc-treeherder\.nss\./.test(r)); +} + +tcc.onTaskDefined(async function (msg) { + // Check for NSS tasks. + if (!containsNssRoute(msg.routes)) { + return; + } + + let taskId = msg.payload.status.taskId; + let task = await tcc.fetchTask(taskId); + let th = task.extra.treeherder; + + // Check for decision tasks. + if (th.symbol != "D") { + return; + } + + let options = {includeHref: true}; + let changesets = await hg.fetchChangesets(th.revision, options); + + for (let changeset of changesets) { + let level = colors.blue("push"); + let desc = changeset.desc.split("\n")[0]; + irc.say(`[${level}] ${changeset.href} - ${changeset.author.name} - ${desc}`); + } +}); + +tcc.onTaskFailed(async function (msg) { + // Check for NSS tasks. + if (!containsNssRoute(msg.routes)) { + return; + } + + let taskId = msg.payload.status.taskId; + let task = await tcc.fetchTask(taskId); + let th = task.extra.treeherder; + + // Determine platform. + let collection = Object.keys(th.collection || {})[0] || "opt"; + let platform = PLATFORMS[th.build.platform] || th.build.platform; + + let level = colors.red("failure"); + let url = TASK_INSPECTOR_URL + taskId; + let authors = await hg.fetchBlamelist(th.revision); + let blame = unique(authors.map(author => author.name)).join(", "); + irc.say(`[${level}] ${url} - ${task.metadata.name} @ ${platform} ${collection} (blame: ${blame})`); +}); + +// Join ASAP. +irc.connect(); diff --git a/src/irc.js b/src/irc.js new file mode 100644 index 0000000..043dc96 --- /dev/null +++ b/src/irc.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import jerk from "jerk"; + +const NICK = "nss-tc"; +const CHANNEL = "#nss-test"; +const SERVER = "irc.mozilla.org"; + +let waiting, bot, connected; +let queue = []; + +function say(msg) { + queue.push([CHANNEL, msg]); + processQueue(); +}; + +function processQueue() { + if (!connected) { + connect(); + return; + } + + if (!queue.length || waiting) { + return; + } + + bot.say.apply(bot, queue.shift()); + + waiting = true; + setTimeout(() => { + waiting = false; + processQueue(); + }, 500); +} + +function connect() { + if (bot) { + return; + } + + function onConnect() { + connected = true; + processQueue(); + } + + bot = jerk(() => {}).connect({ + server: SERVER, + onConnect: onConnect, + channels: [CHANNEL], + waitForPing: true, + nick: NICK + }); +} + +module.exports.say = say; +module.exports.connect = connect; diff --git a/src/tcc.js b/src/tcc.js new file mode 100644 index 0000000..7ef488e --- /dev/null +++ b/src/tcc.js @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import taskcluster from "taskcluster-client"; + +let queueEvents = new taskcluster.QueueEvents(); +var scheduler = new taskcluster.Scheduler(); +let queue = new taskcluster.Queue(); + +function onTaskDefined(callback) { + onTaskEvent("taskDefined", callback); +} + +function onTaskFailed(callback) { + onTaskEvent("taskFailed", async function (msg) { + let {taskId, taskGroupId} = msg.payload.status; + let info = await scheduler.inspectTask(taskGroupId, taskId); + + // Report failures when the last rerun fails. + if (info.reruns == msg.payload.runId) { + callback(msg); + } + }); +} + +function onTaskEvent(type, callback) { + let listener = new taskcluster.PulseListener({ + credentials: { + username: process.env.PG_USERNAME, + password: process.env.PG_PASSWORD + } + }); + + listener.bind(queueEvents[type]({ + provisionerId: "aws-provisioner-v1", + workerType: "hg-worker" + })); + + listener.on("message", callback); + + listener.connect().then(() => { + return listener.resume(); + }); +} + +async function fetchTask(taskId) { + return queue.task(taskId); +} + +module.exports.onTaskDefined = onTaskDefined; +module.exports.onTaskFailed = onTaskFailed; +module.exports.fetchTask = fetchTask;