From c1304403a17f0b3838031cd127a21ba934af61b6 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sat, 31 Jan 2026 22:32:32 +0100 Subject: [PATCH] ci cd --- .gitea/workflows/ci.yml | 32 ++++++ .gitea/workflows/deploy.yml | 201 ++++++++++++++++++++---------------- 2 files changed, 143 insertions(+), 90 deletions(-) create mode 100644 .gitea/workflows/ci.yml diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 00000000..a7e016ec --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI - Lint, Typecheck & Test + +on: + push: + branches-ignore: + - main + pull_request: + +jobs: + quality-assurance: + runs-on: docker + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: πŸ” Lint + run: npm run lint + + - name: πŸ—οΈ Typecheck + run: npm run typecheck + + - name: πŸ§ͺ Test + run: npm run test diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index cbc3e3cc..652aa87c 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -8,42 +8,34 @@ on: - 'v*' jobs: - build-and-deploy: + # ────────────────────────────────────────────────────────────────────────────── + # JOB 1: Prepare & Determine Environment + # ────────────────────────────────────────────────────────────────────────────── + prepare: + name: πŸ” Prepare Environment 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 }} + is_prod: ${{ steps.determine.outputs.is_prod }} + gotify_title: ${{ steps.determine.outputs.gotify_title }} + gotify_priority: ${{ steps.determine.outputs.gotify_priority }} + short_sha: ${{ steps.determine.outputs.short_sha }} + commit_msg: ${{ steps.determine.outputs.commit_msg }} steps: - # ────────────────────────────────────────────────────────────────────────────── - # Workflow Start & Basic Info - # ────────────────────────────────────────────────────────────────────────────── - - name: πŸ“’ Workflow Start - run: | - echo "β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”" - echo "β”‚ πŸš€ KLZ Cables Deployment Workflow gestartet β”‚" - echo "β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€" - echo "β”‚ Repository: ${{ github.repository }} β”‚" - echo "β”‚ Ref: ${{ github.ref }} β”‚" - echo "β”‚ Ref-Name: ${{ github.ref_name }} β”‚" - echo "β”‚ Commit: ${{ github.sha }} β”‚" - echo "β”‚ Actor: ${{ github.actor }} β”‚" - echo "β”‚ Datum: $(date -u +'%Y-%m-%d %H:%M:%S UTC') β”‚" - echo "β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜" - - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - # ────────────────────────────────────────────────────────────────────────────── - # Environment bestimmen + Commit-Message holen - # ────────────────────────────────────────────────────────────────────────────── - name: πŸ” Environment & Version ermitteln id: determine run: | TAG="${{ github.ref_name }}" SHORT_SHA="${{ github.sha }}" SHORT_SHA="${SHORT_SHA:0:9}" - - # Commit-Message holen (erste Zeile) COMMIT_MSG=$(git log -1 --pretty=%s || echo "No commit message available") if [[ "${{ github.ref_type }}" == "branch" && "$TAG" == "main" ]]; then @@ -90,31 +82,59 @@ jobs: echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT echo "commit_msg=$COMMIT_MSG" >> $GITHUB_OUTPUT - - name: ⏭️ Skip Deployment - if: steps.determine.outputs.target == 'skip' - run: | - echo "Deployment ΓΌbersprungen – kein passender Trigger (main oder v*-Tag)" - exit 0 + # ────────────────────────────────────────────────────────────────────────────── + # JOB 2: Quality Assurance (Lint & Test) + # ────────────────────────────────────────────────────────────────────────────── + qa: + name: πŸ§ͺ Quality Assurance + needs: prepare + if: needs.prepare.outputs.target != 'skip' + runs-on: docker + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: πŸ” Lint & Typecheck + run: | + npm run lint + npm run typecheck + + - name: πŸ§ͺ Test + run: npm run test + + # ────────────────────────────────────────────────────────────────────────────── + # JOB 3: Build & Push Docker Image + # ────────────────────────────────────────────────────────────────────────────── + build: + name: πŸ—οΈ Build & Push + needs: [prepare, qa] + runs-on: docker + steps: + - name: Checkout repository + uses: actions/checkout@v4 - # ────────────────────────────────────────────────────────────────────────────── - # Registry Login - # ────────────────────────────────────────────────────────────────────────────── - name: πŸ” Registry Login run: | - echo "πŸ” Login zu registry.infra.mintel.me ..." echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin - # ────────────────────────────────────────────────────────────────────────────── - # Build & Push - # ────────────────────────────────────────────────────────────────────────────── - name: πŸ—οΈ Docker Image bauen & pushen env: - IMAGE_TAG: ${{ steps.determine.outputs.image_tag }} - NEXT_PUBLIC_BASE_URL: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_BASE_URL || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_BASE_URL || secrets.TESTING_NEXT_PUBLIC_BASE_URL || secrets.NEXT_PUBLIC_BASE_URL) }} - NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.TESTING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }} - NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.TESTING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }} + IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} + TARGET: ${{ needs.prepare.outputs.target }} + NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_BASE_URL || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_BASE_URL || secrets.TESTING_NEXT_PUBLIC_BASE_URL || secrets.NEXT_PUBLIC_BASE_URL) }} + NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.TESTING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }} + NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.TESTING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }} run: | - echo "πŸ—οΈ Building β†’ ${{ steps.determine.outputs.target }} / $IMAGE_TAG" + echo "πŸ—οΈ Building β†’ $TARGET / $IMAGE_TAG" docker buildx build \ --pull \ --platform linux/arm64 \ @@ -124,37 +144,43 @@ jobs: -t registry.infra.mintel.me/mintel/klz-cables.com:$IMAGE_TAG \ --push . - # ────────────────────────────────────────────────────────────────────────────── - # Deploy via SSH - # ────────────────────────────────────────────────────────────────────────────── - - name: πŸš€ Deploy to ${{ steps.determine.outputs.target }} - env: - IMAGE_TAG: ${{ steps.determine.outputs.image_tag }} - ENV_FILE: ${{ steps.determine.outputs.env_file }} - TRAEFIK_HOST: ${{ steps.determine.outputs.traefik_host }} - # Secrets wie vorher – mit Fallback-Logik pro Umgebung - NEXT_PUBLIC_BASE_URL: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_BASE_URL || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_BASE_URL || secrets.TESTING_NEXT_PUBLIC_BASE_URL || secrets.NEXT_PUBLIC_BASE_URL) }} - NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.TESTING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }} - NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ steps.determine.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (steps.determine.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.TESTING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }} - SENTRY_DSN: ${{ steps.determine.outputs.target == 'production' && secrets.SENTRY_DSN || (steps.determine.outputs.target == 'staging' && secrets.STAGING_SENTRY_DSN || secrets.TESTING_SENTRY_DSN || secrets.SENTRY_DSN) }} - MAIL_HOST: ${{ secrets.MAIL_HOST }} - MAIL_PORT: ${{ secrets.MAIL_PORT }} - MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} - MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} - MAIL_FROM: ${{ secrets.MAIL_FROM }} - MAIL_RECIPIENTS: ${{ secrets.MAIL_RECIPIENTS }} - run: | - echo "Deploying ${{ steps.determine.outputs.target }} β†’ $IMAGE_TAG" + # ────────────────────────────────────────────────────────────────────────────── + # JOB 4: Deploy via SSH + # ────────────────────────────────────────────────────────────────────────────── + deploy: + name: πŸš€ Deploy + needs: [prepare, build] + runs-on: docker + env: + TARGET: ${{ needs.prepare.outputs.target }} + IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} + ENV_FILE: ${{ needs.prepare.outputs.env_file }} + TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }} + NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_BASE_URL || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_BASE_URL || secrets.TESTING_NEXT_PUBLIC_BASE_URL || secrets.NEXT_PUBLIC_BASE_URL) }} + NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.TESTING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }} + NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.TESTING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }} + SENTRY_DSN: ${{ needs.prepare.outputs.target == 'production' && secrets.SENTRY_DSN || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_SENTRY_DSN || secrets.TESTING_SENTRY_DSN || secrets.SENTRY_DSN) }} + MAIL_HOST: ${{ secrets.MAIL_HOST }} + MAIL_PORT: ${{ secrets.MAIL_PORT }} + MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} + MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} + MAIL_FROM: ${{ secrets.MAIL_FROM }} + MAIL_RECIPIENTS: ${{ secrets.MAIL_RECIPIENTS }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: πŸš€ Deploy to ${{ env.TARGET }} + run: | + echo "Deploying $TARGET β†’ $IMAGE_TAG" - # SSH vorbereiten 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 - # .env-Datei erstellen cat > /tmp/klz-cables.env << EOF - # Generated by CI - ${{ steps.determine.outputs.target }} - $(date -u) + # Generated by CI - $TARGET - $(date -u) NODE_ENV=production NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID @@ -178,65 +204,60 @@ jobs: ssh -o StrictHostKeyChecking=accept-new root@alpha.mintel.me IMAGE_TAG="$IMAGE_TAG" ENV_FILE="$ENV_FILE" TRAEFIK_HOST="$TRAEFIK_HOST" bash << 'EOF' set -e cd /home/deploy/sites/klz-cables.com - chmod 600 "$ENV_FILE" chown deploy:deploy "$ENV_FILE" - echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin - echo "β†’ Pulling image: $IMAGE_TAG" docker compose --env-file "$ENV_FILE" pull - echo "β†’ Starting containers..." docker compose --env-file "$ENV_FILE" up -d - docker system prune -f --filter "until=168h" - echo "β†’ Waiting 15s for warmup..." sleep 15 - echo "β†’ Container status:" docker compose --env-file "$ENV_FILE" ps - if ! docker compose --env-file "$ENV_FILE" ps | grep -q "Up"; then echo "❌ Fehler: Container nicht Up!" docker compose --env-file "$ENV_FILE" logs --tail=150 exit 1 fi - echo "βœ… Deployment erfolgreich!" EOF - rm -f /tmp/klz-cables.env - # ────────────────────────────────────────────────────────────────────────────── - # Summary & Gotify - # ────────────────────────────────────────────────────────────────────────────── + # ────────────────────────────────────────────────────────────────────────────── + # JOB 5: Notifications + # ────────────────────────────────────────────────────────────────────────────── + notifications: + name: πŸ”” Notifications + needs: [prepare, qa, build, deploy] + if: always() + runs-on: docker + steps: - name: πŸ“Š Deployment Summary - if: always() run: | echo "β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”" echo "β”‚ Deployment Summary β”‚" echo "β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€" - echo "β”‚ Status: ${{ job.status }} β”‚" - echo "β”‚ Umgebung: ${{ steps.determine.outputs.target || 'skipped' }} β”‚" - echo "β”‚ Version: ${{ steps.determine.outputs.image_tag }} β”‚" - echo "β”‚ Commit: ${{ steps.determine.outputs.short_sha }} β”‚" - echo "β”‚ Message: ${{ steps.determine.outputs.commit_msg }} β”‚" + echo "β”‚ Status: ${{ needs.deploy.result }} β”‚" + echo "β”‚ Umgebung: ${{ needs.prepare.outputs.target || 'skipped' }} β”‚" + echo "β”‚ Version: ${{ needs.prepare.outputs.image_tag }} β”‚" + echo "β”‚ Commit: ${{ needs.prepare.outputs.short_sha }} β”‚" + echo "β”‚ Message: ${{ needs.prepare.outputs.commit_msg }} β”‚" echo "β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜" - name: πŸ”” Gotify - Success - if: success() + if: needs.deploy.result == 'success' run: | curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ - -F "title=${{ steps.determine.outputs.gotify_title }}" \ - -F "message=Erfolgreich deployt auf **${{ steps.determine.outputs.target }}**\n\nVersion: **${{ steps.determine.outputs.image_tag }}**\nCommit: ${{ steps.determine.outputs.short_sha }} (${{ steps.determine.outputs.commit_msg }})\nVon: ${{ github.actor }}\nRun: ${{ github.run_id }}" \ - -F "priority=${{ steps.determine.outputs.gotify_priority }}" || true + -F "title=${{ needs.prepare.outputs.gotify_title }}" \ + -F "message=Erfolgreich deployt auf **${{ needs.prepare.outputs.target }}**\n\nVersion: **${{ needs.prepare.outputs.image_tag }}**\nCommit: ${{ needs.prepare.outputs.short_sha }} (${{ needs.prepare.outputs.commit_msg }})\nVon: ${{ github.actor }}\nRun: ${{ github.run_id }}" \ + -F "priority=${{ needs.prepare.outputs.gotify_priority }}" || true - name: πŸ”” Gotify - Failure - if: failure() + if: needs.deploy.result == 'failure' || needs.build.result == 'failure' || needs.qa.result == 'failure' run: | curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ - -F "title=❌ Deployment FEHLGESCHLAGEN – ${{ steps.determine.outputs.target || 'unknown' }}" \ - -F "message=**Fehler beim Deploy auf ${{ steps.determine.outputs.target }}**\n\nVersion: ${{ steps.determine.outputs.image_tag || '?' }}\nCommit: ${{ steps.determine.outputs.short_sha || '?' }}\nVon: ${{ github.actor }}\nRun: ${{ github.run_id }}\n\nBitte Logs prΓΌfen!" \ + -F "title=❌ Deployment FEHLGESCHLAGEN – ${{ needs.prepare.outputs.target || 'unknown' }}" \ + -F "message=**Fehler beim Deploy auf ${{ needs.prepare.outputs.target }}**\n\nVersion: ${{ needs.prepare.outputs.image_tag || '?' }}\nCommit: ${{ needs.prepare.outputs.short_sha || '?' }}\nVon: ${{ github.actor }}\nRun: ${{ github.run_id }}\n\nBitte Logs prΓΌfen!" \ -F "priority=8" || true