Compare commits
7 Commits
v2.2.13
...
0c4c6e8dc0
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c4c6e8dc0 | |||
| 3046a19113 | |||
| a4df12ddb3 | |||
| 73542237d5 | |||
| 52b66da16b | |||
| d75a83ccf2 | |||
| 5991bd8392 |
@@ -124,13 +124,13 @@ jobs:
|
|||||||
|
|
||||||
if [[ -n "$UPSTREAM_VERSION" && "$UPSTREAM_VERSION" != "workspace:"* ]]; then
|
if [[ -n "$UPSTREAM_VERSION" && "$UPSTREAM_VERSION" != "workspace:"* ]]; then
|
||||||
# 1. Discovery (Works without token for public repositories)
|
# 1. Discovery (Works without token for public repositories)
|
||||||
UPSTREAM_SHA=$(git ls-remote --tags https://git.infra.mintel.me/mmintel/at-mintel.git "$TAG_TO_WAIT" | grep "$TAG_TO_WAIT" | tail -n1 | awk '{print $1}')
|
UPSTREAM_SHA=$(git ls-remote --tags https://git.infra.mintel.me/mmintel/at-mintel.git "$TAG_TO_WAIT" 2>/dev/null | grep "$TAG_TO_WAIT" | awk '{print $1}' | tail -n1 || echo "")
|
||||||
|
|
||||||
if [[ -z "$UPSTREAM_SHA" ]]; then
|
if [[ -z "$UPSTREAM_SHA" ]]; then
|
||||||
echo "❌ Error: Tag $TAG_TO_WAIT not found in mmintel/at-mintel."
|
echo "⚠️ Warning: Tag $TAG_TO_WAIT not found in mmintel/at-mintel."
|
||||||
exit 1
|
else
|
||||||
|
echo "✅ Tag verified: Found upstream SHA $UPSTREAM_SHA for $TAG_TO_WAIT"
|
||||||
fi
|
fi
|
||||||
echo "✅ Tag verified: Found upstream SHA $UPSTREAM_SHA for $TAG_TO_WAIT"
|
|
||||||
|
|
||||||
# 2. Status Check (Requires GITEA_PAT for cross-repo API access)
|
# 2. Status Check (Requires GITEA_PAT for cross-repo API access)
|
||||||
POLL_TOKEN="${{ secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}"
|
POLL_TOKEN="${{ secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { sendEmail } from '@/lib/mail/mailer';
|
import { sendEmail } from '@/lib/mail/mailer';
|
||||||
|
import { env } from '@/lib/env';
|
||||||
import { render, ContactFormNotification, ConfirmationMessage } from '@mintel/mail';
|
import { render, ContactFormNotification, ConfirmationMessage } from '@mintel/mail';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { getServerAppServices } from '@/lib/services/create-services.server';
|
import { getServerAppServices } from '@/lib/services/create-services.server';
|
||||||
@@ -86,6 +87,7 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!isTestSubmission) {
|
if (!isTestSubmission) {
|
||||||
|
logger.info('Sending internal notification', { recipients: env.MAIL_RECIPIENTS });
|
||||||
const notificationResult = await sendEmail({
|
const notificationResult = await sendEmail({
|
||||||
replyTo: email,
|
replyTo: email,
|
||||||
subject: notificationSubject,
|
subject: notificationSubject,
|
||||||
@@ -97,14 +99,18 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
messageId: notificationResult.messageId,
|
messageId: notificationResult.messageId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.error('Notification email FAILED', {
|
logger.error('Notification email DELIVERY FAILED', {
|
||||||
error: notificationResult.error,
|
error: notificationResult.error,
|
||||||
subject: notificationSubject,
|
subject: notificationSubject,
|
||||||
email,
|
recipients: env.MAIL_RECIPIENTS,
|
||||||
});
|
});
|
||||||
services.errors.captureException(
|
services.errors.captureException(
|
||||||
new Error(`Notification email failed: ${notificationResult.error}`),
|
new Error(`Notification email failed: ${notificationResult.error}`),
|
||||||
{ action: 'sendContactFormAction_notification', email },
|
{
|
||||||
|
action: 'sendContactFormAction_notification',
|
||||||
|
email,
|
||||||
|
recipients: env.MAIL_RECIPIENTS
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -121,6 +127,7 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!isTestSubmission) {
|
if (!isTestSubmission) {
|
||||||
|
logger.info('Sending customer confirmation', { to: email });
|
||||||
const confirmationResult = await sendEmail({
|
const confirmationResult = await sendEmail({
|
||||||
to: email,
|
to: email,
|
||||||
subject: confirmationSubject,
|
subject: confirmationSubject,
|
||||||
@@ -132,7 +139,7 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
messageId: confirmationResult.messageId,
|
messageId: confirmationResult.messageId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.error('Confirmation email FAILED', {
|
logger.error('Confirmation email DELIVERY FAILED', {
|
||||||
error: confirmationResult.error,
|
error: confirmationResult.error,
|
||||||
subject: confirmationSubject,
|
subject: confirmationSubject,
|
||||||
to: email,
|
to: email,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const envExtension = {
|
|||||||
MAIL_USERNAME: z.string().optional(),
|
MAIL_USERNAME: z.string().optional(),
|
||||||
MAIL_PASSWORD: z.string().optional(),
|
MAIL_PASSWORD: z.string().optional(),
|
||||||
MAIL_FROM: z.string().optional(),
|
MAIL_FROM: z.string().optional(),
|
||||||
MAIL_RECIPIENTS: z.string().optional(),
|
MAIL_RECIPIENTS: z.string().trim().optional(),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -32,16 +32,27 @@ interface SendEmailOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function sendEmail({ to, replyTo, subject, html }: SendEmailOptions) {
|
export async function sendEmail({ to, replyTo, subject, html }: SendEmailOptions) {
|
||||||
const recipients = to || config.mail.recipients;
|
|
||||||
const logger = getServerAppServices().logger.child({ component: 'mailer' });
|
const logger = getServerAppServices().logger.child({ component: 'mailer' });
|
||||||
|
|
||||||
|
// Resolve recipients: priority to 'to' override, fallback to global MAIL_RECIPIENTS
|
||||||
|
const resolvedTo = to || config.mail.recipients;
|
||||||
|
|
||||||
|
// Normalize recipients (handle arrays or comma-strings)
|
||||||
|
const recipients = Array.isArray(resolvedTo)
|
||||||
|
? resolvedTo.join(', ')
|
||||||
|
: (resolvedTo?.toString() || '');
|
||||||
|
|
||||||
if (!recipients) {
|
if (!recipients || recipients.trim() === '') {
|
||||||
logger.error('No email recipients configured (MAIL_RECIPIENTS is empty and no "to" provided)', { subject });
|
logger.error('Email delivery ABORTED: No recipients configured', {
|
||||||
|
subject,
|
||||||
|
providedTo: to,
|
||||||
|
configRecipients: config.mail.recipients
|
||||||
|
});
|
||||||
return { success: false as const, error: 'No recipients configured' };
|
return { success: false as const, error: 'No recipients configured' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.mail.from) {
|
if (!config.mail.from) {
|
||||||
logger.error('MAIL_FROM is not configured — cannot send email', { subject, recipients });
|
logger.error('Email delivery ABORTED: MAIL_FROM is missing', { subject, recipients });
|
||||||
return { success: false as const, error: 'MAIL_FROM is not configured' };
|
return { success: false as const, error: 'MAIL_FROM is not configured' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,14 +64,36 @@ export async function sendEmail({ to, replyTo, subject, html }: SendEmailOptions
|
|||||||
html,
|
html,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const info = await getTransporter().sendMail(mailOptions);
|
const transporter = getTransporter();
|
||||||
logger.info('Email sent successfully', { messageId: info.messageId, subject, recipients });
|
logger.info('Attempting to send email via SMTP', {
|
||||||
|
host: config.mail.host,
|
||||||
|
subject,
|
||||||
|
recipients,
|
||||||
|
hasReplyTo: !!replyTo
|
||||||
|
});
|
||||||
|
|
||||||
|
const info = await transporter.sendMail(mailOptions);
|
||||||
|
|
||||||
|
logger.info('Email sent successfully', {
|
||||||
|
messageId: info.messageId,
|
||||||
|
subject,
|
||||||
|
recipients,
|
||||||
|
response: info.response
|
||||||
|
});
|
||||||
|
|
||||||
return { success: true, messageId: info.messageId };
|
return { success: true, messageId: info.messageId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
logger.error('Error sending email', { error: errorMsg, subject, recipients });
|
logger.error('SMTP Transport failed', {
|
||||||
|
error: errorMsg,
|
||||||
|
subject,
|
||||||
|
recipients,
|
||||||
|
config: {
|
||||||
|
host: config.mail.host,
|
||||||
|
user: config.mail.user ? '***' : 'not set'
|
||||||
|
}
|
||||||
|
});
|
||||||
return { success: false, error: errorMsg };
|
return { success: false, error: errorMsg };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,7 @@
|
|||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"preinstall": "npx only-allow pnpm"
|
"preinstall": "npx only-allow pnpm"
|
||||||
},
|
},
|
||||||
"version": "2.2.13",
|
"version": "2.3.17",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
|
|||||||
@@ -38,11 +38,21 @@ function getExpectedTranslation(
|
|||||||
sourcePath: string,
|
sourcePath: string,
|
||||||
sourceLocale: string,
|
sourceLocale: string,
|
||||||
targetLocale: string,
|
targetLocale: string,
|
||||||
): string {
|
alternates: { hreflang: string; href: string }[],
|
||||||
|
): string | null {
|
||||||
const segments = sourcePath.split('/').filter(Boolean);
|
const segments = sourcePath.split('/').filter(Boolean);
|
||||||
// First segment is locale
|
|
||||||
segments[0] = targetLocale;
|
segments[0] = targetLocale;
|
||||||
|
|
||||||
|
// Blog posts have dynamic slugs. If it's a blog post, trust the alternate tag
|
||||||
|
// if the href is present in the sitemap.
|
||||||
|
// The Smoke Test's primary job is ensuring the alternate links point to valid pages.
|
||||||
|
if (segments[1] === (targetLocale === 'de' ? 'blog' : 'blog') && segments.length > 2) {
|
||||||
|
const altLink = alternates.find((a) => a.hreflang === targetLocale);
|
||||||
|
if (altLink) {
|
||||||
|
return new URL(altLink.href).pathname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const map = sourceLocale === 'de' ? SLUG_MAP : REVERSE_SLUG_MAP;
|
const map = sourceLocale === 'de' ? SLUG_MAP : REVERSE_SLUG_MAP;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -50,7 +60,7 @@ function getExpectedTranslation(
|
|||||||
segments
|
segments
|
||||||
.map((seg, i) => {
|
.map((seg, i) => {
|
||||||
if (i === 0) return seg; // locale
|
if (i === 0) return seg; // locale
|
||||||
return map[seg] || seg; // translate or keep (product names like n2x2y stay the same)
|
return map[seg] || seg; // translate or keep
|
||||||
})
|
})
|
||||||
.join('/')
|
.join('/')
|
||||||
);
|
);
|
||||||
@@ -118,7 +128,7 @@ async function main() {
|
|||||||
if (alt.hreflang === locale) continue; // Same locale, skip
|
if (alt.hreflang === locale) continue; // Same locale, skip
|
||||||
|
|
||||||
// 1. Check slug translation is correct
|
// 1. Check slug translation is correct
|
||||||
const expectedPath = getExpectedTranslation(path, locale, alt.hreflang);
|
const expectedPath = getExpectedTranslation(path, locale, alt.hreflang, alternates);
|
||||||
const actualPath = new URL(alt.href).pathname;
|
const actualPath = new URL(alt.href).pathname;
|
||||||
|
|
||||||
if (actualPath !== expectedPath) {
|
if (actualPath !== expectedPath) {
|
||||||
|
|||||||
Reference in New Issue
Block a user