Add useful options for Build & Deploy pipeline in Azure DevOps
// ==UserScript==
// @name ADO Build & Deploy - Run Pipeline+
// @namespace http://tampermonkey.net/
// @version 1.0.2
// @description Add useful options for Build & Deploy pipeline in Azure DevOps
// @author Victor Ros
// @match https://*.visualstudio.com/*/_build?definitionId=3237*
// @match https://dev.azure.com/*/*/_build?definitionId=3237*
// @match https://*.visualstudio.com/*/_build?definitionId=3333*
// @match https://dev.azure.com/*/*/_build?definitionId=3333*
// @match https://*.visualstudio.com/*/_build?definitionId=4569*
// @match https://dev.azure.com/*/*/_build?definitionId=4569*
// @match https://*.visualstudio.com/*/_build?definitionId=5712*
// @match https://dev.azure.com/*/*/_build?definitionId=5712*
// @icon https://www.google.com/s2/favicons?domain=azure.com
// @grant none
// @license MIT
// ==/UserScript==
(async function() {
"use strict";
const log = console.log;
// Constants
const PREFIX_LOG = "[ADO Build & Deploy - Run Pipeline+]";
const TEXT = {
"fr": {
buttonAllServices: "Tous les services"
},
"en": {
buttonAllServices: "All services"
}
};
const LANGUAGE = getLanguage();
const RUN_PIPELINE_SELECTOR = "__bolt-run-pipeline-command";
const POPUP_SELECTOR = ".bolt-panel-callout-content";
const PARAMETERS_SELECTOR = ".padding-horizontal-20.rhythm-vertical-16 > .flex-noshrink";
const BRANCHES_DROPDOWN_INPUT_SELECTOR = ".version-dropdown > input";
const BUTTON_ID = "deploy-all-service-button";
/**
* Returns navigator's language.
* @returns {string} Language. Default "en".
*/
function getLanguage() {
// Get language from navigator variable
let language = (
typeof navigator === "object" &&
navigator.language &&
navigator.language.split("-").shift()
);
// If not text found, set "en" as default
if (typeof TEXT[language] === "undefined") {
language = "en";
}
return language;
}
/**
* Wait for the appearance of the HTML elements with the selector.
* @param {string} _selector - The selector
* @param {object} options - Options
* @param {string} options.name - Selector name (Default to _selector value)
* @param {number} options.maxRetry - Number of retry (Default to 0)
* @param {number} options.timeout - Time to wait before retrying (Default to 1 sec)
* @returns {Promise<void>} Empty promise.
* @async
*/
async function waitFor(_selector, {name = _selector, maxRetry = 0, timeout = 1000} = {}) {
const result = document.querySelectorAll(_selector);
if (result.length > 0) {
log(`${PREFIX_LOG} ${name} found (${result.length})`);
return;
} else if (maxRetry > 0) {
log(`${PREFIX_LOG} Wait for ${name} (Remaining retries: ${--maxRetry})`);
await new Promise((_resolve, _reject) => {
setTimeout(async () => {
try {
await waitFor(_selector, {name, maxRetry, timeout});
_resolve();
} catch (_err) {
_reject(_err);
}
}, timeout);
});
} else {
throw new Error(`Cannot find elements with selector: ${_selector}`);
}
}
/**
* Get services HTML elements from Run Pipeline popup.
* @returns {void} Nothing.
*/
function getServiceElements() {
const popup = document.querySelector(POPUP_SELECTOR);
const elements = popup.querySelectorAll(PARAMETERS_SELECTOR);
const svcElements = [];
let firstServiceFound = false;
// Ignore all parameters until "events-service" parameter
elements.forEach((_elt, _idx) => {
if (_elt.innerHTML.includes("events-service")) {
firstServiceFound = true;
}
if (firstServiceFound) {
svcElements.push(_elt);
}
});
if (svcElements.length === 0) {
log(`${PREFIX_LOG} Something went wrong while retrieving service elements.`);
}
return svcElements;
}
/**
* Add a button to select/unselect all services.
* Recreate the HTML button.
* @returns {HTMLElement} Button "All services".
*/
function createButtonAllServices(_svcElements) {
const firstSvcElement = _svcElements[0];
const buttonAllServices = firstSvcElement.cloneNode(true);
// Unselect by default
buttonAllServices.setAttribute("aria-checked", false);
buttonAllServices.classList.remove("checked");
// Override id from div child
const divChild = buttonAllServices.querySelector(".bolt-checkbox-label");
divChild.setAttribute("id", BUTTON_ID);
divChild.innerHTML = TEXT[LANGUAGE].buttonAllServices;
// Add click event listener
buttonAllServices.addEventListener("click", (_event) => {
_event.stopPropagation();
_event.preventDefault();
const oldChecked = buttonAllServices.getAttribute("aria-checked") === "true";
const newChecked = !oldChecked;
const action = newChecked === true ? "add" : "remove";
// Update aria-checked
buttonAllServices.setAttribute("aria-checked", newChecked);
buttonAllServices.classList[action]("checked");
// Get services' parameters elements and all parameters elements
const svcElements = getServiceElements();
for (let svcElt of svcElements) {
const svcChecked = svcElt.getAttribute("aria-checked") === "true";
// Button is checked but service is not, or button is unchecked and service is.
if (newChecked && !svcChecked || !newChecked && svcChecked) {
svcElt.click();
}
}
buttonAllServices.focus();
});
// Add div element before first service element
firstSvcElement.insertAdjacentElement("beforebegin", buttonAllServices);
return buttonAllServices;
}
async function run() {
log(`${PREFIX_LOG} Init...`);
// Search by ID Run Pipeline button
const buttonRunPipeline = document.getElementById(RUN_PIPELINE_SELECTOR);
if (typeof buttonRunPipeline === "undefined") {
log(`${PREFIX_LOG} Cannot find Run pipeline button`);
return;
} else {
log(`${PREFIX_LOG} Found Run pipeline button`, buttonRunPipeline);
}
buttonRunPipeline.addEventListener("click", async () => {
try {
// Wait 5 sec max for parameters elements to be present in Run Pipeline popup
await waitFor(PARAMETERS_SELECTOR, {name: "Services parameters", maxRetry: 10, timeout: 500});
// Get services' parameters elements and all parameters elements
const svcElements = getServiceElements();
// Add div element before first service element
const buttonAllServices = createButtonAllServices(svcElements);
log(`${PREFIX_LOG} Added button "${TEXT[LANGUAGE].buttonAllServices}"`);
} catch (_err) {
log(`${PREFIX_LOG} ${_err.stack}`);
}
});
log(`${PREFIX_LOG} Finished!`);
}
// Events
window.addEventListener("load", run);
window.removeEventListener("unload", run);
})();