ADO Build & Deploy - Run Pipeline+

Add useful options for Build & Deploy pipeline in Azure DevOps

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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);
})();