Skip to content

Instantly share code, notes, and snippets.

@dscho
Last active January 8, 2020 10:36
Show Gist options
  • Save dscho/cc56e37aabc7b6fef0d9e59a37deb3ab to your computer and use it in GitHub Desktop.
Save dscho/cc56e37aabc7b6fef0d9e59a37deb3ab to your computer and use it in GitHub Desktop.
A TamperMonkey script adding useful tweaks to the Azure Pipelines UI
// ==UserScript==
// @name Azure Pipelines Hacks
// @namespace http://tampermonkey.net/
// @version 0.5
// @description Miscellaneous hacks for Azure Pipelines
// @source https://gist.github.com/dscho/cc56e37aabc7b6fef0d9e59a37deb3ab/
// @updateURL https://gist.github.com/dscho/cc56e37aabc7b6fef0d9e59a37deb3ab/raw/azure-pipelines-hacks.user.js
// @downloadURL https://gist.github.com/dscho/cc56e37aabc7b6fef0d9e59a37deb3ab/raw/azure-pipelines-hacks.user.js
// @author dscho
// @match https://dev.azure.com/*/_build*
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
// ==/UserScript==
/*
* Currently, this only adds one tweak: a "Retry" button on the top, right next to the build number.
*/
(function() {
'use strict';
const isFailedBuild = () => {
for (const el of document.querySelectorAll("svg.bolt-status.failed")) {
if (window.getComputedStyle(el).display !== "none") {
return true;
}
}
return false;
}
const addButton = (container, label, action) => {
if (container.buttonAdded) {
return;
}
container.buttonAdded = true;
const button = document.createElement("button");
button.setAttribute("aria-setsize", "3");
button.classList.add("bolt-header-command-item-button", "bolt-button", "bolt-icon-button", "enabled", "bolt-focus-treatment");
button.setAttribute("data-is-focusable", "true");
button.innerHTML = label;
button.onclick = action;
button.click = action;
container.appendChild(button);
};
var showMessage = (container, message) => {
var out = container.querySelector("#bolt-message-output");
if (!out) {
out = document.createElement("span");
out.id = "bolt-message-output";
container.appendChild(out);
}
out.innerHTML = `<i>${message}</i>`;
}
var retryBuild = function(url, container, e) {
if (e && e.preventDefault) {
e.preventDefault();
}
if (e && e.stopPropagation) {
e.stopPropagation();
}
console.log(this);
console.log(e);
let match = url.match(/^https:\/\/dev\.azure\.com\/(([^\/]*)\/[^\/]*)\/_build\/results\?buildId=([0-9]+)/);
if (!match) {
match = url.match(/^\/(([^\/]*)\/[^\/]*)\/_build\/results\?buildId=([0-9]+)/);
}
if (!match) {
match = url.match(/^https:\/\/([^\.]+)\.visualstudio\.com\/([^\/]+)\/_build\/results\?buildId=([0-9]+)/);
const org = match[1];
match[1] = `${org}/${match[2]}`;
match[2] = org;
}
if (!match) {
showMessage(container, `Could not parse ${url}`);
}
const orgRepo = match[1];
const org = match[2].toLowerCase();
const buildId = match[3]
showMessage(container, "Retrying...");
$.ajax({
type: 'PATCH',
dataType: 'JSON',
accepts: 'application/json',
headers: {
'Accept': 'application/json; api-version=5.1-preview.5; excludeUrls=true',
'Referer': '/${orgRepo}/_build/results?buildId=${buildId}&view=results',
'Content-Type': 'application/json',
'X-VSS-ReauthenticationAction': 'Suppress',
},
url: `/${orgRepo}/_apis/build/builds/${buildId}?retry=true`,
success: (data, status, xhr) => {
showMessage(container, "Started");
console.log(data);
console.log(status);
console.log(xhr);
},
error: (xhr, status, e) => {
showMessage(container, "Retry failed");
console.log(e);
console.log(status);
console.log(xhr);
},
});
};
const url = window.location.toString();
if (url.match(/.*\/_build\/results.*/)) {
const selector = 'a.bolt-link[href*="/_build/results?buildId="]';
if (isFailedBuild()) {
const extra = document.querySelector(selector);
if (extra) {
addButton(extra.parentElement, "Retry", (e) => retryBuild(url, extra.parentElement, e));
}
}
new MutationObserver((events, observer) => {
events.map((event) => {
if (event.type === 'childList') {
Array.prototype.map.call(event.target.querySelectorAll(selector), (extra) => {
if (isFailedBuild()) {
addButton(extra.parentElement, "Retry", (e) => retryBuild(url, extra.parentElement, e));
}
});
}
})
}).observe(document, { attributes: true, childList: true, subtree: true });
}
var match = url.match(/.*\/_build\?definitionId=([0-9]+).*/);
if (match) {
var markupRow = (el) => {
const a = el.parentElement;
if (a.tagName === 'A' && a.href.match(/.*\/_build\/results.*/)) {
var td = a;
for (;;) {
td = td.parentElement;
if (!td) {
return;
}
if (td.tagName === 'TD') {
break;
}
}
const after = td.previousSibling.querySelector('.bolt-table-inline-link-left-padding');
if (after) {
addButton(after.parentElement, "Retry", (e) => retryBuild(a.href, after.parentElement, e));
}
}
};
var selector = ".page-content .pipelines-cell[aria-colindex=\"4\"] svg.bolt-status.failed";
var failed = document.querySelectorAll(selector);
if (failed.length == 0) {
selector = ".definition-details-content svg.bolt-status.failed";
markupRow = (el) => {
const div = el.parentElement.parentElement.previousSibling.previousSibling;
const a = div.querySelector("a");
addButton(a.parentElement, "Retry", (e) => retryBuild(a.href, a.parentElement, e));
};
failed = document.querySelectorAll(selector);
}
failed.forEach((el) => {
markupRow(el);
});
new MutationObserver((events, observer) => {
events.map((event) => {
if (event.type === 'childList') {
Array.prototype.map.call(event.target.querySelectorAll(selector), (el) => {
markupRow(el);
});
}
})
}).observe(document, { attributes: true, childList: true, subtree: true });
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment