diff --git a/README.md b/README.md index fd8f81b..4d78d52 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,82 @@ # Mintel Monorepo -This monorepo manages multiple client websites using a shared technology stack: Next.js, TypeScript, and React. +This monorepo is the central "factory" for all Mintel client websites. It provides a standardized, versioned core of configurations, utilities, and infrastructure templates to ensure consistency, security, and rapid deployment across the entire portfolio. -## Project Structure +## πŸ— Project Structure -- `apps/`: Client websites (e.g., `sample-website`). -- `packages/`: Shared packages under the `@mintel` namespace. - - `@mintel/tsconfig`: Shared TypeScript configurations. - - `@mintel/eslint-config`: Shared ESLint configurations. - - `@mintel/next-config`: Shared Next.js configuration wrapper. - - `@mintel/next-utils`: Reusable logic (i18n, rate limiting, etc.). - - `@mintel/infra`: Infrastructure templates (Docker, Gitea Actions). - - `@mintel/cli`: CLI tool for project setup and migration. +- **`apps/`**: Client website implementations (e.g., `sample-website`). These are consumers of the shared packages. +- **`packages/`**: Shared, versioned npm packages under the `@mintel` namespace. + - [`@mintel/tsconfig`](packages/tsconfig/README.md): Centralized TypeScript configurations. + - [`@mintel/eslint-config`](packages/eslint-config/README.md): Shared linting rules and best practices. + - [`@mintel/next-config`](packages/next-config/README.md): A powerful Next.js configuration wrapper with built-in i18n and Sentry support. + - [`@mintel/next-utils`](packages/next-utils/README.md): Reusable logic for i18n, environment validation, and rate limiting. + - [`@mintel/infra`](packages/infra/README.md): Production-ready Docker and Gitea Actions templates. + - [`@mintel/cli`](packages/cli/README.md): Automation tool for scaffolding new projects. -## Getting Started +## πŸš€ Getting Started ### Prerequisites - - [pnpm](https://pnpm.io/) (v10+) - Node.js (v20+) +- Access to the private registry: `https://npm.infra.mintel.me` ### Installation - ```bash pnpm install ``` ### Development - +To run development mode for all shared packages: ```bash pnpm dev ``` ### Building - +To build all shared packages: ```bash pnpm build ``` -## Creating a New Project +## πŸ›  Creating a New Client Project -Use the Mintel CLI to set up a new project or migrate an existing one: +Never copy-paste files manually. Use the Mintel CLI to scaffold a new project with all best practices pre-configured: ```bash -pnpm --filter @mintel/cli start init apps/my-new-website +pnpm --filter @mintel/cli start init apps/client-name.com ``` -## Versioning and Releasing +This command automatically: +1. Sets up the directory structure. +2. Links all `@mintel` shared packages. +3. Configures `next-intl` (i18n) and Sentry. +4. Injects Docker and Gitea Actions deployment workflows. -We use [Changesets](https://github.com/changesets/changesets) for version management. +## πŸ”„ Release Cycle (Monorepo) -### 1. Add a changeset -When you make a change that requires a version bump, run: +We use [Changesets](https://github.com/changesets/changesets) to manage the versioning and publishing of shared packages. + +### 1. Documenting Changes +When you modify a package in `packages/*`, create a changeset: ```bash pnpm changeset ``` +Follow the prompts to select the package and the version bump type (patch, minor, major). -### 2. Version packages -To bump versions based on accumulated changesets: +### 2. Versioning +When ready to release, bump the versions: ```bash pnpm version-packages ``` +This updates `package.json` files and generates `CHANGELOG.md` entries. -### 3. Publish to registry -To build and publish all changed packages to the private registry: -```bash -pnpm release -``` +### 3. Publishing +The release to the private registry is automated via Gitea Actions. On every push to `main`, the `release.yml` workflow runs `pnpm release`. If new versions are detected, they are published to `https://npm.infra.mintel.me`. -## Deployment +## 🌐 Infrastructure & Deployment -Projects are hosted on Hetzner with Docker and Traefik, deployed via Gitea Actions. See `@mintel/infra` for templates. +Client websites scaffolded via the CLI use a **tag-based deployment** strategy: +- **Push to `main`**: Deploys to the `testing` environment. +- **Git Tag `v*.*.*-rc.*`**: Deploys to the `staging` environment. +- **Git Tag `v*.*.*`**: Deploys to the `production` environment. -## Registry - -Private npm registry: [https://npm.infra.mintel.me](https://npm.infra.mintel.me) +See the [`@mintel/infra`](packages/infra/README.md) package for detailed template documentation. diff --git a/apps/sample-website/CHANGELOG.md b/apps/sample-website/CHANGELOG.md new file mode 100644 index 0000000..eecc068 --- /dev/null +++ b/apps/sample-website/CHANGELOG.md @@ -0,0 +1,8 @@ +# sample-website + +## 0.1.1 + +### Patch Changes + +- Updated dependencies + - @mintel/next-utils@1.0.1 diff --git a/apps/sample-website/package.json b/apps/sample-website/package.json index 09c595e..b7a4fb9 100644 --- a/apps/sample-website/package.json +++ b/apps/sample-website/package.json @@ -1,6 +1,6 @@ { "name": "sample-website", - "version": "0.1.0", + "version": "0.1.1", "private": true, "type": "module", "scripts": { diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md new file mode 100644 index 0000000..6ee3379 --- /dev/null +++ b/packages/cli/CHANGELOG.md @@ -0,0 +1,7 @@ +# @mintel/cli + +## 1.0.1 + +### Patch Changes + +- Initial release of the Mintel factory packages. diff --git a/packages/cli/README.md b/packages/cli/README.md index 03efd63..645d35a 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,26 +1,33 @@ # @mintel/cli -CLI tool for managing the Mintel monorepo and scaffolding new client websites. +The Mintel CLI is the primary automation tool for managing the monorepo and ensuring all client websites follow the same high-quality standards and infrastructure patterns. -## Installation +## πŸš€ Installation + +The CLI is intended to be used within the monorepo: ```bash pnpm install ``` -## Commands +## πŸ›  Commands ### `init ` -Initializes a new website project in the specified path (relative to the monorepo root). +Scaffolds a new, production-ready client website in the specified path. ```bash -pnpm --filter @mintel/cli start init apps/my-new-website +pnpm --filter @mintel/cli start init apps/my-new-website.com ``` -This command will: -1. Create the project directory. -2. Generate `package.json`, `tsconfig.json`, and `eslint.config.mjs` extending `@mintel` defaults. -3. Set up a localized Next.js structure (`src/app/[locale]`). -4. Configure `next-intl` middleware and request config. -5. Inject production-ready `Dockerfile`, `docker-compose.yml`, and Gitea Actions deployment workflows. +#### What it does: +1. **Project Structure**: Creates a modern Next.js directory layout. +2. **Shared Configs**: Generates `package.json`, `tsconfig.json`, and `eslint.config.mjs` that extend the `@mintel` shared packages. +3. **Localization**: Sets up a localized routing structure (`src/app/[locale]`) with `next-intl` pre-configured. +4. **Error Tracking**: Injects Sentry/GlitchTip instrumentation. +5. **Environment Safety**: Adds a validation script (`scripts/validate-env.ts`) to catch missing secrets at build time. +6. **Infrastructure**: Injects the universal `Dockerfile`, `docker-compose.yml`, and the tag-based Gitea Actions deployment workflow. + +## πŸ›  Development + +To add new features to the scaffold (e.g., new shared files or config templates), modify `packages/cli/src/index.ts`. diff --git a/packages/cli/package.json b/packages/cli/package.json index 5e2e99d..fb68995 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@mintel/cli", - "version": "1.0.0", + "version": "1.0.1", "publishConfig": { "access": "public", "registry": "https://npm.infra.mintel.me" diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index cd2ac2b..255d4ae 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -99,6 +99,21 @@ export default nextConfig; eslintConfig ); + // Create env validation script + await fs.ensureDir(path.join(fullPath, "scripts")); + await fs.writeFile( + path.join(fullPath, "scripts/validate-env.ts"), + `import { validateMintelEnv } from "@mintel/next-utils"; + +try { + validateMintelEnv(); + console.log("βœ… Environment variables validated"); +} catch (error) { + process.exit(1); +} +` + ); + // Create basic src structure await fs.ensureDir(path.join(fullPath, "src/app/[locale]")); await fs.writeFile( @@ -186,10 +201,14 @@ export default function RootLayout({ await fs.writeFile( path.join(fullPath, "src/app/[locale]/page.tsx"), - `export default function Home() { + `import { useTranslations } from 'next-intl'; + +export default function Home() { + const t = useTranslations('Index'); + return (
-

Welcome to ${projectName}

+

{t('title')} to ${projectName}

); } diff --git a/packages/eslint-config/CHANGELOG.md b/packages/eslint-config/CHANGELOG.md new file mode 100644 index 0000000..1150566 --- /dev/null +++ b/packages/eslint-config/CHANGELOG.md @@ -0,0 +1,7 @@ +# @mintel/eslint-config + +## 1.0.1 + +### Patch Changes + +- Initial release of the Mintel factory packages. diff --git a/packages/eslint-config/README.md b/packages/eslint-config/README.md index b1302ad..b2255ec 100644 --- a/packages/eslint-config/README.md +++ b/packages/eslint-config/README.md @@ -1,13 +1,28 @@ # @mintel/eslint-config -Shared ESLint configurations for Mintel projects. +Shared ESLint configurations for Mintel projects, enforcing code quality and consistent style across Next.js and TypeScript codebases. -## Usage +## πŸ“¦ Configurations + +### `next` +A comprehensive configuration for Next.js projects. +- **Extends**: `next/core-web-vitals` and `next/typescript`. +- **Custom Rules**: + - `_` prefix for unused variables is allowed. + - `any` type is permitted (for rapid migration/prototyping). + - React unescaped entities check is disabled. + - Image element warnings are enabled (prefer `next/image`). + +## πŸš€ Usage + +### In a Next.js App +Create an `eslint.config.mjs` in your project root: -### Next.js -In your `eslint.config.mjs`: ```javascript import { nextConfig } from "@mintel/eslint-config/next"; export default nextConfig; ``` + +## πŸ›  Development +To add new rules, modify `packages/eslint-config/next.js`. Remember to create a changeset if you make breaking changes. diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index b6b9b06..f9d43df 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@mintel/eslint-config", - "version": "1.0.0", + "version": "1.0.1", "publishConfig": { "access": "public", "registry": "https://npm.infra.mintel.me" diff --git a/packages/infra/CHANGELOG.md b/packages/infra/CHANGELOG.md new file mode 100644 index 0000000..fdaf244 --- /dev/null +++ b/packages/infra/CHANGELOG.md @@ -0,0 +1,7 @@ +# @mintel/infra + +## 1.0.1 + +### Patch Changes + +- Initial release of the Mintel factory packages. diff --git a/packages/infra/README.md b/packages/infra/README.md index 0ca198c..436cbe5 100644 --- a/packages/infra/README.md +++ b/packages/infra/README.md @@ -1,12 +1,20 @@ # @mintel/infra -Infrastructure templates for Mintel projects. +Production-ready infrastructure templates for Mintel client websites, optimized for Hetzner, Traefik, and Gitea Actions. -## Contents +## πŸ“¦ Contents -- `docker/`: Universal `Dockerfile.nextjs` and `docker-compose.template.yml`. -- `gitea/`: Production-ready `deploy-action.yml` for Gitea Actions. +### `docker/` +- **`Dockerfile.nextjs`**: A multi-stage build optimized for Next.js standalone output. Supports build-time ARGs for Umami and Base URL. +- **`docker-compose.template.yml`**: A Traefik-ready compose file with automatic HTTPS redirection, security headers, and network isolation. -## Usage +### `gitea/` +- **`deploy-action.yml`**: A high-transparency deployment workflow. + - **Testing**: Automatic deploy on push to `main`. + - **Staging**: Deploy via `v*-rc.*` tags. + - **Production**: Deploy via `v*.*.*` tags. + - **Features**: Gotify notifications, health checks, and automatic `.env` generation. -These files are automatically injected into new projects by the `@mintel/cli`. +## πŸš€ Usage + +These templates are automatically injected into new projects by the [`@mintel/cli`](../cli/README.md). If you need to update an existing project, you can manually copy them from this package. diff --git a/packages/infra/gitea/deploy-action.yml b/packages/infra/gitea/deploy-action.yml index 352e986..2c649dc 100644 --- a/packages/infra/gitea/deploy-action.yml +++ b/packages/infra/gitea/deploy-action.yml @@ -2,101 +2,162 @@ name: Build & Deploy on: push: - branches: [main, staging] + branches: + - main + tags: + - 'v*' jobs: build-and-deploy: runs-on: docker steps: - # ═══════════════════════════════════════════════════════════════════════════════ - # LOGGING: Workflow Start - Full Transparency - # ═══════════════════════════════════════════════════════════════════════════════ - - name: πŸ“‹ Log Workflow Start + # ────────────────────────────────────────────────────────────────────────────── + # Workflow Start & Basic Info + # ────────────────────────────────────────────────────────────────────────────── + - name: πŸ“’ Workflow Start run: | - echo "πŸš€ Starting deployment for ${{ github.repository }} (${{ github.ref }})" - echo " β€’ Branch: ${{ github.ref_name }}" - echo " β€’ Commit: ${{ github.sha }}" - echo " β€’ Timestamp: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" + echo "β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”" + echo "β”‚ πŸš€ 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 - # ═══════════════════════════════════════════════════════════════════════════════ - # LOGGING: Registry Login Phase - # ═══════════════════════════════════════════════════════════════════════════════ - - name: πŸ” Login to private registry + # ────────────────────────────────────────────────────────────────────────────── + # Environment bestimmen + Commit-Message holen + # ────────────────────────────────────────────────────────────────────────────── + - name: πŸ” Environment & Version ermitteln + id: determine run: | - echo "πŸ” Authenticating with registry.infra.mintel.me..." + TAG="${{ github.ref_name }}" + SHORT_SHA="${{ github.sha }}" + SHORT_SHA="${SHORT_SHA:0:9}" + + # Get base domain from secret or env if possible, otherwise placeholder + # In a real project, you'd likely have a primary domain secret + DOMAIN_BASE=$(echo "${{ secrets.NEXT_PUBLIC_BASE_URL }}" | sed -E 's|https?://||' | sed -E 's|/.*||') + + # 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 + TARGET="testing" + IMAGE_TAG="main-${SHORT_SHA}" + ENV_FILE=".env.testing" + TRAEFIK_HOST="\`testing.${DOMAIN_BASE}\`" + IS_PROD="false" + GOTIFY_TITLE="πŸ§ͺ Testing-Deploy" + GOTIFY_PRIORITY=4 + elif [[ "${{ github.ref_type }}" == "tag" ]]; then + if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + TARGET="production" + IMAGE_TAG="$TAG" + ENV_FILE=".env.prod" + TRAEFIK_HOST="\`${DOMAIN_BASE}\`, \`www.${DOMAIN_BASE}\`" + 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.${DOMAIN_BASE}\`" + IS_PROD="false" + GOTIFY_TITLE="πŸ§ͺ Staging-Deploy (Pre-Release)" + GOTIFY_PRIORITY=5 + else + TARGET="skip" + GOTIFY_TITLE="❓ Unbekannter Tag" + GOTIFY_PRIORITY=3 + fi + else + 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 "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 + + - name: ⏭️ Skip Deployment + if: steps.determine.outputs.target == 'skip' + run: | + echo "Deployment ΓΌbersprungen – kein passender Trigger (main oder v*-Tag)" + exit 0 + + # ────────────────────────────────────────────────────────────────────────────── + # 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 - # ═══════════════════════════════════════════════════════════════════════════════ - # LOGGING: Build Phase - # ═══════════════════════════════════════════════════════════════════════════════ - - name: πŸ—οΈ Build Docker image + # ────────────────────────────────────────────────────────────────────────────── + # Build & Push + # ────────────────────────────────────────────────────────────────────────────── + - name: πŸ—οΈ Docker Image bauen & pushen env: - NEXT_PUBLIC_BASE_URL: ${{ github.ref_name == 'main' && secrets.NEXT_PUBLIC_BASE_URL || (secrets.STAGING_NEXT_PUBLIC_BASE_URL || secrets.NEXT_PUBLIC_BASE_URL) }} - NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ github.ref_name == 'main' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }} - NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ github.ref_name == 'main' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }} + 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) }} run: | - echo "πŸ—οΈ Building Docker image (linux/arm64) for branch ${{ github.ref_name }}..." + echo "πŸ—οΈ Building β†’ ${{ steps.determine.outputs.target }} / $IMAGE_TAG" docker buildx build \ --pull \ --platform linux/arm64 \ --build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \ --build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID="$NEXT_PUBLIC_UMAMI_WEBSITE_ID" \ --build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL="$NEXT_PUBLIC_UMAMI_SCRIPT_URL" \ - -t registry.infra.mintel.me/${{ github.repository }}:${{ github.sha }} \ - -t registry.infra.mintel.me/${{ github.repository }}:latest \ + -t registry.infra.mintel.me/${{ github.repository }}:$IMAGE_TAG \ --push . - # ═══════════════════════════════════════════════════════════════════════════════ - # LOGGING: Deployment Phase - # ═══════════════════════════════════════════════════════════════════════════════ - - name: πŸš€ Deploy to server + # ────────────────────────────────────────────────────────────────────────────── + # Deploy via SSH + # ────────────────────────────────────────────────────────────────────────────── + - name: πŸš€ Deploy to ${{ steps.determine.outputs.target }} env: - NEXT_PUBLIC_BASE_URL: ${{ github.ref_name == 'main' && secrets.NEXT_PUBLIC_BASE_URL || (secrets.STAGING_NEXT_PUBLIC_BASE_URL || secrets.NEXT_PUBLIC_BASE_URL) }} - NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ github.ref_name == 'main' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }} - NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ github.ref_name == 'main' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }} - SENTRY_DSN: ${{ github.ref_name == 'main' && secrets.SENTRY_DSN || (secrets.STAGING_SENTRY_DSN || secrets.SENTRY_DSN) }} - MAIL_HOST: ${{ github.ref_name == 'main' && secrets.MAIL_HOST || (secrets.STAGING_MAIL_HOST || secrets.MAIL_HOST) }} - MAIL_PORT: ${{ github.ref_name == 'main' && secrets.MAIL_PORT || (secrets.STAGING_MAIL_PORT || secrets.MAIL_PORT) }} - MAIL_USERNAME: ${{ github.ref_name == 'main' && secrets.MAIL_USERNAME || (secrets.STAGING_MAIL_USERNAME || secrets.MAIL_USERNAME) }} - MAIL_PASSWORD: ${{ github.ref_name == 'main' && secrets.MAIL_PASSWORD || (secrets.STAGING_MAIL_PASSWORD || secrets.MAIL_PASSWORD) }} - MAIL_FROM: ${{ github.ref_name == 'main' && secrets.MAIL_FROM || (secrets.STAGING_MAIL_FROM || secrets.MAIL_FROM) }} - MAIL_RECIPIENTS: ${{ github.ref_name == 'main' && secrets.MAIL_RECIPIENTS || (secrets.STAGING_MAIL_RECIPIENTS || secrets.MAIL_RECIPIENTS) }} + IMAGE_TAG: ${{ steps.determine.outputs.image_tag }} + ENV_FILE: ${{ steps.determine.outputs.env_file }} + TRAEFIK_HOST: ${{ steps.determine.outputs.traefik_host }} + 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: ${{ steps.determine.outputs.target == 'production' && secrets.MAIL_HOST || (steps.determine.outputs.target == 'staging' && secrets.STAGING_MAIL_HOST || secrets.TESTING_MAIL_HOST || secrets.MAIL_HOST) }} + MAIL_PORT: ${{ steps.determine.outputs.target == 'production' && secrets.MAIL_PORT || (steps.determine.outputs.target == 'staging' && secrets.STAGING_MAIL_PORT || secrets.TESTING_MAIL_PORT || secrets.MAIL_PORT) }} + MAIL_USERNAME: ${{ steps.determine.outputs.target == 'production' && secrets.MAIL_USERNAME || (steps.determine.outputs.target == 'staging' && secrets.STAGING_MAIL_USERNAME || secrets.TESTING_MAIL_USERNAME || secrets.MAIL_USERNAME) }} + MAIL_PASSWORD: ${{ steps.determine.outputs.target == 'production' && secrets.MAIL_PASSWORD || (steps.determine.outputs.target == 'staging' && secrets.STAGING_MAIL_PASSWORD || secrets.TESTING_MAIL_PASSWORD || secrets.MAIL_PASSWORD) }} + MAIL_FROM: ${{ steps.determine.outputs.target == 'production' && secrets.MAIL_FROM || (steps.determine.outputs.target == 'staging' && secrets.STAGING_MAIL_FROM || secrets.TESTING_MAIL_FROM || secrets.MAIL_FROM) }} + MAIL_RECIPIENTS: ${{ steps.determine.outputs.target == 'production' && secrets.MAIL_RECIPIENTS || (steps.determine.outputs.target == 'staging' && secrets.STAGING_MAIL_RECIPIENTS || secrets.TESTING_MAIL_RECIPIENTS || secrets.MAIL_RECIPIENTS) }} run: | - BRANCH=${{ github.ref_name }} - - # Derive domain from NEXT_PUBLIC_BASE_URL (strip https:// and trailing slash) - DOMAIN=$(echo "$NEXT_PUBLIC_BASE_URL" | sed -E 's|https?://||' | sed -E 's|/.*||') - - if [ "$BRANCH" = "main" ]; then - ENV_FILE=.env.prod - # For production, we want both root and www - TRAEFIK_HOST="\`$DOMAIN\`, \`www.$DOMAIN\`" - else - ENV_FILE=.env.staging - TRAEFIK_HOST="\`$DOMAIN\`" - fi - - echo "πŸš€ Deploying branch $BRANCH to $ENV_FILE..." - echo "🌐 Domain: $DOMAIN" - - # Setup SSH + echo "Deploying ${{ steps.determine.outputs.target }} β†’ $IMAGE_TAG" + + # SSH vorbereiten mkdir -p ~/.ssh echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null - - # Create .env file content + + # .env-Datei erstellen cat > /tmp/app.env << EOF - # ============================================================================ - # Environment Configuration ($BRANCH) - # ============================================================================ - # Auto-generated by CI/CD workflow - # ============================================================================ - + # Generated by CI - ${{ steps.determine.outputs.target }} - $(date -u) NODE_ENV=production NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID @@ -108,90 +169,80 @@ jobs: MAIL_PASSWORD=$MAIL_PASSWORD MAIL_FROM=$MAIL_FROM MAIL_RECIPIENTS=$MAIL_RECIPIENTS - - # Deployment variables for docker-compose - IMAGE_TAG=${{ github.sha }} + + IMAGE_TAG=$IMAGE_TAG TRAEFIK_HOST=$TRAEFIK_HOST ENV_FILE=$ENV_FILE EOF - + APP_DIR="/home/deploy/sites/${{ github.event.repository.name }}" ssh -o StrictHostKeyChecking=accept-new root@${{ secrets.SSH_HOST }} "mkdir -p $APP_DIR" scp -o StrictHostKeyChecking=accept-new /tmp/app.env root@${{ secrets.SSH_HOST }}:$APP_DIR/$ENV_FILE scp -o StrictHostKeyChecking=accept-new docker-compose.yml root@${{ secrets.SSH_HOST }}:$APP_DIR/docker-compose.yml - - ssh -o StrictHostKeyChecking=accept-new root@${{ secrets.SSH_HOST }} bash << EOF - set -e - cd $APP_DIR - - 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 images..." - IMAGE_TAG=${{ github.sha }} ENV_FILE=$ENV_FILE TRAEFIK_HOST="$TRAEFIK_HOST" docker compose --env-file $ENV_FILE pull - - echo "πŸš€ Starting containers..." - IMAGE_TAG=${{ github.sha }} ENV_FILE=$ENV_FILE TRAEFIK_HOST="$TRAEFIK_HOST" docker compose --env-file $ENV_FILE up -d - - echo "🧹 Cleaning up old images..." - docker system prune -f - - echo "⏳ Giving the app a few seconds to warm up..." - sleep 10 - - echo "πŸ” Checking container status..." - docker compose --env-file $ENV_FILE ps - - if ! docker compose --env-file $ENV_FILE ps | grep -q "Up"; then - echo "❌ Container failed to start" - docker compose --env-file $ENV_FILE logs --tail=100 - exit 1 - fi - - echo "βœ… Deployment complete!" + + ssh -o StrictHostKeyChecking=accept-new root@${{ secrets.SSH_HOST }} bash << 'EOF' + set -e + APP_DIR="/home/deploy/sites/${{ github.event.repository.name }}" + cd $APP_DIR + + 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" + IMAGE_TAG=$IMAGE_TAG ENV_FILE=$ENV_FILE TRAEFIK_HOST="$TRAEFIK_HOST" docker compose --env-file $ENV_FILE pull + + echo "β†’ Starting containers..." + IMAGE_TAG=$IMAGE_TAG ENV_FILE=$ENV_FILE TRAEFIK_HOST="$TRAEFIK_HOST" 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 auf ${{ steps.determine.outputs.target }}!" EOF - + rm -f /tmp/app.env - # ═══════════════════════════════════════════════════════════════════════════════ - # LOGGING: Workflow Summary - # ═══════════════════════════════════════════════════════════════════════════════ - - name: πŸ“Š Workflow Summary + # ────────────────────────────────────────────────────────────────────────────── + # Summary & Gotify + # ────────────────────────────────────────────────────────────────────────────── + - name: πŸ“Š Deployment Summary if: always() run: | - echo "πŸ“Š Status: ${{ job.status }}" - echo "🎯 Target: ${{ secrets.SSH_HOST }}" - echo "🌿 Branch: ${{ github.ref_name }}" + 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 "β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜" - # ═══════════════════════════════════════════════════════════════════════════════ - # NOTIFICATION: Gotify - # ═══════════════════════════════════════════════════════════════════════════════ - - name: πŸ”” Gotify Notification (Success) + - name: πŸ”” Gotify - Success if: success() run: | - echo "Sending success notification to Gotify..." - curl -k -s -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ - -F "title=βœ… Deployment Success: ${{ github.repository }}" \ - -F "message=The deployment of ${{ github.repository }} (branch: ${{ github.ref_name }}) was successful. - - Commit: ${{ github.sha }} - Actor: ${{ github.actor }} - Run ID: ${{ github.run_id }}" \ - -F "priority=5" + 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 - - name: πŸ”” Gotify Notification (Failure) + - name: πŸ”” Gotify - Failure if: failure() run: | - echo "Sending failure notification to Gotify..." - curl -k -s -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ - -F "title=❌ Deployment Failed: ${{ github.repository }}" \ - -F "message=The deployment of ${{ github.repository }} (branch: ${{ github.ref_name }}) failed! - - Commit: ${{ github.sha }} - Actor: ${{ github.actor }} - Run ID: ${{ github.run_id }} - - Please check the logs for details." \ - -F "priority=8" + 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 "priority=8" || true diff --git a/packages/infra/package.json b/packages/infra/package.json index 4c110df..1d1e920 100644 --- a/packages/infra/package.json +++ b/packages/infra/package.json @@ -1,6 +1,6 @@ { "name": "@mintel/infra", - "version": "1.0.0", + "version": "1.0.1", "publishConfig": { "access": "public", "registry": "https://npm.infra.mintel.me" diff --git a/packages/next-config/CHANGELOG.md b/packages/next-config/CHANGELOG.md new file mode 100644 index 0000000..662f665 --- /dev/null +++ b/packages/next-config/CHANGELOG.md @@ -0,0 +1,7 @@ +# @mintel/next-config + +## 1.0.1 + +### Patch Changes + +- Initial release of the Mintel factory packages. diff --git a/packages/next-config/README.md b/packages/next-config/README.md index 21f5166..fda7be3 100644 --- a/packages/next-config/README.md +++ b/packages/next-config/README.md @@ -1,16 +1,31 @@ # @mintel/next-config -Shared Next.js configuration wrapper for Mintel projects. Integrates `next-intl` and Sentry by default. +A powerful Next.js configuration wrapper that standardizes internationalization, error tracking, and security across all Mintel client websites. -## Usage +## ✨ Features + +- **`next-intl` Integration**: Automatically wraps your config with the internationalization plugin. +- **Sentry/GlitchTip**: Pre-configured error tracking with treeshaking and silent CI builds. +- **Standalone Output**: Optimized for Docker deployments by default. +- **Security Headers**: Strict Content Security Policy (CSP) and SVG safety. +- **Analytics Proxy**: Built-in rewrites for Umami analytics (`/stats/*`) and GlitchTip (`/errors/*`). + +## πŸš€ Usage In your `next.config.ts`: + ```typescript import mintelNextConfig from "@mintel/next-config"; +/** @type {import('next').NextConfig} */ const nextConfig = { - // Your project specific config + // Your project specific config (redirects, etc.) }; export default mintelNextConfig(nextConfig); ``` + +## 🌐 Environment Variables +The following variables are used by this config: +- `NEXT_PUBLIC_UMAMI_SCRIPT_URL`: URL to your Umami instance. +- `SENTRY_DSN`: Your GlitchTip/Sentry DSN. diff --git a/packages/next-config/package.json b/packages/next-config/package.json index ad79155..b36d2ad 100644 --- a/packages/next-config/package.json +++ b/packages/next-config/package.json @@ -1,6 +1,6 @@ { "name": "@mintel/next-config", - "version": "1.0.0", + "version": "1.0.1", "publishConfig": { "access": "public", "registry": "https://npm.infra.mintel.me" diff --git a/packages/next-utils/CHANGELOG.md b/packages/next-utils/CHANGELOG.md new file mode 100644 index 0000000..a06b321 --- /dev/null +++ b/packages/next-utils/CHANGELOG.md @@ -0,0 +1,7 @@ +# @mintel/next-utils + +## 1.0.1 + +### Patch Changes + +- Initial release of the Mintel factory packages. diff --git a/packages/next-utils/README.md b/packages/next-utils/README.md index 31e5b3a..f4cc5f8 100644 --- a/packages/next-utils/README.md +++ b/packages/next-utils/README.md @@ -1,28 +1,47 @@ # @mintel/next-utils -Reusable utilities for Mintel Next.js projects. +A collection of reusable utilities and helpers for Mintel Next.js projects, focusing on internationalization, environment safety, and security. -## Features +## ✨ Features -- **i18n**: Standardized middleware and request configuration for `next-intl`. -- **Env Validation**: Zod-based environment variable validation. -- **Rate Limiting**: Simple in-memory rate limiting for server actions. +### 🌍 Internationalization (i18n) +Standardized helpers for `next-intl`: +- `createMintelMiddleware`: A logging-enabled middleware wrapper. +- `createMintelI18nRequestConfig`: Centralized request configuration for server-side translations. -## Usage +### πŸ” Environment Validation +Zod-based validation to ensure your app never boots with missing secrets: +- `validateMintelEnv`: Validates standard Mintel variables (Mail, Sentry, Umami). -### i18n Middleware +### πŸ›‘ Rate Limiting +- `rateLimit`: A simple in-memory rate limiter for protecting server actions and form submissions. + +## πŸš€ Usage + +### i18n Middleware (`src/middleware.ts`) ```typescript import { createMintelMiddleware } from "@mintel/next-utils"; export default createMintelMiddleware({ locales: ["en", "de"], defaultLocale: "en", + logRequests: true, }); ``` -### Env Validation +### Env Validation (`scripts/validate-env.ts`) ```typescript import { validateMintelEnv } from "@mintel/next-utils"; -const env = validateMintelEnv(); +validateMintelEnv(); +``` + +### Rate Limiting +```typescript +import { rateLimit } from "@mintel/next-utils"; + +export async function myAction(data: any) { + await rateLimit(data.email); + // ... logic +} ``` diff --git a/packages/next-utils/package.json b/packages/next-utils/package.json index 6fe3f2a..f597240 100644 --- a/packages/next-utils/package.json +++ b/packages/next-utils/package.json @@ -1,6 +1,6 @@ { "name": "@mintel/next-utils", - "version": "1.0.0", + "version": "1.0.1", "publishConfig": { "access": "public", "registry": "https://npm.infra.mintel.me" diff --git a/packages/tsconfig/CHANGELOG.md b/packages/tsconfig/CHANGELOG.md new file mode 100644 index 0000000..2d84b32 --- /dev/null +++ b/packages/tsconfig/CHANGELOG.md @@ -0,0 +1,7 @@ +# @mintel/tsconfig + +## 1.0.1 + +### Patch Changes + +- Initial release of the Mintel factory packages. diff --git a/packages/tsconfig/README.md b/packages/tsconfig/README.md index e76fefd..87cb190 100644 --- a/packages/tsconfig/README.md +++ b/packages/tsconfig/README.md @@ -1,21 +1,47 @@ # @mintel/tsconfig -Shared TypeScript configurations for Mintel projects. +Centralized TypeScript configurations for all Mintel projects, ensuring consistent compiler settings and modern target environments. -## Usage +## πŸ“¦ Configurations -### Base Configuration -In your `tsconfig.json`: +### `base.json` +The foundation for all TypeScript projects in the monorepo. +- **Target**: `ES2020` +- **Module Resolution**: `bundler` +- **Strictness**: `strict: false` (aligned with `klz-2026` standards) +- **Features**: Enables `esModuleInterop`, `resolveJsonModule`, and `isolatedModules`. + +### `nextjs.json` +Extends `base.json` with specific settings for Next.js applications. +- **Plugins**: Includes the `next` TypeScript plugin for enhanced IDE support. +- **JSX**: Set to `preserve`. + +## πŸš€ Usage + +### In a Next.js App +Create a `tsconfig.json` in your project root: ```json { - "extends": "@mintel/tsconfig/base.json" + "extends": "@mintel/tsconfig/nextjs.json", + "compilerOptions": { + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] } ``` -### Next.js Configuration -In your `tsconfig.json`: +### In a Library Package +Create a `tsconfig.json` in your package root: ```json { - "extends": "@mintel/tsconfig/nextjs.json" + "extends": "@mintel/tsconfig/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"] } ``` diff --git a/packages/tsconfig/package.json b/packages/tsconfig/package.json index 706cea6..ef26dbe 100644 --- a/packages/tsconfig/package.json +++ b/packages/tsconfig/package.json @@ -1,6 +1,6 @@ { "name": "@mintel/tsconfig", - "version": "1.0.0", + "version": "1.0.1", "publishConfig": { "access": "public", "registry": "https://npm.infra.mintel.me"