Skip to content

Fix runtime switch config and DCA support (#141) #114

Fix runtime switch config and DCA support (#141)

Fix runtime switch config and DCA support (#141) #114

name: Deploy Strategy Switch Console
on:
push:
branches: [main]
paths:
- ".github/workflows/deploy-strategy-switch-console.yml"
- "platform-config.json"
- "python/scripts/build_config.py"
- "python/scripts/sync_strategy_switch_page_asset.py"
- "web/strategy-switch-console/**"
workflow_dispatch:
inputs:
sync_strategy_profiles:
description: "After deploy, write the bundled strategy profile catalog to KV."
required: true
type: boolean
default: true
permissions:
contents: read
concurrency:
group: strategy-switch-console-deploy
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
environment: runtime-strategy-switch
timeout-minutes: 15
env:
WORKER_DIR: web/strategy-switch-console
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_WRANGLER_CONFIG_TOML: ${{ secrets.CLOUDFLARE_WRANGLER_CONFIG_TOML }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID || vars.CLOUDFLARE_ACCOUNT_ID }}
STRATEGY_SWITCH_CONFIG_KV_NAMESPACE_ID: ${{ secrets.STRATEGY_SWITCH_CONFIG_KV_NAMESPACE_ID || vars.STRATEGY_SWITCH_CONFIG_KV_NAMESPACE_ID }}
STRATEGY_SWITCH_CONSOLE_URL: ${{ vars.STRATEGY_SWITCH_CONSOLE_URL }}
STRATEGY_SWITCH_SYNC_TOKEN: ${{ secrets.STRATEGY_SWITCH_SYNC_TOKEN || secrets.RUNTIME_SETTINGS_GH_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "22"
- name: Build config & assets from platform-config.json
run: |
set -euo pipefail
python3 python/scripts/build_platform_config.py
python3 python/scripts/inject_platform_config.py
python3 python/scripts/sync_strategy_switch_page_asset.py
- name: Validate Worker assets
run: |
set -euo pipefail
jq empty web/strategy-switch-console/strategy-profiles.example.json
node --check --input-type=module < web/strategy-switch-console/page_asset.js
node --check --input-type=module < web/strategy-switch-console/strategy_profiles_asset.js
node --check --input-type=module < web/strategy-switch-console/worker.js
- name: Prepare Wrangler config
run: |
set -euo pipefail
if [ -z "${CLOUDFLARE_API_TOKEN:-}" ] && [ -z "${CLOUDFLARE_WRANGLER_CONFIG_TOML:-}" ]; then
echo "CLOUDFLARE_API_TOKEN or CLOUDFLARE_WRANGLER_CONFIG_TOML is required to deploy the strategy switch console." >&2
exit 2
fi
if [ -n "${CLOUDFLARE_WRANGLER_CONFIG_TOML:-}" ]; then
mkdir -p "$HOME/.config/.wrangler/config"
printf '%s' "$CLOUDFLARE_WRANGLER_CONFIG_TOML" > "$HOME/.config/.wrangler/config/default.toml"
chmod 600 "$HOME/.config/.wrangler/config/default.toml"
fi
if [ -z "${STRATEGY_SWITCH_CONFIG_KV_NAMESPACE_ID:-}" ]; then
echo "STRATEGY_SWITCH_CONFIG_KV_NAMESPACE_ID is required so deploy does not drop the STRATEGY_SWITCH_CONFIG binding." >&2
exit 2
fi
python3 - <<'PY'
import os
from pathlib import Path
root = Path(os.environ["WORKER_DIR"])
source = root / "wrangler.toml.example"
target = root / "wrangler.toml"
text = source.read_text(encoding="utf-8")
account_id = os.environ.get("CLOUDFLARE_ACCOUNT_ID", "").strip()
if account_id and "\naccount_id" not in text:
text = text.replace("\n[vars]\n", f"\naccount_id = \"{account_id}\"\n\n[vars]\n", 1)
kv_id = os.environ["STRATEGY_SWITCH_CONFIG_KV_NAMESPACE_ID"].strip()
commented_kv = "\n".join([
"# [[kv_namespaces]]",
"# binding = \"STRATEGY_SWITCH_CONFIG\"",
"# id = \"replace-with-cloudflare-kv-namespace-id\"",
"",
])
active_kv = "\n".join([
"[[kv_namespaces]]",
"binding = \"STRATEGY_SWITCH_CONFIG\"",
f"id = \"{kv_id}\"",
"",
])
if commented_kv in text:
text = text.replace(commented_kv, active_kv, 1)
elif 'binding = "STRATEGY_SWITCH_CONFIG"' not in text:
text = text.rstrip() + "\n\n" + active_kv
target.write_text(text, encoding="utf-8")
PY
- name: Deploy Worker
working-directory: web/strategy-switch-console
run: npx wrangler@4.106.0 deploy --config wrangler.toml
- name: Sync bundled strategy profiles to KV
if: github.event_name != 'workflow_dispatch' || inputs.sync_strategy_profiles
run: |
set -euo pipefail
if [ -z "${STRATEGY_SWITCH_CONSOLE_URL:-}" ]; then
echo "STRATEGY_SWITCH_CONSOLE_URL is required to sync strategy profiles." >&2
exit 2
fi
if [ -z "${STRATEGY_SWITCH_SYNC_TOKEN:-}" ]; then
echo "STRATEGY_SWITCH_SYNC_TOKEN or RUNTIME_SETTINGS_GH_TOKEN is required to sync strategy profiles." >&2
exit 2
fi
expected_count="$(python3 - <<'PY'
import json
from pathlib import Path
profiles = json.loads(Path("web/strategy-switch-console/strategy-profiles.example.json").read_text())
if not isinstance(profiles, list):
raise SystemExit("strategy-profiles.example.json must contain a JSON list")
print(len(profiles))
PY
)"
sync_body="$(mktemp)"
live_body="$(mktemp)"
cleanup() { rm -f "$sync_body" "$live_body"; }
trap cleanup EXIT
for attempt in 1 2 3 4 5 6; do
curl --fail --show-error --silent \
--request POST \
--header "Authorization: Bearer ${STRATEGY_SWITCH_SYNC_TOKEN}" \
--header "Content-Type: application/json" \
--header "Cache-Control: no-cache" \
--data '{"source":"github-actions"}' \
--output "$sync_body" \
"${STRATEGY_SWITCH_CONSOLE_URL%/}/api/internal/sync-strategy-profiles"
python3 -m json.tool "$sync_body"
actual_count="$(python3 - "$sync_body" <<'PY'
import json
import sys
payload = json.load(open(sys.argv[1], encoding="utf-8"))
print(payload.get("strategy_profiles_count", ""))
PY
)"
if [ "$actual_count" = "$expected_count" ]; then
break
fi
if [ "$attempt" = "6" ]; then
echo "Synced strategy profile count ${actual_count:-unknown}; expected $expected_count." >&2
exit 2
fi
echo "Synced strategy profile count ${actual_count:-unknown}; expected $expected_count. Waiting for deployed Worker propagation..." >&2
sleep 5
done
curl --fail --show-error --silent \
--header "Cache-Control: no-cache" \
--output "$live_body" \
"${STRATEGY_SWITCH_CONSOLE_URL%/}/api/strategy-profiles"
live_count="$(python3 - "$live_body" <<'PY'
import json
import sys
payload = json.load(open(sys.argv[1], encoding="utf-8"))
profiles = payload.get("strategyProfiles")
if not isinstance(profiles, list):
raise SystemExit("/api/strategy-profiles did not return strategyProfiles list")
print(len(profiles))
PY
)"
if [ "$live_count" != "$expected_count" ]; then
echo "Live strategy profile count $live_count; expected $expected_count." >&2
exit 2
fi
echo "Strategy profile KV sync verified with $live_count profiles."