Skip to content

Commit da202ff

Browse files
committed
fix(site,ci): fallback to production when dev dataset is missing and fix nightly dataset sync
1 parent c09f2c8 commit da202ff

7 files changed

Lines changed: 169 additions & 74 deletions

File tree

‎.github/workflows/nightly-dataset-sync.yml‎

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,37 @@ jobs:
1111
environment: production
1212

1313
steps:
14-
- name: Reset dev dataset from production
14+
- uses: actions/checkout@v4
15+
16+
- uses: pnpm/action-setup@v4
17+
18+
- uses: actions/setup-node@v4
19+
with:
20+
node-version-file: ".nvmrc"
21+
cache: "pnpm"
22+
23+
- name: Install dependencies
24+
run: pnpm install --frozen-lockfile
25+
26+
- name: Sync production dataset to dev
27+
env:
28+
SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID || 'hfh83o0w' }}
29+
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
1530
run: |
16-
PROJECT_ID="${{ vars.SANITY_STUDIO_PROJECT_ID }}"
17-
[ -z "$PROJECT_ID" ] && PROJECT_ID="hfh83o0w"
18-
TOKEN="${{ secrets.SANITY_AUTH_TOKEN }}"
31+
PROJECT_ID="${SANITY_STUDIO_PROJECT_ID:-hfh83o0w}"
1932
API_HOST="https://api.sanity.io"
20-
API_VERSION="v2025-09-30"
21-
22-
echo "Deleting existing 'dev' dataset..."
23-
curl -s -X DELETE "$API_HOST/$API_VERSION/projects/$PROJECT_ID/datasets/dev" \
24-
-H "Authorization: Bearer $TOKEN" || true
33+
API_VERSION="v2021-06-07"
2534
26-
echo "Cloning 'production' dataset to 'dev'..."
27-
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X PUT \
28-
"$API_HOST/$API_VERSION/projects/$PROJECT_ID/datasets/production/copy" \
29-
-H "Authorization: Bearer $TOKEN" \
35+
echo "Ensuring 'dev' dataset exists (public ACL)..."
36+
curl -s -X PUT "$API_HOST/$API_VERSION/projects/$PROJECT_ID/datasets/dev" \
37+
-H "Authorization: Bearer $SANITY_AUTH_TOKEN" \
3038
-H "Content-Type: application/json" \
31-
-d '{"targetDataset":"dev","skipHistory":true}')
39+
-d '{"aclMode":"public"}' || true
3240
33-
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS:" | cut -d: -f2)
34-
BODY=$(echo "$RESPONSE" | grep -v "HTTP_STATUS:")
41+
echo "Exporting 'production' dataset..."
42+
pnpm --filter @codingcatdev/sanity exec sanity dataset export production /tmp/production.tar.gz --overwrite
3543
36-
echo "Status: $HTTP_STATUS"
37-
echo "Response: $BODY"
44+
echo "Importing into 'dev' dataset (--replace)..."
45+
pnpm --filter @codingcatdev/sanity exec sanity dataset import /tmp/production.tar.gz dev --replace
3846
39-
if [ "$HTTP_STATUS" -ge 200 ] && [ "$HTTP_STATUS" -lt 300 ]; then
40-
echo "Dataset sync initiated successfully!"
41-
else
42-
echo "Dataset sync failed with status $HTTP_STATUS"
43-
exit 1
44-
fi
47+
echo "Dataset sync completed successfully!"

‎apps/sanity/functions/sync-nightly-dataset/index.ts‎

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -37,36 +37,11 @@ export const handler = scheduledEventHandler(async ({ context }) => {
3737
`[Sync Nightly Dataset] Resetting dataset "${targetDataset}" from source "${sourceDataset}" for project ${projectId}...`,
3838
);
3939

40-
// 1. Delete existing target dataset (dev) if it exists
41-
const deleteUrl = `${apiHost}/${apiVersion}/projects/${projectId}/datasets/${targetDataset}`;
42-
console.log(`[Sync Nightly Dataset] Deleting existing dataset "${targetDataset}": ${deleteUrl}`);
43-
44-
const deleteResp = await fetch(deleteUrl, {
45-
method: "DELETE",
46-
headers: {
47-
Authorization: `Bearer ${token}`,
48-
},
49-
});
50-
51-
if (deleteResp.ok) {
52-
console.log(
53-
`[Sync Nightly Dataset] Target dataset "${targetDataset}" deleted successfully (HTTP ${deleteResp.status}).`,
54-
);
55-
} else if (deleteResp.status === 404) {
56-
console.log(
57-
`[Sync Nightly Dataset] Target dataset "${targetDataset}" does not exist yet (HTTP 404). Proceeding to clone.`,
58-
);
59-
} else {
60-
const deleteError = await deleteResp.text();
61-
console.warn(
62-
`[Sync Nightly Dataset] Notice when deleting dataset "${targetDataset}": HTTP ${deleteResp.status} — ${deleteError}`,
63-
);
64-
}
65-
66-
// 2. Clone source dataset (production) to target (dev) via Sanity Enterprise Copy API
40+
// Ensure target dataset exists or try Enterprise Copy API without pre-deleting `dev`,
41+
// because non-Enterprise plans fail on `/copy` and pre-deleting `dev` leaves the dev site with no dataset.
6742
const copyUrl = `${apiHost}/${apiVersion}/projects/${projectId}/datasets/${sourceDataset}/copy`;
6843
console.log(
69-
`[Sync Nightly Dataset] Copying "${sourceDataset}" -> "${targetDataset}": ${copyUrl}`,
44+
`[Sync Nightly Dataset] Attempting dataset copy "${sourceDataset}" -> "${targetDataset}": ${copyUrl}`,
7045
);
7146

7247
const copyResp = await fetch(copyUrl, {
@@ -83,6 +58,15 @@ export const handler = scheduledEventHandler(async ({ context }) => {
8358

8459
if (!copyResp.ok) {
8560
const errorText = await copyResp.text();
61+
if (
62+
copyResp.status === 400 &&
63+
errorText.includes("advanced dataset management")
64+
) {
65+
console.warn(
66+
`[Sync Nightly Dataset] Plan does not include Enterprise dataset copy API. Skipping destructive delete; dataset sync is handled by GitHub Actions CLI export/import.`,
67+
);
68+
return;
69+
}
8670
throw new Error(
8771
`[Sync Nightly Dataset] Failed to initiate dataset copy from "${sourceDataset}" to "${targetDataset}": HTTP ${copyResp.status} — ${errorText}`,
8872
);

‎apps/site/src/lib/sanity/client.ts‎

Lines changed: 122 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
type ClientConfig,
23
type ClientPerspective,
34
createClient,
45
type SanityClient,
@@ -12,6 +13,68 @@ import type { SanityConfig } from "./config";
1213
*/
1314
const cache = new Map<string, SanityClient>();
1415

16+
/**
17+
* Tracks `<projectId>:<dataset>` pairs that returned HTTP 404 ("Dataset not found")
18+
* during the lifetime of the isolate so subsequent queries route directly to
19+
* `production` without an extra round-trip.
20+
*/
21+
const missingDatasets = new Set<string>();
22+
23+
function isDatasetNotFoundError(err: unknown): boolean {
24+
if (!err || typeof err !== "object") {
25+
return false;
26+
}
27+
const status = (err as { statusCode?: number }).statusCode;
28+
const message = (err as { message?: string }).message || "";
29+
return (
30+
status === 404 &&
31+
(message.includes("Dataset") || message.includes("not found"))
32+
);
33+
}
34+
35+
function wrapWithDatasetFallback(
36+
primary: SanityClient,
37+
getFallback: () => SanityClient,
38+
projectId: string,
39+
dataset: string,
40+
): SanityClient {
41+
if (dataset === "production") {
42+
return primary;
43+
}
44+
const datasetKey = `${projectId}:${dataset}`;
45+
const originalFetch = primary.fetch.bind(primary);
46+
primary.fetch = (async (
47+
query: string,
48+
params?: Record<string, unknown>,
49+
options?: unknown,
50+
) => {
51+
if (missingDatasets.has(datasetKey)) {
52+
return (getFallback().fetch as typeof originalFetch)(
53+
query,
54+
params as never,
55+
options as never,
56+
);
57+
}
58+
try {
59+
return await originalFetch(query, params as never, options as never);
60+
} catch (err) {
61+
if (isDatasetNotFoundError(err)) {
62+
missingDatasets.add(datasetKey);
63+
console.warn(
64+
`[Sanity] Dataset "${dataset}" not found for project "${projectId}", falling back to "production".`,
65+
);
66+
return (getFallback().fetch as typeof originalFetch)(
67+
query,
68+
params as never,
69+
options as never,
70+
);
71+
}
72+
throw err;
73+
}
74+
}) as typeof primary.fetch;
75+
return primary;
76+
}
77+
1578
function memo(key: string, make: () => SanityClient): SanityClient {
1679
let client = cache.get(key);
1780
if (!client) {
@@ -21,6 +84,28 @@ function memo(key: string, make: () => SanityClient): SanityClient {
2184
return client;
2285
}
2386

87+
/**
88+
* Creates a Sanity client that automatically falls back to `dataset: "production"`
89+
* if the configured dataset does not exist (HTTP 404).
90+
*/
91+
export function createResilientClient(config: ClientConfig): SanityClient {
92+
const projectId = config.projectId || "hfh83o0w";
93+
const dataset = config.dataset || "production";
94+
if (
95+
dataset !== "production" &&
96+
missingDatasets.has(`${projectId}:${dataset}`)
97+
) {
98+
return createClient({ ...config, dataset: "production" });
99+
}
100+
const primary = createClient(config);
101+
return wrapWithDatasetFallback(
102+
primary,
103+
() => createClient({ ...config, dataset: "production" }),
104+
projectId,
105+
dataset,
106+
);
107+
}
108+
24109
/**
25110
* Anonymous, published-only reads.
26111
*
@@ -35,16 +120,28 @@ function memo(key: string, make: () => SanityClient): SanityClient {
35120
*/
36121
export function publishedClient(config: SanityConfig): SanityClient {
37122
const { projectId, dataset, apiVersion } = config;
38-
return memo(`pub:${projectId}:${dataset}:${apiVersion}`, () =>
39-
createClient({
123+
if (
124+
dataset !== "production" &&
125+
missingDatasets.has(`${projectId}:${dataset}`)
126+
) {
127+
return publishedClient({ ...config, dataset: "production" });
128+
}
129+
return memo(`pub:${projectId}:${dataset}:${apiVersion}`, () => {
130+
const client = createClient({
40131
projectId,
41132
dataset,
42133
apiVersion,
43134
useCdn: true,
44135
perspective: "published",
45136
stega: false,
46-
}),
47-
);
137+
});
138+
return wrapWithDatasetFallback(
139+
client,
140+
() => publishedClient({ ...config, dataset: "production" }),
141+
projectId,
142+
dataset,
143+
);
144+
});
48145
}
49146

50147
/** Authenticated draft/preview reads, with stega and source maps enabled. */
@@ -54,9 +151,19 @@ export function previewClient(
54151
perspective: ClientPerspective,
55152
): SanityClient {
56153
const { projectId, dataset, apiVersion, studioUrl } = config;
154+
if (
155+
dataset !== "production" &&
156+
missingDatasets.has(`${projectId}:${dataset}`)
157+
) {
158+
return previewClient(
159+
{ ...config, dataset: "production" },
160+
token,
161+
perspective,
162+
);
163+
}
57164
const key = `prev:${projectId}:${dataset}:${apiVersion}:${String(perspective)}`;
58-
return memo(key, () =>
59-
createClient({
165+
return memo(key, () => {
166+
const client = createClient({
60167
projectId,
61168
dataset,
62169
apiVersion,
@@ -65,6 +172,13 @@ export function previewClient(
65172
token,
66173
resultSourceMap: "withKeyArraySelector",
67174
stega: { enabled: true, studioUrl },
68-
}),
69-
);
175+
});
176+
return wrapWithDatasetFallback(
177+
client,
178+
() =>
179+
previewClient({ ...config, dataset: "production" }, token, perspective),
180+
projectId,
181+
dataset,
182+
);
183+
});
70184
}

‎apps/site/src/pages/api/chat.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { env } from "cloudflare:workers";
22
import { createMCPClient } from "@ai-sdk/mcp";
33
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
4-
import { createClient } from "@sanity/client";
54
import { isStepCount, jsonSchema, streamText, tool } from "ai";
65
import type { APIRoute } from "astro";
76
import { createWorkersAI } from "workers-ai-provider";
7+
import { createResilientClient } from "@/lib/sanity/client";
88
import {
99
semanticSearchQuery,
1010
textSearchFallbackQuery,
@@ -281,10 +281,10 @@ export const POST: APIRoute = async ({ request }) => {
281281
}
282282
}
283283

284-
// Configure Sanity client (fall back to public 'production' dataset when no read token is present)
284+
// Configure Sanity client (fall back to public 'production' dataset when no read token is present or dataset is missing)
285285
const configuredDataset =
286286
(cfEnv?.PUBLIC_SANITY_DATASET as string) || "production";
287-
const sanityClient = createClient({
287+
const sanityClient = createResilientClient({
288288
projectId: (cfEnv?.PUBLIC_SANITY_PROJECT_ID as string) || "hfh83o0w",
289289
dataset: sanityToken ? configuredDataset : "production",
290290
apiVersion: (cfEnv?.PUBLIC_SANITY_API_VERSION as string) || "2025-09-30",

‎apps/site/src/pages/api/mcp.ts‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { env } from "cloudflare:workers";
2-
import { createClient } from "@sanity/client";
32
import type { APIRoute } from "astro";
3+
import { createResilientClient } from "../../lib/sanity/client";
44
import {
55
semanticSearchQuery,
66
textSearchFallbackQuery,
@@ -313,7 +313,7 @@ function getSanityClient() {
313313
process.env.SANITY_API_READ_TOKEN ||
314314
process.env.SANITY_AUTH_TOKEN;
315315

316-
return createClient({
316+
return createResilientClient({
317317
projectId: (cfEnv?.PUBLIC_SANITY_PROJECT_ID as string) || "hfh83o0w",
318318
dataset: (cfEnv?.PUBLIC_SANITY_DATASET as string) || "production",
319319
apiVersion: (cfEnv?.PUBLIC_SANITY_API_VERSION as string) || "2025-09-30",
@@ -1314,13 +1314,6 @@ async function handleSchemaExplorer(
13141314
"URL slug identifier (e.g. '0-6-nextjs-with-guillermo-rauch')",
13151315
},
13161316
{ name: "date", type: "datetime", description: "Release date" },
1317-
{
1318-
name: "recordingDate",
1319-
type: "datetime",
1320-
description: "Recording date",
1321-
},
1322-
{ name: "season", type: "number", description: "Season number" },
1323-
{ name: "episode", type: "number", description: "Episode number" },
13241317
{
13251318
name: "coverImage",
13261319
type: "image",

‎apps/site/src/pages/api/transcript.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { env } from "cloudflare:workers";
2-
import { createClient } from "@sanity/client";
32
import type { APIRoute } from "astro";
3+
import { createResilientClient } from "../../lib/sanity/client";
44
import { youtubeId as parseYoutubeId } from "../../lib/youtube";
55

66
export const prerender = false;
@@ -18,7 +18,7 @@ function getSanityClient() {
1818
(cfEnv?.SANITY_API_READ_TOKEN as string | undefined) ||
1919
process.env.SANITY_API_READ_TOKEN;
2020

21-
return createClient({
21+
return createResilientClient({
2222
projectId: (cfEnv?.PUBLIC_SANITY_PROJECT_ID as string) || "hfh83o0w",
2323
dataset: (cfEnv?.PUBLIC_SANITY_DATASET as string) || "production",
2424
apiVersion: (cfEnv?.PUBLIC_SANITY_API_VERSION as string) || "2025-09-30",

‎apps/site/wrangler.jsonc‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
1111
"observability": { "enabled": true, "head_sampling_rate": 1 },
1212
"workers_dev": true,
13+
"preview_urls": true,
1314
"ai": { "binding": "AI" },
1415
// Uncomment after clicking "Enable Analytics Engine" once at:
1516
// https://dash.cloudflare.com/c46bd0f3b9ed47b8692cca098030edbf/workers/analytics-engine

0 commit comments

Comments
 (0)