Cloudflare Pages: alte Deployments nach 7 Tagen löschen
Den Source-Code von dem Cloudflare Worker hänge ich unten an.
Unter Cloudflare einen neuen Worker erstellem. Via “Einstellungen” > “Variablen und geheime Schlüssel” folgende Sachen angeben:
Typ | Name | Wert |
Klartext | CLOUDFLARE_ACCOUNT_ID | Die Account-ID, die auch bei Wrangler zum Deployment genutzt wird. |
Geheimnis | CLOUDFLARE_API_SECRET | Den API-Key; kann auch von Wrangler übernommen werden |
Klartext | CLOUDFLARE_PAGENAME | Der Name der Seite; zum Beispiel tinokuptzblog |
Sieht im UI wie folgt aus:

Unter Auslöseereignisse einen neuen Cron-Trigger hinzufügen. Intervall = Tag des Monats, Tag = Täglich, zur UTC-Zeit = 03:00 (beispielsweise).

Der Worker wird anschließend jeden Tag ausgeführt, liest jeden alle Deployments die älter als 7 Tage sind aus und löscht diese.
Er kann via Worker-Overfläche direkt gestartet werden um zu schauen, ob er sauber läuft. Via console.log loggt er, was er tut, und warum er das macht. Das neueste Deployment skippt er automatisch, denn das ist ja i.d.R. das, welches auch live genutzt wird – und somit sowieso gelöscht werden kann.
/E: Update 03/2025 durch einen Bug wurden manchmal deployments nicht zuverlässig gelöscht. Der ist nun behoben 🙂
/E: Update 03/2025 V2: nun werden auch mehrere Pages-Projekte unterstützt, die dieses Script clearen kann, indem CLOUDFLARE_PAGENAME
eine Liste mit mit Komma getrennten Projekten ist (“blog,website,download
“)
Quellcode des Workers
const expirationDays = 7;
/**
* Liest die Base-URL aus
* @param {object} env
* @returns {string}
*/
const getBaseUrl = (env, pagename) => `https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}/pages/projects/${pagename}/deployments`;
/**
* Holt sich die Deployments, und gibt diese als Array zurück.
* Das letzte Deployment kann nicht gelöscht werden - dieses wird also weggeschnitten
* NEU: reduziert auf 15 Deployments, damit wir kein rate limiting hitten
* @returns {Promise<object[]>}
*/
const fetchDeployments = async (env, pagename) => {
console.log(`fetching deployments in ${pagename}`)
const fetchParams = {
headers: {
"Content-Type": "application/json;charset=UTF-8",
"Authorization": `Bearer ${env.CLOUDFLARE_API_TOKEN}`,
},
};
const response = await fetch(getBaseUrl(env, pagename), fetchParams);
var ret = await response.json();
if(!ret.success) {
console.error({ ret })
return [];
}
ret = ret.result;
// @todo: hier beachten, dass die aktuelle Live-Deployment ggfs nicht die aktuellste ist
// Stand jetzt geht der Worker davon aus, dass der letzte deployte auch immer live ist
ret.shift();
// Nun noch mal direkt zur Verarbeitung anpassen
const now = Date.now();
for (var i = 0; i < ret.length; i++) {
ret[i].age = {
created: new Date(ret[i].created_on),
};
ret[i].age.hours = Math.round((now - ret[i].age.created) / (1000 * 60 * 60));
ret[i].age.days = Math.round(ret[i].age.hours / 24);
}
return ret.slice(0, 15);
};
/**
* Löscht ein Deployment
* @param {object} env
* @param {string} id
* @returns {Promise}
*/
const deleteDeployment = async (env, deployment, pagename) => {
console.log(`fetching deployments for ${pagename}`)
const fetchParams = {
method: "DELETE",
headers: {
"Content-Type": "application/json;charset=UTF-8",
"Authorization": `Bearer ${env.CLOUDFLARE_API_TOKEN}`,
},
};
const data = await fetch(`${getBaseUrl(env, pagename)}/${deployment.id}`, fetchParams);
if(data.ok)
return true;
console.error(`failed to delete ${pagename}/${deployment.id}`, await data.text());
console.error(`URL: ${getBaseUrl(env, pagename)}/${deployment.id}`);
}
const runForPage = async (env, pagename) => {
console.log(`running for ${pagename}`)
const deployments = await fetchDeployments(env, pagename);
var promiseQueue = [];
var retJson = { skipped: [], deleted: [] };
for (const deployment of deployments) {
if (deployment.age.days < expirationDays) {
console.log(`skipping deployment ${pagename}/${deployment.short_id}: only ${deployment.age.days} days old`)
retJson.skipped.push(`${pagename}/${deployment.short_id}`)
continue;
}
console.log(`deleting deployment ${pagename}/${deployment.short_id} (${deployment.age.days} days old)`)
promiseQueue.push(deleteDeployment(env, deployment, pagename))
retJson.deleted.push(`${pagename}/${deployment.short_id}`)
}
console.log(`waiting for all ${promiseQueue.length} api calls in ${pagename} to finish`)
await Promise.all(promiseQueue);
console.log(`all calls in ${promiseQueue.length} finished`)
return retJson;
}
export default {
async fetch(_request, env) {
const pages = env.CLOUDFLARE_PAGENAME.split(',');
var retJson = { skipped: [], deleted: [] };
for (const pagename of pages) {
console.log(`currently running for: ${pagename}`)
const ret = await runForPage(env, pagename.trim());
retJson.skipped = retJson.skipped.concat(ret.skipped);
retJson.deleted = retJson.deleted.concat(ret.deleted);
console.log(`run for ${pagename} finished`)
}
return new Response(JSON.stringify(retJson))
},
async scheduled(_event, env) {
const pages = env.CLOUDFLARE_PAGENAME.split(',');
var retJson = { skipped: [], deleted: [] };
for (const pagename of pages) {
console.log(`currently running for: ${pagename}`)
const ret = await runForPage(env, pagename.trim());
retJson.skipped = retJson.skipped.concat(ret.skipped);
retJson.deleted = retJson.deleted.concat(ret.deleted);
console.log(`run for ${pagename} finished`)
}
}
}
Code language: JavaScript (javascript)