name: Deploy to Hetzner (staging) on: push: branches: [master] workflow_dispatch: inputs: reason: description: 'Reason for manual deploy' required: false default: 'manual' concurrency: group: deploy-${{ github.repository }} cancel-in-progress: false env: DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} DEPLOY_USER: ${{ secrets.DEPLOY_USER }} jobs: test-and-deploy: name: CI + Deploy runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@v4 with: fetch-depth: 1 - name: Show commit info run: | echo "Repo: ${{ github.repository }}" echo "Branch: ${{ github.ref }}" echo "Commit: ${{ github.sha }}" echo "Actor: ${{ github.actor }}" # === CI (phpunit / vitest) runs here via repo's existing CI config === # (Most of our repos already have a "Required CI" check; this section # would invoke that. If your repo doesn't have a CI workflow, the # required-check on the branch will block this workflow's deploy step.) - name: Setup SSH uses: webfactory/ssh-agent@v0.9.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/${{ github.event.repository.name }} git rev-parse HEAD > /tmp/last_deploy_sha echo "PRE_SHA=$(cat /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/${{ github.event.repository.name }} git fetch origin master git reset --hard origin/master # PHP repos: composer install + clear cache if [ -f composer.json ]; then composer install --no-dev --optimize-autoloader --no-interaction php artisan cache:clear || true php artisan config:cache || true # Restart php-fpm if used sudo systemctl reload php8.2-fpm || true fi # Node repos: npm ci + build if [ -f package.json ]; then npm ci --ignore-scripts npm run build # Restart node service sudo systemctl reload pleno-vue || sudo systemctl reload nginx || true fi # Restart generic services sudo systemctl reload nginx || true echo "Deploy complete: $(git rev-parse --short HEAD)" ' - name: Smoke test id: smoke continue-on-error: true run: | chmod +x scripts/smoke-test.sh ./scripts/smoke-test.sh - name: Auto-rollback on smoke failure if: steps.smoke.outcome == 'failure' run: | echo "::error::Smoke test failed — rolling back to ${{ steps.pre.outputs.pre_sha }}" ssh "$DEPLOY_USER@$DEPLOY_HOST" ' set -e cd /opt/${{ github.event.repository.name }} git reset --hard ${{ steps.pre.outputs.pre_sha }} if [ -f composer.json ]; then composer install --no-dev --optimize-autoloader --no-interaction sudo systemctl reload php8.2-fpm || true fi if [ -f package.json ]; then npm ci --ignore-scripts npm run build sudo systemctl reload nginx || true fi ' - name: Post Slack status if: always() uses: slackapi/slack-github-action@v1.27.0 with: channel-id: ${{ secrets.AI_DAILY_CHANNEL }} payload: | { "text": "${{ job.status == 'success' && '✅' || '❌' }} Deploy *${{ github.repository }}@${{ github.sha[0:7] }}* — ${{ job.status }}\n${{ steps.smoke.outcome == 'failure' && '⚠️ Auto-rolled back' || '✓ Smoke test passed' }}" } env: SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} - name: Update Linear issue if: success() && steps.deploy.outcome == 'success' run: | # Find Linear issues in this commit's history and post a comment # (uses GitHub's auto-link: if PR body contains "TRU-123" it auto-links) # We skip this here; the OpenClaw cron `f26dfd83` handles Linear updates. echo "Deploy notification will be picked up by OpenClaw cron."