diff --git a/packages/eslint-config/index.js b/packages/eslint-config/index.js index ffefb28..a4b3899 100644 --- a/packages/eslint-config/index.js +++ b/packages/eslint-config/index.js @@ -2,6 +2,9 @@ import js from "@eslint/js"; import tseslint from "typescript-eslint"; export default tseslint.config( + { + ignores: ["**/dist/**", "**/node_modules/**", "**/.next/**"], + }, js.configs.recommended, ...tseslint.configs.recommended, { diff --git a/packages/eslint-config/next.js b/packages/eslint-config/next.js index 097e1cc..e4adbbb 100644 --- a/packages/eslint-config/next.js +++ b/packages/eslint-config/next.js @@ -10,15 +10,27 @@ const compat = new FlatCompat({ }); export const nextConfig = [ + { + ignores: [ + "**/dist/**", + "**/.next/**", + "**/node_modules/**", + "**/.gitea/**", + "**/.changeset/**", + ], + }, ...compat.extends("next/core-web-vitals", "next/typescript"), { rules: { "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_" }, + ], "@typescript-eslint/no-require-imports": "off", "prefer-const": "warn", "react/no-unescaped-entities": "off", - "@next/next/no-img-element": "warn" - } - } + "@next/next/no-img-element": "warn", + }, + }, ]; diff --git a/packages/husky-config/commitlint.js b/packages/husky-config/commitlint.js index 45528cd..ad78d5c 100644 --- a/packages/husky-config/commitlint.js +++ b/packages/husky-config/commitlint.js @@ -1,4 +1,4 @@ -export default { +const config = { extends: ["@commitlint/config-conventional"], rules: { "header-max-length": [2, "always", 150], @@ -6,3 +6,5 @@ export default { "subject-full-stop": [0], }, }; + +export default config; diff --git a/packages/husky-config/lint-staged.js b/packages/husky-config/lint-staged.js index 954b589..19b8afe 100644 --- a/packages/husky-config/lint-staged.js +++ b/packages/husky-config/lint-staged.js @@ -1,4 +1,22 @@ -export default { - "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"], +import path from "path"; + +const buildLintCommand = (filenames) => { + const isNext = + process.env.npm_package_devDependencies_next || + process.env.npm_package_dependencies_next; + + if (isNext) { + return `next lint --fix --file ${filenames + .map((f) => path.relative(process.cwd(), f)) + .join(" --file ")}`; + } + + return "eslint --fix"; +}; + +const config = { + "*.{js,jsx,ts,tsx}": [buildLintCommand, "prettier --write"], "*.{json,md,css,scss}": ["prettier --write"], }; + +export default config; diff --git a/packages/infra/gitea/deploy-action.yml b/packages/infra/gitea/deploy-action.yml index 9bbd557..fbd09e8 100644 --- a/packages/infra/gitea/deploy-action.yml +++ b/packages/infra/gitea/deploy-action.yml @@ -39,17 +39,23 @@ jobs: short_sha: ${{ steps.determine.outputs.short_sha }} commit_msg: ${{ steps.determine.outputs.commit_msg }} 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: 0 + fetch-depth: 2 - name: πŸ” Environment & Version ermitteln id: determine run: | TAG="${{ github.ref_name }}" - SHORT_SHA="${{ github.sha }}" - SHORT_SHA="${SHORT_SHA:0:9}" + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-9) COMMIT_MSG=$(git log -1 --pretty=%s || echo "No commit message available") # Base Domain (e.g. example.com) @@ -79,7 +85,6 @@ jobs: TARGET="production" IMAGE_TAG="$TAG" ENV_FILE=".env.prod" - TRAEFIK_HOST="\${DOMAIN_BASE}, www.\${DOMAIN_BASE}" # Note: Host() backticks usually needed in compose TRAEFIK_HOST="\`\${DOMAIN_BASE}\`, \`www.\${DOMAIN_BASE}\`" NEXT_PUBLIC_BASE_URL="https://\${DOMAIN_BASE}" DIRECTUS_URL="https://cms.\${DOMAIN_BASE}" @@ -88,7 +93,7 @@ jobs: IS_PROD="true" GOTIFY_TITLE="πŸš€ Production-Release" GOTIFY_PRIORITY=6 - elif [[ "$TAG" =~ -rc\. || "$TAG" =~ -beta\. || "$TAG" =~ -alpha\. ]]; then + elif [[ "$TAG" =~ -rc || "$TAG" =~ -beta || "$TAG" =~ -alpha ]]; then TARGET="staging" IMAGE_TAG="$TAG" ENV_FILE=".env.staging" @@ -107,19 +112,21 @@ jobs: TARGET="skip" fi - echo "target=$TARGET" >> $GITHUB_OUTPUT - echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT - echo "env_file=$ENV_FILE" >> $GITHUB_OUTPUT - echo "traefik_host=$TRAEFIK_HOST" >> $GITHUB_OUTPUT - echo "next_public_base_url=$NEXT_PUBLIC_BASE_URL" >> $GITHUB_OUTPUT - echo "directus_url=$DIRECTUS_URL" >> $GITHUB_OUTPUT - echo "directus_host=$DIRECTUS_HOST" >> $GITHUB_OUTPUT - echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT - echo "is_prod=$IS_PROD" >> $GITHUB_OUTPUT - echo "gotify_title=$GOTIFY_TITLE" >> $GITHUB_OUTPUT - echo "gotify_priority=$GOTIFY_PRIORITY" >> $GITHUB_OUTPUT - echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT - echo "commit_msg=$COMMIT_MSG" >> $GITHUB_OUTPUT + { + echo "target=$TARGET" + echo "image_tag=$IMAGE_TAG" + echo "env_file=$ENV_FILE" + echo "traefik_host=$TRAEFIK_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 "short_sha=$SHORT_SHA" + echo "commit_msg=$COMMIT_MSG" + } >> "$GITHUB_OUTPUT" # ────────────────────────────────────────────────────────────────────────────── # JOB 2: Quality Assurance (Lint & Test) @@ -142,12 +149,19 @@ jobs: - name: Install dependencies run: npm ci - - name: πŸ§ͺ Run Checks + - name: πŸ§ͺ Run Checks in Parallel if: github.event.inputs.skip_long_checks != 'true' run: | - npm run lint - npm run typecheck - npm run test + npm run lint & + LINT_PID=$! + npm run typecheck & + TYPE_PID=$! + npm run test & + TEST_PID=$! + + wait $LINT_PID || exit 1 + wait $TYPE_PID || exit 1 + wait $TEST_PID || exit 1 # ────────────────────────────────────────────────────────────────────────────── # JOB 3: Build & Push @@ -161,6 +175,9 @@ jobs: - 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 @@ -174,7 +191,10 @@ jobs: --pull \ --platform linux/arm64 \ --build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \ + --build-arg NEXT_PUBLIC_TARGET="${{ needs.prepare.outputs.target }}" \ -t registry.infra.mintel.me/mintel/${{ github.event.repository.name }}:$IMAGE_TAG \ + --cache-from type=registry,ref=registry.infra.mintel.me/mintel/${{ github.event.repository.name }}:buildcache \ + --cache-to type=registry,ref=registry.infra.mintel.me/mintel/${{ github.event.repository.name }}:buildcache,mode=max \ --push . # ────────────────────────────────────────────────────────────────────────────── @@ -187,16 +207,14 @@ jobs: runs-on: docker 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 }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: πŸš€ Deploy via SSH - env: - IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} - ENV_FILE: ${{ needs.prepare.outputs.env_file }} - TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }} - PROJECT_NAME: ${{ needs.prepare.outputs.project_name }} run: | mkdir -p ~/.ssh echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_ed25519 @@ -208,12 +226,13 @@ jobs: # Generated by CI - $TARGET - $(date -u) NODE_ENV=production IMAGE_TAG=$IMAGE_TAG - TRAEFIK_HOST=$TRAEFIK_HOST + TRAEFIK_HOST=${{ needs.prepare.outputs.traefik_host }} PROJECT_NAME=$PROJECT_NAME ENV_FILE=$ENV_FILE # App Config NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_base_url }} + NEXT_PUBLIC_TARGET=$TARGET # Directus Config DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} @@ -237,10 +256,11 @@ jobs: ssh root@${{ secrets.SSH_HOST }} IMAGE_TAG="$IMAGE_TAG" ENV_FILE="$ENV_FILE" PROJECT_NAME="$PROJECT_NAME" bash << 'EOF' set -e cd "/home/deploy/sites/${{ github.event.repository.name }}" + chmod 600 "$ENV_FILE" 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 --remove-orphans - docker system prune -f --filter "until=168h" + docker system prune -f --filter "until=24h" EOF # ────────────────────────────────────────────────────────────────────────────── @@ -252,10 +272,22 @@ jobs: if: always() runs-on: docker steps: - - name: πŸ”” Gotify + - name: πŸ”” Gotify - Success if: needs.deploy.result == 'success' run: | 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 }}**" \ + -F "message=Erfolgreich deployt auf **${{ needs.prepare.outputs.target }}**\n\nVersion: **${{ needs.prepare.outputs.image_tag }}**\nCommit: ${{ needs.prepare.outputs.short_sha }}\nRun: ${{ github.run_id }}" \ -F "priority=4" || true + + - name: πŸ”” Gotify - Failure + if: | + needs.prepare.result == 'failure' || + needs.qa.result == 'failure' || + needs.build.result == 'failure' || + needs.deploy.result == 'failure' + run: | + curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ + -F "title=❌ Deployment FEHLGESCHLAGEN – ${{ github.event.repository.name }}" \ + -F "message=**Fehler beim Deploy auf ${{ needs.prepare.outputs.target || 'unknown' }}**\n\nRun: ${{ github.run_id }}\nBitte Logs prΓΌfen!" \ + -F "priority=8" || true diff --git a/packages/infra/scripts/sync-directus.sh b/packages/infra/scripts/sync-directus.sh new file mode 100644 index 0000000..c4ea1bc --- /dev/null +++ b/packages/infra/scripts/sync-directus.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# Mintel Directus Sync Engine +# Synchronizes Directus Data (Postgres + Uploads) between Local and Remote + +REMOTE_HOST="${SSH_HOST:-root@alpha.mintel.me}" +ACTION=$1 +ENV=$2 + +# Help +if [ -z "$ACTION" ] || [ -z "$ENV" ]; then + echo "Usage: mintel-sync [push|pull] [testing|staging|production]" + echo "" + echo "Commands:" + echo " push Sync LOCAL data -> REMOTE" + echo " pull Sync REMOTE data -> LOCAL" + echo "" + echo "Environments:" + echo " testing, staging, production" + exit 1 +fi + +PRJ_ID=$(jq -r .name package.json | sed 's/@mintel\///') +case $ENV in + testing) PROJECT_NAME="${PRJ_ID}-testing"; ENV_FILE=".env.testing" ;; + staging) PROJECT_NAME="${PRJ_ID}-staging"; ENV_FILE=".env.staging" ;; + production) PROJECT_NAME="${PRJ_ID}-prod"; ENV_FILE=".env.prod" ;; + *) echo "❌ Invalid environment: $ENV"; exit 1 ;; +esac + +REMOTE_DIR="/home/deploy/sites/${PRJ_ID}.com" + +# DB Details +DB_USER="directus" +DB_NAME="directus" + +echo "πŸ” Detecting local database..." +LOCAL_DB_CONTAINER=$(docker compose ps -q directus-db) +if [ -z "$LOCAL_DB_CONTAINER" ]; then + echo "❌ Local directus-db container not found. Running?" + exit 1 +fi + +if [ "$ACTION" == "push" ]; then + echo "πŸš€ Pushing LOCAL -> $ENV ($PROJECT_NAME)..." + docker exec "$LOCAL_DB_CONTAINER" pg_dump -U "$DB_USER" --clean --if-exists --no-owner --no-privileges "$DB_NAME" > dump.sql + scp dump.sql "$REMOTE_HOST:$REMOTE_DIR/dump.sql" + + REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus-db") + ssh "$REMOTE_HOST" "docker exec -i $REMOTE_DB_CONTAINER psql -U $DB_USER $DB_NAME < $REMOTE_DIR/dump.sql" + + rsync -avz --progress ./directus/uploads/ "$REMOTE_HOST:$REMOTE_DIR/directus/uploads/" + rm dump.sql + ssh "$REMOTE_HOST" "rm $REMOTE_DIR/dump.sql" + echo "✨ Push complete!" + +elif [ "$ACTION" == "pull" ]; then + echo "πŸ“₯ Pulling $ENV -> LOCAL..." + REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus-db") + ssh "$REMOTE_HOST" "docker exec $REMOTE_DB_CONTAINER pg_dump -U $DB_USER --clean --if-exists --no-owner --no-privileges $DB_NAME > $REMOTE_DIR/dump.sql" + scp "$REMOTE_HOST:$REMOTE_DIR/dump.sql" dump.sql + + docker exec -i "$LOCAL_DB_CONTAINER" psql -U "$DB_USER" "$DB_NAME" < dump.sql + rsync -avz --progress "$REMOTE_HOST:$REMOTE_DIR/directus/uploads/" ./directus/uploads/ + rm dump.sql + ssh "$REMOTE_HOST" "rm $REMOTE_DIR/dump.sql" + echo "✨ Pull complete!" +fi diff --git a/packages/infra/templates/website/scripts/setup-directus.ts b/packages/infra/templates/website/scripts/setup-directus.ts index 5fb2285..f37e081 100644 --- a/packages/infra/templates/website/scripts/setup-directus.ts +++ b/packages/infra/templates/website/scripts/setup-directus.ts @@ -1,18 +1,46 @@ -import client, { ensureAuthenticated } from "../src/lib/directus"; +import { + createMintelDirectusClient, + ensureDirectusAuthenticated, +} from "@mintel/next-utils"; import { updateSettings } from "@directus/sdk"; +const client = createMintelDirectusClient(); + async function setupBranding() { - console.log("🎨 Setup Directus Branding..."); - await ensureAuthenticated(); + const prjName = process.env.PROJECT_NAME || "Mintel Project"; + const prjColor = process.env.PROJECT_COLOR || "#82ed20"; + + console.log(`🎨 Setup Directus Branding for ${prjName}...`); + await ensureDirectusAuthenticated(client); + + const cssInjection = ` + +
+

MINTEL INFRASTRUCTURE ENGINE

+

${prjName.toUpperCase()} RELIABILITY.

+
+ `; try { await client.request( updateSettings({ - project_name: process.env.PROJECT_NAME || "Mintel Project", - project_color: process.env.PROJECT_COLOR || "#82ed20", + project_name: prjName, + project_color: prjColor, + public_note: cssInjection, theme_light_overrides: { - primary: process.env.PROJECT_COLOR || "#82ed20", + primary: prjColor, borderRadius: "16px", + navigationBackground: "#000c24", + navigationForeground: "#ffffff", }, } as any), ); diff --git a/packages/next-config/index.js b/packages/next-config/index.js index 80e3360..d7b75d7 100644 --- a/packages/next-config/index.js +++ b/packages/next-config/index.js @@ -1,38 +1,41 @@ -import createNextIntlPlugin from 'next-intl/plugin'; -import { withSentryConfig } from '@sentry/nextjs'; +import createNextIntlPlugin from "next-intl/plugin"; +import { withSentryConfig } from "@sentry/nextjs"; const withNextIntl = createNextIntlPlugin(); /** @type {import('next').NextConfig} */ export const baseNextConfig = { - output: 'standalone', + output: "standalone", images: { dangerouslyAllowSVG: true, - contentDispositionType: 'attachment', + contentDispositionType: "attachment", contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", }, async rewrites() { - const umamiUrl = (process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL || 'https://analytics.infra.mintel.me').replace('/script.js', ''); + const umamiUrl = ( + process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL || + "https://analytics.infra.mintel.me" + ).replace("/script.js", ""); const glitchtipUrl = process.env.SENTRY_DSN ? new URL(process.env.SENTRY_DSN).origin - : 'https://errors.infra.mintel.me'; + : "https://errors.infra.mintel.me"; return [ { - source: '/stats/:path*', + source: "/stats/:path*", destination: `${umamiUrl}/:path*`, }, { - source: '/errors/:path*', + source: "/errors/:path*", destination: `${glitchtipUrl}/:path*`, }, ]; }, }; -export default (config) => { +const withMintelConfig = (config) => { const nextIntlConfig = withNextIntl({ ...baseNextConfig, ...config }); - + return withSentryConfig( nextIntlConfig, { @@ -41,6 +44,8 @@ export default (config) => { }, { authToken: undefined, - } + }, ); }; + +export default withMintelConfig; diff --git a/scripts/sync-versions.ts b/scripts/sync-versions.ts index 0f2e8f9..3cfa79a 100644 --- a/scripts/sync-versions.ts +++ b/scripts/sync-versions.ts @@ -1,6 +1,5 @@ import * as fs from "fs"; import * as path from "path"; -import { execSync } from "child_process"; const tag = process.env.GITHUB_REF_NAME || process.env.TAG;