diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3b8b59b7..e355a0bd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -51,6 +51,43 @@ jobs: SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }} SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }} + - name: Deploy Sanity Ops App (Sanity App SDK) + working-directory: apps/ops + run: | + ORG_ID=$(node -e " + fetch('https://api.sanity.io/v2021-06-07/projects/${{ vars.SANITY_STUDIO_PROJECT_ID }}', { + headers: { Authorization: 'Bearer ' + process.env.SANITY_AUTH_TOKEN } + }) + .then(r => r.json()) + .then(d => { + if (!d.organizationId) throw new Error('No organizationId in project response'); + console.log(d.organizationId); + }) + .catch(err => { console.error(err); process.exit(1); }); + ") + export SANITY_ORGANIZATION_ID="$ORG_ID" + EXISTING_APP_ID=$(node -e " + fetch('https://api.sanity.io/v2024-08-01/user-applications?appType=coreApp&organizationId=' + process.env.SANITY_ORGANIZATION_ID, { + headers: { Authorization: 'Bearer ' + process.env.SANITY_AUTH_TOKEN } + }) + .then(r => r.ok ? r.json() : []) + .then(apps => { + const list = Array.isArray(apps) ? apps : []; + const match = list.find(a => a.title === 'CodingCat Ops & Revenue OS'); + if (match && match.id) console.log(match.id); + }) + .catch(() => {}); + ") + if [ -n "$EXISTING_APP_ID" ]; then + echo "Found existing CodingCat Ops & Revenue OS app: $EXISTING_APP_ID" + SANITY_OPS_APP_ID="$EXISTING_APP_ID" npx sanity deploy -y + else + echo "Creating and deploying new CodingCat Ops & Revenue OS app in organization $ORG_ID..." + npx sanity deploy -y --create --title "CodingCat Ops & Revenue OS" + fi + env: + SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }} + - name: Promote Stack to Organization Scope run: | mkdir -p .sanity @@ -59,11 +96,11 @@ jobs: env: SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }} - - name: Deploy Sanity Blueprints Functions + - name: Deploy Sanity Blueprints & Poll Synchronously Until Completed run: | ORG_ID=$(node -e " fetch('https://api.sanity.io/v2021-06-07/projects/${{ vars.SANITY_STUDIO_PROJECT_ID }}', { - headers: { Authorization: 'Bearer ${{ secrets.SANITY_AUTH_TOKEN }}' } + headers: { Authorization: 'Bearer ' + process.env.SANITY_AUTH_TOKEN } }) .then(r => r.json()) .then(d => { @@ -75,7 +112,62 @@ jobs: echo "Resolved Organization ID: $ORG_ID" mkdir -p .sanity echo "{\"organizationId\":\"$ORG_ID\"}" > .sanity/blueprint.config.json + + echo "=== Previous Stack Info & Logs ===" + pnpm exec sanity blueprints info --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --verbose || true + pnpm exec sanity blueprints logs --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} || true + + echo "=== Starting Blueprint Deployment ===" pnpm exec sanity blueprints deploy --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --no-wait + + echo "=== Synchronously Polling Blueprint Stack Operation Until Terminal State ===" + ORG_ID="$ORG_ID" STACK_NAME="${{ vars.SANITY_STUDIO_DATASET }}" node -e " + const token = process.env.SANITY_AUTH_TOKEN; + const orgId = process.env.ORG_ID; + const stackName = process.env.STACK_NAME; + const headers = { + Accept: 'application/json', + Authorization: 'Bearer ' + token, + 'X-Sanity-Scope-Type': 'organization', + 'X-Sanity-Scope-Id': orgId, + }; + (async () => { + const stacksRes = await fetch('https://api.sanity.io/v2026-06-15/blueprints/stacks', { headers }); + if (!stacksRes.ok) throw new Error('Failed to list stacks: HTTP ' + stacksRes.status); + const stacks = await stacksRes.json(); + const stack = stacks.find(s => s.name === stackName || s.id === stackName); + if (!stack) throw new Error('Stack not found: ' + stackName); + console.log('Polling Stack:', stack.name, '(' + stack.id + ')'); + const deadline = Date.now() + 5 * 60 * 1000; + while (Date.now() < deadline) { + const res = await fetch('https://api.sanity.io/v2026-06-15/blueprints/stacks/' + stack.id, { headers }); + if (!res.ok) throw new Error('Failed to fetch stack ' + stack.id + ': HTTP ' + res.status); + const data = await res.json(); + const op = data.recentOperation; + const status = op?.status || 'NONE'; + console.log('[' + new Date().toISOString() + '] Operation ' + (op?.id || 'n/a') + ' status: ' + status); + if (status === 'COMPLETED' || status === 'NONE') { + console.log('✔ Blueprint Stack operation settled with status:', status); + process.exit(0); + } + if (status === 'FAILED' || status === 'CANCELLED') { + console.error('✖ Blueprint Stack operation failed:', JSON.stringify(op, null, 2)); + const logsRes = await fetch('https://api.sanity.io/v2026-06-15/blueprints/logs?stackId=' + stack.id + (op?.id ? '&operationId=' + op.id : ''), { headers }); + if (logsRes.ok) { + const logs = await logsRes.json(); + console.error('Operation logs:', JSON.stringify(logs, null, 2)); + } + process.exit(1); + } + await new Promise(r => setTimeout(r, 4000)); + } + console.error('✖ Timed out waiting 5 minutes for Blueprint Stack operation to complete'); + process.exit(1); + })().catch(err => { console.error(err); process.exit(1); }); + " + + echo "=== Verified Post-Deploy Stack Info ===" + pnpm exec sanity blueprints info --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --verbose env: SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }} SANITY_API_WRITE_TOKEN: ${{ secrets.SANITY_API_WRITE_TOKEN }} @@ -125,6 +217,43 @@ jobs: SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }} SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }} + - name: Deploy Sanity Ops App (Sanity App SDK) + working-directory: apps/ops + run: | + ORG_ID=$(node -e " + fetch('https://api.sanity.io/v2021-06-07/projects/${{ vars.SANITY_STUDIO_PROJECT_ID }}', { + headers: { Authorization: 'Bearer ' + process.env.SANITY_AUTH_TOKEN } + }) + .then(r => r.json()) + .then(d => { + if (!d.organizationId) throw new Error('No organizationId in project response'); + console.log(d.organizationId); + }) + .catch(err => { console.error(err); process.exit(1); }); + ") + export SANITY_ORGANIZATION_ID="$ORG_ID" + EXISTING_APP_ID=$(node -e " + fetch('https://api.sanity.io/v2024-08-01/user-applications?appType=coreApp&organizationId=' + process.env.SANITY_ORGANIZATION_ID, { + headers: { Authorization: 'Bearer ' + process.env.SANITY_AUTH_TOKEN } + }) + .then(r => r.ok ? r.json() : []) + .then(apps => { + const list = Array.isArray(apps) ? apps : []; + const match = list.find(a => a.title === 'CodingCat Ops & Revenue OS'); + if (match && match.id) console.log(match.id); + }) + .catch(() => {}); + ") + if [ -n "$EXISTING_APP_ID" ]; then + echo "Found existing CodingCat Ops & Revenue OS app: $EXISTING_APP_ID" + SANITY_OPS_APP_ID="$EXISTING_APP_ID" npx sanity deploy -y + else + echo "Creating and deploying new CodingCat Ops & Revenue OS app in organization $ORG_ID..." + npx sanity deploy -y --create --title "CodingCat Ops & Revenue OS" + fi + env: + SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }} + - name: Promote Stack to Organization Scope run: | mkdir -p .sanity @@ -133,11 +262,11 @@ jobs: env: SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }} - - name: Deploy Sanity Blueprints Functions + - name: Deploy Sanity Blueprints & Poll Synchronously Until Completed run: | ORG_ID=$(node -e " fetch('https://api.sanity.io/v2021-06-07/projects/${{ vars.SANITY_STUDIO_PROJECT_ID }}', { - headers: { Authorization: 'Bearer ${{ secrets.SANITY_AUTH_TOKEN }}' } + headers: { Authorization: 'Bearer ' + process.env.SANITY_AUTH_TOKEN } }) .then(r => r.json()) .then(d => { @@ -149,7 +278,62 @@ jobs: echo "Resolved Organization ID: $ORG_ID" mkdir -p .sanity echo "{\"organizationId\":\"$ORG_ID\"}" > .sanity/blueprint.config.json + + echo "=== Previous Stack Info & Logs ===" + pnpm exec sanity blueprints info --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --verbose || true + pnpm exec sanity blueprints logs --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} || true + + echo "=== Starting Blueprint Deployment ===" pnpm exec sanity blueprints deploy --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --no-wait + + echo "=== Synchronously Polling Blueprint Stack Operation Until Terminal State ===" + ORG_ID="$ORG_ID" STACK_NAME="${{ vars.SANITY_STUDIO_DATASET }}" node -e " + const token = process.env.SANITY_AUTH_TOKEN; + const orgId = process.env.ORG_ID; + const stackName = process.env.STACK_NAME; + const headers = { + Accept: 'application/json', + Authorization: 'Bearer ' + token, + 'X-Sanity-Scope-Type': 'organization', + 'X-Sanity-Scope-Id': orgId, + }; + (async () => { + const stacksRes = await fetch('https://api.sanity.io/v2026-06-15/blueprints/stacks', { headers }); + if (!stacksRes.ok) throw new Error('Failed to list stacks: HTTP ' + stacksRes.status); + const stacks = await stacksRes.json(); + const stack = stacks.find(s => s.name === stackName || s.id === stackName); + if (!stack) throw new Error('Stack not found: ' + stackName); + console.log('Polling Stack:', stack.name, '(' + stack.id + ')'); + const deadline = Date.now() + 5 * 60 * 1000; + while (Date.now() < deadline) { + const res = await fetch('https://api.sanity.io/v2026-06-15/blueprints/stacks/' + stack.id, { headers }); + if (!res.ok) throw new Error('Failed to fetch stack ' + stack.id + ': HTTP ' + res.status); + const data = await res.json(); + const op = data.recentOperation; + const status = op?.status || 'NONE'; + console.log('[' + new Date().toISOString() + '] Operation ' + (op?.id || 'n/a') + ' status: ' + status); + if (status === 'COMPLETED' || status === 'NONE') { + console.log('✔ Blueprint Stack operation settled with status:', status); + process.exit(0); + } + if (status === 'FAILED' || status === 'CANCELLED') { + console.error('✖ Blueprint Stack operation failed:', JSON.stringify(op, null, 2)); + const logsRes = await fetch('https://api.sanity.io/v2026-06-15/blueprints/logs?stackId=' + stack.id + (op?.id ? '&operationId=' + op.id : ''), { headers }); + if (logsRes.ok) { + const logs = await logsRes.json(); + console.error('Operation logs:', JSON.stringify(logs, null, 2)); + } + process.exit(1); + } + await new Promise(r => setTimeout(r, 4000)); + } + console.error('✖ Timed out waiting 5 minutes for Blueprint Stack operation to complete'); + process.exit(1); + })().catch(err => { console.error(err); process.exit(1); }); + " + + echo "=== Verified Post-Deploy Stack Info ===" + pnpm exec sanity blueprints info --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --verbose env: SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }} SANITY_API_WRITE_TOKEN: ${{ secrets.SANITY_API_WRITE_TOKEN }} diff --git a/apps/ops/sanity.cli.ts b/apps/ops/sanity.cli.ts index 9da4e20e..c2313ab1 100644 --- a/apps/ops/sanity.cli.ts +++ b/apps/ops/sanity.cli.ts @@ -4,6 +4,15 @@ export default defineCliConfig({ app: { organizationId: process.env.SANITY_ORGANIZATION_ID || "ovF2qiKSO", entry: "./src/index.tsx", + title: "CodingCat Ops & Revenue OS", }, + ...(process.env.SANITY_OPS_APP_ID + ? { + deployment: { + appId: process.env.SANITY_OPS_APP_ID, + }, + } + : {}), }); +