/deploy

This is a /command script written and evolved during the development of Skillomatic. The script would take about 2 minutes to run, and when it failed, I would ask Claude to revise the script accordingly. As it’s presented here, it’s a collection of best practices for what it means to minimize cycle time in development.

Deploy

Deploy Skillomatic to production using SST.

RELATED: This command is paired with /rollback. Keep deployment logic in sync.

When updating: check /rollback for health endpoints, verification steps

Quick Deploy

Run these commands in sequence. Stop if any fails.

  1. Check upload speed (Docker image is ~300MB, need decent upload for MCP changes):
dd if=/dev/zero of=/tmp/speedtest.bin bs=1M count=2 2>/dev/null && curl -s -w "%{speed_upload}" -X POST -H "Content-Type: application/octet-stream" --data-binary @/tmp/speedtest.bin "https://speed.cloudflare.com/__up" -o /dev/null | awk '{mbps = $1 * 8 / 1000000; if (mbps < 10) printf "WARNING: Upload speed is %.1f Mbps - Docker push may take 5+ mins. Consider switching networks.\n", mbps; else printf "OK: Upload speed is %.1f Mbps\n", mbps}'
  1. Check git is clean and get hash:
git diff --quiet && git diff --cached --quiet || echo "ERROR: Uncommitted changes"
  1. Check extension zip version matches manifest, rebuild if needed:
grep '"version"' apps/skillomatic-scraper/manifest.json | sed 's/.*"\([0-9]*\.[0-9]*\.[0-9]*\)".*/\1/'
unzip -p apps/web/public/skillomatic-scraper.zip manifest.json 2>/dev/null | grep '"version"' | sed 's/.*"\([0-9]*\.[0-9]*\.[0-9]*\)".*/\1/'

If versions differ, rebuild the zip:

cd apps/skillomatic-scraper && rm -f ../web/public/skillomatic-scraper.zip && zip -r ../web/public/skillomatic-scraper.zip . -x 'node_modules/*' -x '*.git*' -x '*.DS_Store' && cd ../..
  1. Check what changed since last deploy:
git tag --list '[0-9]*' --sort=-v:refname | head -1

Use the tag number from above:

git diff --name-only <TAG>..HEAD

Analyze the changed files to determine if deployment is needed:

Changed PathAffects
apps/api/API (Lambda)
apps/web/Web (CloudFront)
apps/mcp-server/MCP (ECS)
packages/db/API, MCP (both use DB)
packages/shared/API, Web, MCP (all use shared)
packages/mcp/API (MCP tools in API)
sst.config.tsAll services
skills/None (runtime data)

If NO changes affect deployable services (e.g., only docs, .claude/, skills/), skip to step 9 and report “No deployment needed.”

  1. Run typecheck:
pnpm typecheck
  1. Check Turso auth before schema push:
turso auth whoami

If not logged in, run turso auth login first.

  1. Push schema to prod (ALWAYS run - schema drift can occur even without local changes):
pnpm db:push:prod

If Drizzle warns about data-loss (e.g., adding NOT NULL column without default), do NOT proceed with truncation. Instead:

  1. Add the column manually with a default value:
    turso db shell skillomatic "ALTER TABLE <table> ADD COLUMN <column> <TYPE> NOT NULL DEFAULT <value>;"
  2. Then re-run pnpm db:push:prod - it should now show no changes or safe changes only.

Common columns that need manual addition:

  1. Bump Docker cache bust if MCP-related changes (packages/mcp/, apps/mcp-server/, packages/db/):
# Check current cache bust version
grep 'CACHEBUST=v' apps/mcp-server/Dockerfile

If MCP-related files changed, increment the version number (e.g., v6 → v7):

sed -i '' 's/CACHEBUST=v[0-9]*/CACHEBUST=v<NEW_VERSION>/' apps/mcp-server/Dockerfile
git add apps/mcp-server/Dockerfile && git commit -m "Bump Docker cache bust" && git push
  1. Deploy with git hash:
SKILLOMATIC_DEPLOY=1 GIT_HASH="$(git rev-parse --short HEAD)" pnpm sst deploy --stage production

Note: Always run a full deploy (no --target flag). SST v3’s --target flag has known issues where Lambda code may not update reliably. The MCP Docker build is cached when unchanged, so full deploys are fast.

  1. Verify services are responding and git hashes match (call in parallel):
curl -s "https://api.skillomatic.technology/health" | jq -r '.gitHash'
curl -s "https://mcp.skillomatic.technology/health" | jq -r '.gitHash'
curl -s "https://skillomatic.technology" | grep 'git-hash' | grep -o 'content="[^"]*"' | grep -o '"[^"]*"$' | tr -d '"'

All three hashes must match the local commit. Retry with exponential backoff (2-64s) if CDN hasn’t propagated or MCP hasn’t rolled over (ECS rolling deployment can take up to 2 minutes).

  1. Create and push incremented version tag:
git tag --list '[0-9]*' --sort=-v:refname | head -1

Increment the tag number manually (e.g., if output is 17, use 18):

git tag <NEW_TAG> && git push origin <NEW_TAG>
  1. Report success with deployed services, git hashes, and the new version tag.

Stops on first failure. Always runs full deploy (MCP Docker is cached when unchanged). Uses exponential backoff (2-64s) for CDN propagation. Uses drizzle-kit push to sync schema to Turso.

Note: Schema changes should deprecate columns before removing them to support rollbacks. See /rollback command.

First-Time Setup

Set secrets as SST secrets (one-time). Note: names must match sst.config.ts:

pnpm sst secret set TursoDatabaseUrl "$(turso db show skillomatic --url)" --stage production
pnpm sst secret set TursoAuthToken "$(turso db tokens create skillomatic)" --stage production
pnpm sst secret set JwtSecret "$(openssl rand -hex 32)" --stage production
pnpm sst secret set GoogleClientId "$GOOGLE_CLIENT_ID" --stage production
pnpm sst secret set GoogleClientSecret "$GOOGLE_CLIENT_SECRET" --stage production

Seed Production Database

After schema push or DB recreation:

TURSO_DATABASE_URL=$(turso db show skillomatic --url) \
TURSO_AUTH_TOKEN=$(turso db tokens create skillomatic) \
pnpm --filter @skillomatic/db seed:prod

Debugging

# API Health
curl -s "https://api.skillomatic.technology/health" | jq .

# MCP Server Health
curl -s "https://mcp.skillomatic.technology/health" | jq .

# Login
curl -s -X POST "https://api.skillomatic.technology/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"email":"superadmin@skillomatic.technology","password":"Skillomatic2024"}' | jq .

# API key auth
curl -s -H "Authorization: Bearer sk_live_prod_super_admin_debug_key_2024" \
  "https://api.skillomatic.technology/v1/me" | jq .

Test credentials: superadmin@skillomatic.technology / Skillomatic2024

Troubleshooting

ErrorFix
”Invalid email or password”Seed prod database (see above)
“Failed query” with SQLSchema out of sync - run pnpm db:push:prod
”is not valid JSON” on loginMissing columns in prod DB - check schema with turso db shell skillomatic "PRAGMA table_info(<table>);" and add missing columns manually (see step 5)
Lambda using stale secretsRun pnpm sst secret set TursoAuthToken "$(turso db tokens create skillomatic)" --stage production
Recreate database from scratchturso db destroy skillomatic --yes && turso db create skillomatic, then push schema + seed + update SST secrets

SST Secrets Note

Secrets are accessed at runtime via Resource import (not env vars). When you run sst secret set, SST automatically restarts Lambda containers to pick up new values. Secret names must match exactly:


Rollback

Rollback to a previous production deployment by tag number.

RELATED: This command is paired with /deploy. Keep deployment logic in sync.

When updating: check /deploy for target mapping, health endpoints, verification steps

Usage

Steps

  1. Get current production hashes and find what tag is running:
curl -s "https://api.skillomatic.technology/health" | jq -r '.gitHash'
curl -s "https://mcp.skillomatic.technology/health" | jq -r '.gitHash'
git fetch --tags

Compare the production hash against tags to find which tag is currently deployed.

  1. Determine target tag:
  1. Check if already running target:
  1. Show rollback info and confirm:
git log -1 --format='%h %s' <target_tag>

Ask user to confirm: “Rollback from tag X to tag Y? (y/n)”

  1. Checkout target tag:
git checkout <target_tag>
  1. Run typecheck:
pnpm typecheck
  1. Deploy all services (NO db:push - schema stays current):
GIT_HASH=<target_hash> pnpm sst deploy --stage production

Note: Rollback deploys ALL services (no --target flag) because we’re reverting to a known-good state. Skipping db:push:prod intentionally - schema development should deprecate columns before dropping them, ensuring old code can run against newer schemas.

  1. Verify all services are responding and hashes match (call in parallel):
curl -s "https://api.skillomatic.technology/health" | jq -r '.gitHash'
curl -s "https://mcp.skillomatic.technology/health" | jq -r '.gitHash'
curl -s "https://skillomatic.technology" | grep 'git-hash' | sed 's/.*content="\([^"]*\)".*/\1/'

All three hashes must match the target tag’s commit hash.

Retry with exponential backoff (2-64s) if CDN hasn’t propagated or MCP hasn’t rolled over (ECS rolling deployment can take up to 2 minutes).

  1. Return to main:
git checkout main
  1. Report success with rolled-back tag and git hash.

Troubleshooting

If rollback fails mid-deploy, you’re in detached HEAD state. Run git checkout main to return.

Health Endpoints

ServiceEndpoint
APIhttps://api.skillomatic.technology/health
MCPhttps://mcp.skillomatic.technology/health
Webhttps://skillomatic.technology (check git-hash meta tag)