Last active
March 27, 2023 07:32
-
-
Save cosmith/8544e21ca56acd696952cffb9435ec91 to your computer and use it in GitHub Desktop.
CircleCI price of job user script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function findElementAfterText(text) { | |
const xpathExpression = `//text()[contains(., '${text}')]`; | |
const xpathResult = document.evaluate( | |
xpathExpression, | |
document, | |
null, | |
XPathResult.FIRST_ORDERED_NODE_TYPE, | |
null | |
); | |
return xpathResult.singleNodeValue; | |
} | |
function findResourceClass() { | |
const elementAfterText = findElementAfterText("Executor / Resource Class"); | |
if (elementAfterText) { | |
const text = elementAfterText.parentNode.nextElementSibling.innerText; | |
return text; | |
} else { | |
throw new Error("Element not found"); | |
} | |
} | |
function parseDuration(durationString) { | |
const parts = durationString.split(" "); | |
let seconds = 0; | |
for (const part of parts) { | |
const unit = part.slice(-1); | |
const value = parseInt(part.slice(0, -1), 10); | |
if (unit === "h") { | |
seconds += value * 60 * 60; | |
} else if (unit === "m") { | |
seconds += value * 60; | |
} else if (unit === "s") { | |
seconds += value; | |
} | |
} | |
return seconds; | |
} | |
function formatPriceInDollars(amount) { | |
const formatter = new Intl.NumberFormat("en-US", { | |
style: "currency", | |
currency: "USD", | |
}); | |
return formatter.format(amount); | |
} | |
function displayPriceBox(price) { | |
const priceBox = document.createElement("div"); | |
priceBox.textContent = price; | |
priceBox.className = "price-box"; | |
priceBox.style.position = "fixed"; | |
priceBox.style.top = "10px"; | |
priceBox.style.right = "10px"; | |
priceBox.style.backgroundColor = "red"; | |
priceBox.style.color = "white"; | |
priceBox.style.padding = "8px"; | |
priceBox.style.borderRadius = "4px"; | |
priceBox.style.fontSize = "16px"; | |
priceBox.style.zIndex = 1000; | |
cleanup(); | |
document.body.appendChild(priceBox); | |
} | |
function displayButton(onclick) { | |
const button = document.createElement("button"); | |
button.textContent = "Show price"; | |
button.className = "btn-show-price"; | |
button.style.position = "fixed"; | |
button.style.top = "10px"; | |
button.style.right = "10px"; | |
button.style.backgroundColor = "gray"; | |
button.style.color = "white"; | |
button.style.padding = "8px"; | |
button.style.borderRadius = "4px"; | |
button.style.borderWidth = "0"; | |
button.style.fontSize = "16px"; | |
button.style.zIndex = 1000; | |
button.style.cursor = "pointer"; | |
button.onclick = onclick; | |
document.body.appendChild(button); | |
} | |
function buttonCallback() { | |
// navigate to timing tab | |
const timingTabButton = document.querySelector( | |
"[data-optimizely=Timing-tab]" | |
); | |
if (!timingTabButton) { | |
return; | |
} | |
// hide button | |
cleanup(); | |
timingTabButton.click(); | |
setTimeout(() => { | |
const durationElements = document.querySelectorAll("span[title=Duration]"); | |
const totalSeconds = Array.from(durationElements).reduce( | |
(total, element) => { | |
const durationString = element.textContent.trim(); | |
const durationSeconds = parseDuration(durationString); | |
return total + durationSeconds; | |
}, | |
0 | |
); | |
const totalMinutes = totalSeconds / 60; | |
const resourceClass = findResourceClass(); | |
const pricingTable = { | |
"Docker / Small": 0.003, | |
"Docker / Medium": 0.006, | |
"Docker / Large": 0.012, | |
"Machine / Linux Medium": 0.006, | |
"Machine / Linux X-Large": 0.06, | |
"MacOS / M1 Large": 0.24, | |
}; | |
const priceInDollars = totalMinutes * pricingTable[resourceClass]; | |
if (isNaN(priceInDollars)) { | |
throw new Error(`Resource class not found ${resourceClass}`); | |
} | |
console.log("Total minutes:", Math.round(totalMinutes)); | |
console.log("Total price: ", formatPriceInDollars(priceInDollars)); | |
displayPriceBox(formatPriceInDollars(priceInDollars)); | |
}, 1000); | |
} | |
function removeElement(className) { | |
Array.from(document.querySelectorAll(className)).map((x) => x.remove()); | |
} | |
function cleanup() { | |
removeElement(".btn-show-price"); | |
removeElement(".price-box"); | |
} | |
const workflowRegex = | |
/^https:\/\/app\.circleci\.com\/pipelines\/github\/([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_-]+)\/(\d+)\/workflows\/([a-f0-9-]+)\/jobs\/.*$/; | |
function mutationCallback() { | |
const currentUrl = window.location.href; | |
const matches = workflowRegex.test(currentUrl); | |
if (!matches) { | |
cleanup(); | |
return; | |
} | |
if (lastUrl !== currentUrl) { | |
lastUrl = currentUrl; | |
displayButton(buttonCallback); | |
} | |
} | |
let lastUrl = window.location.href; | |
const observer = new MutationObserver(mutationCallback); | |
observer.observe(document, { | |
childList: true, | |
subtree: true, | |
}); | |
if (workflowRegex.test(window.location.href)) { | |
displayButton(buttonCallback); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment