diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 314c2f84..6fe44799 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -32,11 +32,9 @@ jobs: env: NPM_TOKEN: ${{ secrets.REGISTRY_PASS }} - - name: πŸ” Lint - run: pnpm lint - - - name: πŸ—οΈ Typecheck - run: pnpm typecheck - - - name: πŸ§ͺ Test - run: pnpm test + - name: πŸ§ͺ Parallel Checks + run: | + pnpm lint & + pnpm typecheck & + pnpm test & + wait diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index e7bd39f4..229afbc4 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: Build & Deploy KLZ Cables +name: Build & Deploy on: push: @@ -8,136 +8,80 @@ on: - 'v*' workflow_dispatch: inputs: - skip_long_checks: + skip_checks: description: 'Skip tests? (true/false)' required: false default: 'false' concurrency: - group: ${{ github.workflow }}-${{ (github.ref_type == 'tag' && !contains(github.ref_name, '-')) && 'prod' || (github.ref_type == 'tag' && 'staging' || (github.ref_name == 'main' && 'testing' || github.ref_name)) }} + group: ${{ github.workflow }}-${{ (github.ref_type == 'tag' && !contains(github.ref_name, '-')) && 'prod' || (github.ref_name == 'main' && 'testing' || github.ref_name) }} cancel-in-progress: true jobs: # ────────────────────────────────────────────────────────────────────────────── - # JOB 1: Prepare & Determine Environment + # JOB 1: Prepare Environment # ────────────────────────────────────────────────────────────────────────────── prepare: - name: πŸ” Prepare Environment + 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_host_rule: ${{ steps.determine.outputs.traefik_host_rule }} - primary_host: ${{ steps.determine.outputs.primary_host }} - next_public_base_url: ${{ steps.determine.outputs.next_public_base_url }} + traefik_rule: ${{ steps.determine.outputs.traefik_rule }} + next_public_url: ${{ steps.determine.outputs.next_public_url }} directus_url: ${{ steps.determine.outputs.directus_url }} - directus_host: ${{ steps.determine.outputs.directus_host }} project_name: ${{ steps.determine.outputs.project_name }} - 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 }} 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=6h" - - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 2 - - - name: πŸ” Environment & Version ermitteln + - name: πŸ” Environment ermitteln id: determine shell: bash run: | - TAG="${{ github.ref_name }}" - SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-9) - IMAGE_TAG="sha-${SHORT_SHA}" - COMMIT_MSG=$(git log -1 --pretty=%s || echo "No commit message available") + REF="${{ github.ref_name }}" + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) + DOMAIN="klz-cables.com" + PRJ="klz" - if [[ "${{ github.ref_type }}" == "branch" && "$TAG" == "main" ]]; then - if [[ "$COMMIT_MSG" =~ ^chore: ]]; then - TARGET="skip" - GOTIFY_TITLE="ℹ️ Skip Deploy (Chore)" - GOTIFY_PRIORITY=2 - else - TARGET="testing" - IMAGE_TAG="main-${SHORT_SHA}" - ENV_FILE=".env.testing" - TRAEFIK_HOST="testing.klz-cables.com" - NEXT_PUBLIC_BASE_URL="https://testing.klz-cables.com" - DIRECTUS_URL="https://cms.testing.klz-cables.com" - DIRECTUS_HOST="cms.testing.klz-cables.com" - PROJECT_NAME="klz-cables-testing" - IS_PROD="false" - GOTIFY_TITLE="πŸ§ͺ Testing-Deploy" - GOTIFY_PRIORITY=4 - fi + 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 [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + if [[ "$REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then TARGET="production" - IMAGE_TAG="$TAG" + IMAGE_TAG="$REF" ENV_FILE=".env.prod" - TRAEFIK_HOST="klz-cables.com, www.klz-cables.com" - NEXT_PUBLIC_BASE_URL="https://klz-cables.com" - DIRECTUS_URL="https://cms.klz-cables.com" - DIRECTUS_HOST="cms.klz-cables.com" - PROJECT_NAME="klz-cables-prod" - IS_PROD="true" - GOTIFY_TITLE="πŸš€ Production-Release" - GOTIFY_PRIORITY=6 - elif [[ "$TAG" =~ -rc || "$TAG" =~ -beta || "$TAG" =~ -alpha ]]; then - TARGET="staging" - IMAGE_TAG="$TAG" - ENV_FILE=".env.staging" - TRAEFIK_HOST="staging.klz-cables.com" - NEXT_PUBLIC_BASE_URL="https://staging.klz-cables.com" - DIRECTUS_URL="https://cms.staging.klz-cables.com" - DIRECTUS_HOST="cms.staging.klz-cables.com" - PROJECT_NAME="klz-cables-staging" - IS_PROD="false" - GOTIFY_TITLE="πŸ§ͺ Staging-Deploy (Pre-Release)" - GOTIFY_PRIORITY=5 + TRAEFIK_HOST="${DOMAIN}, www.${DOMAIN}" else - TARGET="skip" - GOTIFY_TITLE="❓ Unbekannter Tag" - GOTIFY_PRIORITY=3 + TARGET="staging" + IMAGE_TAG="$REF" + ENV_FILE=".env.staging" + TRAEFIK_HOST="staging.${DOMAIN}" fi - elif [[ "${{ github.ref_type }}" == "branch" ]]; then + else TARGET="branch" - # Slugify branch name: lowercase, replace non-alphanumeric with -, remove leading/trailing - - SLUG=$(echo "$TAG" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//') + 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" - NEXT_PUBLIC_BASE_URL="https://${SLUG}.branch.mintel.me" - DIRECTUS_URL="https://cms.${SLUG}.branch.mintel.me" - DIRECTUS_HOST="cms.${SLUG}.branch.mintel.me" - PROJECT_NAME="klz-cables-br-${SLUG}" - IS_PROD="false" - GOTIFY_TITLE="🌿 Branch-Deploy ($TAG)" - GOTIFY_PRIORITY=4 - else - TARGET="skip" fi + # Standardize Traefik Rule if [[ "$TRAEFIK_HOST" == *","* ]]; then - # Multi-domain: Host(`a.com`) || Host(`b.com`) - TRAEFIK_HOST_RULE=$(echo "$TRAEFIK_HOST" | sed 's/,/ /g' | awk '{for(i=1;i<=NF;i++) printf "Host(`%s`)%s", $i, (i==NF?"":" || ")}') + TRAEFIK_RULE=$(echo "$TRAEFIK_HOST" | sed 's/,/ /g' | awk '{for(i=1;i<=NF;i++) printf "Host(`%s`)%s", $i, (i==NF?"":" || ")}') PRIMARY_HOST=$(echo "$TRAEFIK_HOST" | cut -d',' -f1 | sed 's/ //g') else - # Single domain: Host(`domain.com`) - TRAEFIK_HOST_RULE="Host(\`$TRAEFIK_HOST\`)" + TRAEFIK_RULE="Host(\`$TRAEFIK_HOST\`)" PRIMARY_HOST="$TRAEFIK_HOST" fi @@ -145,25 +89,19 @@ jobs: echo "target=$TARGET" echo "image_tag=$IMAGE_TAG" echo "env_file=$ENV_FILE" - echo "traefik_host=$TRAEFIK_HOST" - echo "traefik_host_rule=$TRAEFIK_HOST_RULE" - echo "primary_host=$PRIMARY_HOST" - echo "next_public_base_url=$NEXT_PUBLIC_BASE_URL" - echo "directus_url=$DIRECTUS_URL" - echo "directus_host=$DIRECTUS_HOST" - echo "project_name=$PROJECT_NAME" - echo "is_prod=$IS_PROD" - echo "gotify_title=$GOTIFY_TITLE" - echo "gotify_priority=$GOTIFY_PRIORITY" + echo "traefik_host=$PRIMARY_HOST" + echo "traefik_rule=$TRAEFIK_RULE" + echo "next_public_url=https://$PRIMARY_HOST" + echo "directus_url=https://cms.$PRIMARY_HOST" + echo "project_name=$PRJ-$TARGET" echo "short_sha=$SHORT_SHA" - echo "commit_msg=$COMMIT_MSG" } >> "$GITHUB_OUTPUT" # ────────────────────────────────────────────────────────────────────────────── - # JOB 2: Quality Assurance (Lint & Test) + # JOB 2: QA (Lint, Typecheck, Test) # ────────────────────────────────────────────────────────────────────────────── qa: - name: πŸ§ͺ Quality Assurance + name: πŸ§ͺ QA needs: prepare if: needs.prepare.outputs.target != 'skip' runs-on: docker @@ -172,412 +110,133 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - with: - fetch-depth: 1 - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - - name: Setup pnpm uses: pnpm/action-setup@v3 with: version: 10 - run_install: false - - - name: πŸ” Configure Private Registry + - name: πŸ” Registry Auth run: | - REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" - echo "@mintel:registry=https://$REGISTRY" > .npmrc - echo "//$REGISTRY/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc - + echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc + echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc - name: Install dependencies - run: pnpm install - env: - NPM_TOKEN: ${{ secrets.REGISTRY_PASS }} - - - name: πŸ§ͺ Run Checks in Parallel - if: github.event.inputs.skip_long_checks != 'true' + run: pnpm install --frozen-lockfile + - name: πŸ§ͺ Parallel Checks + if: github.event.inputs.skip_checks != 'true' run: | pnpm lint & - LINT_PID=$! pnpm typecheck & - TYPE_PID=$! pnpm test & - TEST_PID=$! - - # Wait for all and fail if any fail - wait $LINT_PID || exit 1 - wait $TYPE_PID || exit 1 - wait $TEST_PID || exit 1 + wait # ────────────────────────────────────────────────────────────────────────────── - # JOB 3: Build & Push Docker Image + # JOB 3: Build & Push # ────────────────────────────────────────────────────────────────────────────── - build-app: - name: πŸ—οΈ Build App + build: + name: πŸ—οΈ Build needs: prepare - if: ${{ needs.prepare.outputs.target != 'skip' }} + if: needs.prepare.outputs.target != 'skip' runs-on: docker container: image: catthehacker/ubuntu:act-latest steps: - name: Checkout repository uses: actions/checkout@v4 - with: - fetch-depth: 1 - - 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: πŸ—οΈ App bauen & pushen - env: - IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} - TARGET: ${{ needs.prepare.outputs.target }} - NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_base_url }} - DIRECTUS_URL: ${{ needs.prepare.outputs.directus_url }} - run: | - docker buildx build \ - --pull \ - --platform linux/arm64 \ - --build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \ - --build-arg NEXT_PUBLIC_TARGET="$TARGET" \ - --build-arg DIRECTUS_URL="$DIRECTUS_URL" \ - --build-arg REGISTRY_HOST="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" \ - --build-arg NPM_TOKEN="${{ secrets.REGISTRY_PASS }}" \ - -t registry.infra.mintel.me/mintel/klz-cables.com:$IMAGE_TAG \ - --cache-from type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache \ - --cache-to type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache,mode=max \ - --push . + 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 + platforms: linux/arm64 + build-args: | + NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }} + NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }} + DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} + NPM_TOKEN=${{ secrets.REGISTRY_PASS }} + tags: registry.infra.mintel.me/mintel/klz-cables.com:${{ needs.prepare.outputs.image_tag }} + cache-from: type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache + cache-to: type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache,mode=max + secrets: | + "NPM_TOKEN=${{ secrets.REGISTRY_PASS }}" # ────────────────────────────────────────────────────────────────────────────── - # JOB 4: Deploy via SSH + # JOB 4: Deploy # ────────────────────────────────────────────────────────────────────────────── deploy: name: πŸš€ Deploy - needs: [prepare, build-app, qa] - if: ${{ needs.prepare.outputs.target != 'skip' }} + 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 }} - ENV_FILE: ${{ needs.prepare.outputs.env_file }} - TRAEFIK_HOST: ${{ needs.prepare.outputs.primary_host }} - NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_base_url }} - 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) }} - UMAMI_API_ENDPOINT: ${{ 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: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }} - MAIL_HOST: ${{ secrets.MAIL_HOST || vars.MAIL_HOST || (needs.prepare.outputs.target == 'production' && (secrets.MAIL_HOST || vars.MAIL_HOST) || (needs.prepare.outputs.target == 'staging' && (secrets.STAGING_MAIL_HOST || vars.STAGING_MAIL_HOST) || (secrets.TESTING_MAIL_HOST || vars.TESTING_MAIL_HOST) || (secrets.MAIL_HOST || vars.MAIL_HOST))) }} - MAIL_PORT: ${{ secrets.MAIL_PORT || vars.MAIL_PORT || (needs.prepare.outputs.target == 'production' && (secrets.MAIL_PORT || vars.MAIL_PORT) || (needs.prepare.outputs.target == 'staging' && (secrets.STAGING_MAIL_PORT || vars.STAGING_MAIL_PORT) || (secrets.TESTING_MAIL_PORT || vars.TESTING_MAIL_PORT) || (secrets.MAIL_PORT || vars.MAIL_PORT))) }} - MAIL_USERNAME: ${{ secrets.MAIL_USERNAME || vars.MAIL_USERNAME || (needs.prepare.outputs.target == 'production' && (secrets.MAIL_USERNAME || vars.MAIL_USERNAME) || (needs.prepare.outputs.target == 'staging' && (secrets.STAGING_MAIL_USERNAME || vars.STAGING_MAIL_USERNAME) || (secrets.TESTING_MAIL_USERNAME || vars.TESTING_MAIL_USERNAME) || (secrets.MAIL_USERNAME || vars.MAIL_USERNAME))) }} - MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD || (needs.prepare.outputs.target == 'production' && secrets.MAIL_PASSWORD || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_MAIL_PASSWORD || secrets.TESTING_MAIL_PASSWORD || secrets.MAIL_PASSWORD)) }} - MAIL_FROM: ${{ secrets.MAIL_FROM || vars.MAIL_FROM || (needs.prepare.outputs.target == 'production' && (secrets.MAIL_FROM || vars.MAIL_FROM) || (needs.prepare.outputs.target == 'staging' && (secrets.STAGING_MAIL_FROM || vars.STAGING_MAIL_FROM) || (secrets.TESTING_MAIL_FROM || vars.TESTING_MAIL_FROM) || (secrets.MAIL_FROM || vars.MAIL_FROM))) }} - MAIL_RECIPIENTS: ${{ secrets.MAIL_RECIPIENTS || vars.MAIL_RECIPIENTS || (needs.prepare.outputs.target == 'production' && (secrets.MAIL_RECIPIENTS || vars.MAIL_RECIPIENTS) || (needs.prepare.outputs.target == 'staging' && (secrets.STAGING_MAIL_RECIPIENTS || vars.STAGING_MAIL_RECIPIENTS) || (secrets.TESTING_MAIL_RECIPIENTS || vars.TESTING_MAIL_RECIPIENTS) || (secrets.MAIL_RECIPIENTS || vars.MAIL_RECIPIENTS))) }} - DIRECTUS_URL: ${{ needs.prepare.outputs.directus_url }} - DIRECTUS_HOST: ${{ needs.prepare.outputs.directus_host }} - PROJECT_NAME: ${{ needs.prepare.outputs.project_name }} - DIRECTUS_KEY: ${{ secrets.DIRECTUS_KEY || (needs.prepare.outputs.target == 'production' && secrets.DIRECTUS_KEY || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_KEY || secrets.TESTING_DIRECTUS_KEY || secrets.DIRECTUS_KEY)) }} - DIRECTUS_SECRET: ${{ secrets.DIRECTUS_SECRET || (needs.prepare.outputs.target == 'production' && secrets.DIRECTUS_SECRET || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_SECRET || secrets.TESTING_DIRECTUS_SECRET || secrets.DIRECTUS_SECRET)) }} - DIRECTUS_ADMIN_EMAIL: ${{ secrets.DIRECTUS_ADMIN_EMAIL || (needs.prepare.outputs.target == 'production' && secrets.DIRECTUS_ADMIN_EMAIL || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_ADMIN_EMAIL || secrets.TESTING_DIRECTUS_ADMIN_EMAIL || secrets.DIRECTUS_ADMIN_EMAIL)) }} - DIRECTUS_ADMIN_PASSWORD: ${{ secrets.DIRECTUS_ADMIN_PASSWORD || (needs.prepare.outputs.target == 'production' && secrets.DIRECTUS_ADMIN_PASSWORD || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_ADMIN_PASSWORD || secrets.TESTING_DIRECTUS_ADMIN_PASSWORD || secrets.DIRECTUS_ADMIN_PASSWORD)) }} - DIRECTUS_DB_NAME: ${{ secrets.DIRECTUS_DB_NAME || 'directus' }} - DIRECTUS_DB_USER: ${{ secrets.DIRECTUS_DB_USER || 'directus' }} - DIRECTUS_DB_PASSWORD: ${{ secrets.DIRECTUS_DB_PASSWORD || (needs.prepare.outputs.target == 'production' && secrets.DIRECTUS_DB_PASSWORD || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_DB_PASSWORD || secrets.TESTING_DIRECTUS_DB_PASSWORD || secrets.DIRECTUS_DB_PASSWORD)) }} - DIRECTUS_API_TOKEN: ${{ secrets.DIRECTUS_API_TOKEN || (needs.prepare.outputs.target == 'production' && secrets.DIRECTUS_API_TOKEN || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_API_TOKEN || secrets.TESTING_DIRECTUS_API_TOKEN || secrets.DIRECTUS_API_TOKEN)) }} - GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }} steps: - name: Checkout repository uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: πŸš€ Deploy to ${{ env.TARGET }} + - name: πŸš€ SSH Deploy shell: bash + env: + TARGET: ${{ needs.prepare.outputs.target }} + IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} + PROJECT_NAME: ${{ needs.prepare.outputs.project_name }} + ENV_FILE: ${{ needs.prepare.outputs.env_file }} + TRAEFIK_RULE: ${{ needs.prepare.outputs.traefik_rule }} run: | - echo "Deploying $TARGET β†’ $IMAGE_TAG" - 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 - # Generated by CI - $TARGET - $(date -u) - # Determine dynamic values before writing the file - LOG_LEVEL=$( [[ "$TARGET" == "testing" || "$TARGET" == "development" ]] && echo "debug" || echo "info" ) - COOKIE_DOMAIN=.$(echo $NEXT_PUBLIC_BASE_URL | sed 's|https://||') - - cat > /tmp/klz-cables.env << EOF - # Generated by CI - $TARGET - $(date -u) + # Generate Environment File + cat > .env.deploy << EOF IMAGE_TAG=$IMAGE_TAG - NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL - UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID - UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT - SENTRY_DSN=$SENTRY_DSN - LOG_LEVEL=$LOG_LEVEL - MAIL_HOST=$MAIL_HOST - MAIL_PORT=$MAIL_PORT - MAIL_USERNAME=$MAIL_USERNAME - MAIL_PASSWORD=$MAIL_PASSWORD - MAIL_FROM=$MAIL_FROM - MAIL_RECIPIENTS=$MAIL_RECIPIENTS - - # Directus - DIRECTUS_URL=$DIRECTUS_URL - DIRECTUS_HOST=$DIRECTUS_HOST - DIRECTUS_KEY=$DIRECTUS_KEY - DIRECTUS_SECRET=$DIRECTUS_SECRET - DIRECTUS_ADMIN_EMAIL=$DIRECTUS_ADMIN_EMAIL - DIRECTUS_ADMIN_PASSWORD=$DIRECTUS_ADMIN_PASSWORD - DIRECTUS_DB_NAME=$DIRECTUS_DB_NAME - DIRECTUS_DB_USER=$DIRECTUS_DB_USER - DIRECTUS_DB_PASSWORD=$DIRECTUS_DB_PASSWORD - DIRECTUS_API_TOKEN=$DIRECTUS_API_TOKEN + NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }} + DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} + DIRECTUS_HOST=cms.${{ needs.prepare.outputs.traefik_host }} INTERNAL_DIRECTUS_URL=http://directus:8055 - GATEKEEPER_PASSWORD=$GATEKEEPER_PASSWORD - + TRAEFIK_HOST_RULE='$TRAEFIK_RULE' + PROJECT_NAME=$PROJECT_NAME TARGET=$TARGET SENTRY_ENVIRONMENT=$TARGET - PROJECT_NAME=$PROJECT_NAME - COOKIE_DOMAIN=$COOKIE_DOMAIN - TRAEFIK_HOST=$TRAEFIK_HOST + SENTRY_DSN=${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }} + GATEKEEPER_PASSWORD=${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }} + AUTH_MIDDLEWARE=$( [[ "$TARGET" == "production" ]] && echo "${PROJECT_NAME}-compress" || echo "${PROJECT_NAME}-auth,${PROJECT_NAME}-compress" ) EOF - # Append complex variables that contain backticks using printf to avoid shell expansion hits - printf "AUTH_MIDDLEWARE=%s\n" "$( [[ "$TARGET" == "production" ]] && echo "${PROJECT_NAME}-compress" || echo "${PROJECT_NAME}-auth,${PROJECT_NAME}-compress" )" >> /tmp/klz-cables.env - printf "TRAEFIK_HOST_RULE='%s'\n" '${{ needs.prepare.outputs.traefik_host_rule }}' >> /tmp/klz-cables.env - - # 1. Cleanup and Create Directories on server BEFORE SCP - ssh -o StrictHostKeyChecking=accept-new root@alpha.mintel.me bash << 'EOF' - set -e - mkdir -p /home/deploy/sites/klz-cables.com/directus/uploads \ - /home/deploy/sites/klz-cables.com/directus/extensions \ - /home/deploy/sites/klz-cables.com/directus/schema - chown -R deploy:deploy /home/deploy/sites/klz-cables.com/directus - - EOF - - # 2. Transfer files - scp -o StrictHostKeyChecking=accept-new /tmp/klz-cables.env root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/$ENV_FILE - scp -o StrictHostKeyChecking=accept-new docker-compose.yml root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/docker-compose.yml - scp -r -o StrictHostKeyChecking=accept-new directus/schema root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/directus/ - - ssh -o StrictHostKeyChecking=accept-new root@alpha.mintel.me IMAGE_TAG="$IMAGE_TAG" ENV_FILE="$ENV_FILE" PROJECT_NAME="$PROJECT_NAME" 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 -p "$PROJECT_NAME" --env-file "$ENV_FILE" pull - - echo "β†’ Starting containers (Waiting for Health)..." - # Uses --wait to ensure containers are healthy before proceeding - # This replaces the brittle 'sleep 15' and manual check - docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" up -d --wait --remove-orphans - - docker system prune -f --filter "until=24h" - - echo "βœ… Deployment successful. Containers are Healthy." - docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" ps - echo "β†’ Applying Directus Schema Snapshot..." - # Note: We check if snapshot exists first to avoid failing if no snapshot is committed yet - if docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" exec -T directus ls /directus/schema/snapshot.yaml >/dev/null 2>&1; then - docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" exec -T directus npx directus schema apply /directus/schema/snapshot.yaml --yes - else - echo "ℹ️ No snapshot.yaml found, skipping schema apply." - fi - - else - echo "ℹ️ No snapshot.yaml found, skipping schema apply." - fi - - echo "βœ… Deployment successful." + # Transfer and Restart + SITE_DIR="/home/deploy/sites/klz-cables.com" + 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 && \ + docker compose -p '$PROJECT_NAME' --env-file '$ENV_FILE' pull && \ + docker compose -p '$PROJECT_NAME' --env-file '$ENV_FILE' up -d --wait --remove-orphans" # ────────────────────────────────────────────────────────────────────────────── - # JOB 5: PageSpeed Test - # ────────────────────────────────────────────────────────────────────────────── - pagespeed: - name: ⚑ PageSpeed - needs: [prepare, deploy] - if: | - always() && - needs.prepare.outputs.target != 'skip' && - needs.deploy.result == 'success' && - github.event.inputs.skip_long_checks != 'true' - runs-on: docker - container: - image: catthehacker/ubuntu:act-latest - # outputs: - # report_url: ${{ steps.save.outputs.report_url }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Setup pnpm - uses: pnpm/action-setup@v3 - with: - version: 10 - run_install: false - - - name: πŸ” Configure Private Registry - run: | - REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" - echo "@mintel:registry=https://$REGISTRY" > .npmrc - echo "//$REGISTRY/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc - - - name: Install dependencies - run: pnpm install - - - name: πŸ₯ Health Check (Wait for 200 OK) - env: - URL: ${{ needs.prepare.outputs.next_public_base_url }} - run: | - echo "Waiting for $URL to be ready..." - timeout=300 - interval=5 - elapsed=0 - - while [ $elapsed -lt $timeout ]; do - STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$URL" || echo "000") - echo "Attempt $((elapsed/interval+1)): Status $STATUS" - - if [ "$STATUS" = "200" ]; then - echo "βœ… Site is healthy!" - exit 0 - fi - - sleep $interval - elapsed=$((elapsed + interval)) - done - - echo "❌ Timeout waiting for site to be healthy." - exit 1 - - - name: πŸ” Install Chromium (Native & ARM64) - run: | - apt-get update - apt-get install -y gnupg wget ca-certificates - - # Detect OS - OS_ID=$(. /etc/os-release && echo $ID) - CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME) - - if [ "$OS_ID" = "debian" ]; then - echo "🎯 Debian detected - installing native chromium" - apt-get install -y chromium - else - echo "🎯 Ubuntu detected - adding xtradeb PPA" - mkdir -p /etc/apt/keyrings - KEY_ID="82BB6851C64F6880" - - # Multi-method Key Fetch - SUCCESS=false - echo "Fetching key $KEY_ID..." - - # Method 1: gpg --recv-keys (standard) - for server in "hkp://keyserver.ubuntu.com:80" "hkp://keyserver.ubuntu.com:11371"; do - if gpg --no-default-keyring --keyring /tmp/xtradeb.gpg --keyserver "$server" --recv-keys "$KEY_ID"; then - gpg --no-default-keyring --keyring /tmp/xtradeb.gpg --export > /etc/apt/keyrings/xtradeb.gpg - SUCCESS=true && break - fi - done - - # Method 2: Direct wget (fallback) - if [ "$SUCCESS" = false ]; then - wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_ID" | gpg --dearmor > /etc/apt/keyrings/xtradeb.gpg && SUCCESS=true - fi - - if [ "$SUCCESS" = true ]; then - 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 - else - echo "⚠️ GPG fetch failed, using legacy apt-key as last resort..." - apt-key adv --keyserver keyserver.ubuntu.com --recv-keys "$KEY_ID" || true - echo "deb http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" > /etc/apt/sources.list.d/xtradeb-ppa.list - fi - - # PRIORITY PINNING: Force PPA over Snap-dummy - 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 || apt-get install -y chromium-browser - fi - - # Force clean paths (remove existing dead links/files if they are snap wrappers) - rm -f /usr/bin/google-chrome /usr/bin/chromium-browser - [ -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 - - echo "βœ… Binary check:" - ls -l /usr/bin/chromium* /usr/bin/google-chrome || true - continue-on-error: true - - - name: πŸ§ͺ Run PageSpeed (Lighthouse) - env: - NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_base_url }} - GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }} - PAGESPEED_LIMIT: 8 - PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium - CHROME_PATH: /usr/bin/chromium - run: pnpm pagespeed:test - - - - # ────────────────────────────────────────────────────────────────────────────── - # JOB 6: Notifications + # JOB 5: Notifications # ────────────────────────────────────────────────────────────────────────────── notifications: - name: πŸ”” Notifications - needs: [prepare, qa, build-app, deploy, pagespeed] + name: πŸ”” Notify + needs: [prepare, deploy] if: always() runs-on: docker container: image: catthehacker/ubuntu:act-latest steps: - - name: πŸ“Š Deployment Summary - run: | - echo "β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”" - echo "β”‚ Deployment Summary β”‚" - echo "β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€" - 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: needs.deploy.result == 'success' + - name: πŸ”” Gotify run: | + STATUS="${{ needs.deploy.result }}" + TITLE="klz-cables.com: $STATUS" + [[ "$STATUS" == "success" ]] && PRIORITY=5 || PRIORITY=8 + curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ - -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=4" || true - - - name: πŸ”” Gotify - Failure - if: | - needs.prepare.result == 'failure' || - needs.qa.result == 'failure' || - needs.build-app.result == 'failure' || - needs.deploy.result == 'failure' || - needs.pagespeed.result == 'failure' - run: | - curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ - -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 + -F "title=$TITLE" \ + -F "message=Deploy to ${{ needs.prepare.outputs.target }} finished with status $STATUS.\nVersion: ${{ needs.prepare.outputs.image_tag }}" \ + -F "priority=$PRIORITY" || true diff --git a/Dockerfile b/Dockerfile index 8e6a669e..7c06e380 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,50 +1,42 @@ -# Start from the pre-built Nextjs Base image +# Stage 1: Builder FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder - WORKDIR /app -# Ensure we are in a clean, standalone environment -RUN rm -rf packages apps pnpm-workspace.yaml 2>/dev/null || true - -# Build-time environment variables for Next.js +# Arguments for build-time configuration ARG NEXT_PUBLIC_BASE_URL -ARG DIRECTUS_URL ARG NEXT_PUBLIC_TARGET +ARG DIRECTUS_URL ARG NPM_TOKEN -ARG REGISTRY_HOST +# Environment variables for Next.js build ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL -ENV DIRECTUS_URL=$DIRECTUS_URL ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET -ENV NPM_TOKEN=$NPM_TOKEN +ENV DIRECTUS_URL=$DIRECTUS_URL ENV SKIP_RUNTIME_ENV_VALIDATION=true +ENV CI=true -# Enable corepack (pnpm is already in base image) +# Enable pnpm RUN corepack enable -# Copy package files -COPY package.json pnpm-lock.yaml* ./ +# Copy lockfile and manifest for dependency installation caching +COPY pnpm-lock.yaml package.json .npmrc* ./ -# Install dependencies based on the preferred package manager -# Create .npmrc for private registry access if token is present -RUN if [ -n "$NPM_TOKEN" ]; then \ - REGISTRY="${REGISTRY_HOST:-npm.infra.mintel.me}" && \ - echo "@mintel:registry=https://$REGISTRY" > .npmrc && \ - echo "//$REGISTRY/:_authToken=$NPM_TOKEN" >> .npmrc; \ - fi +# Install dependencies with cache mount +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + --mount=type=secret,id=NPM_TOKEN \ + export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN 2>/dev/null || echo $NPM_TOKEN) && \ + pnpm install --frozen-lockfile -RUN pnpm install --frozen-lockfile - -# Copy local files +# Copy source code COPY . . -# Build the specific application +# Build application RUN pnpm build -# Production runner image +# Stage 2: Runner FROM registry.infra.mintel.me/mintel/runtime:latest AS runner +WORKDIR /app -# Production environment configuration ENV HOSTNAME="0.0.0.0" ENV PORT=3000 ENV NODE_ENV=production @@ -53,8 +45,6 @@ ENV NODE_ENV=production COPY --from=builder --chown=nextjs:nodejs /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static - -# Ensure the cache directory specifically is writeable (Mintel Standard #16) COPY --from=builder --chown=nextjs:nodejs /app/.next/cache ./.next/cache USER nextjs