Files
klz-cables.com/.gitea/workflows/deploy.yml
Marc Mintel 8ffb1e5f6a
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Successful in 1m27s
Build & Deploy / 🏗️ Build (push) Successful in 3m15s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m30s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Nightly QA / 💨 Smoke & Health (push) Successful in 1m23s
chore(release): 2.3.28 - Add Gotify notifications
2026-06-21 09:34:05 +02:00

478 lines
21 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 }}
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.mintel.me"
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"
} >> "$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" 2>/dev/null | grep "$TAG_TO_WAIT" | awk '{print $1}' | tail -n1 || echo "")
if [[ -z "$UPSTREAM_SHA" ]]; then
echo "⚠️ Warning: Tag $TAG_TO_WAIT not found in mmintel/at-mintel."
else
echo "✅ Tag verified: Found upstream SHA $UPSTREAM_SHA for $TAG_TO_WAIT"
fi
# 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
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 }}"
# ──────────────────────────────────────────────────────────────────────────────
# 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 }}
# 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 || 'noreply@klz-cables.com' }}
MAIL_RECIPIENTS: ${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT || 'info@klz-cables.com' }}
# 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' }}
# Notifications
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
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 ""
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 "# Notifications"
echo "GOTIFY_URL=$GOTIFY_URL"
echo "GOTIFY_TOKEN=$GOTIFY_TOKEN"
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:-unknown}"
fi
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
ssh root@alpha.mintel.me "cd $SITE_DIR && echo '${{ secrets.REGISTRY_PASS }}' | docker login registry.infra.mintel.me -u '${{ secrets.REGISTRY_USER }}' --password-stdin"
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' pull"
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' up -d --remove-orphans"
ssh root@alpha.mintel.me "docker network prune -f"
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: Setup Chrome
id: setup-chrome
uses: browser-actions/setup-chrome@v1
with:
install-dependencies: true
# ── Minimalist Smoke Test ──────────────────────────────────────────
- name: 🌐 Smoke Test (Essential Flow)
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: ${{ steps.setup-chrome.outputs.chrome-path }}
run: npx tsx scripts/smoke.ts
# ──────────────────────────────────────────────────────────────────────────────
# 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 | Perf: $PERF
$URL"
if [[ -z "${{ secrets.GOTIFY_URL }}" || -z "${{ secrets.GOTIFY_TOKEN }}" ]]; then
echo "⚠️ Gotify credentials missing, skipping notification."
exit 0
fi
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
-F "title=$TITLE" \
-F "message=$MESSAGE" \
-F "priority=$PRIORITY" || true