Compare commits

...

12 Commits

Author SHA1 Message Date
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
6 changed files with 105 additions and 47 deletions

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: on:
push: push:
@@ -36,13 +37,6 @@ jobs:
container: container:
image: catthehacker/ubuntu:act-latest image: catthehacker/ubuntu:act-latest
steps: 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 - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@@ -187,6 +181,8 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: 🐳 Set up QEMU
uses: docker/setup-qemu-action@v3
- name: 🐳 Set up Docker Buildx - name: 🐳 Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: 🔐 Registry Login - name: 🔐 Registry Login
@@ -200,6 +196,8 @@ jobs:
build-args: | build-args: |
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }} NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }} 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 }} NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
tags: registry.infra.mintel.me/mintel/mb-grid-solutions:${{ needs.prepare.outputs.image_tag }} 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 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 }} NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
# Secrets mapping (Database & CMS) # 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 }} 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_DB: ${{ secrets.POSTGRES_DB || vars.POSTGRES_DB || 'payload' }}
POSTGRES_USER: ${{ secrets.POSTGRES_USER || vars.POSTGRES_USER || 'directus' }} POSTGRES_USER: ${{ secrets.POSTGRES_USER || vars.POSTGRES_USER || 'directus' }}
@@ -275,6 +273,7 @@ jobs:
GATEKEEPER_ORIGIN="$NEXT_PUBLIC_BASE_URL/gatekeeper" GATEKEEPER_ORIGIN="$NEXT_PUBLIC_BASE_URL/gatekeeper"
# Generate Environment File # Generate Environment File
echo "🔍 Verifying Sentry DSN presence..."
cat > .env.deploy << EOF cat > .env.deploy << EOF
# Generated by CI - $TARGET # Generated by CI - $TARGET
IMAGE_TAG=$IMAGE_TAG IMAGE_TAG=$IMAGE_TAG

View File

@@ -8,6 +8,7 @@ import {
ConfirmationMessage, ConfirmationMessage,
} from "@mintel/mail"; } from "@mintel/mail";
import React from "react"; import React from "react";
import nodemailer from "nodemailer";
export async function POST(req: Request) { export async function POST(req: Request) {
const services = getServerAppServices(); const services = getServerAppServices();
@@ -77,11 +78,33 @@ export async function POST(req: Request) {
services.errors.captureException(payloadError, { phase: "payload_save" }); 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 { try {
const { config } = await import("@/lib/config"); const { config } = await import("@/lib/config");
const clientName = "MB Grid Solutions"; 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 // 2a. Notification to MB Grid
const notificationHtml = await render( const notificationHtml = await render(
React.createElement(ContactFormNotification, { React.createElement(ContactFormNotification, {
@@ -92,19 +115,23 @@ export async function POST(req: Request) {
}), }),
); );
await payload.sendEmail({ try {
from: config.mail.from, const info = await transporter.sendMail({
to: from: config.mail.from,
config.mail.recipients.join(",") || to,
process.env.CONTACT_RECIPIENT || replyTo: email,
"info@mb-grid-solutions.com", subject: `Kontaktanfrage von ${name}`,
replyTo: email, html: notificationHtml,
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 // 2b. Confirmation to the User
try { try {
logger.info("Preparing to send confirmation email", { to: email });
const confirmationHtml = await render( const confirmationHtml = await render(
React.createElement(ConfirmationMessage, { React.createElement(ConfirmationMessage, {
name, name,
@@ -112,16 +139,17 @@ export async function POST(req: Request) {
}), }),
); );
await payload.sendEmail({ const info = await transporter.sendMail({
from: config.mail.from, from: config.mail.from,
to: email, to: email,
subject: `Ihre Kontaktanfrage bei ${clientName}`, subject: `Ihre Kontaktanfrage bei ${clientName}`,
html: confirmationHtml, html: confirmationHtml,
}); });
logger.info("Confirmation email sent successfully", { messageId: info.messageId });
} catch (confirmError) { } catch (confirmError) {
logger.warn( logger.warn(
"Failed to send confirmation email, but notification was sent", "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" - "traefik.docker.network=infra"
# Public Router paths that bypass Gatekeeper auth # 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.entrypoints=websecure"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-public.tls.certresolver=le" - "traefik.http.routers.${PROJECT_NAME:-mb-grid}-public.tls.certresolver=le"
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-public.tls=true" - "traefik.http.routers.${PROJECT_NAME:-mb-grid}-public.tls=true"

View File

@@ -1,6 +1,8 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import createMiddleware from "next-intl/middleware"; import createMiddleware from "next-intl/middleware";
export default createMiddleware({ const intlMiddleware = createMiddleware({
// A list of all locales that are supported // A list of all locales that are supported
locales: ["de"], locales: ["de"],
@@ -11,6 +13,17 @@ export default createMiddleware({
localePrefix: "as-needed", 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 = { export const config = {
// Matcher for all pages and internationalized pathnames // Matcher for all pages and internationalized pathnames
// excluding api, _next, static files, etc. // excluding api, _next, static files, etc.

View File

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

View File

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