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
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.

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",
"version": "0.1.0",
"version": "0.1.1",
"private": true,
"type": "module",
"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
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 <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
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`.

View File

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

View File

@@ -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 (
<main>
<h1>Welcome to ${projectName}</h1>
<h1>{t('title')} to ${projectName}</h1>
</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
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.

View File

@@ -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"

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
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.

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/infra",
"version": "1.0.0",
"version": "1.0.1",
"publishConfig": {
"access": "public",
"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
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.

View File

@@ -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"

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
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
}
```

View File

@@ -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"

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
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"]
}
```

View File

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