feat : upa
CI / Verify (push) Successful in 1m39s
CI / Build immutable images and deploy (push) Successful in 2m46s

This commit is contained in:
2026-10-07 19:24:45 +07:00 Unverified
parent 6d0feb11ca
commit 90ba91b784
4 changed files with 20 additions and 45 deletions
-22
View File
@@ -188,25 +188,3 @@ jobs:
kubectl --kubeconfig "$KUBECONFIG" --namespace "$DEPLOY_NAMESPACE" rollout status \ kubectl --kubeconfig "$KUBECONFIG" --namespace "$DEPLOY_NAMESPACE" rollout status \
deployment/buzz-sheet-discord-worker \ deployment/buzz-sheet-discord-worker \
--timeout=10m --timeout=10m
- name: Publish deployment result
if: always()
continue-on-error: true
env:
DEPLOYMENT_WEBHOOK_SECRET: ${{ secrets.DEPLOYMENT_WEBHOOK_SECRET }}
DEPLOYMENT_JOB_STATUS: ${{ job.status }}
REVISION: ${{ gitea.sha }}
run: |
if [ -z "$DEPLOYMENT_WEBHOOK_SECRET" ]; then
echo "DEPLOYMENT_WEBHOOK_SECRET is not configured; skipping notification"
exit 0
fi
status=failed
if [ "$DEPLOYMENT_JOB_STATUS" = "success" ]; then
status=ready
fi
curl --fail-with-body --silent --show-error --retry 3 \
--header "Authorization: Bearer $DEPLOYMENT_WEBHOOK_SECRET" \
--header "Content-Type: application/json" \
--data "{\"deploymentId\":\"$REVISION\",\"status\":\"$status\"}" \
"$BASE_URL/api/deployments/status"
+18 -18
View File
@@ -1,7 +1,6 @@
import Image from "@/components/ui/resilient-image"; import Image from "@/components/ui/resilient-image";
import Link from "next/link"; import Link from "next/link";
import { TermDetail } from "@/components/public/term-detail";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { buttonVariants } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button";
@@ -50,21 +49,16 @@ function RecommendationIcon({ item, catalog, teamLayout = false }: { item: Item;
if (item.kind === "character") { if (item.kind === "character") {
const hasEquipment = Boolean(value.weapon || value.artifact); const hasEquipment = Boolean(value.weapon || value.artifact);
return ( return (
<div className={cn("flex min-w-0 flex-col gap-1 rounded-lg border bg-card p-1", teamLayout && "w-full sm:p-2 xl:p-1", teamLayout && hasEquipment && "sm:w-[164px] xl:w-full", teamLayout && !hasEquipment && "sm:w-[116px] xl:w-full", teamLayout && !hasEquipment && item.constellationLevel === null && "aspect-square justify-center")}> <div className={cn("flex min-w-0 flex-col gap-1 rounded-lg border bg-card p-1", teamLayout && "w-full gap-2 sm:p-2")}>
<div className={teamLayout && hasEquipment ? "flex flex-col gap-1 sm:grid sm:grid-cols-[100px_40px] sm:gap-2 xl:grid-cols-1 xl:gap-1" : "flex flex-col gap-1"}> <div className="flex flex-col gap-1">
<div className={cn("relative aspect-square overflow-hidden rounded-lg", teamLayout && "sm:size-[100px] xl:size-auto", teamLayout && hasEquipment && "sm:row-span-2 xl:row-auto", characterRarityGradientClass(item.catalogKey, value.rarity))}> <div className={cn("relative aspect-square overflow-hidden rounded-lg", characterRarityGradientClass(item.catalogKey, value.rarity))}>
{value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes={teamLayout ? "(min-width: 640px) 128px, 25vw" : "128px"} className="object-contain" /> : null} {value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes={teamLayout ? "(min-width: 1280px) 128px, (min-width: 640px) 25vw, 50vw" : "128px"} className="object-contain" /> : null}
</div> </div>
{hasEquipment ? <div className={teamLayout ? "grid grid-cols-2 gap-1 sm:row-span-2 sm:grid-cols-1 sm:gap-2 sm:self-center xl:row-auto xl:grid-cols-2 xl:gap-1 xl:self-auto" : "grid grid-cols-2 gap-1"}> {hasEquipment ? <div className="grid grid-cols-2 gap-1">
{value.weapon ? <div className={cn("relative aspect-square overflow-hidden rounded-md border", rarityGradientClass(value.weapon.rarity))}><Image src={value.weapon.imageUrl} alt={value.weapon.name} fill sizes="64px" className="object-contain p-1" /></div> : null} {value.weapon ? <a href={`https://lunaris.moe/weapon/${encodeURIComponent(value.weapon.key)}`} target="_blank" rel="noreferrer" aria-label={`View ${value.weapon.name} on Lunaris`} title={value.weapon.name} className={cn("relative aspect-square overflow-hidden rounded-md border transition-opacity hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", rarityGradientClass(value.weapon.rarity))}><Image src={value.weapon.imageUrl} alt={value.weapon.name} fill sizes={teamLayout ? "(min-width: 1280px) 64px, (min-width: 640px) 12vw, 25vw" : "64px"} className="object-contain p-1" /></a> : null}
{value.artifact ? <div className={cn("relative aspect-square overflow-hidden rounded-md border", rarityGradientClass(value.artifact.rarity))}><Image src={value.artifact.imageUrl} alt={value.artifact.name} fill sizes="64px" className="object-contain p-1" /></div> : null} {value.artifact ? <a href={`https://lunaris.moe/artifact/${encodeURIComponent(value.artifact.key)}`} target="_blank" rel="noreferrer" aria-label={`View ${value.artifact.name} on Lunaris`} title={value.artifact.name} className={cn("relative aspect-square overflow-hidden rounded-md border transition-opacity hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", rarityGradientClass(value.artifact.rarity))}><Image src={value.artifact.imageUrl} alt={value.artifact.name} fill sizes={teamLayout ? "(min-width: 1280px) 64px, (min-width: 640px) 12vw, 25vw" : "64px"} className="object-contain p-1" /></a> : null}
</div> : null} </div> : null}
</div> </div>
{teamLayout && <p className="text-center text-xs font-medium wrap-anywhere">{value.name}</p>}
{teamLayout && hasEquipment && <TermDetail interactive label="อุปกรณ์" title={`อุปกรณ์ของ ${value.name}`} detail={[
value.weapon ? `อาวุธ: ${value.weapon.name}${value.refinement ? ` R${value.refinement}` : ""}` : null,
value.artifact ? `อาร์ติแฟกต์: ${value.artifact.name}` : null,
].filter(Boolean).join("\n")} />}
{teamLayout ? item.constellationLevel !== null || value.weapon && value.refinement ? <div className="flex justify-center gap-1"> {teamLayout ? item.constellationLevel !== null || value.weapon && value.refinement ? <div className="flex justify-center gap-1">
{item.constellationLevel !== null ? <Badge variant="secondary">C{item.constellationLevel}</Badge> : null} {item.constellationLevel !== null ? <Badge variant="secondary">C{item.constellationLevel}</Badge> : null}
{value.weapon && value.refinement ? <Badge variant="secondary">R{value.refinement}</Badge> : null} {value.weapon && value.refinement ? <Badge variant="secondary">R{value.refinement}</Badge> : null}
@@ -74,9 +68,15 @@ function RecommendationIcon({ item, catalog, teamLayout = false }: { item: Item;
} }
return ( return (
<div className="flex min-w-0 flex-col items-center gap-1 text-center"> <div className="flex min-w-0 flex-col items-center gap-1 text-center">
<div className={cn("relative aspect-square w-full overflow-hidden rounded-lg border", item.kind !== "element" && rarityGradientClass(value.rarity))}> {item.kind === "weapon" || item.kind === "artifact" ? (
{value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes="80px" className="object-contain" style={item.kind === "element" && item.element ? { filter: elementIconFilter(item.element) } : undefined} /> : null} <a href={`https://lunaris.moe/${item.kind}/${encodeURIComponent(item.catalogKey ?? "")}`} target="_blank" rel="noreferrer" aria-label={`View ${value.name} on Lunaris`} title={value.name} className={cn("relative aspect-square w-full overflow-hidden rounded-lg border transition-opacity hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", rarityGradientClass(value.rarity))}>
</div> {value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes="100px" className="object-contain" /> : null}
</a>
) : (
<div className="relative aspect-square w-full overflow-hidden rounded-lg border">
{value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes="100px" className="object-contain" style={item.element ? { filter: elementIconFilter(item.element) } : undefined} /> : null}
</div>
)}
<span className="max-w-[100px] truncate text-xs font-medium" title={value.name}>{value.name}</span> <span className="max-w-[100px] truncate text-xs font-medium" title={value.name}>{value.name}</span>
</div> </div>
); );
@@ -104,9 +104,9 @@ function BossCard({ schedule, level, position, catalog }: { schedule: Schedule;
<h3 className="font-heading text-lg font-semibold">ทีมแนะนำ</h3> <h3 className="font-heading text-lg font-semibold">ทีมแนะนำ</h3>
{teams.length ? <div className="grid grid-cols-1 items-start gap-3 xl:grid-cols-2"> {teams.length ? <div className="grid grid-cols-1 items-start gap-3 xl:grid-cols-2">
{teams.map((team) => ( {teams.map((team) => (
<div key={team.id} className="grid w-full min-w-0 grid-cols-4 items-start gap-1 rounded-lg border bg-muted/20 p-2 sm:flex sm:w-fit sm:flex-wrap sm:gap-3 xl:grid xl:grid-cols-4 xl:gap-1"> <div key={team.id} className="grid w-full min-w-0 grid-cols-2 items-start gap-2 rounded-lg border bg-muted/20 p-2 sm:grid-cols-4 sm:gap-3 sm:p-3">
{team.items.slice(0, 4).map((item) => <RecommendationIcon key={`${item.groupId}-${item.position}`} item={item} catalog={catalog} teamLayout />)} {team.items.slice(0, 4).map((item) => <RecommendationIcon key={`${item.groupId}-${item.position}`} item={item} catalog={catalog} teamLayout />)}
{Array.from({ length: Math.max(0, 4 - team.items.length) }, (_, index) => <div key={`empty-${index}`} className="aspect-square w-full rounded-lg border border-dashed bg-muted/30 sm:w-[116px] xl:w-full" aria-hidden="true" />)} {Array.from({ length: Math.max(0, 4 - team.items.length) }, (_, index) => <div key={`empty-${index}`} className="aspect-square w-full rounded-lg border border-dashed bg-muted/30" aria-hidden="true" />)}
</div> </div>
))} ))}
</div> : <p className="text-sm text-muted-foreground">ยังไม่มีทีมแนะนำสำหรับบอสนี้ในระดับที่เลือก</p>} </div> : <p className="text-sm text-muted-foreground">ยังไม่มีทีมแนะนำสำหรับบอสนี้ในระดับที่เลือก</p>}
+1 -1
View File
@@ -137,7 +137,7 @@ export function StygianBossCard({
{children} {children}
<Accordion id={mechanicsId} defaultValue={mechanicsId ? ["overview"] : undefined} className="scroll-mt-6"> <Accordion id={mechanicsId} defaultValue={mechanicsId ? ["overview"] : undefined} className="scroll-mt-6">
<AccordionItem value="overview"> <AccordionItem value="overview">
<AccordionTrigger>ข้อมูลและกลไกของบอส</AccordionTrigger> <AccordionTrigger>Boss Information & Mechanics</AccordionTrigger>
<AccordionContent className="whitespace-pre-line text-muted-foreground"> <AccordionContent className="whitespace-pre-line text-muted-foreground">
<LunarisText <LunarisText
nodes={parseLunarisText(boss.description)} nodes={parseLunarisText(boss.description)}
+1 -4
View File
@@ -162,18 +162,15 @@ describe("production deployment contract", () => {
expect(route).toContain('"deployment-status"'); expect(route).toContain('"deployment-status"');
}); });
it("announces deployment lifecycle results without blocking releases", async () => { it("announces incoming deployments without blocking releases", async () => {
const [workflow, deployment] = await Promise.all([ const [workflow, deployment] = await Promise.all([
repositoryFile(".gitea/workflows/ci.yml"), repositoryFile(".gitea/workflows/ci.yml"),
repositoryFile("k8s/base/deployment.yaml"), repositoryFile("k8s/base/deployment.yaml"),
]); ]);
expect(workflow).toContain("Announce incoming deployment"); expect(workflow).toContain("Announce incoming deployment");
expect(workflow).toContain("Publish deployment result");
expect(workflow).toContain("if: always()");
expect(workflow).toContain("continue-on-error: true"); expect(workflow).toContain("continue-on-error: true");
expect(workflow).toContain('status\\":\\"deploying'); expect(workflow).toContain('status\\":\\"deploying');
expect(workflow).toContain("DEPLOYMENT_JOB_STATUS");
expect(workflow).toContain("/api/deployments/status"); expect(workflow).toContain("/api/deployments/status");
expect(deployment).toContain("DEPLOYMENT_WEBHOOK_SECRET"); expect(deployment).toContain("DEPLOYMENT_WEBHOOK_SECRET");
expect(deployment).toContain("optional: true"); expect(deployment).toContain("optional: true");