Skip to content

Instantly share code, notes, and snippets.

@cosmith
Last active March 27, 2023 07:32
Show Gist options
  • Save cosmith/8544e21ca56acd696952cffb9435ec91 to your computer and use it in GitHub Desktop.
Save cosmith/8544e21ca56acd696952cffb9435ec91 to your computer and use it in GitHub Desktop.
CircleCI price of job user script
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