Greasy Fork is available in English.

ADO Build & Deploy - Run Pipeline+

Add useful options for Build & Deploy pipeline in Azure DevOps

  1. // ==UserScript==
  2. // @name ADO Build & Deploy - Run Pipeline+
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.2
  5. // @description Add useful options for Build & Deploy pipeline in Azure DevOps
  6. // @author Victor Ros
  7. // @match https://*.visualstudio.com/*/_build?definitionId=3237*
  8. // @match https://dev.azure.com/*/*/_build?definitionId=3237*
  9. // @match https://*.visualstudio.com/*/_build?definitionId=3333*
  10. // @match https://dev.azure.com/*/*/_build?definitionId=3333*
  11. // @match https://*.visualstudio.com/*/_build?definitionId=4569*
  12. // @match https://dev.azure.com/*/*/_build?definitionId=4569*
  13. // @match https://*.visualstudio.com/*/_build?definitionId=5712*
  14. // @match https://dev.azure.com/*/*/_build?definitionId=5712*
  15. // @icon https://www.google.com/s2/favicons?domain=azure.com
  16. // @grant none
  17. // @license MIT
  18. // ==/UserScript==
  19.  
  20. (async function() {
  21. "use strict";
  22.  
  23. const log = console.log;
  24.  
  25. // Constants
  26. const PREFIX_LOG = "[ADO Build & Deploy - Run Pipeline+]";
  27. const TEXT = {
  28. "fr": {
  29. buttonAllServices: "Tous les services"
  30. },
  31. "en": {
  32. buttonAllServices: "All services"
  33. }
  34. };
  35. const LANGUAGE = getLanguage();
  36. const RUN_PIPELINE_SELECTOR = "__bolt-run-pipeline-command";
  37. const POPUP_SELECTOR = ".bolt-panel-callout-content";
  38. const PARAMETERS_SELECTOR = ".padding-horizontal-20.rhythm-vertical-16 > .flex-noshrink";
  39. const BRANCHES_DROPDOWN_INPUT_SELECTOR = ".version-dropdown > input";
  40. const BUTTON_ID = "deploy-all-service-button";
  41.  
  42. /**
  43. * Returns navigator's language.
  44. * @returns {string} Language. Default "en".
  45. */
  46. function getLanguage() {
  47. // Get language from navigator variable
  48. let language = (
  49. typeof navigator === "object" &&
  50. navigator.language &&
  51. navigator.language.split("-").shift()
  52. );
  53.  
  54. // If not text found, set "en" as default
  55. if (typeof TEXT[language] === "undefined") {
  56. language = "en";
  57. }
  58.  
  59. return language;
  60. }
  61.  
  62. /**
  63. * Wait for the appearance of the HTML elements with the selector.
  64. * @param {string} _selector - The selector
  65. * @param {object} options - Options
  66. * @param {string} options.name - Selector name (Default to _selector value)
  67. * @param {number} options.maxRetry - Number of retry (Default to 0)
  68. * @param {number} options.timeout - Time to wait before retrying (Default to 1 sec)
  69. * @returns {Promise<void>} Empty promise.
  70. * @async
  71. */
  72. async function waitFor(_selector, {name = _selector, maxRetry = 0, timeout = 1000} = {}) {
  73. const result = document.querySelectorAll(_selector);
  74. if (result.length > 0) {
  75. log(`${PREFIX_LOG} ${name} found (${result.length})`);
  76. return;
  77. } else if (maxRetry > 0) {
  78. log(`${PREFIX_LOG} Wait for ${name} (Remaining retries: ${--maxRetry})`);
  79.  
  80. await new Promise((_resolve, _reject) => {
  81. setTimeout(async () => {
  82. try {
  83. await waitFor(_selector, {name, maxRetry, timeout});
  84. _resolve();
  85. } catch (_err) {
  86. _reject(_err);
  87. }
  88. }, timeout);
  89. });
  90. } else {
  91. throw new Error(`Cannot find elements with selector: ${_selector}`);
  92. }
  93. }
  94.  
  95. /**
  96. * Get services HTML elements from Run Pipeline popup.
  97. * @returns {void} Nothing.
  98. */
  99. function getServiceElements() {
  100. const popup = document.querySelector(POPUP_SELECTOR);
  101. const elements = popup.querySelectorAll(PARAMETERS_SELECTOR);
  102. const svcElements = [];
  103. let firstServiceFound = false;
  104. // Ignore all parameters until "events-service" parameter
  105. elements.forEach((_elt, _idx) => {
  106. if (_elt.innerHTML.includes("events-service")) {
  107. firstServiceFound = true;
  108. }
  109. if (firstServiceFound) {
  110. svcElements.push(_elt);
  111. }
  112. });
  113. if (svcElements.length === 0) {
  114. log(`${PREFIX_LOG} Something went wrong while retrieving service elements.`);
  115. }
  116. return svcElements;
  117. }
  118.  
  119. /**
  120. * Add a button to select/unselect all services.
  121. * Recreate the HTML button.
  122. * @returns {HTMLElement} Button "All services".
  123. */
  124. function createButtonAllServices(_svcElements) {
  125. const firstSvcElement = _svcElements[0];
  126. const buttonAllServices = firstSvcElement.cloneNode(true);
  127. // Unselect by default
  128. buttonAllServices.setAttribute("aria-checked", false);
  129. buttonAllServices.classList.remove("checked");
  130. // Override id from div child
  131. const divChild = buttonAllServices.querySelector(".bolt-checkbox-label");
  132. divChild.setAttribute("id", BUTTON_ID);
  133. divChild.innerHTML = TEXT[LANGUAGE].buttonAllServices;
  134.  
  135. // Add click event listener
  136. buttonAllServices.addEventListener("click", (_event) => {
  137. _event.stopPropagation();
  138. _event.preventDefault();
  139.  
  140. const oldChecked = buttonAllServices.getAttribute("aria-checked") === "true";
  141. const newChecked = !oldChecked;
  142. const action = newChecked === true ? "add" : "remove";
  143.  
  144. // Update aria-checked
  145. buttonAllServices.setAttribute("aria-checked", newChecked);
  146. buttonAllServices.classList[action]("checked");
  147.  
  148. // Get services' parameters elements and all parameters elements
  149. const svcElements = getServiceElements();
  150. for (let svcElt of svcElements) {
  151. const svcChecked = svcElt.getAttribute("aria-checked") === "true";
  152. // Button is checked but service is not, or button is unchecked and service is.
  153. if (newChecked && !svcChecked || !newChecked && svcChecked) {
  154. svcElt.click();
  155. }
  156. }
  157.  
  158. buttonAllServices.focus();
  159. });
  160.  
  161. // Add div element before first service element
  162. firstSvcElement.insertAdjacentElement("beforebegin", buttonAllServices);
  163.  
  164. return buttonAllServices;
  165. }
  166.  
  167. async function run() {
  168. log(`${PREFIX_LOG} Init...`);
  169.  
  170. // Search by ID Run Pipeline button
  171. const buttonRunPipeline = document.getElementById(RUN_PIPELINE_SELECTOR);
  172. if (typeof buttonRunPipeline === "undefined") {
  173. log(`${PREFIX_LOG} Cannot find Run pipeline button`);
  174. return;
  175. } else {
  176. log(`${PREFIX_LOG} Found Run pipeline button`, buttonRunPipeline);
  177. }
  178.  
  179. buttonRunPipeline.addEventListener("click", async () => {
  180. try {
  181. // Wait 5 sec max for parameters elements to be present in Run Pipeline popup
  182. await waitFor(PARAMETERS_SELECTOR, {name: "Services parameters", maxRetry: 10, timeout: 500});
  183.  
  184. // Get services' parameters elements and all parameters elements
  185. const svcElements = getServiceElements();
  186.  
  187. // Add div element before first service element
  188. const buttonAllServices = createButtonAllServices(svcElements);
  189.  
  190. log(`${PREFIX_LOG} Added button "${TEXT[LANGUAGE].buttonAllServices}"`);
  191. } catch (_err) {
  192. log(`${PREFIX_LOG} ${_err.stack}`);
  193. }
  194. });
  195.  
  196. log(`${PREFIX_LOG} Finished!`);
  197. }
  198.  
  199. // Events
  200. window.addEventListener("load", run);
  201. window.removeEventListener("unload", run);
  202. })();