name: CD / VPS on: workflow_run: workflows: - CI types: - completed branches: - main workflow_dispatch: inputs: ref: description: 'Git ref to deploy (branch, tag, or SHA)' required: true default: 'main' permissions: contents: read packages: write concurrency: group: cd-vps-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref_name }} cancel-in-progress: false jobs: detect: runs-on: ubuntu-latest outputs: eligible: ${{ steps.detect.outputs.eligible }} ref: ${{ steps.detect.outputs.ref }} sha: ${{ steps.detect.outputs.sha }} steps: - id: detect run: | eligible=false ref="" sha="" if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then eligible=true ref="${{ inputs.ref }}" elif [[ "${{ github.event_name }}" == "workflow_run" && "${{ github.event.workflow_run.conclusion }}" == "success" ]]; then eligible=true ref="${{ github.event.workflow_run.head_sha }}" fi echo "eligible=$eligible" >> "$GITHUB_OUTPUT" echo "ref=$ref" >> "$GITHUB_OUTPUT" echo "sha=${ref}" >> "$GITHUB_OUTPUT" build-bot: runs-on: ubuntu-latest needs: detect if: ${{ needs.detect.outputs.eligible == 'true' }} outputs: image: ${{ steps.meta.outputs.image }} steps: - uses: actions/checkout@v4 with: ref: ${{ needs.detect.outputs.ref }} - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta run: | owner="${{ github.repository_owner }}" owner_lc="${owner,,}" revision="$(git rev-parse HEAD)" image="ghcr.io/${owner_lc}/household-bot-bot:${revision}" echo "image=$image" >> "$GITHUB_OUTPUT" - uses: docker/build-push-action@v6 with: context: . file: apps/bot/Dockerfile push: true tags: | ${{ steps.meta.outputs.image }} platforms: linux/amd64 provenance: false build-miniapp: runs-on: ubuntu-latest needs: detect if: ${{ needs.detect.outputs.eligible == 'true' }} outputs: image: ${{ steps.meta.outputs.image }} steps: - uses: actions/checkout@v4 with: ref: ${{ needs.detect.outputs.ref }} - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta run: | owner="${{ github.repository_owner }}" owner_lc="${owner,,}" revision="$(git rev-parse HEAD)" image="ghcr.io/${owner_lc}/household-bot-miniapp:${revision}" echo "image=$image" >> "$GITHUB_OUTPUT" - uses: docker/build-push-action@v6 with: context: . file: apps/miniapp/Dockerfile push: true tags: | ${{ steps.meta.outputs.image }} platforms: linux/amd64 provenance: false deploy: runs-on: ubuntu-latest needs: [detect, build-bot, build-miniapp] if: ${{ needs.detect.outputs.eligible == 'true' }} env: VPS_HOST: ${{ vars.VPS_HOST }} VPS_USER: ${{ vars.VPS_USER || 'root' }} VPS_PORT: ${{ vars.VPS_PORT || '22' }} DEPLOY_ROOT: ${{ vars.VPS_DEPLOY_ROOT || '/opt/household-bot' }} BOT_IMAGE: ${{ needs.build-bot.outputs.image }} MINIAPP_IMAGE: ${{ needs.build-miniapp.outputs.image }} steps: - uses: actions/checkout@v4 with: ref: ${{ needs.detect.outputs.ref }} - name: Validate deploy config run: | test -n "$VPS_HOST" test -n "$BOT_IMAGE" test -n "$MINIAPP_IMAGE" test -n "${{ secrets.VPS_SSH_KEY }}" - name: Prepare SSH run: | install -m 700 -d ~/.ssh printf '%s\n' '${{ secrets.VPS_SSH_KEY }}' > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 ssh-keyscan -p "$VPS_PORT" "$VPS_HOST" >> ~/.ssh/known_hosts - name: Upload deploy bundle run: | ssh -p "$VPS_PORT" "$VPS_USER@$VPS_HOST" "mkdir -p '$DEPLOY_ROOT/app'" tar czf - deploy/vps scripts/ops/vps-deploy.sh | \ ssh -p "$VPS_PORT" "$VPS_USER@$VPS_HOST" "tar xzf - -C '$DEPLOY_ROOT/app'" - name: Run remote deploy run: | ssh -p "$VPS_PORT" "$VPS_USER@$VPS_HOST" \ "export DEPLOY_ROOT='$DEPLOY_ROOT' APP_DIR='$DEPLOY_ROOT/app' ENV_DIR='$DEPLOY_ROOT/env' BOT_IMAGE='$BOT_IMAGE' MINIAPP_IMAGE='$MINIAPP_IMAGE' GHCR_USERNAME='${{ github.actor }}' GHCR_TOKEN='${{ secrets.GITHUB_TOKEN }}'; '$DEPLOY_ROOT/app/scripts/ops/vps-deploy.sh'" - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version-file: .bun-version - name: Install dependencies run: bun install --frozen-lockfile - name: Smoke check env: BOT_API_URL: ${{ vars.VPS_BOT_URL || 'https://household-bot.whekin.dev' }} MINI_APP_URL: ${{ vars.VPS_MINIAPP_URL || 'https://household.whekin.dev' }} TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_EXPECTED_WEBHOOK_URL: ${{ vars.VPS_BOT_URL || 'https://household-bot.whekin.dev' }}/webhook/telegram run: bun run ops:deploy:smoke - name: Set Telegram webhook env: TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_WEBHOOK_SECRET: ${{ secrets.TELEGRAM_WEBHOOK_SECRET }} TELEGRAM_WEBHOOK_URL: ${{ vars.VPS_BOT_URL || 'https://household-bot.whekin.dev' }}/webhook/telegram run: | if [[ -z "${TELEGRAM_BOT_TOKEN:-}" || -z "${TELEGRAM_WEBHOOK_SECRET:-}" ]]; then echo "Skipping webhook sync: TELEGRAM_BOT_TOKEN or TELEGRAM_WEBHOOK_SECRET is missing" exit 0 fi bun run ops:telegram:webhook set