Skip to content

Commit 348117d

Browse files
authored
feat(ci): deploy Sanity App SDK coreApp & synchronously poll Blueprint Stack status (#755)
1 parent 2cc72b5 commit 348117d

2 files changed

Lines changed: 197 additions & 4 deletions

File tree

‎.github/workflows/deploy.yml‎

Lines changed: 188 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,43 @@ jobs:
5151
SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }}
5252
SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }}
5353

54+
- name: Deploy Sanity Ops App (Sanity App SDK)
55+
working-directory: apps/ops
56+
run: |
57+
ORG_ID=$(node -e "
58+
fetch('https://api.sanity.io/v2021-06-07/projects/${{ vars.SANITY_STUDIO_PROJECT_ID }}', {
59+
headers: { Authorization: 'Bearer ' + process.env.SANITY_AUTH_TOKEN }
60+
})
61+
.then(r => r.json())
62+
.then(d => {
63+
if (!d.organizationId) throw new Error('No organizationId in project response');
64+
console.log(d.organizationId);
65+
})
66+
.catch(err => { console.error(err); process.exit(1); });
67+
")
68+
export SANITY_ORGANIZATION_ID="$ORG_ID"
69+
EXISTING_APP_ID=$(node -e "
70+
fetch('https://api.sanity.io/v2024-08-01/user-applications?appType=coreApp&organizationId=' + process.env.SANITY_ORGANIZATION_ID, {
71+
headers: { Authorization: 'Bearer ' + process.env.SANITY_AUTH_TOKEN }
72+
})
73+
.then(r => r.ok ? r.json() : [])
74+
.then(apps => {
75+
const list = Array.isArray(apps) ? apps : [];
76+
const match = list.find(a => a.title === 'CodingCat Ops & Revenue OS');
77+
if (match && match.id) console.log(match.id);
78+
})
79+
.catch(() => {});
80+
")
81+
if [ -n "$EXISTING_APP_ID" ]; then
82+
echo "Found existing CodingCat Ops & Revenue OS app: $EXISTING_APP_ID"
83+
SANITY_OPS_APP_ID="$EXISTING_APP_ID" npx sanity deploy -y
84+
else
85+
echo "Creating and deploying new CodingCat Ops & Revenue OS app in organization $ORG_ID..."
86+
npx sanity deploy -y --create --title "CodingCat Ops & Revenue OS"
87+
fi
88+
env:
89+
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
90+
5491
- name: Promote Stack to Organization Scope
5592
run: |
5693
mkdir -p .sanity
@@ -59,11 +96,11 @@ jobs:
5996
env:
6097
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
6198

62-
- name: Deploy Sanity Blueprints Functions
99+
- name: Deploy Sanity Blueprints & Poll Synchronously Until Completed
63100
run: |
64101
ORG_ID=$(node -e "
65102
fetch('https://api.sanity.io/v2021-06-07/projects/${{ vars.SANITY_STUDIO_PROJECT_ID }}', {
66-
headers: { Authorization: 'Bearer ${{ secrets.SANITY_AUTH_TOKEN }}' }
103+
headers: { Authorization: 'Bearer ' + process.env.SANITY_AUTH_TOKEN }
67104
})
68105
.then(r => r.json())
69106
.then(d => {
@@ -75,7 +112,62 @@ jobs:
75112
echo "Resolved Organization ID: $ORG_ID"
76113
mkdir -p .sanity
77114
echo "{\"organizationId\":\"$ORG_ID\"}" > .sanity/blueprint.config.json
115+
116+
echo "=== Previous Stack Info & Logs ==="
117+
pnpm exec sanity blueprints info --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --verbose || true
118+
pnpm exec sanity blueprints logs --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} || true
119+
120+
echo "=== Starting Blueprint Deployment ==="
78121
pnpm exec sanity blueprints deploy --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --no-wait
122+
123+
echo "=== Synchronously Polling Blueprint Stack Operation Until Terminal State ==="
124+
ORG_ID="$ORG_ID" STACK_NAME="${{ vars.SANITY_STUDIO_DATASET }}" node -e "
125+
const token = process.env.SANITY_AUTH_TOKEN;
126+
const orgId = process.env.ORG_ID;
127+
const stackName = process.env.STACK_NAME;
128+
const headers = {
129+
Accept: 'application/json',
130+
Authorization: 'Bearer ' + token,
131+
'X-Sanity-Scope-Type': 'organization',
132+
'X-Sanity-Scope-Id': orgId,
133+
};
134+
(async () => {
135+
const stacksRes = await fetch('https://api.sanity.io/v2026-06-15/blueprints/stacks', { headers });
136+
if (!stacksRes.ok) throw new Error('Failed to list stacks: HTTP ' + stacksRes.status);
137+
const stacks = await stacksRes.json();
138+
const stack = stacks.find(s => s.name === stackName || s.id === stackName);
139+
if (!stack) throw new Error('Stack not found: ' + stackName);
140+
console.log('Polling Stack:', stack.name, '(' + stack.id + ')');
141+
const deadline = Date.now() + 5 * 60 * 1000;
142+
while (Date.now() < deadline) {
143+
const res = await fetch('https://api.sanity.io/v2026-06-15/blueprints/stacks/' + stack.id, { headers });
144+
if (!res.ok) throw new Error('Failed to fetch stack ' + stack.id + ': HTTP ' + res.status);
145+
const data = await res.json();
146+
const op = data.recentOperation;
147+
const status = op?.status || 'NONE';
148+
console.log('[' + new Date().toISOString() + '] Operation ' + (op?.id || 'n/a') + ' status: ' + status);
149+
if (status === 'COMPLETED' || status === 'NONE') {
150+
console.log('✔ Blueprint Stack operation settled with status:', status);
151+
process.exit(0);
152+
}
153+
if (status === 'FAILED' || status === 'CANCELLED') {
154+
console.error('✖ Blueprint Stack operation failed:', JSON.stringify(op, null, 2));
155+
const logsRes = await fetch('https://api.sanity.io/v2026-06-15/blueprints/logs?stackId=' + stack.id + (op?.id ? '&operationId=' + op.id : ''), { headers });
156+
if (logsRes.ok) {
157+
const logs = await logsRes.json();
158+
console.error('Operation logs:', JSON.stringify(logs, null, 2));
159+
}
160+
process.exit(1);
161+
}
162+
await new Promise(r => setTimeout(r, 4000));
163+
}
164+
console.error('✖ Timed out waiting 5 minutes for Blueprint Stack operation to complete');
165+
process.exit(1);
166+
})().catch(err => { console.error(err); process.exit(1); });
167+
"
168+
169+
echo "=== Verified Post-Deploy Stack Info ==="
170+
pnpm exec sanity blueprints info --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --verbose
79171
env:
80172
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
81173
SANITY_API_WRITE_TOKEN: ${{ secrets.SANITY_API_WRITE_TOKEN }}
@@ -125,6 +217,43 @@ jobs:
125217
SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }}
126218
SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }}
127219

220+
- name: Deploy Sanity Ops App (Sanity App SDK)
221+
working-directory: apps/ops
222+
run: |
223+
ORG_ID=$(node -e "
224+
fetch('https://api.sanity.io/v2021-06-07/projects/${{ vars.SANITY_STUDIO_PROJECT_ID }}', {
225+
headers: { Authorization: 'Bearer ' + process.env.SANITY_AUTH_TOKEN }
226+
})
227+
.then(r => r.json())
228+
.then(d => {
229+
if (!d.organizationId) throw new Error('No organizationId in project response');
230+
console.log(d.organizationId);
231+
})
232+
.catch(err => { console.error(err); process.exit(1); });
233+
")
234+
export SANITY_ORGANIZATION_ID="$ORG_ID"
235+
EXISTING_APP_ID=$(node -e "
236+
fetch('https://api.sanity.io/v2024-08-01/user-applications?appType=coreApp&organizationId=' + process.env.SANITY_ORGANIZATION_ID, {
237+
headers: { Authorization: 'Bearer ' + process.env.SANITY_AUTH_TOKEN }
238+
})
239+
.then(r => r.ok ? r.json() : [])
240+
.then(apps => {
241+
const list = Array.isArray(apps) ? apps : [];
242+
const match = list.find(a => a.title === 'CodingCat Ops & Revenue OS');
243+
if (match && match.id) console.log(match.id);
244+
})
245+
.catch(() => {});
246+
")
247+
if [ -n "$EXISTING_APP_ID" ]; then
248+
echo "Found existing CodingCat Ops & Revenue OS app: $EXISTING_APP_ID"
249+
SANITY_OPS_APP_ID="$EXISTING_APP_ID" npx sanity deploy -y
250+
else
251+
echo "Creating and deploying new CodingCat Ops & Revenue OS app in organization $ORG_ID..."
252+
npx sanity deploy -y --create --title "CodingCat Ops & Revenue OS"
253+
fi
254+
env:
255+
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
256+
128257
- name: Promote Stack to Organization Scope
129258
run: |
130259
mkdir -p .sanity
@@ -133,11 +262,11 @@ jobs:
133262
env:
134263
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
135264

136-
- name: Deploy Sanity Blueprints Functions
265+
- name: Deploy Sanity Blueprints & Poll Synchronously Until Completed
137266
run: |
138267
ORG_ID=$(node -e "
139268
fetch('https://api.sanity.io/v2021-06-07/projects/${{ vars.SANITY_STUDIO_PROJECT_ID }}', {
140-
headers: { Authorization: 'Bearer ${{ secrets.SANITY_AUTH_TOKEN }}' }
269+
headers: { Authorization: 'Bearer ' + process.env.SANITY_AUTH_TOKEN }
141270
})
142271
.then(r => r.json())
143272
.then(d => {
@@ -149,7 +278,62 @@ jobs:
149278
echo "Resolved Organization ID: $ORG_ID"
150279
mkdir -p .sanity
151280
echo "{\"organizationId\":\"$ORG_ID\"}" > .sanity/blueprint.config.json
281+
282+
echo "=== Previous Stack Info & Logs ==="
283+
pnpm exec sanity blueprints info --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --verbose || true
284+
pnpm exec sanity blueprints logs --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} || true
285+
286+
echo "=== Starting Blueprint Deployment ==="
152287
pnpm exec sanity blueprints deploy --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --no-wait
288+
289+
echo "=== Synchronously Polling Blueprint Stack Operation Until Terminal State ==="
290+
ORG_ID="$ORG_ID" STACK_NAME="${{ vars.SANITY_STUDIO_DATASET }}" node -e "
291+
const token = process.env.SANITY_AUTH_TOKEN;
292+
const orgId = process.env.ORG_ID;
293+
const stackName = process.env.STACK_NAME;
294+
const headers = {
295+
Accept: 'application/json',
296+
Authorization: 'Bearer ' + token,
297+
'X-Sanity-Scope-Type': 'organization',
298+
'X-Sanity-Scope-Id': orgId,
299+
};
300+
(async () => {
301+
const stacksRes = await fetch('https://api.sanity.io/v2026-06-15/blueprints/stacks', { headers });
302+
if (!stacksRes.ok) throw new Error('Failed to list stacks: HTTP ' + stacksRes.status);
303+
const stacks = await stacksRes.json();
304+
const stack = stacks.find(s => s.name === stackName || s.id === stackName);
305+
if (!stack) throw new Error('Stack not found: ' + stackName);
306+
console.log('Polling Stack:', stack.name, '(' + stack.id + ')');
307+
const deadline = Date.now() + 5 * 60 * 1000;
308+
while (Date.now() < deadline) {
309+
const res = await fetch('https://api.sanity.io/v2026-06-15/blueprints/stacks/' + stack.id, { headers });
310+
if (!res.ok) throw new Error('Failed to fetch stack ' + stack.id + ': HTTP ' + res.status);
311+
const data = await res.json();
312+
const op = data.recentOperation;
313+
const status = op?.status || 'NONE';
314+
console.log('[' + new Date().toISOString() + '] Operation ' + (op?.id || 'n/a') + ' status: ' + status);
315+
if (status === 'COMPLETED' || status === 'NONE') {
316+
console.log('✔ Blueprint Stack operation settled with status:', status);
317+
process.exit(0);
318+
}
319+
if (status === 'FAILED' || status === 'CANCELLED') {
320+
console.error('✖ Blueprint Stack operation failed:', JSON.stringify(op, null, 2));
321+
const logsRes = await fetch('https://api.sanity.io/v2026-06-15/blueprints/logs?stackId=' + stack.id + (op?.id ? '&operationId=' + op.id : ''), { headers });
322+
if (logsRes.ok) {
323+
const logs = await logsRes.json();
324+
console.error('Operation logs:', JSON.stringify(logs, null, 2));
325+
}
326+
process.exit(1);
327+
}
328+
await new Promise(r => setTimeout(r, 4000));
329+
}
330+
console.error('✖ Timed out waiting 5 minutes for Blueprint Stack operation to complete');
331+
process.exit(1);
332+
})().catch(err => { console.error(err); process.exit(1); });
333+
"
334+
335+
echo "=== Verified Post-Deploy Stack Info ==="
336+
pnpm exec sanity blueprints info --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --verbose
153337
env:
154338
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
155339
SANITY_API_WRITE_TOKEN: ${{ secrets.SANITY_API_WRITE_TOKEN }}

‎apps/ops/sanity.cli.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ export default defineCliConfig({
44
app: {
55
organizationId: process.env.SANITY_ORGANIZATION_ID || "ovF2qiKSO",
66
entry: "./src/index.tsx",
7+
title: "CodingCat Ops & Revenue OS",
78
},
9+
...(process.env.SANITY_OPS_APP_ID
10+
? {
11+
deployment: {
12+
appId: process.env.SANITY_OPS_APP_ID,
13+
},
14+
}
15+
: {}),
816
});
917

18+

0 commit comments

Comments
 (0)