Godot 4.6 Web 遊戲自動部署到 Cloudflare Pages:GitHub Actions 踩坑全紀錄
背景
CyberDesk 是一款用 Godot 4.6.2 開發的 Web 遊戲(資安培訓互動劇本),部署在 Cloudflare Pages,網址 game.itsmygo.uk。
我希望達到的自動化流程是:
1. 在 Linear issue 留言 @claude 描述要改什麼
2. Claude Code(GitHub Action)自動開 PR
3. PR 一開,自動跑 Godot Web Export → push 到 PR 分支
4. Cloudflare Pages 自動建 preview URL → 我可以實際試玩
5. OK 就 merge → 觸發 production 部署
Code language: CSS (css)整個流程裡最核心的關鍵就是:讓 GitHub Actions 在 Linux container 內跑 Godot headless export。
聽起來很簡單對吧?這篇就是來告訴你:不簡單。
為什麼需要 CI 來跑 Godot export
原本的部署方式是本機跑一支 deploy.sh:
# deploy.sh(本機版)
GODOT="C:/Users/User/work/Godot_v4.6.2-stable_win64.exe/..."
$GODOT --headless --export-release "Web" build/web/index.html
gzip -f build/web/index.wasm
git add build/web/
git commit -m "..."
git push origin master
Code language: PHP (php)問題是:
- 路徑寫死 Windows:CI 跑不了
- 要記得手動跑:merge 完之後常常忘記,導致線上版本跟 master 不同步
- Claude Code 開 PR 時不會幫你跑 Godot:它只改
.gd檔,不會 export
所以勢必要把這套搬到 GitHub Actions 上。
最終的成功方案(先給魚)
懶得看踩坑過程的可以直接用這份。完整檔案:
.github/workflows/deploy.yml
name: Deploy Web Build
on:
push:
branches: [master]
paths:
- 'scripts/**'
- 'desktop.gd'
- 'desktop.tscn'
- 'project.godot'
- 'export_presets.cfg'
- 'fonts/**'
- 'icon.svg'
- 'deploy-ci.sh'
- '.github/workflows/deploy.yml'
pull_request:
branches: [master]
types: [opened, synchronize, reopened]
paths:
- 'scripts/**'
- 'desktop.gd'
- 'desktop.tscn'
- 'project.godot'
- 'export_presets.cfg'
- 'fonts/**'
- 'icon.svg'
- 'deploy-ci.sh'
- '.github/workflows/deploy.yml'
workflow_dispatch:
concurrency:
group: deploy-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build-and-deploy:
runs-on: ubuntu-latest
timeout-minutes: 15
container:
image: barichello/godot-ci:4.6.2
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.DEPLOY_PAT }}
fetch-depth: 0
ref: ${{ github.head_ref || github.ref }}
- name: Mark workspace safe for git
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Install runtime dependencies
run: |
apt-get update -qq
apt-get install -y -qq --no-install-recommends python3 libfontconfig1
- name: Verify Godot version
run: godot --version
- name: Install Godot 4.6.2 export templates
run: |
TEMPLATE_DIR="/github/home/.local/share/godot/export_templates/4.6.2.stable"
if [ -f "${TEMPLATE_DIR}/web_release.zip" ]; then
echo "Templates 已存在,跳過下載"
exit 0
fi
mkdir -p "${TEMPLATE_DIR}"
cd /tmp
wget -q "https://github.com/godotengine/godot/releases/download/4.6.2-stable/Godot_v4.6.2-stable_export_templates.tpz" \
|| wget -q "https://github.com/godotengine/godot-builds/releases/download/4.6.2-stable/Godot_v4.6.2-stable_export_templates.tpz"
unzip -q Godot_v4.6.2-stable_export_templates.tpz
mv templates/* "${TEMPLATE_DIR}/"
- name: Run deploy-ci.sh (import + export + gzip)
run: |
chmod +x deploy-ci.sh
bash deploy-ci.sh
- name: Commit and push build/web/
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add build/web/
if git diff --cached --quiet; then
echo "build/web/ 沒有變動,跳過 commit"
exit 0
fi
TARGET_BRANCH="${{ github.head_ref || github.ref_name }}"
SOURCE_MSG=$(git log -1 --pretty=%s HEAD)
if [ "${{ github.event_name }}" = "pull_request" ]; then
COMMIT_TYPE="preview"
else
COMMIT_TYPE="deploy"
fi
git commit -F - <<COMMIT_MSG
ci(${COMMIT_TYPE}): web build for ${TARGET_BRANCH} [skip ci]
Source: ${SOURCE_MSG}
Commit: ${{ github.sha }}
COMMIT_MSG
git push origin "HEAD:${TARGET_BRANCH}" || (git pull --rebase origin "${TARGET_BRANCH}" && git push origin "HEAD:${TARGET_BRANCH}")
Code language: PHP (php)deploy-ci.sh
#!/bin/bash
set -e
cd "$(dirname "$0")"
GODOT="${GODOT:-godot}"
cp build/web/index.html build/web/index.html.custom 2>/dev/null || true
echo "Godot import (產生 .godot/imported cache)..."
$GODOT --headless --import 2>&1 || echo "警告:godot --import 回傳非 0,繼續嘗試 export"
echo "Godot Web Export..."
$GODOT --headless --export-release "Web" "build/web/index.html"
if [ -f build/web/index.html.custom ]; then
NEW_CONFIG=$(grep -o 'const GODOT_CONFIG = {.*};' build/web/index.html || true)
cp build/web/index.html.custom build/web/index.html
if [ -n "$NEW_CONFIG" ]; then
export NEW_CONFIG
python3 - <<'PY'
import os, re, sys
new_cfg = os.environ['NEW_CONFIG']
with open('build/web/index.html', 'r', encoding='utf-8') as f:
html = f.read()
new_html, count = re.subn(r'const GODOT_CONFIG = \{.*?\};', new_cfg, html)
if count == 0:
print('錯誤:在 index.html 找不到 GODOT_CONFIG,無法替換', file=sys.stderr)
sys.exit(1)
with open('build/web/index.html', 'w', encoding='utf-8') as f:
f.write(new_html)
print(f'已更新 GODOT_CONFIG(替換 {count} 處)')
PY
fi
rm build/web/index.html.custom
fi
# 壓縮 wasm(CF Pages 25MB 限制)
MAGIC=$(od -An -tx1 -N2 build/web/index.wasm | tr -d ' \n')
if [ "$MAGIC" = "0061" ]; then
gzip -f build/web/index.wasm
mv build/web/index.wasm.gz build/web/index.wasm
fi
Code language: PHP (php)OK,這就是最後可以動的版本。下面是怎麼一步步爬到這裡的。
踩坑記錄
坑 1:barichello/godot-ci:4.6.2 image 缺 export templates
第一次跑 workflow,掛在 export step:
ERROR: Cannot export project with preset "Web" due to configuration errors:
No export template found at the expected path:
/github/home/.local/share/godot/export_templates/4.6.2.stable/web_debug.zip
No export template found at the expected path:
/github/home/.local/share/godot/export_templates/4.6.2.stable/web_release.zip
Code language: JavaScript (javascript)barichello/godot-ci 這個 image 號稱已經內建 Godot 跟 export templates,但實際上 4.6.2 這個 tag 內 templates 沒有被放在 Godot 預期的路徑($HOME/.local/share/godot/export_templates/<version>.stable/)。
可能的原因:
- image build 時是 root,HOME 是
/root/.local/...,但 GitHub Actions runtime HOME 是/github/home/.local/... - 或是 image 內版本字串不一致(
4.6.2-stablevs4.6.2.stable)
最可靠的解法:workflow 自己下載 templates,不依賴 image:
- name: Install Godot 4.6.2 export templates
run: |
TEMPLATE_DIR="/github/home/.local/share/godot/export_templates/4.6.2.stable"
mkdir -p "${TEMPLATE_DIR}"
cd /tmp
wget -q "https://github.com/godotengine/godot/releases/download/4.6.2-stable/Godot_v4.6.2-stable_export_templates.tpz"
unzip -q Godot_v4.6.2-stable_export_templates.tpz
mv templates/* "${TEMPLATE_DIR}/"
Code language: JavaScript (javascript)注意:用 /github/home 這個絕對路徑,不要用 $HOME,因為 container 內 root user 的 HOME 是 /root 但 GitHub Actions step 跑起來時 HOME 變成 /github/home,會錯位。
坑 2:python3 不存在 + 2>/dev/null 把錯誤吞掉了
修好 templates 後,export 終於跑過了,但出現一條警告:
警告:無法更新 GODOT_CONFIG,使用舊值
deploy-ci.sh 裡的這段 python:
python3 -c "
import re
html = open('build/web/index.html','r',encoding='utf-8').read()
new_cfg = '''$NEW_CONFIG'''
...
" 2>/dev/null || echo "警告:無法更新 GODOT_CONFIG,使用舊值"
Code language: PHP (php)為什麼這是個大問題
GODOT_CONFIG 是 Godot Web build 用的 JS 變數,內容長這樣:
const GODOT_CONFIG = {
"executable": "index",
"fileSizes": {
"index.pck": 18754960,
"index.wasm": 9435514
},
...
};
Code language: JavaScript (javascript)fileSizes 必須跟實際的 index.pck / index.wasm 對得上,Godot loader 才能正確顯示載入進度條跟驗證檔案完整性。
如果 python 失敗,index.html 保留舊的 GODOT_CONFIG,但 index.pck 跟 index.wasm 是新的——大小對不上,runtime 會炸。
為什麼 python3 失敗
兩個原因疊加:
barichello/godot-ciimage 沒裝 python3(或裝了但叫python而非python3)2>/dev/null把錯誤吞了——導致根本看不到「python3: command not found」這行訊息,只看到一個輕飄飄的「使用舊值」警告
修正
兩件事一起做:
(1) 確保 python3 存在——在 deploy.yml 加一個 install step:
- name: Install runtime dependencies
run: |
apt-get update -qq
apt-get install -y -qq --no-install-recommends python3 libfontconfig1
Code language: JavaScript (javascript)libfontconfig1 是順手裝的,雖然 fontconfig 警告無害(專案用自訂字型),但 log 看起來乾淨多了。
(2) python 寫法改用 env var + quoted heredoc + 拿掉 2>/dev/null:
原本錯誤寫法:
python3 -c "
new_cfg = '''$NEW_CONFIG''' # ← shell 直接把字串塞進來,可能炸
" 2>/dev/null # ← 錯誤被吞
Code language: PHP (php)正確寫法:
export NEW_CONFIG
python3 - <<'PY' # ← quoted delimiter,python 原始碼不被 shell 解析
import os, re, sys
new_cfg = os.environ['NEW_CONFIG'] # ← 從 env var 拿
new_html, count = re.subn(r'const GODOT_CONFIG = \{.*?\};', new_cfg, html)
if count == 0:
print('錯誤:找不到 GODOT_CONFIG', file=sys.stderr)
sys.exit(1) # ← 失敗就 fail loud
print(f'已更新 GODOT_CONFIG(替換 {count} 處)')
PY
# 不再有 2>/dev/null
Code language: PHP (php)關鍵改變:
<<'PY'(單引號 quoted heredoc)讓 python source code 不會被 shell 二次解析- 透過
export NEW_CONFIG環境變數傳值,避免 shell 字串內插解析破壞 GODOT_CONFIG 內容 re.subn回傳替換次數,count 為 0 時直接 fail- 拿掉
2>/dev/null,下次出問題時看得到真正的錯誤
坑 3:Cloudflare Pages 邊緣快取讓我以為 deploy 沒成功
修好前面兩坑之後,PR merge → workflow 跑 → commit build/web/ 回 master,全部成功。但開 game.itsmygo.uk 還是看到舊版第 9 關。
第一個直覺:「deploy-ci.sh 是不是沒跑?」
但實際上:
git log --oneline -5
# a494cf3 ci(deploy): web build for master [skip ci] ← bot 自動 commit
# abe14ef Merge pull request #8 ...第9關... ← 我的 merge
git show --stat a494cf3
# build/web/index.html | 2 +-
# build/web/index.pck | Bin 18752080 -> 18754960 bytes ← 真的有變動!
Code language: PHP (php)所以 deploy 完全正常,問題出在:
- CF Pages 邊緣節點快取舊版
index.pck - 瀏覽器自己也快取 binary asset
驗證方法:
# 直接抓線上的 pck 檔案大小,跟 git 對照
curl -sI https://game.itsmygo.uk/index.pck | grep -i content-length
Code language: PHP (php)如果回傳 18752080 → 是舊版(快取沒清) 如果回傳 18754960 → 是新版
解法:
- 無痕視窗測試
- 或 hard refresh(
Ctrl + Shift + R) - 或設定 CF Pages cache rules,讓
.pck/.wasm不要 cache 太久
重點:下次怎麼跟 AI 講才能快速解決
這次踩坑的過程,前期我問 AI 的方式很模糊:「網站沒更新,怎麼辦?」這種問法 AI 只能猜。後來我學會把資訊講清楚,每次問題都能在 1-2 輪內解決。
❌ 不要這樣問
我的 deploy 沒跑 / 網站沒更新 / 怎麼辦
AI 會給你 5 個可能的方向,你還是要自己一個一個試。
✅ 應該這樣問
1. 把實際看到的 vs 預期的講清楚
我預期 PR merge 後
game.itsmygo.uk會看到第 9 關的新介面(AI 助手對話框)
但實際打開還是舊的(記事本介面)
我已經 hard refresh 過了
這樣 AI 馬上知道:
- 預期:第 9 關介面變化
- 實際:沒變化
- 排除:瀏覽器快取已試過
2. 把錯誤訊息**完整**貼上來
❌ 不要說:「export 失敗了」 ✅ 要貼:
ERROR: Cannot export project with preset "Web" due to configuration errors:
No export template found at the expected path:
/github/home/.local/share/godot/export_templates/4.6.2.stable/web_debug.zip
at: _fs_changed (editor/editor_node.cpp:1332)
Code language: JavaScript (javascript)從錯誤訊息裡 AI 可以看到:
- 路徑
/github/home/.local/share/godot/...→ 知道 HOME 是哪 - 檔名
web_debug.zip→ 知道是 export templates 問題 - 版本
4.6.2.stable→ 知道版本字串格式
3. 把「驗證證據」一起給
問題:「為什麼沒更新?」應該補上:
# git 端的證據
$ git log --oneline -5
a494cf3 ci(deploy): web build for master [skip ci]
abe14ef Merge pull request #8 from skcht/claude/issue-6
$ git show --stat a494cf3
build/web/index.pck | Bin 18752080 -> 18754960 bytes
# 線上的證據
$ curl -sI https://game.itsmygo.uk/index.pck | grep content-length
content-length: 18752080 ← 舊的!
Code language: PHP (php)兩邊一比對,AI 馬上看出:「git 是 18754960,線上是 18752080,所以 CF 還沒部署或有快取」——不用猜了。
4. 不要用 2>/dev/null 或 || true 把錯誤吞掉
這個是這次最大的教訓。原本我有:
python3 -c "..." 2>/dev/null || echo "警告:使用舊值"
Code language: JavaScript (javascript)結果問題出現時,log 只看到「警告:使用舊值」,根本不知道 python3 為什麼失敗。後來才發現是 python3 在 container 內根本沒裝。
正確姿勢:錯誤就讓它 fail loud。如果真的要 fallback,至少要把 stderr 抓出來印給人看。
# 錯誤示範
command 2>/dev/null || fallback
# 正確示範
if ! output=$(command 2>&1); then
echo "command 失敗:$output" >&2
fallback
fi
Code language: PHP (php)5. 把環境上下文一起講
當你問 CI 問題,要附:
- 什麼 image / OS —
barichello/godot-ci:4.6.2(Ubuntu 22.04) - 跑什麼指令 —
godot --headless --export-release "Web" - 本機跑得起來嗎? — 對,本機 Windows + Godot 4.6.2 OK
AI 看到「本機 OK,CI 不 OK」就會優先往「環境差異」方向找問題(缺套件、路徑差異、權限),而不是去懷疑你的 GDScript。
6. 一次只問一個問題
❌ 「我的 deploy 跑了但網站沒更新而且 fontconfig 也報錯」
這樣 AI 會分散注意力,可能去處理 fontconfig(無害警告)而忽略真正的問題。
✅ 「網站沒更新第 9 關內容。我看到 a494cf3 commit 確實有改 index.pck,但線上抓到的還是舊版 size。」
聚焦核心問題,無關的次要 issue 之後再問。
結論
整個踩坑過程的時間軸大概是:
寫 deploy.yml + deploy-ci.sh → 半小時
踩坑 1(templates) → 10 分鐘解決
踩坑 2(python3 + 吞錯誤) → 1 小時(因為 2>/dev/null 一直誤導)
踩坑 3(CF 快取以為沒 deploy) → 20 分鐘(差點打掉重做)
Code language: JavaScript (javascript)最大的時間浪費就在踩坑 2,根本原因是自己把錯誤訊息蓋掉了。如果一開始 deploy-ci.sh 不要寫 2>/dev/null,第一次跑就能直接看到「python3: command not found」,10 秒鐘解決。
**一條教訓**:寫 CI script 寧可吵也不要安靜。錯誤訊息的成本永遠遠低於 debug 時間的成本。
附:完整的觸發流程
成功部署後,現在的流程是:
- 在 Linear issue 留言
@claude 把第 X 關的 Y 改成 Z - Linear 透過 GitHub Issue Sync 把留言同步到 GitHub
- GitHub Action
claude.yml偵測到@claude→ Claude Code 開新分支 → 改.gd→ 開 PR claude-code-review.yml自動 review PRdeploy.yml自動跑 Godot export → push 到 PR 分支- Cloudflare Pages 偵測 PR 分支變動 → 自動建 preview URL
- 我打開 preview URL 實際試玩
- OK 就 merge →
deploy.yml再跑一次 → push 到 master → CF Pages 部署正式版
從「在 Linear 寫一句中文」到「線上看到結果」的過程中,我只需要做兩件事:寫需求 + 點 merge。
值得花這 1.5 小時。