Skip to content

feat(syndication): create Google Task / Calendar reminder when Hashno… #149

feat(syndication): create Google Task / Calendar reminder when Hashno…

feat(syndication): create Google Task / Calendar reminder when Hashno… #149

Workflow file for this run

name: Deploy Sanity Studio
on:
workflow_dispatch:
push:
branches: [dev, main]
paths:
- "apps/sanity/**"
- "apps/ops/**"
- "sanity.blueprint.ts"
- "package.json"
- "pnpm-lock.yaml"
- ".github/workflows/deploy.yml"
jobs:
deploy-dev:
if: github.ref == 'refs/heads/dev' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
environment: dev
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Export Sanity production dataset backup before deploy
run: |
curl -f -sS "https://hfh83o0w.api.sanity.io/v2021-06-07/data/export/production" | gzip > /tmp/sanity-production-backup.ndjson.gz
- name: Upload Sanity production dataset backup artifact
uses: actions/upload-artifact@v4
with:
name: sanity-production-predeploy-backup-dev-${{ github.sha }}
path: /tmp/sanity-production-backup.ndjson.gz
retention-days: 30
- name: Sync Enriched Guest Socials to Sanity Dataset
run: node apps/sanity/scripts/sync-guest-socials.mjs
env:
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
SANITY_API_WRITE_TOKEN: ${{ secrets.SANITY_API_WRITE_TOKEN }}
SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }}
SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }}
- name: Deploy Sanity Studio
run: npx sanity deploy -y
working-directory: apps/sanity
env:
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
SANITY_STUDIO_HOSTNAME: ${{ vars.SANITY_STUDIO_HOSTNAME }}
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"
if [ "${{ vars.SANITY_STUDIO_DATASET }}" = "dev" ]; then
APP_TITLE="CodingCat Ops & Revenue OS (Dev)"
else
APP_TITLE="CodingCat Ops & Revenue OS (Production)"
fi
export APP_TITLE
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 targetTitle = process.env.APP_TITLE;
const isDev = process.env.SANITY_APP_DATASET === 'dev';
const match = list.find(a =>
a.title === targetTitle ||
(isDev && (a.id === 'yiugiba2sjnhpehqmky6nulc' || a.title === 'CodingCat Ops & Revenue OS'))
);
if (match && match.id) console.log(match.id);
})
.catch(() => {});
")
if [ -n "$EXISTING_APP_ID" ]; then
echo "Found existing $APP_TITLE app: $EXISTING_APP_ID"
SANITY_OPS_APP_ID="$EXISTING_APP_ID" npx sanity deploy -y
else
echo "Creating and deploying new $APP_TITLE app in organization $ORG_ID..."
npx sanity deploy -y --create --title "$APP_TITLE"
fi
env:
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }}
SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }}
SANITY_APP_DATASET: ${{ vars.SANITY_STUDIO_DATASET }}
SANITY_APP_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }}
- name: Promote Stack to Organization Scope
run: |
mkdir -p .sanity
echo '{"projectId":"${{ vars.SANITY_STUDIO_PROJECT_ID }}"}' > .sanity/blueprint.config.json
pnpm exec sanity blueprints promote --project-id ${{ vars.SANITY_STUDIO_PROJECT_ID }} --stack ${{ vars.SANITY_STUDIO_DATASET }} --force --verbose || true
env:
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
- 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 ' + process.env.SANITY_AUTH_TOKEN }
})
.then(r => r.json())
.then(d => {
if (!d.organizationId) throw new Error('No organizationId in project response: ' + JSON.stringify(d));
console.log(d.organizationId);
})
.catch(err => { console.error(err); process.exit(1); });
")
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 }} --verbose || true
echo "=== Blueprint Deployment Plan ==="
pnpm exec sanity blueprints plan --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --verbose || true
echo "=== Starting Blueprint Deployment (--verbose) ==="
pnpm exec sanity blueprints deploy --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --no-wait --verbose
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 & Logs (--verbose) ==="
pnpm exec sanity blueprints info --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --verbose
pnpm exec sanity blueprints logs --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --limit 50 --verbose || true
env:
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
SANITY_API_WRITE_TOKEN: ${{ secrets.SANITY_API_WRITE_TOKEN }}
SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }}
SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }}
YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }}
PRIVATE_DEVTO: ${{ secrets.PRIVATE_DEVTO }}
PRIVATE_HASHNODE: ${{ secrets.PRIVATE_HASHNODE }}
deploy-production:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Export Sanity production dataset backup before deploy
run: |
curl -f -sS "https://hfh83o0w.api.sanity.io/v2021-06-07/data/export/production" | gzip > /tmp/sanity-production-backup.ndjson.gz
env:
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
- name: Upload Sanity production dataset backup artifact
uses: actions/upload-artifact@v4
with:
name: sanity-production-predeploy-backup-${{ github.sha }}
path: /tmp/sanity-production-backup.ndjson.gz
retention-days: 30
- name: Sync Enriched Guest Socials to Sanity Dataset
run: node apps/sanity/scripts/sync-guest-socials.mjs
env:
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
SANITY_API_WRITE_TOKEN: ${{ secrets.SANITY_API_WRITE_TOKEN }}
SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }}
SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }}
- name: Deploy Sanity Studio
run: npx sanity deploy -y
working-directory: apps/sanity
env:
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
SANITY_STUDIO_HOSTNAME: ${{ vars.SANITY_STUDIO_HOSTNAME }}
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"
if [ "${{ vars.SANITY_STUDIO_DATASET }}" = "dev" ]; then
APP_TITLE="CodingCat Ops & Revenue OS (Dev)"
else
APP_TITLE="CodingCat Ops & Revenue OS (Production)"
fi
export APP_TITLE
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 targetTitle = process.env.APP_TITLE;
const isDev = process.env.SANITY_APP_DATASET === 'dev';
const match = list.find(a =>
a.title === targetTitle ||
(isDev && (a.id === 'yiugiba2sjnhpehqmky6nulc' || a.title === 'CodingCat Ops & Revenue OS'))
);
if (match && match.id) console.log(match.id);
})
.catch(() => {});
")
if [ -n "$EXISTING_APP_ID" ]; then
echo "Found existing $APP_TITLE app: $EXISTING_APP_ID"
SANITY_OPS_APP_ID="$EXISTING_APP_ID" npx sanity deploy -y
else
echo "Creating and deploying new $APP_TITLE app in organization $ORG_ID..."
npx sanity deploy -y --create --title "$APP_TITLE"
fi
env:
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }}
SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }}
SANITY_APP_DATASET: ${{ vars.SANITY_STUDIO_DATASET }}
SANITY_APP_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }}
- name: Promote Stack to Organization Scope
run: |
mkdir -p .sanity
echo '{"projectId":"${{ vars.SANITY_STUDIO_PROJECT_ID }}"}' > .sanity/blueprint.config.json
pnpm exec sanity blueprints promote --project-id ${{ vars.SANITY_STUDIO_PROJECT_ID }} --stack ${{ vars.SANITY_STUDIO_DATASET }} --force --verbose || true
env:
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
- 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 ' + process.env.SANITY_AUTH_TOKEN }
})
.then(r => r.json())
.then(d => {
if (!d.organizationId) throw new Error('No organizationId in project response: ' + JSON.stringify(d));
console.log(d.organizationId);
})
.catch(err => { console.error(err); process.exit(1); });
")
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 }} --verbose || true
echo "=== Blueprint Deployment Plan ==="
pnpm exec sanity blueprints plan --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --verbose || true
echo "=== Starting Blueprint Deployment (--verbose) ==="
pnpm exec sanity blueprints deploy --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --no-wait --verbose
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 & Logs (--verbose) ==="
pnpm exec sanity blueprints info --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --verbose
pnpm exec sanity blueprints logs --organization-id "$ORG_ID" --stack ${{ vars.SANITY_STUDIO_DATASET }} --limit 50 --verbose || true
env:
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
SANITY_API_WRITE_TOKEN: ${{ secrets.SANITY_API_WRITE_TOKEN }}
SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }}
SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }}
YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }}
PRIVATE_DEVTO: ${{ secrets.PRIVATE_DEVTO }}
PRIVATE_HASHNODE: ${{ secrets.PRIVATE_HASHNODE }}