name: Deploy pleno-vue to Hetzner (staging) on: push: branches: [master] workflow_dispatch: concurrency: group: deploy-pleno-vue cancel-in-progress: false env: DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} DEPLOY_USER: ${{ secrets.DEPLOY_USER }} jobs: test-and-deploy: name: Build + Deploy runs-on: ubuntu-latest timeout-minutes: 25 steps: - uses: actions/checkout@v4 with: fetch-depth: 1 - name: Setup Node uses: actions/setup-node@v7 with: node-version: '20' cache: 'npm' - name: Install + Build run: | npm ci --ignore-scripts npm run build - name: Setup SSH uses: webfactory/ssh-agent@v0.10.0 with: ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }} - name: Add host key run: | mkdir -p ~/.ssh ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null - name: Pre-deploy snapshot id: pre run: | ssh "$DEPLOY_USER@$DEPLOY_HOST" ' set -e cd /opt/pleno-vue git rev-parse HEAD > /tmp/last_deploy_sha echo "pre_sha=$(cat /tmp/last_deploy_sha)" >> $GITHUB_OUTPUT ' - name: Deploy id: deploy run: | ssh "$DEPLOY_USER@$DEPLOY_HOST" ' set -e cd /opt/pleno-vue git fetch origin master git reset --hard origin/master npm ci --ignore-scripts npm run build sudo systemctl reload nginx || true sudo systemctl reload pleno-vue || true echo "Deploy complete: $(git rev-parse --short HEAD)" ' - name: Smoke test id: smoke continue-on-error: true env: SMOKE_BASE_URL: ${{ secrets.SMOKE_BASE_URL }} run: | bash scripts/smoke-test.sh "$SMOKE_BASE_URL" - name: Sergii Review Batch smoke test (TRU-96) id: smoke_sergii continue-on-error: true env: SMOKE_BASE_URL: ${{ secrets.SMOKE_BASE_URL }} run: | bash scripts/smoke-test-sergii.sh "$SMOKE_BASE_URL" - name: Auto-rollback on smoke failure if: steps.smoke.outcome == 'failure' || steps.smoke_sergii.outcome == 'failure' run: | reason="$([ "${{ steps.smoke.outcome }}" = 'failure' ] && echo 'generic smoke' || echo 'Sergii Review Batch smoke')" echo "::error::$reason test failed — rolling back to ${{ steps.pre.outputs.pre_sha }}" ssh "$DEPLOY_USER@$DEPLOY_HOST" ' set -e cd /opt/pleno-vue git reset --hard ${{ steps.pre.outputs.pre_sha }} npm ci --ignore-scripts npm run build sudo systemctl reload nginx || true ' - name: Post Slack status if: always() uses: slackapi/slack-github-action@v4.0.0 with: channel-id: ${{ secrets.AI_DAILY_CHANNEL }} payload: | { "text": "${{ job.status == 'success' && '✅' || '❌' }} Deploy *pleno-vue@${{ github.sha[0:7] }}* — ${{ job.status }}\n${{ steps.smoke.outcome == 'failure' && '⚠️ Generic smoke FAILED → auto-rolled back' || steps.smoke_sergii.outcome == 'failure' && '⚠️ Sergii Review Batch smoke FAILED → auto-rolled back' || '✓ Smoke (generic + Sergii) passed' }}" } env: SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}