解決Cloudflare Pages部署紀錄過多導致無法刪除專案的問題

最近要在Cloudflare Pages刪除專案遇到無法刪除的問題,官方文件說這是一個已知問題:

You may not be able to delete your Pages project if it has a high number (over 100) of deployments. The Cloudflare team is tracking this issue.

搜尋網路上有很多自動刪除部署紀錄的方式(包含官方),但我覺得有點麻煩和複雜,所以請Gemini寫一個無腦執行的Python腳本(簡單測個幾次,修正一些錯誤,並加入一些功能)。

注意

本文由 Gemini 3 Pro 語言模型產生。

📌 前言

當 Cloudflare Pages 專案累積超過 100 個部署紀錄時,嘗試刪除專案會出現 Your project has too many deployments to be deleted 錯誤。本教學使用一個安全的 Python 腳本,透過 API 批次清理舊的部署紀錄,讓您能順利刪除專案。

特色:

  • ✅ 免安裝依賴:使用 Linux/macOS/Windows 內建 Python 即可執行。
  • ✅ 安全機制:保留最近 N 筆紀錄、防無限迴圈、支援 Ctrl+C 強制中斷。

第一步:取得 Account ID(帳戶 ID)

這是您在 Cloudflare 的身分識別碼。

  1. 登入 Cloudflare Dashboard
  2. 點擊左側列表的 Workers & Pages
  3. 在頁面的右側欄位(通常要稍微往下滑一點),找到 Account details
  4. 複製 Account ID 下方那串英數字串(例如:b279f1d51...)。

第二步:建立 API Token(API 權限)

我們需要一把專門用來「管理 Cloudflare Pages」的鑰匙。

  1. 點擊右上角的人頭圖示,選擇 My Profile
  2. 點擊左側的 API Tokens
  3. 點擊藍色的 Create Token
  4. 重要:不要選範本,請點擊最下方的 Create Custom Token 旁邊的 Get started
  5. 依照以下設定填寫:
    • Token name:隨便取(例如:CF Pages deployments delete script)。
    • Permissions
      • 第一欄選 Account
      • 第二欄選 Cloudflare Pages
      • 第三欄選 Edit(重要!必須是編輯權限才能刪除)。
    • Account Resources
      • 第一欄選 Include
      • 第二欄選 All accounts(或者指定您的帳戶)。
  6. 點擊 Continue to summaryCreate Token
  7. 複製顯示出來的 API Token(這串密碼只會顯示這一次,請妥善保存)。

第三步:準備 Python 腳本

  1. 在您的電腦上建立一個新檔案,命名為 delete-cf-pages-deployments.py
  2. 將以下程式碼完整複製並貼上存檔:
    import urllib.request
    import urllib.error
    import json
    import time
    import ssl
    import sys
    import getpass
    
    # ==========================================
    # Cloudflare Pages 部署紀錄清理工具 v3.0 (翻頁修正版)
    # 更新日誌:修復當「保留筆數」大於每頁筆數(25)時,程式會誤判提早結束的問題
    # ==========================================
    
    def get_input(prompt, hidden=False):
        if hidden:
            return getpass.getpass(prompt)
        return input(prompt).strip()
    
    def make_request(url, token, method="GET"):
        try:
            req = urllib.request.Request(
                url,
                headers={
                    "Authorization": f"Bearer {token}",
                    "Content-Type": "application/json"
                },
                method=method
            )
            context = ssl.create_default_context()
            if hasattr(ssl, '_create_unverified_context'):
                context = ssl._create_unverified_context()
    
            with urllib.request.urlopen(req, context=context) as response:
                if method == "DELETE":
                    return True
                return json.loads(response.read().decode())
        except urllib.error.HTTPError as e:
            return {"error": e.code, "reason": e.reason}
        except Exception as e:
            print(f"❌ 發生未預期的錯誤: {e}")
            sys.exit(1)
    
    def main():
        print("\n🧹 Cloudflare Pages 歷史部署清理工具 v3.0")
        print("========================================")
    
        account_id = get_input("請輸入 Account ID: ")
        project_name = get_input("請輸入 Project Name (專案名稱): ")
        api_token = get_input("請輸入 API Token (輸入時隱藏): ", hidden=True)
    
        keep_str = get_input("請問要保留最近幾筆紀錄?(預設 3,包含線上版): ")
        try:
            keep_num = int(keep_str) if keep_str else 3
        except ValueError:
            keep_num = 3
    
        if not all([account_id, project_name, api_token]):
            print("❌ 錯誤:所有欄位皆為必填。")
            return
    
        base_url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/pages/projects/{project_name}/deployments"
    
        print(f"\n🔍 正在連接專案 [{project_name}] ...")
    
        # 測試連線
        initial_check = make_request(f"{base_url}?per_page=1", api_token)
        if "error" in initial_check:
            print(f"❌ 連接失敗!代碼: {initial_check['error']} ({initial_check['reason']})")
            return
    
        print("✅ 連接成功!")
        print(f"⚠️  警告:將刪除 [{project_name}] 舊部署,僅保留最近 {keep_num} 筆 (不重複)。")
        confirm = get_input(f"請輸入 'yes' 確認開始執行: ")
        if confirm.lower() != 'yes':
            print("已取消操作。")
            return
    
        total_deleted = 0
        kept_ids = set()
        current_page = 1  # [v3.0 新增] 頁碼控制
    
        while True:
            # [v3.0 修改] 請求時帶入當前頁碼
            request_url = f"{base_url}?per_page=25&page={current_page}"
            data = make_request(request_url, api_token)
            deployments = data.get("result", [])
    
            # 如果這一頁沒資料了,代表真的找完了
            if not deployments:
                print(f"\n🎉 第 {current_page} 頁無資料,掃描結束!")
                break
    
            print(f"\n📋 讀取第 {current_page} 頁 (本頁 {len(deployments)} 筆)...")
    
            batch_deleted = 0
    
            for dep in deployments:
                d_id = dep['id']
    
                # 1. 檢查是否已經在保留名單中
                if d_id in kept_ids:
                    continue
    
                # 2. 如果保留名額還沒滿,就加入保留名單
                if len(kept_ids) < keep_num:
                    kept_ids.add(d_id)
                    # 這裡不印 log 了,避免大量保留時洗版,只在最後統計顯示
                    continue
    
                # 3. 名額已滿 -> 刪除!
                result = make_request(f"{base_url}/{d_id}?force=true", api_token, method="DELETE")
    
                if result is True:
                    print(f"   ✅ 已刪除: {d_id}")
                    batch_deleted += 1
                    total_deleted += 1
                    time.sleep(0.2)
                else:
                    print(f"   ⏭️  跳過: {d_id} (可能是線上版本或鎖定中)")
                    # 雖然刪除失敗(例如線上版),但也算處理過了,不應該卡住
                    # 但因為線上版通常在第一頁,所以邏輯上不會影響翻頁
    
            # [v3.0 核心邏輯] 翻頁判斷
            if batch_deleted > 0:
                # 情況 A: 這一頁有刪除東西
                # 當我們刪除項目後,後面的資料會「遞補」上來填補空缺
                # 所以我們應該「停留在同一頁」再檢查一次,看看遞補上來的是不是也要刪
                print(f"   🔄 本頁有刪除 {batch_deleted} 筆,將重新掃描本頁面以處理遞補項目...")
                # current_page 不變
            else:
                # 情況 B: 這一頁全部都保留(或跳過)
                # 代表這一頁的檢查已經完美結束,我們必須「翻到下一頁」去找更舊的資料
                print(f"   ✅ 本頁所有項目皆保留,準備翻到下一頁...")
                current_page += 1
    
            print(f"⏳ 休息 1 秒...")
            time.sleep(1)
    
        print("\n" + "="*40)
        print(f"🏁 清理完成!")
        print(f"🛡️  最終保留: {len(kept_ids)} 筆")
        print(f"🗑️  實際刪除: {total_deleted} 筆")
        print("="*40)
    
    if __name__ == "__main__":
        try:
            main()
        except KeyboardInterrupt:
            print("\n\n❌ 使用者強制中斷程式。")
    

第四步:執行與刪除

  1. 開啟終端機。

  2. 進入您存放腳本的資料夾(例如 cd Downloads)。

  3. 執行指令:

    python3 delete-cf-pages-deployments.py
    
  4. 依照畫面提示輸入:

    • Account ID:貼上第一步取得的 ID。
    • Project Name:貼上您想刪除的專案名稱(例如 my-blog)。
    • API Token:貼上第二步建立的密鑰(輸入時畫面不會顯示,這是正常的,貼上後按 Enter 即可)。
    • 保留筆數:建議輸入 35(若要完全刪除專案,留少一點比較好刪)。
  5. 輸入 yes 確認執行。

腳本會開始自動運作,您會看到它一筆一筆地刪除。

第五步:回到 Cloudflare 刪除專案

當腳本跑完並顯示 🏁 清理完成! 後:

  1. 回到 Cloudflare DashboardWorkers & Pages
  2. 點進該專案 → Settings
  3. 滑到最下方點擊 Delete project
  4. 這次您應該就能刪除成功了!

💡 小提醒

  • 可以中斷嗎?可以。隨時按 Ctrl + C 可強制停止腳本,Cloudflare 端也會立刻停止動作,不會繼續刪除。
  • 為什麼要設定保留筆數?
    • 為了備份(推薦):建議保留 3~5 筆,這樣當新版網站有問題時,您還有舊版本的紀錄可以一鍵還原(Rollback)。
    • 為了徹底刪除:如果您是為了刪除整個專案而執行此腳本,可以輸入 01,腳本會刪除所有能刪的紀錄,只留下最後一個系統鎖定無法刪除的線上版本(這是正常現象,接著再去網頁版刪除專案即可)。