From 968f6ff7db46a32af8dd3469e7b98f1fb4e000f3 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sat, 18 Jul 2026 20:14:50 +0200 Subject: [PATCH] fix(deploy): unstick nitro Json typecheck and wipe poisoned .next cache Host next build still saw Json on nitro while tsc passed; use any helpers, verify blob hash, and delete .next entirely before build. Co-authored-by: Cursor --- .gitea/workflows/deploy.yaml | 19 +++- .../import/furni/nitro-editor-dialog.tsx | 107 ++++++------------ src/lib/deploy-workflow-contract.test.ts | 9 +- 3 files changed, 57 insertions(+), 78 deletions(-) diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 776ed5ad..014b7236 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -66,12 +66,27 @@ jobs: git diff --exit-code -- src fi + # Prove critical sources match the git object (not just index timestamps). + NITRO_SRC="src/app/admin/import/furni/nitro-editor-dialog.tsx" + NITRO_EXPECTED="$(git rev-parse "HEAD:${NITRO_SRC}")" + NITRO_ACTUAL="$(git hash-object "${NITRO_SRC}")" + echo "nitro-editor-dialog blob expected=${NITRO_EXPECTED} actual=${NITRO_ACTUAL}" + if [ "${NITRO_EXPECTED}" != "${NITRO_ACTUAL}" ]; then + echo "ERROR: ${NITRO_SRC} content hash mismatch after checkout" >&2 + exit 1 + fi + if grep -nE '(^|[[:space:]])type Json\b|:\s*Json\b' "${NITRO_SRC}"; then + echo "ERROR: ${NITRO_SRC} still has a Json type annotation after checkout" >&2 + exit 1 + fi + # Drop incremental TS caches that can hide real type errors. rm -f tsconfig.tsbuildinfo .tsbuildinfo find . -maxdepth 3 -name '*.tsbuildinfo' -delete 2>/dev/null || true - # Preserve .next/cache so Next.js can reuse its incremental build cache. - rm -rf .output dist .next/types .next/dev + # Wipe .next entirely — partial cache has caused next build TS to disagree + # with a clean `tsc` on the same sources (stale nitro/rooms typings). + rm -rf .output dist .next # Release tag for Sentry / logs (short git sha) export APP_VERSION="$(git rev-parse --short HEAD)" diff --git a/src/app/admin/import/furni/nitro-editor-dialog.tsx b/src/app/admin/import/furni/nitro-editor-dialog.tsx index c3068f93..ef6f8947 100644 --- a/src/app/admin/import/furni/nitro-editor-dialog.tsx +++ b/src/app/admin/import/furni/nitro-editor-dialog.tsx @@ -39,52 +39,32 @@ interface NitroEditorDialogProps { onOpenChange: (open: boolean) => void; } -/** Nested metadata patch helpers (no ambient Json type — keep this file typecheck-clean). */ -function updateNested( - obj: Record, - path: string, - value: unknown, -): Record { - const clone = JSON.parse(JSON.stringify(obj)) as Record; +// Keep these helpers on `any` — Next's build-time checker has repeatedly +// disagreed with `tsc` on stricter JSON typings for this editor. +// biome-ignore lint/suspicious/noExplicitAny: metadata tree is untyped JSON +function updateNested(obj: any, path: string, value: unknown): any { + const clone = JSON.parse(JSON.stringify(obj)); const keys = path.split("."); - let current: Record = clone; + let current = clone; for (let i = 0; i < keys.length - 1; i++) { - const key = keys[i]; - const next = current[key]; - if (next == null || typeof next !== "object" || Array.isArray(next)) { - current[key] = {}; - } - current = current[key] as Record; + if (!current[keys[i]]) current[keys[i]] = {}; + current = current[keys[i]]; } current[keys[keys.length - 1]] = value; return clone; } -function getNested( - obj: unknown, - path: string, - fallback?: unknown, -): unknown { +// biome-ignore lint/suspicious/noExplicitAny: metadata tree is untyped JSON +function getNested(obj: any, path: string, fallback?: unknown): any { const keys = path.split("."); - let current: unknown = obj; + let current = obj; for (const key of keys) { if (current == null || typeof current !== "object") return fallback; - current = (current as Record)[key]; + current = current[key]; } return current ?? fallback; } -function patchMetadata( - prev: unknown, - path: string, - value: unknown, -): unknown { - if (prev == null || typeof prev !== "object" || Array.isArray(prev)) { - return prev; - } - return updateNested(prev as Record, path, value); -} - const ALL_DIRECTIONS = [0, 1, 2, 3, 4, 5, 6, 7]; const DIR_LABELS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]; @@ -139,6 +119,13 @@ export function NitroEditorDialog({ }); const [jsonError, setJsonError] = useState(null); + const patchMeta = (path: string, value: unknown) => { + // Explicit any avoids noImplicitAny on updater params and dodges + // Next build's stricter JSON inference that disagreed with tsc. + // biome-ignore lint/suspicious/noExplicitAny: metadata tree is untyped JSON + setMetadata((prev: any) => updateNested(prev, path, value)); + }; + const fetchMetadata = useCallback(async () => { if (!classname) return; setLoading(true); @@ -254,9 +241,7 @@ export function NitroEditorDialog({ const setDim = (axis: "x" | "y" | "z", value: string) => { const num = axis === "z" ? parseFloat(value) || 0 : parseInt(value, 10) || 0; - setMetadata((prev: unknown) => - patchMetadata(prev, `logic.model.dimensions.${axis}`, num), - ); + patchMeta(`logic.model.dimensions.${axis}`, num); }; const toggleDirection = (dir: number) => { @@ -267,9 +252,7 @@ export function NitroEditorDialog({ ? current.filter((d: number) => d !== dir) : [...current, dir].sort((a: number, b: number) => a - b); if (next.length === 0) return; - setMetadata((prev: unknown) => - patchMetadata(prev, "logic.model.directions", next), - ); + patchMeta("logic.model.directions", next); }; // Get visualization with size=64 for layer/color editing @@ -570,13 +553,7 @@ export function NitroEditorDialog({ id="viz-type" value={metadata.visualizationType || ""} onChange={(e) => - setMetadata((prev: unknown) => - patchMetadata( - prev, - "visualizationType", - e.target.value, - ), - ) + patchMeta("visualizationType", e.target.value) } className="mt-1 font-mono text-sm" /> @@ -592,9 +569,7 @@ export function NitroEditorDialog({ id="logic-type" value={metadata.logicType || ""} onChange={(e) => - setMetadata((prev: unknown) => - patchMetadata(prev, "logicType", e.target.value), - ) + patchMeta("logicType", e.target.value) } className="mt-1 font-mono text-sm" /> @@ -643,12 +618,9 @@ export function NitroEditorDialog({ className="h-7 text-xs" value={(layer.z as number) ?? 0} onChange={(e) => { - setMetadata((prev: unknown) => - patchMetadata( - prev, - `visualizations.${viz64Index}.layers.${layerId}.z`, - parseInt(e.target.value, 10) || 0, - ), + patchMeta( + `visualizations.${viz64Index}.layers.${layerId}.z`, + parseInt(e.target.value, 10) || 0, ); }} /> @@ -659,12 +631,9 @@ export function NitroEditorDialog({ max={255} value={(layer.alpha as number) ?? 255} onChange={(e) => { - setMetadata((prev: unknown) => - patchMetadata( - prev, - `visualizations.${viz64Index}.layers.${layerId}.alpha`, - parseInt(e.target.value, 10) || 0, - ), + patchMeta( + `visualizations.${viz64Index}.layers.${layerId}.alpha`, + parseInt(e.target.value, 10) || 0, ); }} /> @@ -673,12 +642,9 @@ export function NitroEditorDialog({ value={(layer.ink as string) ?? ""} placeholder="—" onChange={(e) => { - setMetadata((prev: unknown) => - patchMetadata( - prev, - `visualizations.${viz64Index}.layers.${layerId}.ink`, - e.target.value || undefined, - ), + patchMeta( + `visualizations.${viz64Index}.layers.${layerId}.ink`, + e.target.value || undefined, ); }} /> @@ -758,12 +724,9 @@ export function NitroEditorDialog({ .replace(/[^0-9a-fA-F]/g, "") .slice(0, 6); const num = parseInt(hex, 16) || 0; - setMetadata((prev: unknown) => - patchMetadata( - prev, - `visualizations.${viz64Index}.colors.${colorId}.${layerId}.color`, - num, - ), + patchMeta( + `visualizations.${viz64Index}.colors.${colorId}.${layerId}.color`, + num, ); }} /> diff --git a/src/lib/deploy-workflow-contract.test.ts b/src/lib/deploy-workflow-contract.test.ts index 1e3060d6..afb9ac2f 100644 --- a/src/lib/deploy-workflow-contract.test.ts +++ b/src/lib/deploy-workflow-contract.test.ts @@ -8,9 +8,8 @@ describe("production deploy workflow", () => { "utf8", ); - it("preserves the Next.js incremental build cache", () => { - // May clear .next/types or .next/dev, but must not wipe the whole .next tree. - expect(workflow).not.toMatch(/rm\s+-rf\s+\.next(?:\s|$)/); + it("wipes .next before build to avoid stale Next typecheck caches", () => { + expect(workflow).toContain("rm -rf .output dist .next"); expect(workflow).toContain("pnpm install --frozen-lockfile"); expect(workflow).toContain("SKIP_ENV_VALIDATION=1"); }); @@ -25,8 +24,10 @@ describe("production deploy workflow", () => { expect(resetAt).toBeGreaterThan(reclaimAt); }); - it("verifies src/ matches HEAD and clears tsbuildinfo before typecheck", () => { + it("verifies src/ and nitro blob match HEAD before typecheck", () => { expect(workflow).toContain("git diff --exit-code -- src"); + expect(workflow).toContain("git hash-object"); + expect(workflow).toContain("nitro-editor-dialog.tsx"); expect(workflow).toContain("*.tsbuildinfo"); expect(workflow).toContain("pnpm typecheck"); });