fix: deploy step-notifications and GitHub Actions pipeline #53
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy | ||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| types: [closed] | ||
| workflow_dispatch: | ||
| inputs: | ||
| dry_run: | ||
| description: 'Dry run — skip VPS changes, just run quality gates and notify' | ||
| required: false | ||
| default: 'false' | ||
| skip_tests: | ||
| description: 'Skip unit tests (emergency deploy only)' | ||
| required: false | ||
| default: 'false' | ||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
| env: | ||
| BOT_TOKEN: ${{ secrets.BOT_TOKEN }} | ||
| AFFILIATE_SOURCE: GambleCodez | ||
| ADMIN_IDS: "6668510825" | ||
| DISCORD_CODE_GENERATION_IMAGE_URL: "https://github.com/gamblecodezcom/Runewager/blob/38c1bbab2d954238829852981514ce92c49d8b10/images/discord_code_generation.png" | ||
| DISCORD_VERIFY_IMAGE_URL: "https://github.com/gamblecodezcom/Runewager/blob/38c1bbab2d954238829852981514ce92c49d8b10/images/discord_verify.png" | ||
| MAX_ANALYTICS_EVENTS: "2000" | ||
| MAX_ONBOARDING_STEPS_HISTORY: "500" | ||
| MINI_APP_CLAIM_URL: "https://t.me/RuneWager_bot/claim" | ||
| MINI_APP_PLAY_URL: "https://t.me/RuneWager_bot/Play" | ||
| MINI_APP_PROFILE_URL: "https://t.me/RuneWager_bot/profile" | ||
| PORT: "3000" | ||
| PROMO_AMOUNT_SC: "3.5" | ||
| PROMO_BONUS_RULE: "Bonus added once your account is verified and meets eligibility requirements. Limit once per account/IP. New users are defined as accounts that have never redeemed a promo code before." | ||
| PROMO_CLAIM_LIMIT: "600" | ||
| PROMO_CODE: SPORTS3.5 | ||
| PROMO_ENTRY_IMAGE_URL: "https://github.com/gamblecodezcom/Runewager/blob/225428f813f2b3caf74f9716fff4b13ef0b74992/images/promo_entry.png" | ||
| RW_DISCORD_JOIN: "https://discord.gg/runewagers" | ||
| RW_DISCORD_LINK: "https://discord.com/channels/1100486422395355197/1249181934811349052" | ||
| RW_DISCORD_SUPPORT: "https://discord.com/channels/1100486422395355197/1249182067296567338" | ||
| DEPLOY_MODE: normal | ||
| jobs: | ||
| # --------------------------------------------------------------------------- | ||
| # Job 1: Quality Gates — syntax, tests, audit, metadata, secrets, guards | ||
| # --------------------------------------------------------------------------- | ||
| quality-gates: | ||
| name: Quality Gates | ||
| runs-on: ubuntu-latest | ||
| # Run on: direct push to main, merged PR to main, or manual workflow_dispatch. | ||
| # Closed-without-merge PRs are excluded by the merged == true check. | ||
| if: > | ||
| github.event_name == 'push' || | ||
| github.event_name == 'workflow_dispatch' || | ||
| (github.event_name == 'pull_request' && | ||
| github.event.pull_request.merged == true) | ||
| outputs: | ||
| commit_hash: ${{ steps.meta.outputs.commit_hash }} | ||
| deploy_time: ${{ steps.meta.outputs.deploy_time }} | ||
| version: ${{ steps.meta.outputs.version }} | ||
| started_at: ${{ steps.start.outputs.started_at }} | ||
| duration_seconds: ${{ steps.duration.outputs.duration_seconds }} | ||
| steps: | ||
| - name: Record start time | ||
| id: start | ||
| run: echo "started_at=$(date +%s)" >> "$GITHUB_OUTPUT" | ||
| - name: Guard — verify valid merge to main | ||
| if: github.event_name == 'pull_request' | ||
| run: | | ||
| echo "PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}" | ||
| echo "Merged into: ${{ github.event.pull_request.base.ref }}" | ||
| echo "Merge commit: ${{ github.event.pull_request.merge_commit_sha }}" | ||
| echo "✅ Valid merge to main confirmed — proceeding with quality gates" | ||
| - uses: actions/checkout@v4 | ||
| - name: Guard — skip if commit says so | ||
| run: | | ||
| MSG=$(git log -1 --format="%s %b") | ||
| if echo "$MSG" | grep -qi '\[skip deploy\]'; then | ||
| echo "Commit message contains [skip deploy] — aborting." | ||
| exit 1 | ||
| fi | ||
| echo "✅ No [skip deploy] flag found" | ||
| - name: Verify required secrets exist | ||
| env: | ||
| _BOT_TOKEN: ${{ secrets.BOT_TOKEN }} | ||
| _SERVER_KEY: ${{ secrets.SERVER_KEY }} | ||
| _SERVER_HOST: ${{ secrets.SERVER_HOST }} | ||
| _SERVER_USER: ${{ secrets.SERVER_USER }} | ||
| run: | | ||
| MISSING=0 | ||
| [ -z "$_BOT_TOKEN" ] && echo "❌ Missing secret: BOT_TOKEN" && MISSING=1 | ||
| [ -z "$_SERVER_KEY" ] && echo "❌ Missing secret: SERVER_KEY" && MISSING=1 | ||
| [ -z "$_SERVER_HOST" ]&& echo "❌ Missing secret: SERVER_HOST"&& MISSING=1 | ||
| [ -z "$_SERVER_USER" ]&& echo "❌ Missing secret: SERVER_USER"&& MISSING=1 | ||
| [ "$MISSING" -eq 1 ] && exit 1 | ||
| echo "✅ All required secrets present" | ||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
| cache: 'npm' | ||
| - name: Verify Node version is 20.x | ||
| run: | | ||
| echo "Node: $(node -v)" | ||
| echo "npm: $(npm -v)" | ||
| NODE_MAJOR=$(node -v | sed 's/v\([0-9]*\).*/\1/') | ||
| if [ "$NODE_MAJOR" -lt 20 ]; then | ||
| echo "ERROR: Node 20+ required, found $(node -v)" >&2 | ||
| exit 1 | ||
| fi | ||
| echo "✅ Node $(node -v) OK" | ||
| - name: Install deps | ||
| run: npm ci | ||
| - name: Build metadata | ||
| id: meta | ||
| run: | | ||
| echo "commit_hash=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" | ||
| echo "deploy_time=$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT" | ||
| echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT" | ||
| - name: Syntax check | ||
| run: npm run check | ||
| - name: Tests | ||
| if: github.event.inputs.skip_tests != 'true' | ||
| run: npm test | ||
| - name: Security audit (non-blocking) | ||
| run: npm audit --audit-level=high || true | ||
| - name: Compute duration | ||
| id: duration | ||
| run: | | ||
| STARTED="${{ steps.start.outputs.started_at }}" | ||
| NOW=$(date +%s) | ||
| echo "duration_seconds=$((NOW - STARTED))" >> "$GITHUB_OUTPUT" | ||
| - name: Notify deploy start | ||
| env: | ||
| BOT_TOKEN: ${{ secrets.BOT_TOKEN }} | ||
| run: | | ||
| chmod +x scripts/notify-telegram.sh | ||
| ./scripts/notify-telegram.sh "🚀 *Deploy Starting* | ||
| Repo: \`${{ github.repository }}\` | ||
| Branch: \`${{ github.ref_name }}\` | ||
| Commit: \`${{ steps.meta.outputs.commit_hash }}\` | ||
| Version: \`${{ steps.meta.outputs.version }}\` | ||
| Mode: \`${DEPLOY_MODE}\` | ||
| By: \`${{ github.actor }}\`" | ||
| # --------------------------------------------------------------------------- | ||
| # Job 2: Deploy to VPS — SSH into VPS and run deploy.sh | ||
| # Only triggers after quality gates pass on a valid PR merge to main. | ||
| # --------------------------------------------------------------------------- | ||
| deploy: | ||
| name: Deploy to VPS | ||
| runs-on: ubuntu-latest | ||
| needs: quality-gates | ||
| if: needs.quality-gates.result == 'success' | ||
| env: | ||
| DEPLOY_PASS: ${{ secrets.DEPLOY_PASS }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Install sshpass (password SSH fallback) | ||
| run: sudo apt-get install -y sshpass | ||
| - name: Setup SSH | ||
| env: | ||
| SERVER_KEY: ${{ secrets.SERVER_KEY }} | ||
| SERVER_HOST: ${{ secrets.SERVER_HOST }} | ||
| run: | | ||
| mkdir -p ~/.ssh | ||
| chmod 700 ~/.ssh | ||
| printf '%s\n' "$SERVER_KEY" > ~/.ssh/deploy_key | ||
| chmod 600 ~/.ssh/deploy_key | ||
| ssh-keyscan -H "$SERVER_HOST" >> ~/.ssh/known_hosts | ||
| cat >> ~/.ssh/config <<EOF | ||
| Host deploy_target | ||
| HostName $SERVER_HOST | ||
| User ${{ secrets.SERVER_USER }} | ||
| IdentityFile ~/.ssh/deploy_key | ||
| StrictHostKeyChecking yes | ||
| ConnectTimeout 15 | ||
| EOF | ||
| - name: Save pre-deploy SHA | ||
| id: pre | ||
| run: | | ||
| SHA=$(ssh deploy_target "cd /var/www/html/Runewager && git rev-parse HEAD || echo none") | ||
| echo "sha=$SHA" >> "$GITHUB_OUTPUT" | ||
| - name: Pre-deploy health snapshot | ||
| run: | | ||
| ssh deploy_target " | ||
| set -e | ||
| cd /var/www/html/Runewager | ||
| echo 'Checking .env exists...' | ||
| if [ ! -f .env ]; then | ||
| echo 'ERROR: .env missing on VPS' >&2 | ||
| exit 1 | ||
| fi | ||
| echo 'Checking port 3000 usage...' | ||
| lsof -i :3000 || true | ||
| echo 'Checking memory/disk...' | ||
| free -m || true | ||
| df -h . || true | ||
| echo 'Checking disk free threshold...' | ||
| FREE_KB=\$(df . | awk 'NR==2 {print \$4}') | ||
| FREE_MB=\$((FREE_KB / 1024)) | ||
| if [ \"\$FREE_MB\" -lt 100 ]; then | ||
| echo 'WARNING: Less than 100MB free disk space (' \$FREE_MB 'MB). Proceeding with caution.' | ||
| else | ||
| echo \"✅ Disk: \${FREE_MB}MB free\" | ||
| fi | ||
| " | ||
| - name: Deploy via deploy.sh (with password fallback) | ||
| env: | ||
| SERVER_HOST: ${{ secrets.SERVER_HOST }} | ||
| SERVER_USER: ${{ secrets.SERVER_USER }} | ||
| run: | | ||
| DEPLOY_CMD="bash /var/www/html/Runewager/deploy.sh github" | ||
| # First attempt: SSH with key | ||
| if ssh deploy_target "$DEPLOY_CMD"; then | ||
| echo "✅ Deployed via SSH key" | ||
| else | ||
| echo "⚠️ SSH key attempt failed — waiting 120 seconds before password retry…" | ||
| sleep 120 | ||
| # Second attempt: password fallback via sshpass | ||
| if sshpass -p "$DEPLOY_PASS" ssh \ | ||
| -o StrictHostKeyChecking=no \ | ||
| -o ConnectTimeout=15 \ | ||
| "${SERVER_USER}@${SERVER_HOST}" "$DEPLOY_CMD"; then | ||
| echo "✅ Deployed via password fallback" | ||
| else | ||
| echo "❌ Both SSH attempts failed — sending admin alert" | ||
| chmod +x scripts/notify-telegram.sh | ||
| ./scripts/notify-telegram.sh "❌ Deploy failed — SSH could not connect to VPS after key + password retry. Manual intervention required." | ||
| exit 1 | ||
| fi | ||
| fi | ||
| - name: Download deploy report from VPS | ||
| if: always() | ||
| run: | | ||
| ssh deploy_target "cat /tmp/deploy-report.txt 2>/dev/null || echo 'No deploy report file found on server.'" \ | ||
| > /tmp/deploy-report.txt || echo 'No deploy report available.' > /tmp/deploy-report.txt | ||
| - name: Upload deploy report as artifact | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: deploy-summary | ||
| path: /tmp/deploy-report.txt | ||
| if-no-files-found: ignore | ||
| retention-days: 7 | ||
| - name: Notify success | ||
| if: success() | ||
| env: | ||
| BOT_TOKEN: ${{ secrets.BOT_TOKEN }} | ||
| run: | | ||
| chmod +x scripts/notify-telegram.sh | ||
| ./scripts/notify-telegram.sh "✅ *Deploy Successful* | ||
| Commit: \`${{ needs.quality-gates.outputs.commit_hash }}\` | ||
| Version: \`${{ needs.quality-gates.outputs.version }}\` | ||
| Time: \`${{ needs.quality-gates.outputs.deploy_time }}\`" | ||
| - name: Rollback on failure | ||
| if: failure() | ||
| env: | ||
| BOT_TOKEN: ${{ secrets.BOT_TOKEN }} | ||
| run: | | ||
| PRE="${{ steps.pre.outputs.sha }}" | ||
| chmod +x scripts/notify-telegram.sh | ||
| ./scripts/notify-telegram.sh "❌ *Deploy Failed — Rolling Back* | ||
| Previous SHA: \`${PRE}\` | ||
| Reason: Deployment failed — see GitHub Actions logs for full stack trace." | ||
| ssh deploy_target " | ||
| cd /var/www/html/Runewager | ||
| if [ \"$PRE\" != \"none\" ] && [ -n \"$PRE\" ]; then | ||
| git reset --hard $PRE | ||
| npm ci --omit=dev || true | ||
| systemctl restart runewager.service || bash prod-run.sh restart || true | ||
| else | ||
| echo 'No previous SHA recorded, skipping git rollback' | ||
| fi | ||
| " | ||
| ./scripts/notify-telegram.sh "🔄 *Rollback Complete* | ||
| Restored to: \`${PRE}\`" | ||
| - name: Final deploy report | ||
| if: always() | ||
| env: | ||
| BOT_TOKEN: ${{ secrets.BOT_TOKEN }} | ||
| run: | | ||
| STATUS="${{ job.status }}" | ||
| COMMIT="${{ needs.quality-gates.outputs.commit_hash }}" | ||
| VERSION="${{ needs.quality-gates.outputs.version }}" | ||
| TIME="${{ needs.quality-gates.outputs.deploy_time }}" | ||
| DURATION="${{ needs.quality-gates.outputs.duration_seconds }}" | ||
| REPORT_BODY=$(cat /tmp/deploy-report.txt 2>/dev/null || echo "No deploy report available.") | ||
| if [ "$STATUS" = "success" ]; then | ||
| MSG="🎉 *Final Deploy Report* | ||
| Status: ✅ Success | ||
| Commit: \`$COMMIT\` | ||
| Version: \`$VERSION\` | ||
| Time: \`$TIME\` | ||
| Duration: \`${DURATION}s\` | ||
| $REPORT_BODY" | ||
| else | ||
| MSG="⚠️ *Final Deploy Report* | ||
| Status: ❌ Failure | ||
| Commit: \`$COMMIT\` | ||
| Version: \`$VERSION\` | ||
| Time: \`$TIME\` | ||
| Duration: \`${DURATION}s\` | ||
| Reason: Deployment failed — see GitHub Actions logs. | ||
| $REPORT_BODY" | ||
| fi | ||
| chmod +x scripts/notify-telegram.sh | ||
| ./scripts/notify-telegram.sh "$MSG" | ||
| - name: Post-deploy health check (30s delay) | ||
| if: success() | ||
| env: | ||
| BOT_TOKEN: ${{ secrets.BOT_TOKEN }} | ||
| run: | | ||
| echo "Waiting 30 seconds before post-deploy health check..." | ||
| sleep 30 | ||
| HEALTH_STATUS="FAIL" | ||
| if ssh deploy_target "curl -fsS http://127.0.0.1:3000/health" >/dev/null 2>&1; then | ||
| HEALTH_STATUS="OK" | ||
| fi | ||
| chmod +x scripts/notify-telegram.sh | ||
| ./scripts/notify-telegram.sh "📡 *Post-Deploy Health Check (30s)* | ||
| Status: \`${HEALTH_STATUS}\` | ||
| Commit: \`${{ needs.quality-gates.outputs.commit_hash }}\`" | ||
| # --------------------------------------------------------------------------- | ||
| # Job 3: Dry-run report (only when dry_run=true) | ||
| # --------------------------------------------------------------------------- | ||
| dry-run-report: | ||
| name: Dry Run Report | ||
| runs-on: ubuntu-latest | ||
| needs: quality-gates | ||
| if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true' }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Send dry-run notification | ||
| env: | ||
| BOT_TOKEN: ${{ secrets.BOT_TOKEN }} | ||
| run: | | ||
| chmod +x scripts/notify-telegram.sh | ||
| ./scripts/notify-telegram.sh "🧪 *Dry Run Only — No VPS Changes* | ||
| Repo: \`${{ github.repository }}\` | ||
| Branch: \`${{ github.ref_name }}\` | ||
| Commit: \`${{ needs.quality-gates.outputs.commit_hash }}\` | ||
| Version: \`${{ needs.quality-gates.outputs.version }}\` | ||
| Time: \`${{ needs.quality-gates.outputs.deploy_time }}\` | ||
| By: \`${{ github.actor }}\` | ||
| Quality gates passed. VPS was NOT modified." | ||