name: Deploy SPA CF Workers[Worker一体化部署] on: push: branches: [main, master] paths: - "apps/ui/**" - "apps/worker/**" workflow_dispatch: inputs: from_panel: description: "是否由部署控制面板触发 / triggered from deployment control panel" required: false default: "auto" deploy_action: description: "部署动作:update/init(默认 update)" required: false default: "update" deploy_target: description: "部署目标:frontend/backend/both/auto(默认 auto)" required: false default: "auto" apply_migrations: description: "数据库迁移:true/false/auto(默认 auto,仅迁移文件变更时执行)" required: false default: "auto" repository_dispatch: types: [deploy-spa-button] jobs: check-config: runs-on: ubuntu-latest outputs: should_deploy: ${{ steps.check.outputs.should_deploy }} deploy_action: ${{ steps.plan.outputs.deploy_action }} deploy_target: ${{ steps.plan.outputs.deploy_target }} apply_migrations: ${{ steps.plan.outputs.apply_migrations }} deploy_ui: ${{ steps.plan.outputs.deploy_ui }} deploy_worker: ${{ steps.plan.outputs.deploy_worker }} should_migrate: ${{ steps.plan.outputs.should_migrate }} ui_changed: ${{ steps.changes.outputs.ui_changed }} worker_changed: ${{ steps.changes.outputs.worker_changed }} migrations_changed: ${{ steps.changes.outputs.migrations_changed }} steps: - name: 📥 检出代码 uses: actions/checkout@v4 with: fetch-depth: 0 - name: 🔍 检查部署配置 id: check run: | if [[ "${{ github.event_name }}" == "repository_dispatch" ]]; then echo "should_deploy=true" >> $GITHUB_OUTPUT echo "✅ 手动触发(repository_dispatch),忽略开关,允许部署" exit 0 fi if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.from_panel }}" != "true" ]]; then echo "should_deploy=true" >> $GITHUB_OUTPUT echo "✅ 手动触发(workflow_dispatch),忽略开关,允许部署" exit 0 fi SPA_ENABLED="${{ vars.SPA_DEPLOY }}" [ -z "$SPA_ENABLED" ] && SPA_ENABLED="true" if [ "$SPA_ENABLED" = "true" ]; then echo "should_deploy=true" >> $GITHUB_OUTPUT echo "✅ SPA 自动部署已开启,允许部署" else echo "should_deploy=false" >> $GITHUB_OUTPUT echo "⏸️ SPA 自动部署已关闭,跳过部署" fi - name: 🔎 检测变更范围 id: changes run: | echo "ui_changed=false" >> $GITHUB_OUTPUT echo "worker_changed=false" >> $GITHUB_OUTPUT echo "migrations_changed=false" >> $GITHUB_OUTPUT if [[ "${{ github.event_name }}" == "push" ]]; then before="${{ github.event.before }}" if [[ -z "$before" || "$before" == "0000000000000000000000000000000000000000" ]]; then echo "ui_changed=true" >> $GITHUB_OUTPUT echo "worker_changed=true" >> $GITHUB_OUTPUT echo "migrations_changed=true" >> $GITHUB_OUTPUT exit 0 fi if ! git cat-file -e "$before^{commit}" 2>/dev/null; then echo "⚠️ before 提交不存在,默认视为全量变更" echo "ui_changed=true" >> $GITHUB_OUTPUT echo "worker_changed=true" >> $GITHUB_OUTPUT echo "migrations_changed=true" >> $GITHUB_OUTPUT exit 0 fi CHANGED=$(git diff --name-only "$before" "${{ github.sha }}") if echo "$CHANGED" | grep -q "^apps/ui/"; then echo "ui_changed=true" >> $GITHUB_OUTPUT fi if echo "$CHANGED" | grep -q "^apps/worker/"; then echo "worker_changed=true" >> $GITHUB_OUTPUT fi if echo "$CHANGED" | grep -q "^apps/worker/migrations/"; then echo "migrations_changed=true" >> $GITHUB_OUTPUT fi fi - name: 🧭 生成部署计划 id: plan run: | event="${{ github.event_name }}" deploy_action="${{ github.event.inputs.deploy_action }}" deploy_target="${{ github.event.inputs.deploy_target }}" apply_migrations="${{ github.event.inputs.apply_migrations }}" if [[ "$event" == "repository_dispatch" ]]; then if [[ -n "${{ github.event.client_payload.deploy_target }}" ]]; then deploy_target="${{ github.event.client_payload.deploy_target }}" fi if [[ -n "${{ github.event.client_payload.apply_migrations }}" ]]; then apply_migrations="${{ github.event.client_payload.apply_migrations }}" fi if [[ -n "${{ github.event.client_payload.deploy_action }}" ]]; then deploy_action="${{ github.event.client_payload.deploy_action }}" fi fi if [[ -z "$deploy_action" ]]; then deploy_action="update" fi if [[ -z "$deploy_target" ]]; then deploy_target="auto" fi if [[ -z "$apply_migrations" ]]; then apply_migrations="auto" fi if [[ "$deploy_action" == "init" ]]; then deploy_target="both" apply_migrations="true" fi ui_changed="${{ steps.changes.outputs.ui_changed }}" worker_changed="${{ steps.changes.outputs.worker_changed }}" migrations_changed="${{ steps.changes.outputs.migrations_changed }}" deploy_ui="false" deploy_worker="false" case "$deploy_target" in frontend) deploy_ui="true" deploy_worker="true" ;; backend) deploy_ui="false" deploy_worker="true" ;; both) deploy_ui="true" deploy_worker="true" ;; auto|*) if [[ "$event" == "push" ]]; then if [[ "$ui_changed" == "true" ]]; then deploy_ui="true" fi if [[ "$worker_changed" == "true" ]]; then deploy_worker="true" fi else deploy_ui="true" deploy_worker="true" fi ;; esac should_migrate="false" if [[ "$apply_migrations" == "true" ]]; then should_migrate="true" elif [[ "$apply_migrations" == "auto" ]]; then if [[ "$event" == "push" && "$migrations_changed" == "true" ]]; then should_migrate="true" fi fi echo "deploy_action=$deploy_action" >> $GITHUB_OUTPUT echo "deploy_target=$deploy_target" >> $GITHUB_OUTPUT echo "apply_migrations=$apply_migrations" >> $GITHUB_OUTPUT echo "deploy_ui=$deploy_ui" >> $GITHUB_OUTPUT echo "deploy_worker=$deploy_worker" >> $GITHUB_OUTPUT echo "should_migrate=$should_migrate" >> $GITHUB_OUTPUT init-spa: needs: check-config if: needs.check-config.outputs.should_deploy == 'true' && needs.check-config.outputs.deploy_action == 'init' runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v1 with: bun-version: "1.3.9" - name: Show init plan run: | echo "deploy_action=${{ needs.check-config.outputs.deploy_action }}" echo "deploy_target=${{ needs.check-config.outputs.deploy_target }}" echo "apply_migrations=${{ needs.check-config.outputs.apply_migrations }}" echo "deploy_ui=${{ needs.check-config.outputs.deploy_ui }}" echo "deploy_worker=${{ needs.check-config.outputs.deploy_worker }}" echo "should_migrate=${{ needs.check-config.outputs.should_migrate }}" - name: Check if D1 database exists id: check-db env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} run: | echo "检查 D1 数据库是否已存在..." DATABASE_LIST=$(bunx wrangler d1 list --json --config apps/worker/wrangler.toml 2>/dev/null || echo "[]") EXISTING_DB=$(echo "$DATABASE_LIST" | jq -r '.[] | select(.name=="api-worker") | .uuid') if [ -n "$EXISTING_DB" ]; then echo "db_exists=true" >> $GITHUB_OUTPUT echo "✅ D1 已存在,跳过初始化" else echo "db_exists=false" >> $GITHUB_OUTPUT fi - name: Install dependencies if: steps.check-db.outputs.db_exists != 'true' run: bun install - name: Build UI if: steps.check-db.outputs.db_exists != 'true' && (needs.check-config.outputs.deploy_ui == 'true' || needs.check-config.outputs.deploy_worker == 'true') run: bun run --filter api-worker-ui build - name: Verify UI build if: steps.check-db.outputs.db_exists != 'true' && (needs.check-config.outputs.deploy_ui == 'true' || needs.check-config.outputs.deploy_worker == 'true') run: | if [ ! -d "apps/ui/dist" ]; then echo "❌ UI 构建失败:dist 目录不存在" exit 1 fi if [ ! -f "apps/ui/dist/index.html" ]; then echo "❌ UI 构建失败:index.html 不存在" exit 1 fi echo "✅ UI 构建产物已就绪" - name: Disable wrangler telemetry if: steps.check-db.outputs.db_exists != 'true' run: bunx wrangler telemetry disable - name: Create or verify Queue if: steps.check-db.outputs.db_exists != 'true' env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} run: | echo "检查并创建 Queue..." QUEUE_LIST_RAW=$(bunx wrangler queues list --json --config apps/worker/wrangler.toml 2>/dev/null || echo "[]") QUEUE_EXISTS=$(QUEUE_LIST_RAW="$QUEUE_LIST_RAW" node - <<'NODE' const raw = process.env.QUEUE_LIST_RAW || ""; const target = "usage-events"; const first = raw.search(/[\[{]/); let jsonText = raw; if (first >= 0) { jsonText = raw.slice(first); } let list = null; try { list = JSON.parse(jsonText); } catch { const last = raw.lastIndexOf("]"); if (first >= 0 && last > first) { try { list = JSON.parse(raw.slice(first, last + 1)); } catch {} } } if (!Array.isArray(list)) { console.log("unknown"); process.exit(0); } const exists = list.some((item) => item && item.name === target); console.log(exists ? "true" : "false"); NODE ) if [ "$QUEUE_EXISTS" = "true" ]; then echo "✅ Queue 已存在: usage-events" else echo "创建 Queue: usage-events" CREATE_OUTPUT="" CREATE_STATUS=0 set +e CREATE_OUTPUT=$(bunx wrangler queues create usage-events --config apps/worker/wrangler.toml 2>&1) CREATE_STATUS=$? set -e if [ $CREATE_STATUS -eq 0 ]; then echo "✅ Queue 已创建: usage-events" else if echo "$CREATE_OUTPUT" | grep -Eqi "already exists|already taken|code: 11009"; then echo "✅ Queue 已存在: usage-events" else echo "❌ 队列创建失败,输出如下:" echo "$CREATE_OUTPUT" exit $CREATE_STATUS fi fi fi - name: Create D1 database (init only) if: steps.check-db.outputs.db_exists != 'true' env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} run: | echo "创建 D1 数据库..." CREATE_OUTPUT=$(bunx wrangler d1 create api-worker --config apps/worker/wrangler.toml 2>&1) if echo "$CREATE_OUTPUT" | grep -q "database_id"; then DATABASE_ID=$(echo "$CREATE_OUTPUT" | grep -oP "database_id = \"\K[^\"]+") if [ -z "$DATABASE_ID" ]; then DATABASE_ID=$(echo "$CREATE_OUTPUT" | grep -oP "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})") fi else LIST_OUTPUT=$(bunx wrangler d1 list --json --config apps/worker/wrangler.toml 2>/dev/null || echo "[]") DATABASE_ID=$(echo "$LIST_OUTPUT" | jq -r '.[] | select(.name=="api-worker") | .uuid') fi if [ -z "$DATABASE_ID" ]; then echo "❌ 无法创建或找到 D1 数据库" exit 1 fi sed -i -E "s/(database_id = \")([^\"]+)(\")/\\1$DATABASE_ID\\3/" apps/worker/wrangler.toml echo "database_id=$DATABASE_ID" >> $GITHUB_ENV - name: Apply D1 migrations (remote) if: steps.check-db.outputs.db_exists != 'true' env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} run: bunx wrangler d1 migrations apply DB --remote --config apps/worker/wrangler.toml - name: Deploy to Cloudflare Workers (SPA Mode) if: steps.check-db.outputs.db_exists != 'true' && (needs.check-config.outputs.deploy_worker == 'true' || needs.check-config.outputs.deploy_ui == 'true') env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} run: bunx wrangler deploy --config apps/worker/wrangler.toml deploy-spa: needs: check-config if: needs.check-config.outputs.should_deploy == 'true' && needs.check-config.outputs.deploy_action != 'init' runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v1 with: bun-version: "1.3.9" - name: Show deploy plan run: | echo "deploy_action=${{ needs.check-config.outputs.deploy_action }}" echo "deploy_target=${{ needs.check-config.outputs.deploy_target }}" echo "apply_migrations=${{ needs.check-config.outputs.apply_migrations }}" echo "ui_changed=${{ needs.check-config.outputs.ui_changed }}" echo "worker_changed=${{ needs.check-config.outputs.worker_changed }}" echo "migrations_changed=${{ needs.check-config.outputs.migrations_changed }}" echo "deploy_ui=${{ needs.check-config.outputs.deploy_ui }}" echo "deploy_worker=${{ needs.check-config.outputs.deploy_worker }}" echo "should_migrate=${{ needs.check-config.outputs.should_migrate }}" - name: Check if deploy button trigger id: check-deploy-button run: | if [[ "${{ github.event_name }}" == "repository_dispatch" && "${{ github.event.action }}" == "deploy-spa-button" ]]; then echo "is_deploy_button=true" >> $GITHUB_OUTPUT else echo "is_deploy_button=false" >> $GITHUB_OUTPUT fi - name: Install dependencies run: bun install - name: Build UI if: needs.check-config.outputs.deploy_ui == 'true' || needs.check-config.outputs.deploy_worker == 'true' run: bun run --filter api-worker-ui build - name: Verify UI build if: needs.check-config.outputs.deploy_ui == 'true' || needs.check-config.outputs.deploy_worker == 'true' run: | if [ ! -d "apps/ui/dist" ]; then echo "❌ UI 构建失败:dist 目录不存在" exit 1 fi if [ ! -f "apps/ui/dist/index.html" ]; then echo "❌ UI 构建失败:index.html 不存在" exit 1 fi echo "✅ UI 构建产物已就绪" - name: Disable wrangler telemetry run: bunx wrangler telemetry disable - name: Create or verify Queue if: needs.check-config.outputs.deploy_worker == 'true' || needs.check-config.outputs.should_migrate == 'true' env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} run: | echo "检查并创建 Queue..." QUEUE_LIST_RAW=$(bunx wrangler queues list --json --config apps/worker/wrangler.toml 2>/dev/null || echo "[]") QUEUE_EXISTS=$(QUEUE_LIST_RAW="$QUEUE_LIST_RAW" node - <<'NODE' const raw = process.env.QUEUE_LIST_RAW || ""; const target = "usage-events"; const first = raw.search(/[\[{]/); let jsonText = raw; if (first >= 0) { jsonText = raw.slice(first); } let list = null; try { list = JSON.parse(jsonText); } catch { const last = raw.lastIndexOf("]"); if (first >= 0 && last > first) { try { list = JSON.parse(raw.slice(first, last + 1)); } catch {} } } if (!Array.isArray(list)) { console.log("unknown"); process.exit(0); } const exists = list.some((item) => item && item.name === target); console.log(exists ? "true" : "false"); NODE ) if [ "$QUEUE_EXISTS" = "true" ]; then echo "✅ Queue 已存在: usage-events" else echo "创建 Queue: usage-events" CREATE_OUTPUT="" CREATE_STATUS=0 set +e CREATE_OUTPUT=$(bunx wrangler queues create usage-events --config apps/worker/wrangler.toml 2>&1) CREATE_STATUS=$? set -e if [ $CREATE_STATUS -eq 0 ]; then echo "✅ Queue 已创建: usage-events" else if echo "$CREATE_OUTPUT" | grep -Eqi "already exists|already taken|code: 11009"; then echo "✅ Queue 已存在: usage-events" else echo "❌ 队列创建失败,输出如下:" echo "$CREATE_OUTPUT" exit $CREATE_STATUS fi fi fi - name: Create or verify D1 Database if: needs.check-config.outputs.deploy_worker == 'true' || needs.check-config.outputs.deploy_ui == 'true' || needs.check-config.outputs.should_migrate == 'true' env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} run: | echo "检查并创建 D1 数据库..." DATABASE_LIST=$(bunx wrangler d1 list --json --config apps/worker/wrangler.toml 2>/dev/null || echo "[]") EXISTING_DB=$(echo "$DATABASE_LIST" | jq -r '.[] | select(.name=="api-worker") | .uuid') if [ -n "$EXISTING_DB" ]; then echo "✅ 找到现有 D1 数据库: $EXISTING_DB" DATABASE_ID=$EXISTING_DB else CREATE_OUTPUT=$(bunx wrangler d1 create api-worker --config apps/worker/wrangler.toml 2>&1) if echo "$CREATE_OUTPUT" | grep -q "database_id"; then DATABASE_ID=$(echo "$CREATE_OUTPUT" | grep -oP "database_id = \"\K[^\"]+") if [ -z "$DATABASE_ID" ]; then DATABASE_ID=$(echo "$CREATE_OUTPUT" | grep -oP "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})") fi else LIST_OUTPUT=$(bunx wrangler d1 list --json --config apps/worker/wrangler.toml 2>/dev/null || echo "[]") DATABASE_ID=$(echo "$LIST_OUTPUT" | jq -r '.[] | select(.name=="api-worker") | .uuid') if [ -z "$DATABASE_ID" ]; then echo "❌ 无法创建或找到 D1 数据库" exit 1 fi fi fi sed -i -E "s/(database_id = \")([^\"]+)(\")/\\1$DATABASE_ID\\3/" apps/worker/wrangler.toml echo "database_id=$DATABASE_ID" >> $GITHUB_ENV - name: Apply D1 migrations (remote) if: needs.check-config.outputs.should_migrate == 'true' env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} run: bunx wrangler d1 migrations apply DB --remote --config apps/worker/wrangler.toml - name: Deploy to Cloudflare Workers (SPA Mode) if: needs.check-config.outputs.deploy_worker == 'true' || needs.check-config.outputs.deploy_ui == 'true' env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} run: bunx wrangler deploy --config apps/worker/wrangler.toml - name: Display Success Information if: steps.check-deploy-button.outputs.is_deploy_button == 'true' && success() run: | echo "====================================================" echo "🎉 SPA Worker 已成功部署到 Cloudflare Workers!" echo "====================================================" echo "后续步骤:" echo "1. 访问您的 Worker URL" echo "2. 登录后立刻修改管理员密码" echo "3. 配置相关系统参数" echo "====================================================" - name: Notify deployment status if: always() run: | if [ "${{ job.status }}" == "success" ]; then echo "✅ SPA Worker 部署成功!" else echo "❌ SPA Worker 部署失败!" fi