@@ -52,105 +52,99 @@ jobs:
5252 wc -l /tmp/production.ndjson
5353
5454 echo "3. Importing documents into 'dev' dataset via Sanity Mutate API..."
55- node -e '
56- const fs = require("fs");
57- const projectId = process.env.SANITY_STUDIO_PROJECT_ID || "hfh83o0w";
58- const tokens = [process.env.SANITY_API_WRITE_TOKEN, process.env.SANITY_AUTH_TOKEN].filter(Boolean);
59- const lines = fs.readFileSync("/tmp/production.ndjson", "utf8").trim().split("\n").filter(Boolean);
60- const allDocs = lines.map((l) => JSON.parse(l));
61- // Exclude sanity.imageAsset and sanity.fileAsset if mutate API rejects system asset docs without binary upload,
62- // or include content docs and weaken references to assets/missing docs so all content imports cleanly.
63- const contentDocs = allDocs.filter((d) => d._type !== "sanity.imageAsset" && d._type !== "sanity.fileAsset");
64- const contentIds = new Set(contentDocs.map((d) => d._id));
65-
66- function sanitizeDoc(obj) {
67- if (!obj || typeof obj !== "object") return obj;
68- if (Array.isArray(obj)) return obj.map(sanitizeDoc);
69- const copy = {};
70- for (const [k, v] of Object.entries(obj)) {
71- if (k === "_rev") continue;
72- copy[k] = sanitizeDoc(v);
73- }
74- if (typeof copy._ref === "string" && !contentIds.has(copy._ref)) {
75- copy._weak = true;
76- }
77- return copy;
55+ cat << 'EOF' > /tmp/sync.cjs
56+ const fs = require("fs");
57+ const projectId = process.env.SANITY_STUDIO_PROJECT_ID || "hfh83o0w";
58+ const tokens = [process.env.SANITY_API_WRITE_TOKEN, process.env.SANITY_AUTH_TOKEN].filter(Boolean);
59+ const lines = fs.readFileSync("/tmp/production.ndjson", "utf8").trim().split("\n").filter(Boolean);
60+ const allDocs = lines.map((l) => JSON.parse(l));
61+ const contentDocs = allDocs.filter((d) => d._type !== "sanity.imageAsset" && d._type !== "sanity.fileAsset");
62+ const contentIds = new Set(contentDocs.map((d) => d._id));
63+
64+ function sanitizeDoc(obj) {
65+ if (!obj || typeof obj !== "object") return obj;
66+ if (Array.isArray(obj)) return obj.map(sanitizeDoc);
67+ const copy = {};
68+ for (const [k, v] of Object.entries(obj)) {
69+ if (k === "_rev") continue;
70+ copy[k] = sanitizeDoc(v);
7871 }
79-
80- const sanitized = contentDocs.map(sanitizeDoc);
81- console.log(`Prepared ${sanitized.length} content documents (from ${allDocs.length} total lines) for dataset "dev"...`);
82-
83- async function mutateBatch(mutations) {
84- let lastErr = "";
85- for (const token of tokens) {
86- const res = await fetch(`https://${projectId}.api.sanity.io/v2021-06-07/data/mutate/dev?visibility=async&autoGenerateArrayKeys=true`, {
87- method: "POST",
88- headers: {
89- Authorization: `Bearer ${token}`,
90- "Content-Type": "application/json",
91- },
92- body: JSON.stringify({ mutations }),
93- });
94- if (res.ok) return;
95- lastErr = `HTTP ${res.status}: ${await res.text()}`;
96- }
97- throw new Error(`Mutate batch failed: ${lastErr}`);
72+ if (typeof copy._ref === "string" && !contentIds.has(copy._ref)) {
73+ copy._weak = true;
9874 }
99-
100- (async () => {
101- // Helper to chunk mutations by both count and byte size (< 1.5MB, well below Sanity's 4MB limit)
102- async function sendChunked(items, label) {
103- let batch = [];
104- let batchBytes = 0;
105- let sent = 0;
106- const MAX_ITEMS = 50;
107- const MAX_BYTES = 1500000;
108-
109- for (const item of items) {
110- const mutation = { createOrReplace: item };
111- const itemBytes = Buffer.byteLength(JSON.stringify(mutation), "utf8");
112- if (batch.length > 0 && (batch.length >= MAX_ITEMS || batchBytes + itemBytes > MAX_BYTES)) {
113- await mutateBatch(batch);
114- sent += batch.length;
115- if (sent % 500 < batch.length) {
116- console.log(`[${label}] Imported ${sent} / ${items.length}...`);
117- }
118- batch = [];
119- batchBytes = 0;
120- }
121- batch.push(mutation);
122- batchBytes += itemBytes;
123- }
124- if (batch.length > 0) {
125- await mutateBatch(batch);
126- sent += batch.length;
75+ return copy;
76+ }
77+
78+ const sanitized = contentDocs.map(sanitizeDoc);
79+ console.log(`Prepared ${sanitized.length} content documents (from ${allDocs.length} total lines) for dataset "dev"...`);
80+
81+ async function mutateBatch(mutations) {
82+ let lastErr = "";
83+ for (const token of tokens) {
84+ const res = await fetch(`https://${projectId}.api.sanity.io/v2021-06-07/data/mutate/dev?visibility=async&autoGenerateArrayKeys=true`, {
85+ method: "POST",
86+ headers: {
87+ Authorization: `Bearer ${token}`,
88+ "Content-Type": "application/json",
89+ },
90+ body: JSON.stringify({ mutations }),
91+ });
92+ if (res.ok) return;
93+ lastErr = `HTTP ${res.status}: ${await res.text()}`;
94+ }
95+ throw new Error(`Mutate batch failed: ${lastErr}`);
96+ }
97+
98+ async function sendChunked(items, label) {
99+ let batch = [];
100+ let batchBytes = 0;
101+ let sent = 0;
102+ const MAX_ITEMS = 50;
103+ const MAX_BYTES = 1500000;
104+
105+ for (const item of items) {
106+ const mutation = { createOrReplace: item };
107+ const itemBytes = Buffer.byteLength(JSON.stringify(mutation), "utf8");
108+ if (batch.length > 0 && (batch.length >= MAX_ITEMS || batchBytes + itemBytes > MAX_BYTES)) {
109+ await mutateBatch(batch);
110+ sent += batch.length;
111+ if (sent % 500 < batch.length) {
112+ console.log(`[${label}] Imported ${sent} / ${items.length}...`);
127113 }
128- console.log(`[${label}] Completed (${sent} / ${items.length}).`);
129- }
130-
131- // Pass 1: Create stubs so all internal document references resolve
132- const stubs = sanitized.map((d) => ({ _id: d._id, _type: d._type }));
133- await sendChunked(stubs, "Pass 1: Stubs");
134-
135- // Pass 2: Populate full document bodies
136- await sendChunked(sanitized, "Pass 2: Content Docs");
137-
138- // Pass 3 (best-effort): Import asset metadata documents so Studio Media Library resolves in dev
139- const assetDocs = allDocs
140- .filter((d) => d._type === "sanity.imageAsset" || d._type === "sanity.fileAsset")
141- .map(({ _rev, ...rest }) => rest);
142- try {
143- await sendChunked(assetDocs, "Pass 3: Asset Docs");
144- } catch (assetErr) {
145- console.warn("Note: Asset metadata docs skipped (served via CDN):", assetErr.message);
114+ batch = [];
115+ batchBytes = 0;
146116 }
117+ batch.push(mutation);
118+ batchBytes += itemBytes;
119+ }
120+ if (batch.length > 0) {
121+ await mutateBatch(batch);
122+ sent += batch.length;
123+ }
124+ console.log(`[${label}] Completed (${sent} / ${items.length}).`);
125+ }
126+
127+ (async () => {
128+ const stubs = sanitized.map((d) => ({ _id: d._id, _type: d._type }));
129+ await sendChunked(stubs, "Pass 1: Stubs");
130+ await sendChunked(sanitized, "Pass 2: Content Docs");
131+
132+ const assetDocs = allDocs
133+ .filter((d) => d._type === "sanity.imageAsset" || d._type === "sanity.fileAsset")
134+ .map(({ _rev, ...rest }) => rest);
135+ try {
136+ await sendChunked(assetDocs, "Pass 3: Asset Docs");
137+ } catch (assetErr) {
138+ console.warn("Note: Asset metadata docs skipped (served via CDN):", assetErr.message);
139+ }
147140
148- console.log("Dataset sync completed successfully!");
149- })().catch((err) => {
150- console.error(err);
151- process.exit(1);
152- });
153- '
141+ console.log("Dataset sync completed successfully!");
142+ })().catch((err) => {
143+ console.error(err);
144+ process.exit(1);
145+ });
146+ EOF
147+ node /tmp/sync.cjs
154148
155149 - name : Upload Sanity production dataset backup artifact
156150 uses : actions/upload-artifact@v4
0 commit comments