Files
pwap/.github/workflows/quality-gate.yml
T
pezkuwichain 06ed9734c6 ci(security): Faz 3 + ekstra — runner consolidation, auto-rollback, cosign, SRI, dep cleanup
* Faz 3.1 — All CI jobs moved to self-hosted pwap-runner (DEV VPS).
  No more dependency on GitHub-hosted runners — supply-chain attack
  surface from GHA runner image compromise eliminated.
* Faz 3.3 — Automatic rollback on health-check fail. Each deploy stamps
  /.deploy-sha into the artifact. On health-check failure, the deploy
  job reads the previous SHA from the live site, pulls that image, and
  redeploys. Telegram notification differentiates: rolled-back-OK,
  rollback-also-failed, no-prev-available, manual-rollback-needed.
* E.3 — cosign keyless image signing. build-image signs the GHCR
  manifest via Sigstore Fulcio (OIDC, no long-lived keys). deploy-app
  and deploy-pex verify the signature before extracting /dist —
  unsigned or tampered images cannot deploy. Identity-pinned to this
  workflow file.
* E.5 — Subresource Integrity (SRI). vite-plugin-subresource-integrity
  injects sha384 integrity= into <script>/<link> tags at build time.
  CDN/proxy compromise cannot inject tampered JS — browser blocks on
  hash mismatch.
* E.2 — Dependabot triage. 14 alerts: 7 high + 4 moderate cleared via
  npm audit fix + npm overrides (elliptic, create-ecdh). 6 low
  (transitive in vite-plugin-node-polyfills chain) accepted; the
  upstream fix proposes a semver-major DOWNGRADE which makes no sense.
* E.1 — Branch protection on main: CI Gate  required, 1 review
  required, force-push and deletion blocked.
2026-05-09 12:08:49 +03:00

673 lines
25 KiB
YAML

name: Quality Gate & Deploy
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
workflow_dispatch:
inputs:
rollback_to:
description: 'Rollback to git SHA (skips build, redeploys old image). Empty = normal deploy.'
required: false
default: ''
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write # version bump commit
packages: write # GHCR push
env:
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
REGISTRY: ghcr.io
IMAGE_NAME: pezkuwichain/pwap-web
jobs:
# ========================================
# WEB APP - LINT, TEST & BUILD
# ========================================
web:
name: Web App
runs-on: pwap-runner
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Checkout Pezkuwi-SDK (for docs generation)
run: |
git clone https://git.pezkuwichain.io/pezkuwichain/pezkuwi-sdk.git Pezkuwi-SDK || \
git clone https://github.com/pezkuwichain/pezkuwi-sdk.git Pezkuwi-SDK
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Cache npm dependencies
uses: actions/cache@v4
with:
path: web/node_modules
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json') }}
restore-keys: |
${{ runner.os }}-web-
- name: Install dependencies
working-directory: ./web
run: npm install
- name: Run Linter
working-directory: ./web
run: npm run lint
- name: Run Tests
working-directory: ./web
run: npm run test
- name: Build Project
working-directory: ./web
run: npm run build
env:
VITE_NETWORK: MAINNET
VITE_WS_ENDPOINT: wss://rpc.pezkuwichain.io
VITE_WS_ENDPOINT_FALLBACK_1: wss://mainnet.pezkuwichain.io
VITE_ASSET_HUB_ENDPOINT: wss://asset-hub-rpc.pezkuwichain.io
VITE_PEOPLE_CHAIN_ENDPOINT: wss://people-rpc.pezkuwichain.io
VITE_WALLETCONNECT_PROJECT_ID: 8292a793b7640e8364c378e331e76d04
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: web-dist
path: web/dist/
# ========================================
# BUILD & PUSH DOCKER IMAGE TO GHCR
# Immutable artifact for audit + rollback (vs ephemeral GHA artifact).
# Tagged with git SHA so any commit can be redeployed by SHA.
# ========================================
build-image:
name: Build & Push Image
runs-on: pwap-runner
needs: [web, telegram-gate]
if: |
github.ref == 'refs/heads/main' &&
(github.event_name == 'push' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to == ''))
permissions:
contents: read
packages: write
id-token: write # cosign keyless signing via Sigstore OIDC
outputs:
image_sha: ${{ steps.meta.outputs.image_sha }}
image_digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Install cosign
uses: sigstore/cosign-installer@v3
with:
cosign-release: 'v2.4.1'
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract image metadata
id: meta
run: |
SHORT_SHA="${GITHUB_SHA:0:7}"
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "image_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_OUTPUT
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
context: ./
file: ./web/Dockerfile
push: true
tags: |
${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.short_sha }}
${{ steps.meta.outputs.image }}:latest
build-args: |
VITE_NETWORK=MAINNET
VITE_WS_ENDPOINT=wss://rpc.pezkuwichain.io
VITE_WS_ENDPOINT_FALLBACK_1=wss://mainnet.pezkuwichain.io
VITE_ASSET_HUB_ENDPOINT=wss://asset-hub-rpc.pezkuwichain.io
VITE_PEOPLE_CHAIN_ENDPOINT=wss://people-rpc.pezkuwichain.io
VITE_WALLETCONNECT_PROJECT_ID=8292a793b7640e8364c378e331e76d04
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
cache-from: type=registry,ref=${{ steps.meta.outputs.image }}:cache
cache-to: type=registry,ref=${{ steps.meta.outputs.image }}:cache,mode=max
provenance: false
- name: Sign image with cosign (keyless, Sigstore Fulcio)
env:
COSIGN_EXPERIMENTAL: '1'
run: |
IMAGE_DIGEST="${{ steps.meta.outputs.image }}@${{ steps.build.outputs.digest }}"
echo "Signing $IMAGE_DIGEST"
cosign sign --yes "$IMAGE_DIGEST"
echo "✅ Image signed (transparency log: rekor.sigstore.dev)"
# ========================================
# TELEGRAM CEO APPROVAL GATE
# Runs on self-hosted pwap-runner (DEV VPS) where pexsec-bot.service
# writes the gate file to /tmp/pexsec-gates/<sha> when CEO clicks
# Approve/Cancel in Telegram. 30-minute timeout = deploy cancelled.
# ========================================
telegram-gate:
name: Telegram deploy approval
runs-on: pwap-runner
needs: [web, security-audit]
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
steps:
- name: Send approval request and wait
env:
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
SHA: ${{ github.sha }}
ACTOR: ${{ github.actor }}
MESSAGE: ${{ github.event.head_commit.message }}
run: |
SHORT="${SHA:0:7}"
GATE_DIR="/tmp/pexsec-gates"
mkdir -p "$GATE_DIR" 2>/dev/null || true
rm -f "$GATE_DIR/$SHORT" 2>/dev/null || true
# Strip Markdown special chars to prevent Telegram parse errors
SAFE_MSG=$(echo "${MESSAGE}" | head -1 | tr -d '_*`[]()#|{}!' | cut -c1-120)
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "{
\"chat_id\": \"${CEO_CHAT_ID}\",
\"parse_mode\": \"Markdown\",
\"text\": \"🚀 *pwap/web Deploy Approval*\\n\\n\`${SHORT}\` — ${ACTOR}\\n\\n_${SAFE_MSG}_\\n\\nTargets: app.pezkuwichain.io + pex.mom\",
\"reply_markup\": {
\"inline_keyboard\": [[
{\"text\": \"✅ Approve\", \"callback_data\": \"deploy_approve:${SHORT}\"},
{\"text\": \"❌ Cancel\", \"callback_data\": \"deploy_cancel:${SHORT}\"}
]]
}
}"
echo "Waiting for Telegram approval (max 30 min)..."
TIMEOUT=1800
ELAPSED=0
while [ $ELAPSED -lt $TIMEOUT ]; do
if [ -f "$GATE_DIR/$SHORT" ]; then
DECISION=$(cat "$GATE_DIR/$SHORT")
rm -f "$GATE_DIR/$SHORT" 2>/dev/null || true
if [ "$DECISION" = "approved" ]; then
echo "Deploy approved."
exit 0
else
echo "Deploy cancelled."
exit 1
fi
fi
sleep 10
ELAPSED=$((ELAPSED + 10))
done
echo "No approval received within 30 minutes — deploy cancelled."
exit 1
# ========================================
# VERSION BUMP (RUNS BEFORE BOTH DEPLOYS)
# ========================================
bump-version:
name: Bump Version
runs-on: pwap-runner
needs: [web, security-audit, telegram-gate, build-image]
# Skip on rollback (workflow_dispatch with rollback_to set)
if: |
github.ref == 'refs/heads/main' &&
(github.event_name == 'push' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to == ''))
outputs:
new_version: ${{ steps.bump.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Bump version
id: bump
working-directory: ./web
run: |
npm version patch --no-git-tag-version
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
cd ..
git add web/package.json
git commit -m "chore(web): bump version to $VERSION [skip ci]" || echo "No version change"
git push || echo "Nothing to push"
# ========================================
# DEPLOY TO app.pezkuwichain.io (DEV VPS)
# Pulls SHA-tagged image from GHCR, extracts /dist, scp to VPS.
# Health check + auto-rollback to .deploy-tag-prev on failure.
# ========================================
deploy-app:
name: Deploy app.pezkuwichain.io
runs-on: pwap-runner
needs: [telegram-gate, bump-version, build-image]
if: |
always() &&
needs.telegram-gate.result == 'success' &&
((github.event_name == 'push' && needs.build-image.result == 'success' && needs.bump-version.result == 'success') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to != ''))
permissions:
contents: read
packages: read
env:
DOMAIN: app.pezkuwichain.io
TARGET_PATH: /var/www/subdomains/app
steps:
- name: Determine image SHA
id: sha
run: |
if [ -n "${{ github.event.inputs.rollback_to }}" ]; then
echo "sha=${{ github.event.inputs.rollback_to }}" >> $GITHUB_OUTPUT
echo "Rolling back to: ${{ github.event.inputs.rollback_to }}"
else
echo "sha=${{ needs.build-image.outputs.image_sha }}" >> $GITHUB_OUTPUT
fi
- name: Capture currently-live SHA (for auto-rollback)
id: prev
run: |
# /.deploy-sha is written into every deploy; read what's live now
PREV=$(curl -sf --max-time 5 "https://${{ env.DOMAIN }}/.deploy-sha" | head -c 40 | tr -dc 'a-f0-9' || echo "")
echo "Previous live SHA: ${PREV:-unknown}"
echo "prev=$PREV" >> $GITHUB_OUTPUT
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install cosign (for verify)
uses: sigstore/cosign-installer@v3
with:
cosign-release: 'v2.4.1'
- name: Verify image signature (cosign keyless)
env:
COSIGN_EXPERIMENTAL: '1'
run: |
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
echo "Verifying signature for $IMAGE"
# Identity = workflow that built this image (build-image job in this repo)
cosign verify "$IMAGE" \
--certificate-identity-regexp "^https://github.com/pezkuwichain/pwap/.github/workflows/quality-gate.yml@" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
> /dev/null
echo "✅ Signature valid — image was built by trusted CI"
- name: Extract /dist from image
run: |
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
docker pull "$IMAGE"
CID=$(docker create "$IMAGE")
mkdir -p dist
docker cp "$CID:/dist/." dist/
docker rm "$CID" >/dev/null
# Stamp this build's SHA into dist so future deploys can read PREV
echo "${{ steps.sha.outputs.sha }}" > dist/.deploy-sha
ls -la dist/ | head -10
- name: Deploy to DEV VPS
uses: appleboy/scp-action@v1.0.0
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_SSH_PORT || 2222 }}
source: 'dist/*'
target: '/var/www/subdomains/app'
strip_components: 1
- name: Health check (60s window)
id: healthcheck
run: |
for i in 1 2 3 4 5 6; do
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
echo "✅ ${{ env.DOMAIN }} healthy"
exit 0
fi
echo "Attempt $i/6 failed, retrying in 10s..."
sleep 10
done
echo "❌ Health check failed for ${{ env.DOMAIN }}"
exit 1
# ── Automatic rollback: pull PREV SHA image, redeploy, recheck ──
- name: Auto-rollback to previous SHA
id: rollback
if: failure() && steps.healthcheck.conclusion == 'failure' && steps.prev.outputs.prev != ''
run: |
PREV="${{ steps.prev.outputs.prev }}"
echo "🔄 Rolling back ${{ env.DOMAIN }} to $PREV"
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$PREV"
docker pull "$IMAGE"
CID=$(docker create "$IMAGE")
rm -rf dist && mkdir dist
docker cp "$CID:/dist/." dist/
docker rm "$CID" >/dev/null
echo "$PREV" > dist/.deploy-sha
echo "rollback_sha=$PREV" >> $GITHUB_OUTPUT
- name: SCP rollback artifact
if: steps.rollback.outcome == 'success'
uses: appleboy/scp-action@v1.0.0
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_SSH_PORT || 2222 }}
source: 'dist/*'
target: '/var/www/subdomains/app'
strip_components: 1
- name: Re-health-check after rollback
if: steps.rollback.outcome == 'success'
id: healthcheck_rb
run: |
for i in 1 2 3 4 5 6; do
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
echo "✅ Rolled back successfully — ${{ env.DOMAIN }} healthy on ${{ steps.rollback.outputs.rollback_sha }}"
exit 0
fi
sleep 10
done
echo "❌ Rollback also failed!"
exit 1
- name: Post-deploy notification
if: success()
run: |
echo "✅ Deployed image ${{ steps.sha.outputs.sha }} to ${{ env.DOMAIN }}"
- name: Notify failure (Telegram)
if: failure()
env:
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
NEW_SHA: ${{ steps.sha.outputs.sha }}
PREV_SHA: ${{ steps.prev.outputs.prev }}
ROLLBACK_OUTCOME: ${{ steps.rollback.outcome }}
RECHECK_OUTCOME: ${{ steps.healthcheck_rb.outcome }}
run: |
if [ "$RECHECK_OUTCOME" = "success" ]; then
MSG="⚠️ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed health check, AUTO-ROLLED-BACK to $PREV_SHA. Site healthy."
elif [ "$ROLLBACK_OUTCOME" = "success" ]; then
MSG="🚨 pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed AND rollback to $PREV_SHA also failed. Manual intervention needed."
elif [ -z "$PREV_SHA" ]; then
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. No previous SHA available (first deploy?). Manual rollback: gh workflow run quality-gate.yml -f rollback_to=<sha>"
else
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. Auto-rollback was not attempted. Manual: gh workflow run quality-gate.yml -f rollback_to=$PREV_SHA"
fi
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-d "chat_id=${CEO_CHAT_ID}" --data-urlencode "text=$MSG"
# ========================================
# DEPLOY TO pex.mom (VPS3 — geo-redundant mirror)
# ========================================
deploy-pex:
name: Deploy pex.mom
runs-on: pwap-runner
needs: [telegram-gate, bump-version, build-image]
if: |
always() &&
needs.telegram-gate.result == 'success' &&
((github.event_name == 'push' && needs.build-image.result == 'success' && needs.bump-version.result == 'success') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to != ''))
permissions:
contents: read
packages: read
env:
DOMAIN: pex.mom
TARGET_PATH: /var/www/pex.mom
steps:
- name: Determine image SHA
id: sha
run: |
if [ -n "${{ github.event.inputs.rollback_to }}" ]; then
echo "sha=${{ github.event.inputs.rollback_to }}" >> $GITHUB_OUTPUT
else
echo "sha=${{ needs.build-image.outputs.image_sha }}" >> $GITHUB_OUTPUT
fi
- name: Capture currently-live SHA (for auto-rollback)
id: prev
run: |
PREV=$(curl -sf --max-time 5 "https://${{ env.DOMAIN }}/.deploy-sha" | head -c 40 | tr -dc 'a-f0-9' || echo "")
echo "Previous live SHA: ${PREV:-unknown}"
echo "prev=$PREV" >> $GITHUB_OUTPUT
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install cosign (for verify)
uses: sigstore/cosign-installer@v3
with:
cosign-release: 'v2.4.1'
- name: Verify image signature (cosign keyless)
env:
COSIGN_EXPERIMENTAL: '1'
run: |
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
echo "Verifying signature for $IMAGE"
cosign verify "$IMAGE" \
--certificate-identity-regexp "^https://github.com/pezkuwichain/pwap/.github/workflows/quality-gate.yml@" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
> /dev/null
echo "✅ Signature valid — image was built by trusted CI"
- name: Extract /dist from image
run: |
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
docker pull "$IMAGE"
CID=$(docker create "$IMAGE")
mkdir -p dist
docker cp "$CID:/dist/." dist/
docker rm "$CID" >/dev/null
echo "${{ steps.sha.outputs.sha }}" > dist/.deploy-sha
- name: Deploy to VPS3
uses: appleboy/scp-action@v1.0.0
with:
host: ${{ secrets.VPS_PEX_HOST }}
username: ${{ secrets.VPS_PEX_USER }}
key: ${{ secrets.VPS_PEX_SSH_KEY }}
port: ${{ secrets.VPS_PEX_SSH_PORT || 22 }}
source: 'dist/*'
target: '/var/www/pex.mom'
strip_components: 1
- name: Health check (60s window)
id: healthcheck
run: |
for i in 1 2 3 4 5 6; do
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
echo "✅ ${{ env.DOMAIN }} healthy"
exit 0
fi
echo "Attempt $i/6 failed, retrying in 10s..."
sleep 10
done
echo "❌ Health check failed for ${{ env.DOMAIN }}"
exit 1
- name: Auto-rollback to previous SHA
id: rollback
if: failure() && steps.healthcheck.conclusion == 'failure' && steps.prev.outputs.prev != ''
run: |
PREV="${{ steps.prev.outputs.prev }}"
echo "🔄 Rolling back ${{ env.DOMAIN }} to $PREV"
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$PREV"
docker pull "$IMAGE"
CID=$(docker create "$IMAGE")
rm -rf dist && mkdir dist
docker cp "$CID:/dist/." dist/
docker rm "$CID" >/dev/null
echo "$PREV" > dist/.deploy-sha
echo "rollback_sha=$PREV" >> $GITHUB_OUTPUT
- name: SCP rollback artifact
if: steps.rollback.outcome == 'success'
uses: appleboy/scp-action@v1.0.0
with:
host: ${{ secrets.VPS_PEX_HOST }}
username: ${{ secrets.VPS_PEX_USER }}
key: ${{ secrets.VPS_PEX_SSH_KEY }}
port: ${{ secrets.VPS_PEX_SSH_PORT || 22 }}
source: 'dist/*'
target: '/var/www/pex.mom'
strip_components: 1
- name: Re-health-check after rollback
if: steps.rollback.outcome == 'success'
id: healthcheck_rb
run: |
for i in 1 2 3 4 5 6; do
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
echo "✅ Rolled back successfully — ${{ env.DOMAIN }} healthy on ${{ steps.rollback.outputs.rollback_sha }}"
exit 0
fi
sleep 10
done
echo "❌ Rollback also failed!"
exit 1
- name: Post-deploy notification
if: success()
run: |
echo "✅ Deployed image ${{ steps.sha.outputs.sha }} to ${{ env.DOMAIN }}"
- name: Notify failure (Telegram)
if: failure()
env:
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
NEW_SHA: ${{ steps.sha.outputs.sha }}
PREV_SHA: ${{ steps.prev.outputs.prev }}
ROLLBACK_OUTCOME: ${{ steps.rollback.outcome }}
RECHECK_OUTCOME: ${{ steps.healthcheck_rb.outcome }}
run: |
if [ "$RECHECK_OUTCOME" = "success" ]; then
MSG="⚠️ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed health check, AUTO-ROLLED-BACK to $PREV_SHA. Site healthy."
elif [ "$ROLLBACK_OUTCOME" = "success" ]; then
MSG="🚨 pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed AND rollback to $PREV_SHA also failed. Manual intervention needed."
elif [ -z "$PREV_SHA" ]; then
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. No previous SHA available (first deploy?). Manual rollback: gh workflow run quality-gate.yml -f rollback_to=<sha>"
else
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. Auto-rollback was not attempted. Manual: gh workflow run quality-gate.yml -f rollback_to=$PREV_SHA"
fi
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-d "chat_id=${CEO_CHAT_ID}" --data-urlencode "text=$MSG"
# ========================================
# SECURITY CHECKS (BLOCKING)
# npm audit (high + critical) + TruffleHog secret scan
# ========================================
security-audit:
name: Security Audit
runs-on: pwap-runner
needs: [web]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: ${{ github.event_name == 'pull_request' && 0 || 1 }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Web — npm audit (high + critical)
working-directory: ./web
run: |
npm install
npm audit --audit-level=high
- name: TruffleHog — PR diff (verified secrets only)
if: github.event_name == 'pull_request'
uses: trufflesecurity/trufflehog@main
with:
base: ${{ github.event.pull_request.base.sha }}
head: ${{ github.event.pull_request.head.sha }}
extra_args: --only-verified
- name: TruffleHog — full repo scan (verified secrets only)
if: github.event_name != 'pull_request'
uses: trufflesecurity/trufflehog@main
with:
path: ./
extra_args: --only-verified
# ========================================
# CI GATE — explicit merge-block
# All required checks must succeed (or be skipped, e.g. for rollback path).
# Branch protection on main should require this job's success.
# ========================================
ci-gate:
name: CI Gate ✅
runs-on: pwap-runner
needs: [web, security-audit]
if: always()
steps:
- name: Verify all required jobs succeeded or were intentionally skipped
run: |
results='${{ toJSON(needs) }}'
echo "$results" | python3 -c "
import json, sys
needs = json.load(sys.stdin)
failed = [name for name, job in needs.items() if job['result'] not in ('success', 'skipped')]
if failed:
print('❌ Required jobs failed: ' + ', '.join(failed))
sys.exit(1)
print('✅ All required CI jobs passed or skipped')
"