Cloudflare Pages: Auto Delete Old Deployments

Maintain a clean deployment history by automating the cleanup of old Cloudflare Pages deployments using a simple Worker script.

Cloudflare Pages automatically creates new deployments whenever you push changes to your repository. While this continuous deployment feature is incredibly convenient, it can lead to an ever-growing list of deployment histories that you might never need again.

Solution: Create and deploy a Cloudflare Worker script that periodically deletes deployments older than a specified time by calling the Cloudflare API.

package.json

{
  "private": true,
  "type": "module",
  "scripts": {
    "build": "cross-env ENVIRONMENT=production esbuild workers/pages-cleanup/src/index.ts --bundle --format=esm --minify --outfile=workers/pages-cleanup/dist/index.js --legal-comments=none",
    "dev": "npm run watch && npm run start",
    "publish": "wrangler publish --config workers/pages-cleanup/wrangler.toml",
    "start": "miniflare workers/pages-cleanup/dist/index.js --wrangler-config workers/pages-cleanup/wrangler.toml --watch",
    "watch": "cross-env ENVIRONMENT=development esbuild workers/pages-cleanup/src/index.ts --bundle --format=esm --minify --outfile=workers/pages-cleanup/dist/index.js --sourcemap --watch --legal-comments=none"
  },
  "devDependencies": {
    "@cloudflare/workers-types": "^4.20230801.0",
    "wrangler": "^3.4.0"
  }
}

types.d.ts

/// <reference types="@cloudflare/workers-types" />

src/index.ts

interface ScheduleEnv {
  readonly ACCOUNT_ID: string;
  readonly PROJECT_NAME: string;
  readonly API_TOKEN: string;
  readonly EXPIRATION_DAYS: string;
}

function debugLog(message: string): void {
  console.debug(`[${new Date().toISOString()}] ${message}`);
}

const scheduled: ExportedHandlerScheduledHandler<ScheduleEnv> = async (
  event,
  env
) => {
  debugLog(
    `Account "${env.ACCOUNT_ID}", project "${
      env.PROJECT_NAME
    }". Deleting all deployments ${
      env.EXPIRATION_DAYS
    } days older than ${new Date(event.scheduledTime).toISOString()}...`
  );

  const endpoint = `https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/pages/projects/${env.PROJECT_NAME}/deployments`;
  const init = {
    headers: {
      "Content-Type": "application/json;charset=UTF-8",
      Authorization: `Bearer ${env.API_TOKEN}`,
    },
  };
  const deployments: { id: string; created_on: string }[] = [];
  let count = 0;
  let total: number | null = null;
  let page = 1;
  do {
    const response: any = await (
      await fetch(`${endpoint}?page=${page}`, init)
    ).json();
    if (!total) {
      total = response.result_info.total_count as number;
      debugLog(`Found ${total} deployments in project.`);
    }
    count += response.result_info.count;
    debugLog(`Retrieved ${count} deployments, ${total - count} remaining.`);
    deployments.push(...response.result);
    page++;
  } while (count < total);
  debugLog(`All ${deployments.length} deployments retrieved.`);

  let deleted = 0;
  await Promise.all(
    deployments.map(async (deployment) => {
      if (
        (event.scheduledTime - new Date(deployment.created_on).getTime()) /
          86400000 >
        parseInt(env.EXPIRATION_DAYS)
      ) {
        await fetch(`${endpoint}/${deployment.id}`, {
          method: "DELETE",
          ...init,
        });
        debugLog(
          `Deleted deployment ${deployment.id} (created on ${deployment.created_on}).`
        );
        deleted++;
      }
    })
  );
  debugLog(`Done. Deleted ${deleted} deployments.`);
};

export default { scheduled };