Infinite Pages delete error

Hi. Tried this in an open channel, but no dice. There's no Pages Help forum, so I'll try here, too. Here's the situation: I can't delete an old Pages project. The error says "too many deployments." There's a CF link to a project and npm command that deletes the deployments via the terminal. However, that isn't working, either. https://developers.cloudflare.com/pages/platform/known-issues/#delete-a-project-with-a-high-number-of-deployments (I don't really know what version of Pages it's on — two or three.) Error:
Listing next 30 deployments, this may take a while...
Failed to list deployments on page 1... retrying (1/5)
Failed to list deployments on page 1... retrying (2/5)
Failed to list deployments on page 1... retrying (3/5)
Failed to list deployments on page 1... retrying (4/5)
Failed to list deployments on page 1... retrying (5/5)
Failed to list deployments on page 1.
Error: Could not fetch deployments for ...
Listing next 30 deployments, this may take a while...
Failed to list deployments on page 1... retrying (1/5)
Failed to list deployments on page 1... retrying (2/5)
Failed to list deployments on page 1... retrying (3/5)
Failed to list deployments on page 1... retrying (4/5)
Failed to list deployments on page 1... retrying (5/5)
Failed to list deployments on page 1.
Error: Could not fetch deployments for ...
I believe I passed the script the right API token, account ID, and project name. Is my only option to try and delete a few hundred deployments by hand? Lol. I can do it, but it's hilariously tragic. More so, if it fails. Any thoughts, ideas, opinions, or better CF scripts?
5 Replies
louren.co
louren.co2mo ago
I followed this https://medium.com/@jaredpotter1/connecting-puppeteer-to-existing-chrome-window-8a10828149e0 , signed it to cloudflare on that chrome instance, and then vibe coded this, but the max I could delete was like 30 deployments before some error from the page being rerendered.
import puppeteer from "puppeteer";
async function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

(async () => {
const browser = await puppeteer.connect({
browserWSEndpoint:
"ws://<WEBSOCKET_FROM_THE_ARTICLE>",
}); // set true for headless
const page = await browser.newPage();

await page.goto(
"https://dash.cloudflare.com/<SOME-NUMBER>/pages/view/<PAGES_PROJECT_NAME>",
{ waitUntil: "networkidle0" }
);

while (true) {
// Re-query all currently visible menu triggers
const buttons = await page.$$('[data-testid="more-options-menu-trigger"]');

// Find the first button with an enabled delete button in its menu
let foundEnabled = false;

for (let i = 0; i < buttons.length; i++) {
// REFRESH the handle — don’t use buttons[i] directly
const currentButtons = await page.$$(
'[data-testid="more-options-menu-trigger"]'
);
const button = currentButtons[i];
if (!button) continue;

try {
await button.click();
await delay(300);

const menu = await page.evaluateHandle((btn) => {
let sibling = btn.nextElementSibling;
while (sibling) {
if (sibling.getAttribute("role") === "menu") return sibling;
sibling = sibling.nextElementSibling;
}
return null;
}, button);
import puppeteer from "puppeteer";
async function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

(async () => {
const browser = await puppeteer.connect({
browserWSEndpoint:
"ws://<WEBSOCKET_FROM_THE_ARTICLE>",
}); // set true for headless
const page = await browser.newPage();

await page.goto(
"https://dash.cloudflare.com/<SOME-NUMBER>/pages/view/<PAGES_PROJECT_NAME>",
{ waitUntil: "networkidle0" }
);

while (true) {
// Re-query all currently visible menu triggers
const buttons = await page.$$('[data-testid="more-options-menu-trigger"]');

// Find the first button with an enabled delete button in its menu
let foundEnabled = false;

for (let i = 0; i < buttons.length; i++) {
// REFRESH the handle — don’t use buttons[i] directly
const currentButtons = await page.$$(
'[data-testid="more-options-menu-trigger"]'
);
const button = currentButtons[i];
if (!button) continue;

try {
await button.click();
await delay(300);

const menu = await page.evaluateHandle((btn) => {
let sibling = btn.nextElementSibling;
while (sibling) {
if (sibling.getAttribute("role") === "menu") return sibling;
sibling = sibling.nextElementSibling;
}
return null;
}, button);
Medium
Connecting Puppeteer to Existing Chrome Window
Be itself Puppeteer (https://github.com/GoogleChrome/puppeteer) is a great tool created by Google to assist with automated UI testing…
louren.co
louren.co2mo ago
if (!menu) {
await page.keyboard.press("Escape");
continue;
}

const deleteBtnHandle = await menu.evaluateHandle((menuEl) => {
return (
Array.from(menuEl.querySelectorAll("button[role='menuitem']")).find(
(b) =>
b.innerText.includes("Delete deployment") &&
!b.disabled &&
!b.hasAttribute("disabled")
) || null
);
});

const deleteBtn = deleteBtnHandle.asElement();
if (deleteBtn) {
foundEnabled = true;

await deleteBtn.click();

try {
const confirmBtn = await page.waitForSelector(
'[data-testid="confirm"]',
{
visible: true,
timeout: 3000,
}
);
await confirmBtn.click();
} catch {
console.log("No confirm appeared, skipping");
}

await delay(800); // let virtualized list render next items
await page.keyboard.press("Escape");
break; // only handle one deletion per while iteration
}

await page.keyboard.press("Escape");
} catch (err) {
console.log("Button detached or click failed, skipping this one");
}
}

if (!foundEnabled) {
console.log("No more enabled Delete buttons visible, finished!");
break;
}
}
if (!menu) {
await page.keyboard.press("Escape");
continue;
}

const deleteBtnHandle = await menu.evaluateHandle((menuEl) => {
return (
Array.from(menuEl.querySelectorAll("button[role='menuitem']")).find(
(b) =>
b.innerText.includes("Delete deployment") &&
!b.disabled &&
!b.hasAttribute("disabled")
) || null
);
});

const deleteBtn = deleteBtnHandle.asElement();
if (deleteBtn) {
foundEnabled = true;

await deleteBtn.click();

try {
const confirmBtn = await page.waitForSelector(
'[data-testid="confirm"]',
{
visible: true,
timeout: 3000,
}
);
await confirmBtn.click();
} catch {
console.log("No confirm appeared, skipping");
}

await delay(800); // let virtualized list render next items
await page.keyboard.press("Escape");
break; // only handle one deletion per while iteration
}

await page.keyboard.press("Escape");
} catch (err) {
console.log("Button detached or click failed, skipping this one");
}
}

if (!foundEnabled) {
console.log("No more enabled Delete buttons visible, finished!");
break;
}
}
abelsj60
abelsj60OP2mo ago
That's a very clever idea. I'll take a deeper look at it later. Thanks.
Walshy
Walshy2mo ago
You may need to turn down the DEPLOYMENTS_PER_PAGE and PAGINATION_BATCH_SIZE variables in the script
abelsj60
abelsj60OP2mo ago
Another good option — thanks, I'll look at that, too. I'm struggling with some favicons and will look at this afterwards. For the life of me, I'll never understand why favicons are a hassle. In the end, I did a lot of hand deletion until I got it small enough that the dashboard UI worked. It may have been around 100, at some point, I stopped looking too closely. I did try adjusting Walshy's variables, but, even when I set them both to 1, the script still errored out.

Did you find this page helpful?