Files
klz-cables.com/.gitea/workflows/deploy.yml
Marc Mintel f1d0227260
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 9s
Build & Deploy / 🧪 QA (push) Successful in 3m3s
Build & Deploy / 🏗️ Build (push) Successful in 19m36s
Build & Deploy / 🚀 Deploy (push) Successful in 1m25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 21s
fix(deploy): auto-run payload migrations on deploy, hide cms error details from visitors
2026-03-04 17:57:01 +01:00

598 lines
28 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Build & Deploy
on:
push:
branches:
- '**'
tags:
- 'v*'
workflow_dispatch:
inputs:
skip_checks:
description: 'Skip tests? (true/false)'
required: false
default: 'false'
env:
PUPPETEER_SKIP_DOWNLOAD: "true"
COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com"
concurrency:
group: deploy-pipeline
cancel-in-progress: true
jobs:
# ──────────────────────────────────────────────────────────────────────────────
# JOB 1: Prepare Environment
# ──────────────────────────────────────────────────────────────────────────────
prepare:
name: 🔍 Prepare
runs-on: docker
outputs:
target: ${{ steps.determine.outputs.target }}
image_tag: ${{ steps.determine.outputs.image_tag }}
env_file: ${{ steps.determine.outputs.env_file }}
traefik_host: ${{ steps.determine.outputs.traefik_host }}
traefik_rule: ${{ steps.determine.outputs.traefik_rule }}
next_public_url: ${{ steps.determine.outputs.next_public_url }}
project_name: ${{ steps.determine.outputs.project_name }}
short_sha: ${{ steps.determine.outputs.short_sha }}
slug: ${{ steps.determine.outputs.slug }}
gatekeeper_host: ${{ steps.determine.outputs.gatekeeper_host }}
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: 🧹 Maintenance (High Density Cleanup)
shell: bash
run: |
echo "Purging old build layers and dangling images..."
docker image prune -f
docker builder prune -f --filter "until=24h"
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: 🔍 Environment ermitteln
id: determine
shell: bash
run: |
REF="${{ github.ref_name }}"
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
DOMAIN="klz-cables.com"
PRJ="klz"
if [[ "${{ github.ref_type }}" == "branch" && "$REF" == "main" ]]; then
TARGET="testing"
IMAGE_TAG="main-${SHORT_SHA}"
ENV_FILE=".env.testing"
TRAEFIK_HOST="testing.${DOMAIN}"
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
if [[ "$REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
TARGET="production"
IMAGE_TAG="$REF"
ENV_FILE=".env.prod"
TRAEFIK_HOST="${DOMAIN}, www.${DOMAIN}"
else
TARGET="staging"
IMAGE_TAG="$REF"
ENV_FILE=".env.staging"
TRAEFIK_HOST="staging.${DOMAIN}"
fi
else
TARGET="branch"
SLUG=$(echo "$REF" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
IMAGE_TAG="branch-${SLUG}-${SHORT_SHA}"
ENV_FILE=".env.branch-${SLUG}"
TRAEFIK_HOST="${SLUG}.branch.klz-cables.com"
fi
# Standardize Traefik Rule (escaped backticks for Traefik v3)
if [[ "$TRAEFIK_HOST" == *","* ]]; then
TRAEFIK_RULE=$(echo "$TRAEFIK_HOST" | sed 's/,/ /g' | awk '{for(i=1;i<=NF;i++) printf "Host(\x60%s\x60)%s", $i, (i==NF?"":" || ")}')
PRIMARY_HOST=$(echo "$TRAEFIK_HOST" | cut -d',' -f1 | sed 's/ //g')
else
TRAEFIK_RULE='Host(`'"$TRAEFIK_HOST"'`)'
PRIMARY_HOST="$TRAEFIK_HOST"
fi
GATEKEEPER_HOST="gatekeeper.$PRIMARY_HOST"
{
echo "target=$TARGET"
echo "image_tag=$IMAGE_TAG"
echo "env_file=$ENV_FILE"
echo "traefik_host=$PRIMARY_HOST"
echo "traefik_rule=$TRAEFIK_RULE"
echo "gatekeeper_host=$GATEKEEPER_HOST"
echo "next_public_url=https://$PRIMARY_HOST"
if [[ "$TARGET" == "production" ]]; then
echo "project_name=klz-cablescom"
elif [[ "$TARGET" == "branch" ]]; then
echo "project_name=$PRJ-branch-$SLUG"
else
echo "project_name=$PRJ-$TARGET"
fi
echo "short_sha=$SHORT_SHA"
echo "slug=$SLUG"
} >> "$GITHUB_OUTPUT"
# ⏳ Wait for Upstream Packages/Images if Tagged
if [[ "${{ github.ref_type }}" == "tag" ]]; then
echo "🔎 Checking for @mintel dependencies in package.json..."
# Extract any @mintel/ version (they should be synced in monorepo)
UPSTREAM_VERSION=$(grep -o '"@mintel/.*": "[^"]*"' package.json | grep -v "next-utils" | cut -d'"' -f4 | sed 's/\^//; s/\~//' | sort -V | tail -1)
TAG_TO_WAIT="v$UPSTREAM_VERSION"
if [[ -n "$UPSTREAM_VERSION" && "$UPSTREAM_VERSION" != "workspace:"* ]]; then
# 1. Discovery (Works without token for public repositories)
UPSTREAM_SHA=$(git ls-remote --tags https://git.infra.mintel.me/mmintel/at-mintel.git "$TAG_TO_WAIT" | grep "$TAG_TO_WAIT" | tail -n1 | awk '{print $1}')
if [[ -z "$UPSTREAM_SHA" ]]; then
echo "❌ Error: Tag $TAG_TO_WAIT not found in mmintel/at-mintel."
exit 1
fi
echo "✅ Tag verified: Found upstream SHA $UPSTREAM_SHA for $TAG_TO_WAIT"
# 2. Status Check (Requires GITEA_PAT for cross-repo API access)
POLL_TOKEN="${{ secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}"
if [[ -n "$POLL_TOKEN" ]]; then
echo "⏳ GITEA_PAT found. Checking upstream build status..."
curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://git.infra.mintel.me/mmintel/at-mintel/raw/branch/main/packages/infra/scripts/wait-for-upstream.sh" > wait-for-upstream.sh
chmod +x wait-for-upstream.sh
GITEA_TOKEN="$POLL_TOKEN" ./wait-for-upstream.sh "mmintel/at-mintel" "$TAG_TO_WAIT"
else
echo " No GITEA_PAT secret found. Skipping build status wait (Actions API is restricted)."
echo " If this build fails, ensure that mmintel/at-mintel $TAG_TO_WAIT has finished its Docker build."
fi
fi
fi
# ──────────────────────────────────────────────────────────────────────────────
# JOB 2: QA (Lint, Typecheck, Test)
# ──────────────────────────────────────────────────────────────────────────────
qa:
name: 🧪 QA
needs: prepare
if: needs.prepare.outputs.target != 'skip'
runs-on: docker
env:
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: 🔐 Registry Auth
run: |
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
- name: Install dependencies
run: |
pnpm store prune
pnpm install --no-frozen-lockfile
- name: 🔒 Security Audit
run: pnpm audit --audit-level high || echo "⚠️ Audit found vulnerabilities (non-blocking)"
- name: 🧪 QA Checks
if: github.event.inputs.skip_checks != 'true'
env:
TURBO_TELEMETRY_DISABLED: "1"
run: npx turbo run lint typecheck test --cache-dir=".turbo"
# ──────────────────────────────────────────────────────────────────────────────
# JOB 3: Build & Push
# ──────────────────────────────────────────────────────────────────────────────
build:
name: 🏗️ Build
needs: [prepare, qa]
if: needs.prepare.outputs.target != 'skip'
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: 🐳 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 🔐 Registry Login
run: |
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: 🏗️ Build and Push
uses: docker/build-push-action@v5
with:
context: .
push: true
provenance: false
platforms: linux/amd64
build-args: |
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
tags: registry.infra.mintel.me/mintel/klz-2026:${{ needs.prepare.outputs.image_tag }}
secrets: |
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
- name: 🔄 Build and Push Migrator
uses: docker/build-push-action@v5
with:
context: .
push: true
provenance: false
platforms: linux/amd64
target: migrator
build-args: |
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
tags: registry.infra.mintel.me/mintel/klz-2026:migrate-${{ needs.prepare.outputs.image_tag }}
secrets: |
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
# ──────────────────────────────────────────────────────────────────────────────
# JOB 4: Deploy
# ──────────────────────────────────────────────────────────────────────────────
deploy:
name: 🚀 Deploy
needs: [prepare, build, qa]
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
env:
TARGET: ${{ needs.prepare.outputs.target }}
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
PROJECT_NAME: ${{ needs.prepare.outputs.project_name }}
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }}
GATEKEEPER_HOST: ${{ needs.prepare.outputs.gatekeeper_host }}
SLUG: ${{ needs.prepare.outputs.slug }}
# Secrets mapping (Payload CMS)
PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET || 'you-need-to-set-a-payload-secret' }}
PAYLOAD_DB_NAME: ${{ secrets.PAYLOAD_DB_NAME || vars.PAYLOAD_DB_NAME || 'payload' }}
PAYLOAD_DB_USER: ${{ secrets.PAYLOAD_DB_USER || vars.PAYLOAD_DB_USER || 'payload' }}
PAYLOAD_DB_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_PAYLOAD_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_PAYLOAD_DB_PASSWORD) || secrets.PAYLOAD_DB_PASSWORD || vars.PAYLOAD_DB_PASSWORD || 'payload' }}
# Secrets mapping (Mail)
MAIL_HOST: ${{ secrets.SMTP_HOST || vars.SMTP_HOST }}
MAIL_PORT: ${{ secrets.SMTP_PORT || vars.SMTP_PORT || '587' }}
MAIL_USERNAME: ${{ secrets.SMTP_USER || vars.SMTP_USER }}
MAIL_PASSWORD: ${{ secrets.SMTP_PASS || vars.SMTP_PASS }}
MAIL_FROM: ${{ secrets.SMTP_FROM || vars.SMTP_FROM }}
MAIL_RECIPIENTS: ${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT }}
# Monitoring
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }}
# Gatekeeper
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
# Analytics
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
# Container Registry (standalone)
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASS: ${{ secrets.REGISTRY_PASS }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: 📝 Generate Environment
shell: bash
env:
TRAEFIK_RULE: ${{ needs.prepare.outputs.traefik_rule }}
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
run: |
# Middleware Selection Logic
# Regular app routes get auth on non-production
# Unprotected routes (/stats, /errors) never get auth
LOG_LEVEL=$( [[ "$TARGET" == "testing" || "$TARGET" == "development" ]] && echo "debug" || echo "info" )
COOKIE_DOMAIN=.$(echo $NEXT_PUBLIC_BASE_URL | sed 's|https://||')
STD_MW="${PROJECT_NAME}-ratelimit,${PROJECT_NAME}-forward,${PROJECT_NAME}-compress"
if [[ "$TARGET" == "production" ]]; then
AUTH_MIDDLEWARE="$STD_MW"
COMPOSE_PROFILES=""
else
# Order: Ratelimit -> Forward (Proto) -> Auth -> Compression
AUTH_MIDDLEWARE="${PROJECT_NAME}-ratelimit,${PROJECT_NAME}-forward,${PROJECT_NAME}-auth,${PROJECT_NAME}-compress"
COMPOSE_PROFILES="gatekeeper"
fi
AUTH_MIDDLEWARE_UNPROTECTED="$STD_MW"
# Gatekeeper Origin
GATEKEEPER_ORIGIN="${NEXT_PUBLIC_BASE_URL}/gatekeeper"
{
echo "# Generated by CI - $TARGET"
echo "IMAGE_TAG=$IMAGE_TAG"
echo "NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL"
echo "GATEKEEPER_ORIGIN=$GATEKEEPER_ORIGIN"
echo "SENTRY_DSN=$SENTRY_DSN"
echo "LOG_LEVEL=$LOG_LEVEL"
echo "MAIL_HOST=$MAIL_HOST"
echo "MAIL_PORT=$MAIL_PORT"
echo "MAIL_USERNAME=$MAIL_USERNAME"
echo "MAIL_PASSWORD=$MAIL_PASSWORD"
echo "MAIL_FROM=$MAIL_FROM"
echo "MAIL_RECIPIENTS=$MAIL_RECIPIENTS"
echo ""
echo "# Payload CMS"
echo "PAYLOAD_SECRET=$PAYLOAD_SECRET"
echo "PAYLOAD_DB_NAME=$PAYLOAD_DB_NAME"
echo "PAYLOAD_DB_USER=$PAYLOAD_DB_USER"
echo "PAYLOAD_DB_PASSWORD=$PAYLOAD_DB_PASSWORD"
echo ""
echo "# Gatekeeper"
echo "GATEKEEPER_PASSWORD=$GATEKEEPER_PASSWORD"
echo "AUTH_COOKIE_NAME=klz_gatekeeper_session"
echo "COOKIE_DOMAIN=$COOKIE_DOMAIN"
echo ""
echo "# Analytics"
echo "UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID"
echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT"
echo ""
echo "TARGET=$TARGET"
echo "SENTRY_ENVIRONMENT=$TARGET"
echo "PROJECT_NAME=$PROJECT_NAME"
printf 'TRAEFIK_HOST_RULE=%s\n' "$TRAEFIK_RULE"
echo "TRAEFIK_HOST=$TRAEFIK_HOST"
echo "GATEKEEPER_HOST=$GATEKEEPER_HOST"
echo "TRAEFIK_ENTRYPOINT=websecure"
echo "TRAEFIK_TLS=true"
echo "TRAEFIK_CERT_RESOLVER=le"
echo "ENV_FILE=$ENV_FILE"
echo "COMPOSE_PROFILES=$COMPOSE_PROFILES"
echo "AUTH_MIDDLEWARE=$AUTH_MIDDLEWARE"
echo "AUTH_MIDDLEWARE_UNPROTECTED=$AUTH_MIDDLEWARE_UNPROTECTED"
} > .env.deploy
echo "--- Generated .env.deploy ---"
cat .env.deploy
echo "----------------------------"
- name: 🚀 SSH Deploy
shell: bash
env:
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
run: |
mkdir -p ~/.ssh
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
# Transfer and Restart
if [[ "$TARGET" == "production" ]]; then
SITE_DIR="/home/deploy/sites/klz-cables.com"
elif [[ "$TARGET" == "testing" ]]; then
SITE_DIR="/home/deploy/sites/testing.klz-cables.com"
elif [[ "$TARGET" == "staging" ]]; then
SITE_DIR="/home/deploy/sites/staging.klz-cables.com"
else
SITE_DIR="/home/deploy/sites/branch.klz-cables.com/$SLUG"
fi
# Transfer files
ssh root@alpha.mintel.me "mkdir -p $SITE_DIR"
scp .env.deploy root@alpha.mintel.me:$SITE_DIR/$ENV_FILE
scp docker-compose.yml root@alpha.mintel.me:$SITE_DIR/docker-compose.yml
# Execute remote commands — alpha is pre-logged into registry.infra.mintel.me
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE pull && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE up -d --remove-orphans"
# Sanitize Payload Migrations: Replace 'dev' push entries with proper migration names.
# Without this, Payload prompts interactively for confirmation and blocks forever in Docker.
DB_CONTAINER="${{ needs.prepare.outputs.project_name }}-klz-db-1"
echo "⏳ Waiting for database container to be ready..."
for i in $(seq 1 15); do
if ssh root@alpha.mintel.me "docker exec $DB_CONTAINER pg_isready -U payload -q 2>/dev/null"; then
echo "✅ Database is ready."
break
fi
echo " Attempt $i/15..."
sleep 2
done
echo "🔧 Sanitizing payload_migrations table (if exists)..."
REMOTE_DB_USER=$(ssh root@alpha.mintel.me "grep -h '^PAYLOAD_DB_USER=' $SITE_DIR/.env* 2>/dev/null | tail -1 | cut -d= -f2" || echo "payload")
REMOTE_DB_NAME=$(ssh root@alpha.mintel.me "grep -h '^PAYLOAD_DB_NAME=' $SITE_DIR/.env* 2>/dev/null | tail -1 | cut -d= -f2" || echo "payload")
REMOTE_DB_USER="${REMOTE_DB_USER:-payload}"
REMOTE_DB_NAME="${REMOTE_DB_NAME:-payload}"
# Run Payload migrations via a temporary container before restarting the app.
# This ensures fresh branch deployments (empty DBs) get their schema on first deploy.
echo "🔄 Running Payload migrations..."
MIGRATOR_IMAGE="registry.infra.mintel.me/mintel/klz-2026:migrate-$IMAGE_TAG"
ssh root@alpha.mintel.me "
echo '${{ steps.auth.outputs.token }}' | docker login registry.infra.mintel.me -u '${{ steps.auth.outputs.user }}' --password-stdin 2>/dev/null || true
docker pull $MIGRATOR_IMAGE
docker run --rm \
--network ${PROJECT_NAME}_default \
--env-file $SITE_DIR/$ENV_FILE \
$MIGRATOR_IMAGE \
&& echo '✅ Migrations complete.' \
|| echo '⚠️ Migrations failed or already up-to-date — continuing.'
"
# Restart app to pick up clean migration state
APP_CONTAINER="${PROJECT_NAME}-klz-app-1"
ssh root@alpha.mintel.me "docker restart $APP_CONTAINER"
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'"
- name: 🧹 Post-Deploy Cleanup (Runner)
if: always()
run: docker builder prune -f --filter "until=1h"
# ──────────────────────────────────────────────────────────────────────────────
# JOB 5: Post-Deploy Verification (Smoke Tests + Quality Gates)
# ──────────────────────────────────────────────────────────────────────────────
post_deploy_checks:
name: 🧪 Post-Deploy Verification
needs: [prepare, deploy]
if: needs.deploy.result == 'success' && needs.prepare.outputs.target != 'branch'
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: 🔐 Registry Auth
run: |
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
- name: Install dependencies
id: deps
run: |
pnpm store prune
pnpm install --no-frozen-lockfile
- name: 📦 Cache APT Packages
uses: actions/cache@v4
with:
path: /var/cache/apt/archives
key: apt-cache-${{ runner.os }}-${{ runner.arch }}-chromium
- name: 💾 Cache Chromium
id: cache-chromium
uses: actions/cache@v4
with:
path: /usr/bin/chromium
key: ${{ runner.os }}-chromium-native-${{ hashFiles('package.json') }}
- name: 🔍 Install Chromium (Native & ARM64)
if: steps.cache-chromium.outputs.cache-hit != 'true'
run: |
rm -f /etc/apt/apt.conf.d/docker-clean
apt-get update
apt-get install -y gnupg wget ca-certificates
OS_ID=$(. /etc/os-release && echo $ID)
CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME)
if [ "$OS_ID" = "debian" ]; then
apt-get install -y chromium
else
mkdir -p /etc/apt/keyrings
KEY_ID="82BB6851C64F6880"
wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_ID" | gpg --dearmor > /etc/apt/keyrings/xtradeb.gpg
echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" > /etc/apt/sources.list.d/xtradeb-ppa.list
printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" > /etc/apt/preferences.d/xtradeb
apt-get update
apt-get install -y --allow-downgrades chromium
fi
[ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/google-chrome
[ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/chromium-browser
# ── Critical Smoke Tests (MUST pass) ──────────────────────────────────
- name: 🏥 CMS Deep Health Check
env:
DEPLOY_URL: ${{ needs.prepare.outputs.next_public_url }}
GK_PASS: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
run: |
echo "Waiting 10s for app to fully start..."
sleep 10
echo "Checking basic health..."
curl -sf "$DEPLOY_URL/health" || { echo "❌ Basic health check failed"; exit 1; }
echo "✅ Basic health OK"
echo "Checking CMS DB connectivity..."
RESPONSE=$(curl -sf "$DEPLOY_URL/api/health/cms?gk_bypass=$GK_PASS" 2>&1) || {
echo "❌ CMS health check failed!"
echo "$RESPONSE"
echo ""
echo "This usually means Payload CMS migrations failed or DB tables are missing."
echo "Check: docker logs \$APP_CONTAINER | grep -i error"
exit 1
}
echo "✅ CMS health: $RESPONSE"
- name: 🚀 OG Image Check
if: always() && steps.deps.outcome == 'success'
env:
TEST_URL: ${{ needs.prepare.outputs.next_public_url }}
run: pnpm run check:og
- name: 🌐 Core Smoke Tests (HTTP, API, Locale)
if: always() && steps.deps.outcome == 'success'
uses: https://git.infra.mintel.me/mmintel/at-mintel/.gitea/actions/core-smoke-tests@main
with:
TARGET_URL: ${{ needs.prepare.outputs.next_public_url }}
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }}
- name: 📝 E2E Form Submission Test
if: always() && steps.deps.outcome == 'success'
env:
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
run: pnpm run check:forms
# ──────────────────────────────────────────────────────────────────────────────
# JOB 7: Notifications
# ──────────────────────────────────────────────────────────────────────────────
notifications:
name: 🔔 Notify
needs: [prepare, deploy, post_deploy_checks]
if: always()
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: 🔔 Gotify
shell: bash
run: |
DEPLOY="${{ needs.deploy.result }}"
SMOKE="${{ needs.post_deploy_checks.result }}"
PERF="${{ needs.post_deploy_checks.result }}"
TARGET="${{ needs.prepare.outputs.target }}"
VERSION="${{ needs.prepare.outputs.image_tag }}"
URL="${{ needs.prepare.outputs.next_public_url }}"
# Gotify priority scale:
# 1-3 = low (silent/info)
# 4-5 = normal
# 6-7 = high (warning)
# 8-10 = critical (alarm)
if [[ "$DEPLOY" != "success" ]]; then
PRIORITY=10
EMOJI="🚨"
STATUS_LINE="DEPLOY FAILED"
elif [[ "$SMOKE" != "success" ]]; then
PRIORITY=8
EMOJI="⚠️"
STATUS_LINE="Smoke tests failed"
elif [[ "$PERF" != "success" ]]; then
PRIORITY=5
EMOJI="📉"
STATUS_LINE="Performance degraded"
else
PRIORITY=2
EMOJI="✅"
STATUS_LINE="All checks passed"
fi
TITLE="$EMOJI klz-cables.com $VERSION -> $TARGET"
MESSAGE="$STATUS_LINE | Deploy: $DEPLOY | Smoke: $SMOKE | $URL"
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
-F "title=$TITLE" \
-F "message=$MESSAGE" \
-F "priority=$PRIORITY" || true