feat : dev on prod
CI / Build immutable images and deploy (push) Successful in 1m52s

This commit is contained in:
2026-09-16 20:19:27 +07:00 Unverified
parent 2799103dd0
commit 910afdeb71
11 changed files with 87 additions and 4 deletions
+3
View File
@@ -1,6 +1,9 @@
# Public site origin used for SEO metadata, sitemap, and robots.txt.
BASE_URL=https://guide.sudloh.com
# Public AdSense publisher client ID. Leave unset to disable ads locally.
NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID=ca-pub-9687404323559597
# Local-only demo mode serves sanitized fixtures and bypasses external services.
# Keep this false in every deployed environment.
BUZZ_DEMO_MODE=false
+2
View File
@@ -10,6 +10,7 @@ on:
env:
BASE_URL: https://guide.sudloh.com
NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID: ca-pub-9687404323559597
REGISTRY_IMAGE: registry.neko-piranha.ts.net/astral/buzz-sheet
DEPLOY_NAMESPACE: buzz-sheet
KUBE_API_SERVER: https://100.112.189.33:6443/
@@ -32,6 +33,7 @@ jobs:
docker build \
--target app \
--build-arg BASE_URL="$BASE_URL" \
--build-arg NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID="$NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID" \
--build-arg NEXT_DEPLOYMENT_ID=${{ gitea.sha }} \
--build-arg VCS_REF=${{ gitea.sha }} \
--tag ${{ env.REGISTRY_IMAGE }}:${{ gitea.sha }} \
+2
View File
@@ -23,8 +23,10 @@ FROM node:22-bookworm-slim AS next-builder
WORKDIR /app
ARG NEXT_DEPLOYMENT_ID=container-build
ARG BASE_URL
ARG NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID
ENV BASE_URL=${BASE_URL}
ENV NEXT_DEPLOYMENT_ID=${NEXT_DEPLOYMENT_ID}
ENV NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID=${NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID}
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
-1
View File
@@ -1 +0,0 @@
google.com, pub-9687404323559597, DIRECT, f08c47fec0942fa0
+7 -1
View File
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { GoogleAdsense } from "@/components/public/google-adsense";
import { publicSections, StructuredGuideShell } from "@/components/public/structured-guide";
import { getCachedPublicGuide } from "@/lib/cache/public";
@@ -25,5 +26,10 @@ export default async function GuideSectionPage({ params }: { params: Params }) {
const { character, page } = await params;
const guide = await getCachedPublicGuide(character);
if (!guide || !publicSections(guide).some((section) => section.slug === page)) notFound();
return <StructuredGuideShell guide={guide} section={page} />;
return (
<>
<GoogleAdsense />
<StructuredGuideShell guide={guide} section={page} />
</>
);
}
+7 -1
View File
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { GoogleAdsense } from "@/components/public/google-adsense";
import { StructuredGuideShell } from "@/components/public/structured-guide";
import { getCachedPublicGuide } from "@/lib/cache/public";
@@ -31,5 +32,10 @@ export default async function GuideOverviewPage({
}) {
const guide = await getCachedPublicGuide((await params).character);
if (!guide) notFound();
return <StructuredGuideShell guide={guide} />;
return (
<>
<GoogleAdsense />
<StructuredGuideShell guide={guide} />
</>
);
}
+2
View File
@@ -2,6 +2,7 @@ import { connection } from "next/server";
import { Suspense } from "react";
import { LiveRefresh } from "@/components/live-refresh";
import { GoogleAdsense } from "@/components/public/google-adsense";
import { SiteFooter } from "@/components/public/site-footer";
import { StructuredDirectory } from "@/components/public/structured-directory";
import { SiteHeader } from "@/components/public/site-header";
@@ -16,6 +17,7 @@ async function DirectoryContent() {
export default function HomePage() {
return (
<div className="flex min-h-svh flex-col bg-background">
<GoogleAdsense />
<LiveRefresh endpoint="/api/events/directory" />
<SiteHeader />
<main className="mx-auto flex w-full max-w-7xl flex-col gap-8 px-4 py-10 sm:px-6 sm:py-14">
+30
View File
@@ -0,0 +1,30 @@
"use client";
import Script from "next/script";
import { authClient } from "@/lib/auth/client";
const clientId = process.env.NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID?.trim();
export function GoogleAdsense() {
const { data: session, error, isPending } = authClient.useSession();
if (
process.env.NODE_ENV !== "production" ||
!clientId ||
isPending ||
error ||
session
) {
return null;
}
return (
<Script
id="google-adsense"
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${encodeURIComponent(clientId)}`}
strategy="afterInteractive"
crossOrigin="anonymous"
/>
);
}
+1
View File
@@ -0,0 +1 @@
google.com, pub-9687404323559597, DIRECT, f08c47fec0942fa0
+12 -1
View File
@@ -91,7 +91,12 @@ describe("production deployment contract", () => {
expect(dockerfile.match(/^USER 1000:1000$/gmu)).toHaveLength(2);
expect(dockerfile).toContain('ENTRYPOINT ["bun", "scripts/migrate.ts"]');
expect(dockerfile).toContain('CMD ["bun", "server.js"]');
expect(dockerfile).toContain("ARG BASE_URL\nENV BASE_URL=${BASE_URL}");
expect(dockerfile).toContain("ARG BASE_URL");
expect(dockerfile).toContain("ENV BASE_URL=${BASE_URL}");
expect(dockerfile).toContain("ARG NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID");
expect(dockerfile).toContain(
"ENV NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID=${NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID}",
);
expect(dockerfile).toContain("backend/discord.ts");
expect(dockerfile).toContain("./worker/discord.js");
});
@@ -106,6 +111,9 @@ describe("production deployment contract", () => {
expect(workflow).toContain("kubectl kustomize k8s/");
expect(workflow).toContain("--target app");
expect(workflow).toContain('--build-arg BASE_URL="$BASE_URL"');
expect(workflow).toContain(
'--build-arg NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID="$NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID"',
);
expect(workflow).toContain('\\"BASE_URL\\":\\"$BASE_URL\\"');
expect(workflow).toContain("--target migration");
expect(workflow).not.toContain("platforms: linux/arm64");
@@ -167,6 +175,9 @@ describe("environment template contract", () => {
expect(environmentExample).toMatch(new RegExp(`^${key}=`, "mu"));
}
expect(environmentExample).toContain("BUZZ_DEMO_MODE=false");
expect(environmentExample).toContain(
"NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID=ca-pub-9687404323559597",
);
});
it("runs Discord catalog sync as an isolated worker", async () => {
+21
View File
@@ -55,4 +55,25 @@ describe("public rendering performance", () => {
sitemap: "https://guide.example.test/sitemap.xml",
});
});
it("loads AdSense only after confirming the visitor is signed out", async () => {
const [adsense, home, guide, guideSection, adsTxt] = await Promise.all([
repositoryFile("components/public/google-adsense.tsx"),
repositoryFile("app/page.tsx"),
repositoryFile("app/[character]/page.tsx"),
repositoryFile("app/[character]/[page]/page.tsx"),
repositoryFile("public/ads.txt"),
]);
expect(adsense).toContain("authClient.useSession()");
expect(adsense).toContain('process.env.NODE_ENV !== "production"');
expect(adsense).toMatch(/isPending \|\|\s+error \|\|\s+session/u);
expect(adsense).toContain("pagead2.googlesyndication.com");
expect(home).toContain("<GoogleAdsense />");
expect(guide).toContain("<GoogleAdsense />");
expect(guideSection).toContain("<GoogleAdsense />");
expect(adsTxt.trim()).toBe(
"google.com, pub-9687404323559597, DIRECT, f08c47fec0942fa0",
);
});
});