|
51 | 51 | SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }} |
52 | 52 | SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }} |
53 | 53 |
|
| 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 | + |
54 | 91 | - name: Promote Stack to Organization Scope |
55 | 92 | run: | |
56 | 93 | mkdir -p .sanity |
@@ -59,11 +96,11 @@ jobs: |
59 | 96 | env: |
60 | 97 | SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }} |
61 | 98 |
|
62 | | - - name: Deploy Sanity Blueprints Functions |
| 99 | + - name: Deploy Sanity Blueprints & Poll Synchronously Until Completed |
63 | 100 | run: | |
64 | 101 | ORG_ID=$(node -e " |
65 | 102 | 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 } |
67 | 104 | }) |
68 | 105 | .then(r => r.json()) |
69 | 106 | .then(d => { |
@@ -75,7 +112,62 @@ jobs: |
75 | 112 | echo "Resolved Organization ID: $ORG_ID" |
76 | 113 | mkdir -p .sanity |
77 | 114 | 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 ===" |
78 | 121 | 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 |
79 | 171 | env: |
80 | 172 | SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }} |
81 | 173 | SANITY_API_WRITE_TOKEN: ${{ secrets.SANITY_API_WRITE_TOKEN }} |
@@ -125,6 +217,43 @@ jobs: |
125 | 217 | SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }} |
126 | 218 | SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }} |
127 | 219 |
|
| 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 | + |
128 | 257 | - name: Promote Stack to Organization Scope |
129 | 258 | run: | |
130 | 259 | mkdir -p .sanity |
@@ -133,11 +262,11 @@ jobs: |
133 | 262 | env: |
134 | 263 | SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }} |
135 | 264 |
|
136 | | - - name: Deploy Sanity Blueprints Functions |
| 265 | + - name: Deploy Sanity Blueprints & Poll Synchronously Until Completed |
137 | 266 | run: | |
138 | 267 | ORG_ID=$(node -e " |
139 | 268 | 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 } |
141 | 270 | }) |
142 | 271 | .then(r => r.json()) |
143 | 272 | .then(d => { |
@@ -149,7 +278,62 @@ jobs: |
149 | 278 | echo "Resolved Organization ID: $ORG_ID" |
150 | 279 | mkdir -p .sanity |
151 | 280 | 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 ===" |
152 | 287 | 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 |
153 | 337 | env: |
154 | 338 | SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }} |
155 | 339 | SANITY_API_WRITE_TOKEN: ${{ secrets.SANITY_API_WRITE_TOKEN }} |
|