Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions .github/workflows/deploy-api.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
name: Deploy API to Cloudflare Workers

on:
push:
branches:
- main
paths:
- "apps/api-ground-codes/**"
- "packages/ground-codes/**"
- "packages/geoint/region-dist/**"
- "packages/codebook/codebook-dist/**"
- ".github/workflows/deploy-api.yml"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
workflow_dispatch:
inputs:
import_all_regions:
description: "Import every missing region dataset before deploying"
required: false
type: boolean
default: false

concurrency:
group: deploy-api-production
cancel-in-progress: false

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "22"
package-manager-cache: false

- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
version: 9.0.0

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Type check API
run: pnpm --filter api-ground-codes check-types

- name: Test API
run: pnpm --filter api-ground-codes test

- name: Build ground-codes package
run: pnpm --filter ground-codes build

- name: Install Wrangler
run: pnpm install -g wrangler

- name: Apply PostGIS schema
env:
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
PGSSLMODE: require
PGSSLREJECTUNAUTHORIZED: "0"
run: pnpm --filter api-ground-codes data:apply-postgis-schema

- name: Detect changed region datasets
id: regions
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.import_all_regions }}" = "true" ]; then
echo "datasets=__all_missing__" >> "$GITHUB_OUTPUT"
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "datasets=" >> "$GITHUB_OUTPUT"
else
DATASETS="$(node apps/api-ground-codes/scripts/list-changed-region-datasets.mjs "${{ github.event.before }}" "${{ github.sha }}")"
echo "datasets=${DATASETS}" >> "$GITHUB_OUTPUT"
fi

- name: Import changed region datasets
if: steps.regions.outputs.datasets != ''
env:
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
PGSSLMODE: require
PGSSLREJECTUNAUTHORIZED: "0"
REGION_IMPORT_BATCH_SIZE: "1500"
run: |
if [ "${{ steps.regions.outputs.datasets }}" = "__all_missing__" ]; then
REGION_IMPORT_MODE=missing pnpm --filter api-ground-codes data:import-postgis
else
REGION_DATASETS="${{ steps.regions.outputs.datasets }}" REGION_IMPORT_MODE=replace pnpm --filter api-ground-codes data:import-postgis
fi

- name: Deploy API Worker
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
wrangler deploy \
--config apps/api-ground-codes/wrangler.toml \
--keep-vars \
--var API_RUNTIME_TAG:workspace \
--var GIT_COMMIT_SHA:${{ github.sha }} \
--tag ${{ github.sha }} \
--message "Deploy API Worker from ${GITHUB_SHA}"

- name: Run production smoke
run: pnpm production:smoke
42 changes: 42 additions & 0 deletions .github/workflows/production-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ on:
types:
- completed

permissions:
contents: read
issues: write

jobs:
smoke:
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
Expand Down Expand Up @@ -43,3 +47,41 @@ jobs:
curl -X POST https://api.getmoshi.app/api/webhook \
-H "Content-Type: application/json" \
-d "{\"token\":\"${MOSHI_WEBHOOK_TOKEN}\",\"title\":\"ground.codes smoke failed\",\"message\":\"Production smoke failed on ${GITHUB_REF_NAME}. Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}\"}"

- name: Open smoke failure issue
if: failure() && env.MOSHI_WEBHOOK_TOKEN == ''
uses: actions/github-script@v8
with:
script: |
const title = "Production smoke failed";
const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
const message = [
`Production smoke failed on ${process.env.GITHUB_REF_NAME}.`,
"",
`Run: ${runUrl}`,
"",
"MOSHI_WEBHOOK_TOKEN is not configured, so this issue is the fallback notification.",
].join("\n");
const { owner, repo } = context.repo;
const { data: issues } = await github.rest.issues.listForRepo({
owner,
repo,
state: "open",
per_page: 50,
});
const existing = issues.find((issue) => issue.title === title);
if (existing) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: existing.number,
body: message,
});
} else {
await github.rest.issues.create({
owner,
repo,
title,
body: message,
});
}
1 change: 1 addition & 0 deletions apps/api-ground-codes/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
PORT=3000
CORS_ALLOWED_ORIGINS=https://ground.codes
API_RATE_LIMIT_PER_MINUTE=600
SUPABASE_DB_URL=postgresql://postgres:password@db.example.supabase.co:5432/postgres
37 changes: 35 additions & 2 deletions apps/api-ground-codes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,44 @@ set does not silently drift back to stale npm artifacts. `pnpm
runtime:update-pins` rewrites all three package pins together so Railway cannot
end up with a mixed runtime.

### ☁️ Cloudflare Workers + Supabase PostGIS

The API now has a Worker entrypoint at `src/worker.ts` and a Supabase/PostGIS
region store. `api.ground.codes/*` is routed to the `api-ground-codes` Worker,
so Cloudflare is the production serving layer while Supabase/PostGIS holds the
full generated region lookup data.

Apply the Supabase schema and import a small dataset first:

```bash
SUPABASE_DB_URL=postgresql://... \
REGION_DATASETS=region-2,region-2-mongolian \
REGION_IMPORT_MODE=replace \
pnpm --filter api-ground-codes data:import-postgis
```

Omit `REGION_DATASETS` to import every generated `region-dist/*.json` dataset.
The default `REGION_IMPORT_MODE=missing` skips datasets whose full row count is
already present, which makes the full import resumable.

Create the Cloudflare Hyperdrive binding against that database, then replace the
placeholder id in `wrangler.toml`:

```bash
pnpm dlx wrangler hyperdrive create ground-codes-supabase \
--connection-string "$SUPABASE_DB_URL"
```

At runtime the Worker reads `env.HYPERDRIVE.connectionString` and installs the
PostGIS-backed region store before handling requests. The Bun entrypoint
continues to use the existing local file/LevelDB data path unless a store is
installed explicitly.

### 🛰️ Production Monitoring

The `Production Smoke` workflow runs every 30 minutes and after the web deploy
workflow completes. It checks the Railway API, the Cloudflare Pages web app, and
API route metrics. It records per-check response times for `/readyz`,
workflow completes. It checks the Cloudflare Worker API, the Cloudflare Pages
web app, and API route metrics. It records per-check response times for `/readyz`,
`/v1/encode`, `/v1/search`, `/metrics`, `robots.txt`, and `sitemap.xml`, and
writes the timing table to the GitHub run summary. Add a `MOSHI_WEBHOOK_TOKEN`
repository secret to send a webhook alert when the smoke workflow fails.
Expand Down
6 changes: 5 additions & 1 deletion apps/api-ground-codes/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"build": "node scripts/build.mjs",
"start": "bun run src/index.ts",
"check-types": "tsc --noEmit",
"data:apply-postgis-schema": "node scripts/apply-postgis-schema.mjs",
"data:import-postgis": "node scripts/import-postgis-regions.mjs",
"test": "bun test src/*.test.ts"
},
"dependencies": {
Expand All @@ -18,9 +20,11 @@
"@repo/codebook": "workspace:*",
"@sinclair/typebox": "^0.34.30",
"elysia": "latest",
"ground-codes": "workspace:*"
"ground-codes": "workspace:*",
"pg": "^8.21.0"
},
"devDependencies": {
"@types/pg": "^8.20.0",
"bun-types": "latest"
},
"module": "src/index.js"
Expand Down
30 changes: 30 additions & 0 deletions apps/api-ground-codes/scripts/apply-postgis-schema.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { readFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { Client } from "pg";

const here = dirname(fileURLToPath(import.meta.url));
const schemaPath = resolve(here, "../supabase/001_ground_code_regions.sql");

const connectionString =
process.env.SUPABASE_DB_URL ?? process.env.DATABASE_URL;
if (!connectionString) {
console.error("Set SUPABASE_DB_URL or DATABASE_URL before applying schema.");
process.exit(1);
}

const client = new Client({
connectionString,
ssl:
process.env.PGSSLMODE === "require"
? { rejectUnauthorized: process.env.PGSSLREJECTUNAUTHORIZED !== "0" }
: undefined,
});

await client.connect();
try {
await client.query(await readFile(schemaPath, "utf8"));
console.log("Applied Supabase PostGIS schema.");
} finally {
await client.end();
}
Loading