From 8ba81809b0474630999528e03a2da9c1b896b95e Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Wed, 11 Feb 2026 11:05:53 +0100 Subject: [PATCH] chore: standardize --- .gitea/workflows/deploy.yml | 88 +++++++++++++++++++++++++------------ apps/web/lib/env.ts | 41 ++++++++++------- apps/web/package.json | 2 +- pnpm-lock.yaml | 35 ++++++++++++--- 4 files changed, 114 insertions(+), 52 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 9b5fb94..20095b6 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -128,7 +128,11 @@ jobs: if: github.event.inputs.skip_checks != 'true' run: | pnpm lint - pnpm build + pnpm --filter "@mintel/web" exec tsc --noEmit + pnpm --filter "@mintel/web" test + - name: 🏗️ Build Test + if: github.event.inputs.skip_checks != 'true' + run: pnpm build # ────────────────────────────────────────────────────────────────────────────── # JOB 3: Build & Push @@ -181,17 +185,17 @@ jobs: DIRECTUS_URL: ${{ needs.prepare.outputs.directus_url }} DIRECTUS_HOST: cms.${{ needs.prepare.outputs.traefik_host }} - # Directus - DIRECTUS_API_TOKEN: ${{ secrets.DIRECTUS_API_TOKEN || vars.DIRECTUS_API_TOKEN }} - DIRECTUS_ADMIN_EMAIL: ${{ secrets.DIRECTUS_ADMIN_EMAIL || vars.DIRECTUS_ADMIN_EMAIL || 'admin@mintel.me' }} - DIRECTUS_ADMIN_PASSWORD: ${{ secrets.DIRECTUS_ADMIN_PASSWORD || vars.DIRECTUS_ADMIN_PASSWORD }} + # Secrets mapping (Directus) + DIRECTUS_KEY: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_KEY) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_KEY) || secrets.DIRECTUS_KEY || vars.DIRECTUS_KEY }} + DIRECTUS_SECRET: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_SECRET) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_SECRET) || secrets.DIRECTUS_SECRET || vars.DIRECTUS_SECRET }} + DIRECTUS_ADMIN_EMAIL: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_ADMIN_EMAIL) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_ADMIN_EMAIL) || secrets.DIRECTUS_ADMIN_EMAIL || vars.DIRECTUS_ADMIN_EMAIL || 'admin@mintel.me' }} + DIRECTUS_ADMIN_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_ADMIN_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_ADMIN_PASSWORD) || secrets.DIRECTUS_ADMIN_PASSWORD || vars.DIRECTUS_ADMIN_PASSWORD }} DIRECTUS_DB_NAME: ${{ secrets.DIRECTUS_DB_NAME || vars.DIRECTUS_DB_NAME || 'directus' }} DIRECTUS_DB_USER: ${{ secrets.DIRECTUS_DB_USER || vars.DIRECTUS_DB_USER || 'directus' }} - DIRECTUS_DB_PASSWORD: ${{ secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD }} - DIRECTUS_KEY: ${{ secrets.DIRECTUS_KEY || vars.DIRECTUS_KEY }} - DIRECTUS_SECRET: ${{ secrets.DIRECTUS_SECRET || vars.DIRECTUS_SECRET }} + DIRECTUS_DB_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_DB_PASSWORD) || secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD || 'directus' }} + DIRECTUS_API_TOKEN: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_API_TOKEN) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_API_TOKEN) || secrets.DIRECTUS_API_TOKEN || vars.DIRECTUS_API_TOKEN }} - # Mail + # 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 }} @@ -212,17 +216,11 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - - name: 🚀 SSH Deploy + - name: 📝 Generate Environment shell: bash env: - ENV_FILE: ${{ needs.prepare.outputs.env_file }} TRAEFIK_RULE: ${{ needs.prepare.outputs.traefik_rule }} 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 - # Generate Environment File cat > .env.deploy << EOF # Generated by CI - $TARGET @@ -271,28 +269,64 @@ jobs: # AUTH_MIDDLEWARE logic printf "AUTH_MIDDLEWARE=%s\n" "$( [[ "$TARGET" == "production" ]] && echo "compress" || echo "${PROJECT_NAME}-auth,compress" )" >> .env.deploy + - 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 SITE_DIR="/home/deploy/sites/mintel.me" - ssh root@alpha.mintel.me "mkdir -p $SITE_DIR" + ssh root@alpha.mintel.me "mkdir -p $SITE_DIR/directus/schema $SITE_DIR/directus/uploads $SITE_DIR/directus/extensions" 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 bash << EOF - set -e - cd /home/deploy/sites/mintel.me - echo '${{ secrets.REGISTRY_PASS }}' | docker login registry.infra.mintel.me -u '${{ secrets.REGISTRY_USER }}' --password-stdin - docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '${{ needs.prepare.outputs.env_file }}' pull - docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '${{ needs.prepare.outputs.env_file }}' up -d --wait --remove-orphans - docker system prune -f --filter "until=24h" - EOF + 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" + + # Apply Directus Schema Snapshot if available + ssh root@alpha.mintel.me "cd $SITE_DIR && if docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' exec -T directus ls /directus/schema/snapshot.yaml >/dev/null 2>&1; then echo '→ Applying Directus Schema Snapshot...' && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' exec -T directus npx directus schema apply /directus/schema/snapshot.yaml --yes; fi" + + ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'" # ────────────────────────────────────────────────────────────────────────────── - # JOB 5: Notifications + # JOB 5: Health Check + # ────────────────────────────────────────────────────────────────────────────── + healthcheck: + name: 🩺 Health Check + needs: [prepare, deploy] + if: needs.deploy.result == 'success' + runs-on: docker + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: 🔍 Smoke Test + run: | + URL="${{ needs.prepare.outputs.next_public_url }}" + echo "Checking health of $URL..." + for i in {1..12}; do + if curl -s -f "$URL" > /dev/null; then + echo "✅ Health check passed!" + exit 0 + fi + echo "Waiting for service to be ready... ($i/12)" + sleep 10 + done + echo "❌ Health check failed after 2 minutes." + exit 1 + + # ────────────────────────────────────────────────────────────────────────────── + # JOB 6: Notifications # ────────────────────────────────────────────────────────────────────────────── notifications: name: 🔔 Notify - needs: [prepare, deploy] + needs: [prepare, deploy, healthcheck] if: always() runs-on: docker container: diff --git a/apps/web/lib/env.ts b/apps/web/lib/env.ts index eb31703..dcfcca4 100644 --- a/apps/web/lib/env.ts +++ b/apps/web/lib/env.ts @@ -1,30 +1,37 @@ import { z } from "zod"; -import { validateMintelEnv, mintelEnvSchema } from "@mintel/next-utils"; +import { + validateMintelEnv, + mintelEnvSchema, + withMintelRefinements, +} from "@mintel/next-utils"; + +const envExtension = { + AUTH_COOKIE_NAME: z.string().default("mintel_gatekeeper_session"), + + // Analytics + UMAMI_WEBSITE_ID: z.string().optional(), + NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.string().optional(), + UMAMI_API_ENDPOINT: z.string().url().optional(), + + // Features + SHOW_BLOG: z + .string() + .transform((v) => v === "true") + .default("false"), +}; /** * Environment variable schema for the main website. * Extends the default Mintel environment schema. */ -export const envSchema = z.object({ - ...mintelEnvSchema, - - // Project specific overrides or additions - AUTH_COOKIE_NAME: z.string().default("mintel_gatekeeper_session"), - - // Analytics provider toggle - NEXT_PUBLIC_ANALYTICS_PROVIDER: z - .enum(["plausible", "umami"]) - .default("plausible"), - - // Plausible specifics (to be standardized later if needed) - NEXT_PUBLIC_PLAUSIBLE_DOMAIN: z.string().default("mintel.me"), - NEXT_PUBLIC_PLAUSIBLE_SCRIPT_URL: z.string().url().optional(), -}); +export const envSchema = withMintelRefinements( + z.object(mintelEnvSchema).extend(envExtension), +); /** * Validated environment object. */ -export const env = validateMintelEnv(envSchema.shape); +export const env = validateMintelEnv(envExtension); /** * For legacy compatibility with existing code. diff --git a/apps/web/package.json b/apps/web/package.json index 32bdda5..bf64223 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -81,7 +81,7 @@ "@mintel/eslint-config": "^1.6.0", "@mintel/husky-config": "^1.6.0", "@mintel/next-config": "^1.6.0", - "@mintel/next-utils": "^1.6.0", + "@mintel/next-utils": "^1.7.15", "@mintel/tsconfig": "^1.6.0", "@next/eslint-plugin-next": "^16.1.6", "@tailwindcss/typography": "^0.5.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e986e72..c3521cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,7 +124,7 @@ importers: version: 10.38.0 '@sentry/nextjs': specifier: 10.38.0 - version: 10.38.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.96.1) + version: 10.38.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.96.1) '@types/canvas-confetti': specifier: ^1.9.0 version: 1.9.0 @@ -232,8 +232,8 @@ importers: specifier: ^1.6.0 version: 1.7.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(webpack@5.96.1) '@mintel/next-utils': - specifier: ^1.6.0 - version: 1.6.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + specifier: ^1.7.15 + version: 1.7.15(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) '@mintel/tsconfig': specifier: ^1.6.0 version: 1.6.0 @@ -1485,6 +1485,9 @@ packages: '@mintel/next-utils@1.6.0': resolution: {integrity: sha512-gdC+QSEx+mGV3T4TO0wckWr0KCnb8dBitkJwYHrTE3h6nD3m01QziW00sqsN9H6nZc66wcbTGgW9oFOb2xoPJg==} + '@mintel/next-utils@1.7.15': + resolution: {integrity: sha512-CqSe3eHamq9zLs+AJxGOPypTLchw/oZ3JcLkor007PcUDMTv/Lspfv5oCaXK2s0FeIOJaa2QwSGPDI1h5/3ZVw==} + '@mintel/tsconfig@1.6.0': resolution: {integrity: sha512-8qx34GB9dfUFIdEF3wINgXN0cTYVQMcfDB5QFLX/HdjT+nXS/7bjjH5ofnEhsNAXv0jDse1UcL/C69O/Le01pg==} @@ -8670,7 +8673,7 @@ snapshots: '@mintel/next-config@1.7.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(webpack@5.96.1)': dependencies: - '@sentry/nextjs': 10.38.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.96.1) + '@sentry/nextjs': 10.38.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.96.1) next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-intl: 4.8.2(@swc/helpers@0.5.18)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3) transitivePeerDependencies: @@ -8695,7 +8698,25 @@ snapshots: dependencies: '@directus/sdk': 21.0.0 next: 15.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - next-intl: 4.8.2(@swc/helpers@0.5.18)(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + next-intl: 4.8.2(@swc/helpers@0.5.18)(next@15.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + zod: 3.22.3 + transitivePeerDependencies: + - '@babel/core' + - '@opentelemetry/api' + - '@playwright/test' + - '@swc/helpers' + - babel-plugin-macros + - babel-plugin-react-compiler + - react + - react-dom + - sass + - typescript + + '@mintel/next-utils@1.7.15(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)': + dependencies: + '@directus/sdk': 21.0.0 + next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next-intl: 4.8.2(@swc/helpers@0.5.18)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3) zod: 3.22.3 transitivePeerDependencies: - '@babel/core' @@ -9655,7 +9676,7 @@ snapshots: '@sentry/utils': 7.120.4 localforage: 1.10.0 - '@sentry/nextjs@10.38.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.96.1)': + '@sentry/nextjs@10.38.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.96.1)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.39.0 @@ -13394,7 +13415,7 @@ snapshots: next-intl-swc-plugin-extractor@4.8.2: {} - next-intl@4.8.2(@swc/helpers@0.5.18)(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3): + next-intl@4.8.2(@swc/helpers@0.5.18)(next@15.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3): dependencies: '@formatjs/intl-localematcher': 0.5.10 '@parcel/watcher': 2.5.6