Compare commits
10 Commits
eeb0920afd
...
v1.15.20
| Author | SHA1 | Date | |
|---|---|---|---|
| 12848a4f23 | |||
| 0a9cb81841 | |||
| c505050cb4 | |||
| d5d0d77180 | |||
| 9399be3ea9 | |||
| 7ec82027a8 | |||
| 631d051c2e | |||
| 1a8758a88c | |||
| 7d6d34dd7b | |||
| 1f0de18755 |
@@ -154,6 +154,30 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
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
|
||||||
|
|
||||||
|
- name: 🧹 Nuclear Runner Cleanup
|
||||||
|
run: |
|
||||||
|
echo "Disk space before nuclear prune:"
|
||||||
|
df -h
|
||||||
|
|
||||||
|
# Massive cleanup of pre-installed runner software to free up ~5GB+
|
||||||
|
# These are standard on VM-based runners and often unnecessary
|
||||||
|
sudo rm -rf /usr/share/dotnet || true
|
||||||
|
sudo rm -rf /usr/local/lib/android || true
|
||||||
|
sudo rm -rf /opt/ghc || true
|
||||||
|
sudo rm -rf /opt/hostedtoolcache/CodeQL || true
|
||||||
|
|
||||||
|
# Comprehensive Docker purge
|
||||||
|
docker system prune -af --volumes
|
||||||
|
docker builder prune -af
|
||||||
|
docker buildx prune -af
|
||||||
|
|
||||||
|
# Clean temp build artifacts
|
||||||
|
rm -rf /tmp/docker-actions-toolkit-* || true
|
||||||
|
|
||||||
|
echo "Disk space after nuclear prune:"
|
||||||
|
df -h
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: 🏗️ Build and Push
|
- name: 🏗️ Build and Push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
@@ -176,8 +200,9 @@ jobs:
|
|||||||
PAYLOAD_SECRET=${{ secrets.PAYLOAD_SECRET || 'secret' }}
|
PAYLOAD_SECRET=${{ secrets.PAYLOAD_SECRET || 'secret' }}
|
||||||
BUILD_ID=${{ github.sha }}
|
BUILD_ID=${{ github.sha }}
|
||||||
tags: registry.infra.mintel.me/mintel/mintel.me:${{ needs.prepare.outputs.image_tag }}
|
tags: registry.infra.mintel.me/mintel/mintel.me:${{ needs.prepare.outputs.image_tag }}
|
||||||
|
# Temporarily disable registry cache export to save runner disk/bandwidth
|
||||||
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache-${{ needs.prepare.outputs.target }}
|
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache-${{ needs.prepare.outputs.target }}
|
||||||
cache-to: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache-${{ needs.prepare.outputs.target }},mode=max
|
# cache-to: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache-${{ needs.prepare.outputs.target }},mode=max
|
||||||
secrets: |
|
secrets: |
|
||||||
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
@@ -432,6 +457,7 @@ jobs:
|
|||||||
- name: 🌐 Check Production URL
|
- name: 🌐 Check Production URL
|
||||||
shell: sh
|
shell: sh
|
||||||
run: |
|
run: |
|
||||||
|
apk add --no-cache curl
|
||||||
# Wait longer (up to 2 minutes) for Next.js to fully prime
|
# Wait longer (up to 2 minutes) for Next.js to fully prime
|
||||||
COUNT=0
|
COUNT=0
|
||||||
MAX=24
|
MAX=24
|
||||||
@@ -454,17 +480,44 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: 🌐 Check Case Study Assets
|
- name: 🌐 Check Case Study Assets and Rewrites
|
||||||
shell: sh
|
shell: sh
|
||||||
run: |
|
run: |
|
||||||
URL="${{ needs.prepare.outputs.next_public_url }}/case-studies/klz-cables"
|
BASE_URL="${{ needs.prepare.outputs.next_public_url }}"
|
||||||
echo "Verifying case study $URL..."
|
|
||||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$URL")
|
echo "Verifying diagnostic rewrite..."
|
||||||
|
STATUS=$(curl -s -L -o /dev/null -w "%{http_code}" "$BASE_URL/robots-test")
|
||||||
|
if [ "$STATUS" != "200" ]; then
|
||||||
|
echo "❌ Diagnostic rewrite /robots-test failed! (Status: $STATUS)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Diagnostic rewrite OK"
|
||||||
|
|
||||||
|
echo "Verifying case study page..."
|
||||||
|
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/case-studies/klz-cables")
|
||||||
if [ "$STATUS" != "200" ]; then
|
if [ "$STATUS" != "200" ]; then
|
||||||
echo "❌ Case study page failed! (Status: $STATUS)"
|
echo "❌ Case study page failed! (Status: $STATUS)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "✅ Case study page is UP"
|
echo "✅ Case study page UP"
|
||||||
|
|
||||||
|
echo "Verifying breeze CSS (root path)..."
|
||||||
|
CSS_PATH="/assets/klz-cables.com/wp-content/cache/breeze-minification/css/breeze_klz-cables-com-1-10895.css"
|
||||||
|
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$CSS_PATH")
|
||||||
|
if [ "$STATUS" != "200" ]; then
|
||||||
|
echo "❌ Root asset path failed! (Status: $STATUS)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Root asset path OK"
|
||||||
|
|
||||||
|
echo "Verifying breeze CSS (relative path from case-study)..."
|
||||||
|
REL_CSS_PATH="/case-studies/assets/klz-cables.com/wp-content/cache/breeze-minification/css/breeze_klz-cables-com-1-10895.css"
|
||||||
|
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$REL_CSS_PATH")
|
||||||
|
if [ "$STATUS" != "200" ]; then
|
||||||
|
echo "❌ Relative asset path failed! (Status: $STATUS)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Relative asset path OK"
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# JOB 6: Notifications
|
# JOB 6: Notifications
|
||||||
|
|||||||
14
Dockerfile
14
Dockerfile
@@ -39,6 +39,9 @@ ENV CI=true
|
|||||||
# Copy manifest files specifically for better layer caching
|
# Copy manifest files specifically for better layer caching
|
||||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json .npmrc* ./
|
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json .npmrc* ./
|
||||||
COPY apps/web/package.json ./apps/web/package.json
|
COPY apps/web/package.json ./apps/web/package.json
|
||||||
|
# Install build dependencies for native modules (like canvas)
|
||||||
|
RUN apk add --no-cache python3 make g++ pkgconfig pixman-dev cairo-dev pango-dev libjpeg-turbo-dev giflib-dev librsvg-dev
|
||||||
|
|
||||||
# Copy sibling monorepo for linked dependencies (cloned during CI)
|
# Copy sibling monorepo for linked dependencies (cloned during CI)
|
||||||
# Placing it inside /app so relative links like ../../_at-mintel resolve correctly!
|
# Placing it inside /app so relative links like ../../_at-mintel resolve correctly!
|
||||||
COPY _at-mintel* /app/_at-mintel/
|
COPY _at-mintel* /app/_at-mintel/
|
||||||
@@ -72,17 +75,14 @@ WORKDIR /app
|
|||||||
|
|
||||||
# Copy standalone output and static files (Monorepo paths)
|
# Copy standalone output and static files (Monorepo paths)
|
||||||
# Note: Base image already handles the non-root user and basic env
|
# Note: Base image already handles the non-root user and basic env
|
||||||
COPY --from=builder /app/apps/web/public ./apps/web/public
|
COPY --from=builder --chown=1001:65533 /app/apps/web/public ./apps/web/public
|
||||||
COPY --from=builder /app/apps/web/.next/standalone ./
|
COPY --from=builder --chown=1001:65533 /app/apps/web/.next/standalone ./
|
||||||
COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static
|
COPY --from=builder --chown=1001:65533 /app/apps/web/.next/static ./apps/web/.next/static
|
||||||
|
|
||||||
# Explicitly copy Payload dynamically generated importMap.js excluded by Standalone tracing
|
# Explicitly copy Payload dynamically generated importMap.js excluded by Standalone tracing
|
||||||
COPY --from=builder /app/apps/web/app/(payload)/admin/importMap.js ./apps/web/app/(payload)/admin/importMap.js
|
COPY --from=builder --chown=1001:65533 /app/apps/web/app/(payload)/admin/importMap.js ./apps/web/app/(payload)/admin/importMap.js
|
||||||
|
|
||||||
# Fix permissions for the non-root user (Standard uid/gid from base image)
|
# Fix permissions for the non-root user (Standard uid/gid from base image)
|
||||||
# We do this as root before switching users
|
|
||||||
USER root
|
|
||||||
RUN chown -R 1001:65533 /app
|
|
||||||
USER nextjs
|
USER nextjs
|
||||||
|
|
||||||
# Start from the app directory to ensure references solve correctly
|
# Start from the app directory to ensure references solve correctly
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const nextConfig = {
|
|||||||
'sharp',
|
'sharp',
|
||||||
'puppeteer',
|
'puppeteer',
|
||||||
'require-in-the-middle',
|
'require-in-the-middle',
|
||||||
'import-in-the-middle' // Sentry 10+ instrumentation dependencies
|
'import-in-the-middle'
|
||||||
],
|
],
|
||||||
transpilePackages: [
|
transpilePackages: [
|
||||||
'@mintel/content-engine',
|
'@mintel/content-engine',
|
||||||
@@ -37,24 +37,6 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
async rewrites() {
|
|
||||||
return [
|
|
||||||
// Umami proxy rewrite handled in app/stats/api/send/route.ts
|
|
||||||
// Sentry relay handled in app/errors/api/relay/route.ts
|
|
||||||
{
|
|
||||||
source: '/assets/:path*',
|
|
||||||
destination: '/showcase/klz-cables.com/assets/:path*',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
source: '/wp-content/:path*',
|
|
||||||
destination: '/showcase/klz-cables.com/assets/klz-cables.com/wp-content/:path*',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
source: '/wp-includes/:path*',
|
|
||||||
destination: '/showcase/klz-cables.com/assets/klz-cables.com/wp-includes/:path*',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
async redirects() {
|
async redirects() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -67,7 +49,54 @@ const nextConfig = {
|
|||||||
outputFileTracingRoot: path.join(dirname, '../../'),
|
outputFileTracingRoot: path.join(dirname, '../../'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const withMDX = createMDX({
|
const withMDX = createMDX({});
|
||||||
// Add markdown plugins here, as desired
|
|
||||||
});
|
// Execute wrappers
|
||||||
export default withPayload(withMintelConfig(withMDX(nextConfig)));
|
let config = withPayload(withMintelConfig(withMDX(nextConfig)));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects our critical showcase rewrites into a resolved Next.js config object.
|
||||||
|
*/
|
||||||
|
function injectShowcaseRewrites(resolvedConfig) {
|
||||||
|
const originalRewrites = resolvedConfig.rewrites;
|
||||||
|
|
||||||
|
resolvedConfig.rewrites = async () => {
|
||||||
|
const existing = await originalRewrites?.() || [];
|
||||||
|
|
||||||
|
// Normalize existing to object pattern
|
||||||
|
const normalized = Array.isArray(existing)
|
||||||
|
? { afterFiles: existing, beforeFiles: [], fallback: [] }
|
||||||
|
: existing;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...normalized,
|
||||||
|
beforeFiles: [
|
||||||
|
...(normalized.beforeFiles || []),
|
||||||
|
// Diagnostic rule
|
||||||
|
{ source: '/robots-test', destination: '/robots.txt' },
|
||||||
|
// Showcase Assets
|
||||||
|
{ source: '/assets/:path*', destination: '/showcase/klz-cables.com/assets/:path*' },
|
||||||
|
{ source: '/wp-content/:path*', destination: '/showcase/klz-cables.com/assets/klz-cables.com/wp-content/:path*' },
|
||||||
|
{ source: '/wp-includes/:path*', destination: '/showcase/klz-cables.com/assets/klz-cables.com/wp-includes/:path*' },
|
||||||
|
{ source: '/:folder(case-studies|work|blog)/assets/:path*', destination: '/showcase/klz-cables.com/assets/:path*' },
|
||||||
|
{ source: '/:folder(case-studies|work|blog)/wp-content/:path*', destination: '/showcase/klz-cables.com/assets/klz-cables.com/wp-content/:path*' },
|
||||||
|
{ source: '/:folder(case-studies|work|blog)/wp-includes/:path*', destination: '/showcase/klz-cables.com/assets/klz-cables.com/wp-includes/:path*' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return resolvedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle both standard objects and functional wrappers (like withPayload)
|
||||||
|
if (typeof config === 'function') {
|
||||||
|
const originalConfigWrapper = config;
|
||||||
|
config = async (phase, args) => {
|
||||||
|
const resolved = await originalConfigWrapper(phase, args);
|
||||||
|
return injectShowcaseRewrites(resolved);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
config = injectShowcaseRewrites(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ export function ContactForm({
|
|||||||
onBack={() => setFlow("discovery")} // Can keep, but won't be seen if we hide button
|
onBack={() => setFlow("discovery")} // Can keep, but won't be seen if we hide button
|
||||||
onSubmit={() => handleSubmit()}
|
onSubmit={() => handleSubmit()}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
|
error={error}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ interface DirectMessageFlowProps {
|
|||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
|
error?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DirectMessageFlow = ({
|
export const DirectMessageFlow = ({
|
||||||
@@ -57,10 +58,32 @@ export const DirectMessageFlow = ({
|
|||||||
onBack,
|
onBack,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
|
error,
|
||||||
}: DirectMessageFlowProps) => {
|
}: DirectMessageFlowProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-3xl mx-auto px-4 py-12">
|
<div className="w-full max-w-3xl mx-auto px-4 py-12">
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
|
{error && (
|
||||||
|
<Reveal width="100%" delay={0.1}>
|
||||||
|
<div className="bg-red-50 border border-red-100 rounded-2xl p-6 flex items-start gap-4 mb-8">
|
||||||
|
<div className="p-2 bg-red-100 rounded-lg text-red-600">
|
||||||
|
<Mail size={20} className="animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h3 className="text-red-900 font-bold text-sm uppercase tracking-wider">
|
||||||
|
Übertragungsfehler
|
||||||
|
</h3>
|
||||||
|
<p className="text-red-700/80 font-medium text-base">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 text-[10px] font-mono text-red-400 uppercase tracking-widest">
|
||||||
|
Status: FEHLER_BEI_SEQUENZ_INIT
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
)}
|
||||||
|
|
||||||
<Reveal width="100%" delay={0.2}>
|
<Reveal width="100%" delay={0.2}>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<span className="text-[10px] font-mono text-green-600 uppercase tracking-[0.3em] font-bold">
|
<span className="text-[10px] font-mono text-green-600 uppercase tracking-[0.3em] font-bold">
|
||||||
|
|||||||
@@ -45,11 +45,23 @@ export async function sendEmail({
|
|||||||
subject,
|
subject,
|
||||||
html,
|
html,
|
||||||
}: SendEmailOptions) {
|
}: SendEmailOptions) {
|
||||||
const recipients = to || env.MAIL_RECIPIENTS;
|
let recipients = to || env.MAIL_RECIPIENTS;
|
||||||
|
let from = env.MAIL_FROM;
|
||||||
|
|
||||||
|
if (!from) {
|
||||||
|
from = "info@mintel.me";
|
||||||
|
console.warn("MAIL_FROM is empty. Using fallback: info@mintel.me");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!recipients) {
|
||||||
|
recipients = "marc@mintel.me";
|
||||||
|
console.warn("MAIL_RECIPIENTS is empty. Using fallback: marc@mintel.me");
|
||||||
|
}
|
||||||
|
|
||||||
const transporter = getTransporter();
|
const transporter = getTransporter();
|
||||||
|
|
||||||
const mailOptions = {
|
const mailOptions = {
|
||||||
from: env.MAIL_FROM,
|
from,
|
||||||
to: recipients,
|
to: recipients,
|
||||||
replyTo,
|
replyTo,
|
||||||
subject,
|
subject,
|
||||||
|
|||||||
Reference in New Issue
Block a user