This commit is contained in:
Коммит
b501efe68b
|
@ -0,0 +1,4 @@
|
||||||
|
npm-debug.log
|
||||||
|
node_modules/
|
||||||
|
*.swp
|
||||||
|
lib/
|
|
@ -0,0 +1,2 @@
|
||||||
|
web: node lib/index server
|
||||||
|
worker: node lib/index worker
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "nss-taskcluster",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"author": "Tim Taubert <ttaubert@mozilla.com>",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -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();
|
|
@ -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;
|
|
@ -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;
|
Загрузка…
Ссылка в новой задаче