Compare commits

...

16 Commits

Author SHA1 Message Date
8427d348dd fix: ensure smtp metadata logger doesn't crash in tests
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m13s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 14m2s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 11s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 2m51s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / call-qa-workflow (push) Failing after 40s
2026-04-15 00:05:45 +02:00
7109b3c32e fix: add detailed smtp logging to debug confirmation email
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 4s
🚀 Build & Deploy / 🧪 QA (push) Failing after 49s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🏗️ Build (push) Successful in 13m44s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-14 22:28:45 +02:00
18717e7faf fix: resolve vitest hoisting issue in contact api test
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 7s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m9s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 13m8s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 13s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m26s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / call-qa-workflow (push) Failing after 34s
2026-04-13 22:32:16 +02:00
3478f0f42f fix: remove problematic docker maintenance step from prepare job
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 5s
🚀 Build & Deploy / 🧪 QA (push) Failing after 39s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 12m34s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-13 21:45:42 +02:00
836cc66334 chore: re-trigger pipeline after network failure in #2226
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Failing after 13s
🚀 Build & Deploy / 🧪 QA (push) Has been skipped
🚀 Build & Deploy / 🏗️ Build (push) Has been skipped
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-13 19:11:33 +02:00
9bb2186f7a test: update contact api test to mock nodemailer
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Failing after 6s
🚀 Build & Deploy / 🧪 QA (push) Has been skipped
🚀 Build & Deploy / 🏗️ Build (push) Has been skipped
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-13 19:11:04 +02:00
55123132f4 fix(mail): bypass payload mail adapter and use standalone nodemailer
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
🚀 Build & Deploy / 🧪 QA (push) Failing after 40s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 12m4s
🚀 Build & Deploy / 🚀 Deploy (push) Has been skipped
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-13 17:28:19 +02:00
bf1dfe4015 fix(mail): robust recipient logic and sentry dsn verification
All checks were successful
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 5s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m18s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 12m3s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 14s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m58s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-13 11:49:09 +02:00
f03e417eb7 fix(prod): stabilize production environment and fix email adapter initialization
Some checks failed
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 7s
🚀 Build & Deploy / 🧪 QA (push) Successful in 1m41s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 12m22s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 13s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m26s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / call-qa-workflow (push) Failing after 34s
2026-04-12 15:09:39 +02:00
e0312aeba9 chore: trigger deployment after infra disk cleanup
All checks were successful
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 6s
🚀 Build & Deploy / 🧪 QA (push) Successful in 2m17s
🚀 Build & Deploy / 🏗️ Build (push) Successful in 17m23s
🚀 Build & Deploy / 🚀 Deploy (push) Successful in 13s
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 3m17s
🚀 Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-04-12 11:29:44 +02:00
d5c0b91c97 fix(analytics): restore Umami tracking by fixing build-args and bypassing gatekeeper
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 14s
Build & Deploy / 🏗️ Build (push) Failing after 5m6s
Build & Deploy / 🧪 QA (push) Failing after 5m22s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
Nightly QA / call-qa-workflow (push) Failing after 33s
2026-04-12 01:55:48 +02:00
155918e265 chore: trigger deployment after infrastructure and db fixes
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 14s
Build & Deploy / 🧪 QA (push) Successful in 8m56s
Build & Deploy / 🏗️ Build (push) Successful in 15m30s
Build & Deploy / 🚀 Deploy (push) Successful in 15s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 4m18s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / call-qa-workflow (push) Failing after 33s
2026-04-10 14:13:24 +02:00
fb5b55f1dd fix(ci): add QEMU support for multi-arch builds (linux/amd64)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Successful in 2m2s
Build & Deploy / 🏗️ Build (push) Successful in 15m12s
Build & Deploy / 🚀 Deploy (push) Failing after 1m36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-10 12:44:00 +02:00
9f00828b80 fix: restore glitchtip nesting and fix type errors
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 2m32s
Build & Deploy / 🏗️ Build (push) Successful in 5m10s
Build & Deploy / 🚀 Deploy (push) Successful in 42s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 4m17s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-09 12:24:33 +02:00
bb0debe29d fix: correct syntax error in lib/config.ts
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Failing after 1m0s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Failing after 3m27s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / call-qa-workflow (push) Failing after 43s
2026-04-09 12:17:28 +02:00
f2e855fff9 chore: fix sentry logging instrumentation and infrastructure alignment
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Failing after 1m9s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Failing after 3m36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-04-09 11:32:47 +02:00
10 changed files with 121 additions and 49 deletions

View File

@@ -74,6 +74,7 @@ AUTH_COOKIE_NAME=mintel_gatekeeper_session
# Sentry / Glitchtip (Error Tracking)
SENTRY_DSN=
NEXT_PUBLIC_SENTRY_DSN=
# Gotify (In-App Notifications)
# GOTIFY_URL=

View File

@@ -1,4 +1,5 @@
name: Build & Deploy
# Infrastructure Maintenance: Production stabilization and email adapter fix triggered on 2026-04-12.
name: 🚀 Build & Deploy
on:
push:
@@ -36,13 +37,6 @@ jobs:
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: 🧹 Maintenance (High Density Cleanup)
shell: bash
run: |
echo "Purging old build layers and dangling images..."
docker image prune -f
docker builder prune -f --filter "until=6h"
- name: Checkout repository
uses: actions/checkout@v4
with:
@@ -187,6 +181,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: 🐳 Set up QEMU
uses: docker/setup-qemu-action@v3
- name: 🐳 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 🔐 Registry Login
@@ -200,6 +196,8 @@ jobs:
build-args: |
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
NEXT_PUBLIC_UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
tags: registry.infra.mintel.me/mintel/mb-grid-solutions:${{ needs.prepare.outputs.image_tag }}
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/mb-grid-solutions:buildcache
@@ -223,7 +221,7 @@ jobs:
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
# Secrets mapping (Database & CMS)
PAYLOAD_SECRET: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_PAYLOAD_SECRET) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_PAYLOAD_SECRET) || secrets.PAYLOAD_SECRET || secrets.DIRECTUS_SECRET || vars.PAYLOAD_SECRET || 'you-need-to-set-a-payload-secret' }}
PAYLOAD_SECRET: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_PAYLOAD_SECRET) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_PAYLOAD_SECRET) || secrets.PAYLOAD_SECRET || secrets.MINTEL_PRIVATE_TOKEN || secrets.DIRECTUS_SECRET || vars.PAYLOAD_SECRET || 'you-need-to-set-a-payload-secret' }}
DATABASE_URI: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DATABASE_URI) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DATABASE_URI) || secrets.DATABASE_URI || vars.DATABASE_URI }}
POSTGRES_DB: ${{ secrets.POSTGRES_DB || vars.POSTGRES_DB || 'payload' }}
POSTGRES_USER: ${{ secrets.POSTGRES_USER || vars.POSTGRES_USER || 'directus' }}
@@ -275,12 +273,14 @@ jobs:
GATEKEEPER_ORIGIN="$NEXT_PUBLIC_BASE_URL/gatekeeper"
# Generate Environment File
echo "🔍 Verifying Sentry DSN presence..."
cat > .env.deploy << EOF
# Generated by CI - $TARGET
IMAGE_TAG=$IMAGE_TAG
NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
GATEKEEPER_ORIGIN=$GATEKEEPER_ORIGIN
SENTRY_DSN=$SENTRY_DSN
NEXT_PUBLIC_SENTRY_DSN=$SENTRY_DSN
PROJECT_COLOR=$PROJECT_COLOR
LOG_LEVEL=$LOG_LEVEL

View File

@@ -8,6 +8,7 @@ import {
ConfirmationMessage,
} from "@mintel/mail";
import React from "react";
import nodemailer from "nodemailer";
export async function POST(req: Request) {
const services = getServerAppServices();
@@ -77,11 +78,33 @@ export async function POST(req: Request) {
services.errors.captureException(payloadError, { phase: "payload_save" });
}
// 2. Email sending via Payload (which uses configured nodemailer)
// 2. Email sending via standalone Nodemailer (bypassing Payload adapter)
try {
const { config } = await import("@/lib/config");
const clientName = "MB Grid Solutions";
// Robust recipient resolution
const recipients = Array.isArray(config.mail.recipients)
? config.mail.recipients.filter(Boolean)
: [];
const to =
recipients.length > 0
? recipients.join(",")
: process.env.CONTACT_RECIPIENT || "info@mb-grid-solutions.com";
logger.info("Instantiating standalone nodemailer transport");
const transporter = nodemailer.createTransport({
host: config.mail.host,
port: config.mail.port,
auth: {
user: config.mail.user,
pass: config.mail.pass,
},
});
logger.info("Preparing to send notification email", { to, host: config.mail.host });
// 2a. Notification to MB Grid
const notificationHtml = await render(
React.createElement(ContactFormNotification, {
@@ -92,19 +115,23 @@ export async function POST(req: Request) {
}),
);
await payload.sendEmail({
from: config.mail.from,
to:
config.mail.recipients.join(",") ||
process.env.CONTACT_RECIPIENT ||
"info@mb-grid-solutions.com",
replyTo: email,
subject: `Kontaktanfrage von ${name}`,
html: notificationHtml,
});
try {
const info = await transporter.sendMail({
from: config.mail.from,
to,
replyTo: email,
subject: `Kontaktanfrage von ${name}`,
html: notificationHtml,
});
logger.info("Notification email sent successfully", { messageId: info?.messageId });
} catch (notifyError) {
logger.error("Failed to send notification email", { error: notifyError });
throw notifyError; // Re-throw to be caught by the outer SMTP catch
}
// 2b. Confirmation to the User
try {
logger.info("Preparing to send confirmation email", { to: email });
const confirmationHtml = await render(
React.createElement(ConfirmationMessage, {
name,
@@ -112,16 +139,17 @@ export async function POST(req: Request) {
}),
);
await payload.sendEmail({
const info = await transporter.sendMail({
from: config.mail.from,
to: email,
subject: `Ihre Kontaktanfrage bei ${clientName}`,
html: confirmationHtml,
});
logger.info("Confirmation email sent successfully", { messageId: info?.messageId });
} catch (confirmError) {
logger.warn(
"Failed to send confirmation email, but notification was sent",
{ error: confirmError },
{ error: confirmError instanceof Error ? confirmError.message : String(confirmError) },
);
}

View File

@@ -20,7 +20,7 @@ services:
- "traefik.docker.network=infra"
# Public Router paths that bypass Gatekeeper auth
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-public.rule=Host(`${TRAEFIK_HOST:-mb-grid-solutions.localhost}`) && PathRegexp(`^/([a-z]{2}/)?(health|login|gatekeeper|uploads|media|robots\\.txt|manifest\\.webmanifest|sitemap(-[0-9]+)?\\.xml|(.*/)?api/og(/.*)?|(.*/)?opengraph-image.*)`)"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-public.rule=Host(`${TRAEFIK_HOST:-mb-grid-solutions.localhost}`) && PathRegexp(`^/([a-z]{2}/)?(health|login|gatekeeper|uploads|media|robots\\.txt|manifest\\.webmanifest|sitemap(-[0-9]+)?\\.xml|(.*/)?api/og(/.*)?|(.*/)?opengraph-image.*|stats(/.*)?|errors(/.*)?)`)"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-public.entrypoints=websecure"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-public.tls.certresolver=le"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-public.tls=true"

10
instrumentation.ts Normal file
View File

@@ -0,0 +1,10 @@
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config');
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config');
}
}

View File

@@ -37,8 +37,8 @@ function createConfig() {
errors: {
glitchtip: {
// Use SENTRY_DSN for both server and client (proxied)
dsn: env.SENTRY_DSN,
// Use NEXT_PUBLIC_SENTRY_DSN if available (required for browser-side SDK)
dsn: env.NEXT_PUBLIC_SENTRY_DSN || env.SENTRY_DSN,
// The proxied origin used in the frontend
proxyPath: "/errors",
enabled: Boolean(env.SENTRY_DSN),

View File

@@ -17,6 +17,8 @@ import {
const envExtension = {
// Project specific overrides or additions
AUTH_COOKIE_NAME: z.string().default("mb_gatekeeper_session"),
NEXT_PUBLIC_SENTRY_DSN: z.string().optional(),
SENTRY_DSN: z.string().optional(),
};
/**

View File

@@ -1,6 +1,8 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import createMiddleware from "next-intl/middleware";
export default createMiddleware({
const intlMiddleware = createMiddleware({
// A list of all locales that are supported
locales: ["de"],
@@ -11,6 +13,17 @@ export default createMiddleware({
localePrefix: "as-needed",
});
export default function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Explicit bypass for analytics and error tracking paths
if (pathname.startsWith("/stats") || pathname.startsWith("/errors")) {
return NextResponse.next();
}
return intlMiddleware(request);
}
export const config = {
// Matcher for all pages and internationalized pathnames
// excluding api, _next, static files, etc.

View File

@@ -50,24 +50,29 @@ export default buildConfig({
},
prodMigrations: migrations,
}),
...(process.env.SMTP_HOST
? {
email: nodemailerAdapter({
defaultFromAddress:
process.env.SMTP_FROM || "info@mb-grid-solutions.com",
defaultFromName: "MB Grid Solutions CMS",
transportOptions: {
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || "587"),
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
secure: process.env.SMTP_SECURE === "true",
},
}),
}
: {}),
email: nodemailerAdapter({
defaultFromAddress:
process.env.MAIL_FROM ||
process.env.SMTP_FROM ||
"info@mb-grid-solutions.com",
defaultFromName: "MB Grid Solutions CMS",
transportOptions: {
host: process.env.MAIL_HOST || process.env.SMTP_HOST || "localhost",
port: parseInt(
process.env.MAIL_PORT || process.env.SMTP_PORT || "587",
),
auth: {
user:
process.env.MAIL_USERNAME ||
process.env.MAIL_USER ||
process.env.SMTP_USER,
pass: process.env.MAIL_PASSWORD || process.env.SMTP_PASS,
},
secure:
process.env.MAIL_SECURE === "true" ||
process.env.SMTP_SECURE === "true",
},
}),
sharp,
plugins: [
...(process.env.S3_ENDPOINT

View File

@@ -6,6 +6,19 @@ const { mockCreate, mockSendEmail } = vi.hoisted(() => ({
mockSendEmail: vi.fn(),
}));
// Mock Nodemailer
const { mockSendMail } = vi.hoisted(() => ({
mockSendMail: vi.fn().mockResolvedValue({ messageId: "mock-message-id" }),
}));
vi.mock("nodemailer", () => ({
default: {
createTransport: vi.fn().mockReturnValue({
sendMail: mockSendMail,
}),
},
}));
vi.mock("payload", () => ({
getPayload: vi.fn().mockResolvedValue({
create: mockCreate,
@@ -101,7 +114,7 @@ describe("Contact API Integration", () => {
// But it actually does NOTHING internally
expect(mockCreate).not.toHaveBeenCalled();
expect(mockSendEmail).not.toHaveBeenCalled();
expect(mockSendMail).not.toHaveBeenCalled();
});
it("should successfully save to Payload and send emails", async () => {
@@ -140,10 +153,10 @@ describe("Contact API Integration", () => {
});
// 2. Verify Email Sending
// Note: sendEmail is called twice (Notification + User Confirmation)
expect(mockSendEmail).toHaveBeenCalledTimes(2);
// Note: sendMail is called twice (Notification + User Confirmation)
expect(mockSendMail).toHaveBeenCalledTimes(2);
expect(mockSendEmail).toHaveBeenNthCalledWith(
expect(mockSendMail).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
subject: "Kontaktanfrage von Jane Doe",
@@ -151,7 +164,7 @@ describe("Contact API Integration", () => {
}),
);
expect(mockSendEmail).toHaveBeenNthCalledWith(
expect(mockSendMail).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
to: "jane@example.com",