chore: prepare first release 1.0.1
Some checks failed
Code Quality / lint-and-build (push) Failing after 25s
Release Packages / release (push) Failing after 40s

This commit is contained in:
2026-02-01 01:01:16 +01:00
parent 9a0900e3ff
commit c0a739867f
23 changed files with 432 additions and 217 deletions

View File

@@ -1,77 +1,82 @@
# Mintel Monorepo # 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`). - **`apps/`**: Client website implementations (e.g., `sample-website`). These are consumers of the shared packages.
- `packages/`: Shared packages under the `@mintel` namespace. - **`packages/`**: Shared, versioned npm packages under the `@mintel` namespace.
- `@mintel/tsconfig`: Shared TypeScript configurations. - [`@mintel/tsconfig`](packages/tsconfig/README.md): Centralized TypeScript configurations.
- `@mintel/eslint-config`: Shared ESLint configurations. - [`@mintel/eslint-config`](packages/eslint-config/README.md): Shared linting rules and best practices.
- `@mintel/next-config`: Shared Next.js configuration wrapper. - [`@mintel/next-config`](packages/next-config/README.md): A powerful Next.js configuration wrapper with built-in i18n and Sentry support.
- `@mintel/next-utils`: Reusable logic (i18n, rate limiting, etc.). - [`@mintel/next-utils`](packages/next-utils/README.md): Reusable logic for i18n, environment validation, and rate limiting.
- `@mintel/infra`: Infrastructure templates (Docker, Gitea Actions). - [`@mintel/infra`](packages/infra/README.md): Production-ready Docker and Gitea Actions templates.
- `@mintel/cli`: CLI tool for project setup and migration. - [`@mintel/cli`](packages/cli/README.md): Automation tool for scaffolding new projects.
## Getting Started ## 🚀 Getting Started
### Prerequisites ### Prerequisites
- [pnpm](https://pnpm.io/) (v10+) - [pnpm](https://pnpm.io/) (v10+)
- Node.js (v20+) - Node.js (v20+)
- Access to the private registry: `https://npm.infra.mintel.me`
### Installation ### Installation
```bash ```bash
pnpm install pnpm install
``` ```
### Development ### Development
To run development mode for all shared packages:
```bash ```bash
pnpm dev pnpm dev
``` ```
### Building ### Building
To build all shared packages:
```bash ```bash
pnpm build 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 ```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 We use [Changesets](https://github.com/changesets/changesets) to manage the versioning and publishing of shared packages.
When you make a change that requires a version bump, run:
### 1. Documenting Changes
When you modify a package in `packages/*`, create a changeset:
```bash ```bash
pnpm changeset pnpm changeset
``` ```
Follow the prompts to select the package and the version bump type (patch, minor, major).
### 2. Version packages ### 2. Versioning
To bump versions based on accumulated changesets: When ready to release, bump the versions:
```bash ```bash
pnpm version-packages pnpm version-packages
``` ```
This updates `package.json` files and generates `CHANGELOG.md` entries.
### 3. Publish to registry ### 3. Publishing
To build and publish all changed packages to the private registry: 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`.
```bash
pnpm release
```
## 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 See the [`@mintel/infra`](packages/infra/README.md) package for detailed template documentation.
Private npm registry: [https://npm.infra.mintel.me](https://npm.infra.mintel.me)

View File

@@ -0,0 +1,8 @@
# sample-website
## 0.1.1
### Patch Changes
- Updated dependencies
- @mintel/next-utils@1.0.1

View File

@@ -1,6 +1,6 @@
{ {
"name": "sample-website", "name": "sample-website",
"version": "0.1.0", "version": "0.1.1",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -0,0 +1,7 @@
# @mintel/cli
## 1.0.1
### Patch Changes
- Initial release of the Mintel factory packages.

View File

@@ -1,26 +1,33 @@
# @mintel/cli # @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 ```bash
pnpm install pnpm install
``` ```
## Commands ## 🛠 Commands
### `init <path>` ### `init <path>`
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 ```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: #### What it does:
1. Create the project directory. 1. **Project Structure**: Creates a modern Next.js directory layout.
2. Generate `package.json`, `tsconfig.json`, and `eslint.config.mjs` extending `@mintel` defaults. 2. **Shared Configs**: Generates `package.json`, `tsconfig.json`, and `eslint.config.mjs` that extend the `@mintel` shared packages.
3. Set up a localized Next.js structure (`src/app/[locale]`). 3. **Localization**: Sets up a localized routing structure (`src/app/[locale]`) with `next-intl` pre-configured.
4. Configure `next-intl` middleware and request config. 4. **Error Tracking**: Injects Sentry/GlitchTip instrumentation.
5. Inject production-ready `Dockerfile`, `docker-compose.yml`, and Gitea Actions deployment workflows. 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`.

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/cli", "name": "@mintel/cli",
"version": "1.0.0", "version": "1.0.1",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://npm.infra.mintel.me" "registry": "https://npm.infra.mintel.me"

View File

@@ -99,6 +99,21 @@ export default nextConfig;
eslintConfig 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 // Create basic src structure
await fs.ensureDir(path.join(fullPath, "src/app/[locale]")); await fs.ensureDir(path.join(fullPath, "src/app/[locale]"));
await fs.writeFile( await fs.writeFile(
@@ -186,10 +201,14 @@ export default function RootLayout({
await fs.writeFile( await fs.writeFile(
path.join(fullPath, "src/app/[locale]/page.tsx"), 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 ( return (
<main> <main>
<h1>Welcome to ${projectName}</h1> <h1>{t('title')} to ${projectName}</h1>
</main> </main>
); );
} }

View File

@@ -0,0 +1,7 @@
# @mintel/eslint-config
## 1.0.1
### Patch Changes
- Initial release of the Mintel factory packages.

View File

@@ -1,13 +1,28 @@
# @mintel/eslint-config # @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 ```javascript
import { nextConfig } from "@mintel/eslint-config/next"; import { nextConfig } from "@mintel/eslint-config/next";
export default nextConfig; export default nextConfig;
``` ```
## 🛠 Development
To add new rules, modify `packages/eslint-config/next.js`. Remember to create a changeset if you make breaking changes.

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/eslint-config", "name": "@mintel/eslint-config",
"version": "1.0.0", "version": "1.0.1",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://npm.infra.mintel.me" "registry": "https://npm.infra.mintel.me"

View File

@@ -0,0 +1,7 @@
# @mintel/infra
## 1.0.1
### Patch Changes
- Initial release of the Mintel factory packages.

View File

@@ -1,12 +1,20 @@
# @mintel/infra # @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`. ### `docker/`
- `gitea/`: Production-ready `deploy-action.yml` for Gitea Actions. - **`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.

View File

@@ -2,101 +2,162 @@ name: Build & Deploy
on: on:
push: push:
branches: [main, staging] branches:
- main
tags:
- 'v*'
jobs: jobs:
build-and-deploy: build-and-deploy:
runs-on: docker runs-on: docker
steps: steps:
# ═══════════════════════════════════════════════════════════════════════════════ # ──────────────────────────────────────────────────────────────────────────────
# LOGGING: Workflow Start - Full Transparency # Workflow Start & Basic Info
# ═══════════════════════════════════════════════════════════════════════════════ # ──────────────────────────────────────────────────────────────────────────────
- name: 📋 Log Workflow Start - name: 📢 Workflow Start
run: | run: |
echo "🚀 Starting deployment for ${{ github.repository }} (${{ github.ref }})" echo "┌──────────────────────────────────────────────────────────────┐"
echo " • Branch: ${{ github.ref_name }}" echo "│ 🚀 Deployment Workflow gestartet │"
echo " • Commit: ${{ github.sha }}" echo "├──────────────────────────────────────────────────────────────┤"
echo " • Timestamp: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" 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 - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
# ═══════════════════════════════════════════════════════════════════════════════ # ──────────────────────────────────────────────────────────────────────────────
# LOGGING: Registry Login Phase # Environment bestimmen + Commit-Message holen
# ═══════════════════════════════════════════════════════════════════════════════ # ──────────────────────────────────────────────────────────────────────────────
- name: 🔐 Login to private registry - name: 🔍 Environment & Version ermitteln
id: determine
run: | 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 echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
# ═══════════════════════════════════════════════════════════════════════════════ # ──────────────────────────────────────────────────────────────────────────────
# LOGGING: Build Phase # Build & Push
# ═══════════════════════════════════════════════════════════════════════════════ # ──────────────────────────────────────────────────────────────────────────────
- name: 🏗️ Build Docker image - name: 🏗️ Docker Image bauen & pushen
env: 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) }} IMAGE_TAG: ${{ steps.determine.outputs.image_tag }}
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_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_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) }} 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: | run: |
echo "🏗️ Building Docker image (linux/arm64) for branch ${{ github.ref_name }}..." echo "🏗️ Building → ${{ steps.determine.outputs.target }} / $IMAGE_TAG"
docker buildx build \ docker buildx build \
--pull \ --pull \
--platform linux/arm64 \ --platform linux/arm64 \
--build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \ --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_WEBSITE_ID="$NEXT_PUBLIC_UMAMI_WEBSITE_ID" \
--build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL="$NEXT_PUBLIC_UMAMI_SCRIPT_URL" \ --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 }}:$IMAGE_TAG \
-t registry.infra.mintel.me/${{ github.repository }}:latest \
--push . --push .
# ═══════════════════════════════════════════════════════════════════════════════ # ──────────────────────────────────────────────────────────────────────────────
# LOGGING: Deployment Phase # Deploy via SSH
# ═══════════════════════════════════════════════════════════════════════════════ # ──────────────────────────────────────────────────────────────────────────────
- name: 🚀 Deploy to server - name: 🚀 Deploy to ${{ steps.determine.outputs.target }}
env: 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) }} IMAGE_TAG: ${{ steps.determine.outputs.image_tag }}
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) }} ENV_FILE: ${{ steps.determine.outputs.env_file }}
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) }} TRAEFIK_HOST: ${{ steps.determine.outputs.traefik_host }}
SENTRY_DSN: ${{ github.ref_name == 'main' && secrets.SENTRY_DSN || (secrets.STAGING_SENTRY_DSN || secrets.SENTRY_DSN) }} 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) }}
MAIL_HOST: ${{ github.ref_name == 'main' && secrets.MAIL_HOST || (secrets.STAGING_MAIL_HOST || secrets.MAIL_HOST) }} 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) }}
MAIL_PORT: ${{ github.ref_name == 'main' && secrets.MAIL_PORT || (secrets.STAGING_MAIL_PORT || secrets.MAIL_PORT) }} 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) }}
MAIL_USERNAME: ${{ github.ref_name == 'main' && secrets.MAIL_USERNAME || (secrets.STAGING_MAIL_USERNAME || secrets.MAIL_USERNAME) }} 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_PASSWORD: ${{ github.ref_name == 'main' && secrets.MAIL_PASSWORD || (secrets.STAGING_MAIL_PASSWORD || secrets.MAIL_PASSWORD) }} 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_FROM: ${{ github.ref_name == 'main' && secrets.MAIL_FROM || (secrets.STAGING_MAIL_FROM || secrets.MAIL_FROM) }} 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_RECIPIENTS: ${{ github.ref_name == 'main' && secrets.MAIL_RECIPIENTS || (secrets.STAGING_MAIL_RECIPIENTS || secrets.MAIL_RECIPIENTS) }} 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: | run: |
BRANCH=${{ github.ref_name }} echo "Deploying ${{ steps.determine.outputs.target }} → $IMAGE_TAG"
# Derive domain from NEXT_PUBLIC_BASE_URL (strip https:// and trailing slash) # SSH vorbereiten
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
mkdir -p ~/.ssh mkdir -p ~/.ssh
echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_ed25519 echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
# Create .env file content # .env-Datei erstellen
cat > /tmp/app.env << EOF cat > /tmp/app.env << EOF
# ============================================================================ # Generated by CI - ${{ steps.determine.outputs.target }} - $(date -u)
# Environment Configuration ($BRANCH)
# ============================================================================
# Auto-generated by CI/CD workflow
# ============================================================================
NODE_ENV=production NODE_ENV=production
NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
@@ -108,90 +169,80 @@ jobs:
MAIL_PASSWORD=$MAIL_PASSWORD MAIL_PASSWORD=$MAIL_PASSWORD
MAIL_FROM=$MAIL_FROM MAIL_FROM=$MAIL_FROM
MAIL_RECIPIENTS=$MAIL_RECIPIENTS MAIL_RECIPIENTS=$MAIL_RECIPIENTS
# Deployment variables for docker-compose IMAGE_TAG=$IMAGE_TAG
IMAGE_TAG=${{ github.sha }}
TRAEFIK_HOST=$TRAEFIK_HOST TRAEFIK_HOST=$TRAEFIK_HOST
ENV_FILE=$ENV_FILE ENV_FILE=$ENV_FILE
EOF EOF
APP_DIR="/home/deploy/sites/${{ github.event.repository.name }}" APP_DIR="/home/deploy/sites/${{ github.event.repository.name }}"
ssh -o StrictHostKeyChecking=accept-new root@${{ secrets.SSH_HOST }} "mkdir -p $APP_DIR" 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 /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 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 ssh -o StrictHostKeyChecking=accept-new root@${{ secrets.SSH_HOST }} bash << 'EOF'
set -e set -e
cd $APP_DIR APP_DIR="/home/deploy/sites/${{ github.event.repository.name }}"
cd $APP_DIR
chmod 600 $ENV_FILE
chown deploy:deploy $ENV_FILE 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 "${{ 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 "→ 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=${{ github.sha }} ENV_FILE=$ENV_FILE TRAEFIK_HOST="$TRAEFIK_HOST" docker compose --env-file $ENV_FILE up -d echo "→ Starting containers..."
IMAGE_TAG=$IMAGE_TAG 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 docker system prune -f --filter "until=168h"
echo "⏳ Giving the app a few seconds to warm up..." echo "→ Waiting 15s for warmup..."
sleep 10 sleep 15
echo "🔍 Checking container status..." echo " Container status:"
docker compose --env-file $ENV_FILE ps docker compose --env-file $ENV_FILE ps
if ! docker compose --env-file $ENV_FILE ps | grep -q "Up"; then if ! docker compose --env-file $ENV_FILE ps | grep -q "Up"; then
echo "❌ Container failed to start" echo "❌ Fehler: Container nicht Up!"
docker compose --env-file $ENV_FILE logs --tail=100 docker compose --env-file $ENV_FILE logs --tail=150
exit 1 exit 1
fi fi
echo "✅ Deployment complete!" echo "✅ Deployment erfolgreich auf ${{ steps.determine.outputs.target }}!"
EOF EOF
rm -f /tmp/app.env rm -f /tmp/app.env
# ═══════════════════════════════════════════════════════════════════════════════ # ──────────────────────────────────────────────────────────────────────────────
# LOGGING: Workflow Summary # Summary & Gotify
# ═══════════════════════════════════════════════════════════════════════════════ # ──────────────────────────────────────────────────────────────────────────────
- name: 📊 Workflow Summary - name: 📊 Deployment Summary
if: always() if: always()
run: | run: |
echo "📊 Status: ${{ job.status }}" echo "┌──────────────────────────────┐"
echo "🎯 Target: ${{ secrets.SSH_HOST }}" echo "│ Deployment Summary │"
echo "🌿 Branch: ${{ github.ref_name }}" 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 "└──────────────────────────────┘"
# ═══════════════════════════════════════════════════════════════════════════════ - name: 🔔 Gotify - Success
# NOTIFICATION: Gotify
# ═══════════════════════════════════════════════════════════════════════════════
- name: 🔔 Gotify Notification (Success)
if: success() if: success()
run: | run: |
echo "Sending success notification to Gotify..." curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
curl -k -s -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ -F "title=${{ steps.determine.outputs.gotify_title }}" \
-F "title=✅ Deployment Success: ${{ github.repository }}" \ -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 "message=The deployment of ${{ github.repository }} (branch: ${{ github.ref_name }}) was successful. -F "priority=${{ steps.determine.outputs.gotify_priority }}" || true
Commit: ${{ github.sha }}
Actor: ${{ github.actor }}
Run ID: ${{ github.run_id }}" \
-F "priority=5"
- name: 🔔 Gotify Notification (Failure) - name: 🔔 Gotify - Failure
if: failure() if: failure()
run: | run: |
echo "Sending failure notification to Gotify..." curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
curl -k -s -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \ -F "title=❌ Deployment FEHLGESCHLAGEN ${{ steps.determine.outputs.target || 'unknown' }}" \
-F "title=❌ Deployment Failed: ${{ github.repository }}" \ -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 "message=The deployment of ${{ github.repository }} (branch: ${{ github.ref_name }}) failed! -F "priority=8" || true
Commit: ${{ github.sha }}
Actor: ${{ github.actor }}
Run ID: ${{ github.run_id }}
Please check the logs for details." \
-F "priority=8"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/infra", "name": "@mintel/infra",
"version": "1.0.0", "version": "1.0.1",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://npm.infra.mintel.me" "registry": "https://npm.infra.mintel.me"

View File

@@ -0,0 +1,7 @@
# @mintel/next-config
## 1.0.1
### Patch Changes
- Initial release of the Mintel factory packages.

View File

@@ -1,16 +1,31 @@
# @mintel/next-config # @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`: In your `next.config.ts`:
```typescript ```typescript
import mintelNextConfig from "@mintel/next-config"; import mintelNextConfig from "@mintel/next-config";
/** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
// Your project specific config // Your project specific config (redirects, etc.)
}; };
export default mintelNextConfig(nextConfig); 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.

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/next-config", "name": "@mintel/next-config",
"version": "1.0.0", "version": "1.0.1",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://npm.infra.mintel.me" "registry": "https://npm.infra.mintel.me"

View File

@@ -0,0 +1,7 @@
# @mintel/next-utils
## 1.0.1
### Patch Changes
- Initial release of the Mintel factory packages.

View File

@@ -1,28 +1,47 @@
# @mintel/next-utils # @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`. ### 🌍 Internationalization (i18n)
- **Env Validation**: Zod-based environment variable validation. Standardized helpers for `next-intl`:
- **Rate Limiting**: Simple in-memory rate limiting for server actions. - `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 ```typescript
import { createMintelMiddleware } from "@mintel/next-utils"; import { createMintelMiddleware } from "@mintel/next-utils";
export default createMintelMiddleware({ export default createMintelMiddleware({
locales: ["en", "de"], locales: ["en", "de"],
defaultLocale: "en", defaultLocale: "en",
logRequests: true,
}); });
``` ```
### Env Validation ### Env Validation (`scripts/validate-env.ts`)
```typescript ```typescript
import { validateMintelEnv } from "@mintel/next-utils"; 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
}
``` ```

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/next-utils", "name": "@mintel/next-utils",
"version": "1.0.0", "version": "1.0.1",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://npm.infra.mintel.me" "registry": "https://npm.infra.mintel.me"

View File

@@ -0,0 +1,7 @@
# @mintel/tsconfig
## 1.0.1
### Patch Changes
- Initial release of the Mintel factory packages.

View File

@@ -1,21 +1,47 @@
# @mintel/tsconfig # @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 ### `base.json`
In your `tsconfig.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 ```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 a Library Package
In your `tsconfig.json`: Create a `tsconfig.json` in your package root:
```json ```json
{ {
"extends": "@mintel/tsconfig/nextjs.json" "extends": "@mintel/tsconfig/base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
} }
``` ```

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mintel/tsconfig", "name": "@mintel/tsconfig",
"version": "1.0.0", "version": "1.0.1",
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://npm.infra.mintel.me" "registry": "https://npm.infra.mintel.me"