fix(deploy): unstick nitro Json typecheck and wipe poisoned .next cache
Local Build and Deploy / deploy (push) Failing after 1m21s

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 <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-18 20:14:50 +02:00
1 parent 1abbf3fde7
commit 968f6ff7db
3 files changed
+57 -78

No files matched your search

+17 -2
View File
@@ -66,12 +66,27 @@ jobs:
git diff --exit-code -- src git diff --exit-code -- src
fi 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. # Drop incremental TS caches that can hide real type errors.
rm -f tsconfig.tsbuildinfo .tsbuildinfo rm -f tsconfig.tsbuildinfo .tsbuildinfo
find . -maxdepth 3 -name '*.tsbuildinfo' -delete 2>/dev/null || true find . -maxdepth 3 -name '*.tsbuildinfo' -delete 2>/dev/null || true
# Preserve .next/cache so Next.js can reuse its incremental build cache. # Wipe .next entirely — partial cache has caused next build TS to disagree
rm -rf .output dist .next/types .next/dev # 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) # Release tag for Sentry / logs (short git sha)
export APP_VERSION="$(git rev-parse --short HEAD)" export APP_VERSION="$(git rev-parse --short HEAD)"
@@ -39,52 +39,32 @@ interface NitroEditorDialogProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
/** Nested metadata patch helpers (no ambient Json type — keep this file typecheck-clean). */ // Keep these helpers on `any` — Next's build-time checker has repeatedly
function updateNested( // disagreed with `tsc` on stricter JSON typings for this editor.
obj: Record<string, unknown>, // biome-ignore lint/suspicious/noExplicitAny: metadata tree is untyped JSON
path: string, function updateNested(obj: any, path: string, value: unknown): any {
value: unknown, const clone = JSON.parse(JSON.stringify(obj));
): Record<string, unknown> {
const clone = JSON.parse(JSON.stringify(obj)) as Record<string, unknown>;
const keys = path.split("."); const keys = path.split(".");
let current: Record<string, unknown> = clone; let current = clone;
for (let i = 0; i < keys.length - 1; i++) { for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i]; if (!current[keys[i]]) current[keys[i]] = {};
const next = current[key]; current = current[keys[i]];
if (next == null || typeof next !== "object" || Array.isArray(next)) {
current[key] = {};
}
current = current[key] as Record<string, unknown>;
} }
current[keys[keys.length - 1]] = value; current[keys[keys.length - 1]] = value;
return clone; return clone;
} }
function getNested( // biome-ignore lint/suspicious/noExplicitAny: metadata tree is untyped JSON
obj: unknown, function getNested(obj: any, path: string, fallback?: unknown): any {
path: string,
fallback?: unknown,
): unknown {
const keys = path.split("."); const keys = path.split(".");
let current: unknown = obj; let current = obj;
for (const key of keys) { for (const key of keys) {
if (current == null || typeof current !== "object") return fallback; if (current == null || typeof current !== "object") return fallback;
current = (current as Record<string, unknown>)[key]; current = current[key];
} }
return current ?? fallback; 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<string, unknown>, path, value);
}
const ALL_DIRECTIONS = [0, 1, 2, 3, 4, 5, 6, 7]; const ALL_DIRECTIONS = [0, 1, 2, 3, 4, 5, 6, 7];
const DIR_LABELS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]; const DIR_LABELS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
@@ -139,6 +119,13 @@ export function NitroEditorDialog({
}); });
const [jsonError, setJsonError] = useState<string | null>(null); const [jsonError, setJsonError] = useState<string | null>(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 () => { const fetchMetadata = useCallback(async () => {
if (!classname) return; if (!classname) return;
setLoading(true); setLoading(true);
@@ -254,9 +241,7 @@ export function NitroEditorDialog({
const setDim = (axis: "x" | "y" | "z", value: string) => { const setDim = (axis: "x" | "y" | "z", value: string) => {
const num = const num =
axis === "z" ? parseFloat(value) || 0 : parseInt(value, 10) || 0; axis === "z" ? parseFloat(value) || 0 : parseInt(value, 10) || 0;
setMetadata((prev: unknown) => patchMeta(`logic.model.dimensions.${axis}`, num);
patchMetadata(prev, `logic.model.dimensions.${axis}`, num),
);
}; };
const toggleDirection = (dir: number) => { const toggleDirection = (dir: number) => {
@@ -267,9 +252,7 @@ export function NitroEditorDialog({
? current.filter((d: number) => d !== dir) ? current.filter((d: number) => d !== dir)
: [...current, dir].sort((a: number, b: number) => a - b); : [...current, dir].sort((a: number, b: number) => a - b);
if (next.length === 0) return; if (next.length === 0) return;
setMetadata((prev: unknown) => patchMeta("logic.model.directions", next);
patchMetadata(prev, "logic.model.directions", next),
);
}; };
// Get visualization with size=64 for layer/color editing // Get visualization with size=64 for layer/color editing
@@ -570,13 +553,7 @@ export function NitroEditorDialog({
id="viz-type" id="viz-type"
value={metadata.visualizationType || ""} value={metadata.visualizationType || ""}
onChange={(e) => onChange={(e) =>
setMetadata((prev: unknown) => patchMeta("visualizationType", e.target.value)
patchMetadata(
prev,
"visualizationType",
e.target.value,
),
)
} }
className="mt-1 font-mono text-sm" className="mt-1 font-mono text-sm"
/> />
@@ -592,9 +569,7 @@ export function NitroEditorDialog({
id="logic-type" id="logic-type"
value={metadata.logicType || ""} value={metadata.logicType || ""}
onChange={(e) => onChange={(e) =>
setMetadata((prev: unknown) => patchMeta("logicType", e.target.value)
patchMetadata(prev, "logicType", e.target.value),
)
} }
className="mt-1 font-mono text-sm" className="mt-1 font-mono text-sm"
/> />
@@ -643,12 +618,9 @@ export function NitroEditorDialog({
className="h-7 text-xs" className="h-7 text-xs"
value={(layer.z as number) ?? 0} value={(layer.z as number) ?? 0}
onChange={(e) => { onChange={(e) => {
setMetadata((prev: unknown) => patchMeta(
patchMetadata( `visualizations.${viz64Index}.layers.${layerId}.z`,
prev, parseInt(e.target.value, 10) || 0,
`visualizations.${viz64Index}.layers.${layerId}.z`,
parseInt(e.target.value, 10) || 0,
),
); );
}} }}
/> />
@@ -659,12 +631,9 @@ export function NitroEditorDialog({
max={255} max={255}
value={(layer.alpha as number) ?? 255} value={(layer.alpha as number) ?? 255}
onChange={(e) => { onChange={(e) => {
setMetadata((prev: unknown) => patchMeta(
patchMetadata( `visualizations.${viz64Index}.layers.${layerId}.alpha`,
prev, parseInt(e.target.value, 10) || 0,
`visualizations.${viz64Index}.layers.${layerId}.alpha`,
parseInt(e.target.value, 10) || 0,
),
); );
}} }}
/> />
@@ -673,12 +642,9 @@ export function NitroEditorDialog({
value={(layer.ink as string) ?? ""} value={(layer.ink as string) ?? ""}
placeholder="—" placeholder="—"
onChange={(e) => { onChange={(e) => {
setMetadata((prev: unknown) => patchMeta(
patchMetadata( `visualizations.${viz64Index}.layers.${layerId}.ink`,
prev, e.target.value || undefined,
`visualizations.${viz64Index}.layers.${layerId}.ink`,
e.target.value || undefined,
),
); );
}} }}
/> />
@@ -758,12 +724,9 @@ export function NitroEditorDialog({
.replace(/[^0-9a-fA-F]/g, "") .replace(/[^0-9a-fA-F]/g, "")
.slice(0, 6); .slice(0, 6);
const num = parseInt(hex, 16) || 0; const num = parseInt(hex, 16) || 0;
setMetadata((prev: unknown) => patchMeta(
patchMetadata( `visualizations.${viz64Index}.colors.${colorId}.${layerId}.color`,
prev, num,
`visualizations.${viz64Index}.colors.${colorId}.${layerId}.color`,
num,
),
); );
}} }}
/> />
+5 -4
View File
@@ -8,9 +8,8 @@ describe("production deploy workflow", () => {
"utf8", "utf8",
); );
it("preserves the Next.js incremental build cache", () => { it("wipes .next before build to avoid stale Next typecheck caches", () => {
// May clear .next/types or .next/dev, but must not wipe the whole .next tree. expect(workflow).toContain("rm -rf .output dist .next");
expect(workflow).not.toMatch(/rm\s+-rf\s+\.next(?:\s|$)/);
expect(workflow).toContain("pnpm install --frozen-lockfile"); expect(workflow).toContain("pnpm install --frozen-lockfile");
expect(workflow).toContain("SKIP_ENV_VALIDATION=1"); expect(workflow).toContain("SKIP_ENV_VALIDATION=1");
}); });
@@ -25,8 +24,10 @@ describe("production deploy workflow", () => {
expect(resetAt).toBeGreaterThan(reclaimAt); 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 diff --exit-code -- src");
expect(workflow).toContain("git hash-object");
expect(workflow).toContain("nitro-editor-dialog.tsx");
expect(workflow).toContain("*.tsbuildinfo"); expect(workflow).toContain("*.tsbuildinfo");
expect(workflow).toContain("pnpm typecheck"); expect(workflow).toContain("pnpm typecheck");
}); });