Skip to content
Draft
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
15 changes: 15 additions & 0 deletions .cursorignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
node_modules/*

dist/*
build/*
out/*
.next/*
.vercel/*
.netlify/*
.turbo/*
.vite/*
.webpack/*
.rollup.cache/*
.rpt2_cache/*
.rts2_cache_cjs/*
116 changes: 62 additions & 54 deletions .github/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ Complete reference for the deployment system. See [README.md](README.md) for qui
| `wranglerConfig` | string | `"wrangler.jsonc"` | Wrangler config file path |
| `wranglerEnv` | string | `"production"` | Wrangler environment |
| `healthCheckPath` | string | `"/"` | Path for health check |
| `requiresSecrets` | string[] | See below | Required GitHub secrets |
| `requiresSecrets` | string[] | `[]` | _(Optional)_ Extra secrets not in wrangler.jsonc (e.g. build-time) |

**Default Secrets:** `["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID"]`
> **SSOT:** `wrangler.jsonc` is the single source of truth for resource secrets. Placeholders in `env.production` /
> `env.preview` are auto-detected by the substitution script. `requiresSecrets` is optional and additive — only needed
> for secrets that don't appear in wrangler (e.g. build-time env vars). Base secrets (`CLOUDFLARE_API_TOKEN`,
> `CLOUDFLARE_ACCOUNT_ID`) are always verified automatically.

## Framework Examples

Expand All @@ -45,8 +48,7 @@ Complete reference for the deployment system. See [README.md](README.md) for qui
"buildCommand": "build",
"workerBuildCommand": null,
"outputDirectory": "dist",
"verifyPaths": ["dist", "cloudflare-worker.ts"],
"requiresSecrets": ["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID", "D1_DATABASE_ID", "KV_NAMESPACE_ID"]
"verifyPaths": ["dist", "cloudflare-worker.ts"]
}
```

Expand Down Expand Up @@ -187,20 +189,23 @@ node .github/scripts/discover-deployable-apps.mjs

## Wrangler Configuration

The workflow generates `wrangler.production.jsonc` by substituting placeholders in your `wrangler.jsonc`:
The workflow generates `wrangler.production.jsonc` (or `wrangler.preview.jsonc` for PR preview) by running
`.github/scripts/substitute-wrangler-secrets.py` **before** any wrangler deploy command. Order: Build → Generate config
(substitute secrets) → Verify → Deploy. The `cloudflare/wrangler-action` deploy step uses
`--config wrangler.production.jsonc` (or `wrangler.preview.jsonc`). Build steps do not use wrangler config.

**Source (wrangler.jsonc):**
**Convention:** Placeholder values in `wrangler.jsonc` are `ALL_CAPS_SNAKE_CASE` strings (e.g. `D1_DATABASE_ID`). The
substitution script **auto-detects** these from the target `env.production` or `env.preview` section and substitutes
each from the full GitHub Secrets bag (`${{ toJson(secrets) }}`). No key list, no per-secret env wiring, no mapping
table.

**Source (wrangler.jsonc env.production):**

```jsonc
{
"d1_databases": [
{
"database_id": "PRODUCTION_D1_DATABASE_ID",
},
],
"kv_namespaces": [
{
"id": "PRODUCTION_KV_NAMESPACE_ID",
"database_id": "D1_DATABASE_ID", // placeholder = GitHub Secret name
},
],
}
Expand All @@ -212,23 +217,28 @@ The workflow generates `wrangler.production.jsonc` by substituting placeholders
{
"d1_databases": [
{
"database_id": "abc123...", // ← From GitHub secret
},
],
"kv_namespaces": [
{
"id": "xyz789...", // ← From GitHub secret
"database_id": "abc123...", // ← substituted from GitHub Secret
},
],
}
```

**Supported placeholders:**
**Default placeholders:**

- env.production: `D1_DATABASE_ID`, `KV_NAMESPACE_ID`
- env.preview: `D1_PREVIEW_DATABASE_ID`, `KV_PREVIEW_NAMESPACE_ID`

**Multi-app:** Same placeholder name across apps → same GitHub Secret → shared resource. Different names → isolated.
Prefixing (e.g. `APP_1_D1_DATABASE_ID`) is a convention for clarity, not a requirement.

- `PRODUCTION_D1_DATABASE_ID` → `${{ secrets.D1_DATABASE_ID }}`
- `PRODUCTION_KV_NAMESPACE_ID` → `${{ secrets.KV_NAMESPACE_ID }}`
To add a new resource placeholder: set the GitHub Secret name as the value in `wrangler.jsonc` env section, add the
secret to GitHub. That's it — the script auto-detects and substitutes.

Add more by editing the workflow's "Generate production wrangler config" step.
`requiresSecrets` / `requiresPreviewSecrets` in `cloudflare-config.json` are **optional** and **additive** — used only
for early fail-fast verification. The Verify step **derives** placeholders from `wrangler.jsonc` (single source of
truth), then merges with base secrets (CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID) and any extra from
`requiresSecrets`. No drift: wrangler placeholders are always correct. Add `requiresSecrets` only for secrets not in
wrangler (e.g. build-time).

## Error Messages Reference

Expand Down Expand Up @@ -318,14 +328,16 @@ Make sure your app has a wrangler.jsonc file in its root directory.

### Secret Substitution Failed

From `.github/scripts/substitute-wrangler-secrets.py`:

```
❌ ERROR: Secret substitution incomplete

The following placeholders were not replaced:
PRODUCTION_D1_DATABASE_ID
Placeholders detected in env.production but no matching GitHub Secret:
• D1_DATABASE_ID

This usually means the corresponding GitHub secrets are not set.
Check that all required secrets are configured in repository settings.
Add the missing secrets in GitHub repository settings:
Settings → Secrets and variables → Actions → New repository secret
```

## Deployment Targets
Expand All @@ -345,10 +357,11 @@ Worker deployment indicators:
```
ottabase/
├── .github/
│ ├── scripts/
│ │ ├── discover-deployable-apps.mjs
│ │ └── substitute-wrangler-secrets.py # Substitutes secrets into wrangler config
│ ├── workflows/
│ │ └── deploy.yml # Main workflow
│ ├── scripts/
│ │ └── discover-deployable-apps.mjs
│ ├── README.md # Quick start
│ └── DEPLOYMENT.md # This file
Expand All @@ -365,41 +378,36 @@ ottabase/

## Extending the System

### Add Custom Secrets
### Add Custom Secrets or a Second App

**1. Add to app config:**
The substitution script auto-detects `ALL_CAPS_SNAKE_CASE` placeholder values from the target `env` section in
`wrangler.jsonc` and substitutes them from GitHub Secrets. You never edit the Python script or the workflow files. There
are only **2 places** to update:

```json
{
"requiresSecrets": ["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID", "MY_CUSTOM_SECRET"]
}
```
#### Example: Adding `APP_1` with its own isolated D1 database

**2. Update workflow:** Add case in "Verify required secrets" step:
**1. `wrangler.jsonc`** — use the secret name as the placeholder value:

```yaml
"MY_CUSTOM_SECRET")
if [ -z "${{ secrets.MY_CUSTOM_SECRET }}" ]; then
MISSING_SECRETS+=("$SECRET_NAME")
fi
;;
```jsonc
// apps/app-1/wrangler.jsonc → env.production
"d1_databases": [{
"binding": "OBCF_D1",
"database_name": "app1-db",
"database_id": "APP_1_D1_DATABASE_ID" // ← auto-detected as placeholder
}]
```

**3. Use in wrangler config:**
**2. GitHub repo → Settings → Secrets** — add `APP_1_D1_DATABASE_ID` with the actual D1 UUID.

```jsonc
{
"vars": {
"MY_VAR": "PRODUCTION_MY_CUSTOM_SECRET",
},
}
```
Done. No workflow edits, no `cloudflare-config.json` edits, no Python script edits.

**4. Add substitution:**
> **Sharing rule:** If two apps both use `D1_DATABASE_ID` as their placeholder, they resolve to the same GitHub Secret →
> same database. If App 1 uses `APP_1_D1_DATABASE_ID`, it gets its own isolated resource. Just naming.

```yaml
sed -e "s/PRODUCTION_MY_CUSTOM_SECRET/${{ secrets.MY_CUSTOM_SECRET }}/g"
```
#### Optional: Early verification

Add secret names to `requiresSecrets` in `cloudflare-config.json` for fail-fast checking **before** the build runs. This
is optional — if omitted, missing secrets are caught later at substitution time.

### Add New App Type

Expand Down
57 changes: 37 additions & 20 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ Already configured; push to `main` or open PRs as usual.
| `outputDirectory` | `"dist"` | Dir to verify after build |
| `verifyPaths` | `["dist", "cloudflare-worker.ts"]` | Paths that must exist after build |
| `wranglerConfig` | `"wrangler.jsonc"` | Wrangler config file |
| `requiresSecrets` | See below | GitHub secrets required for this app |
| `requiresSecrets` | `[]` | _(Optional)_ Extra secrets not in wrangler.jsonc (e.g. build-time) |

> **SSOT:** Placeholders in `wrangler.jsonc` `env.production` / `env.preview` are auto-detected. `requiresSecrets` is
> only for secrets that don't appear in wrangler.

### Minimal examples

Expand Down Expand Up @@ -84,33 +87,45 @@ Already configured; push to `main` or open PRs as usual.
"buildCommand": "build",
"outputDirectory": "dist",
"verifyPaths": ["dist", "cloudflare-worker.ts"],
"wranglerConfig": "wrangler.jsonc",
"requiresSecrets": ["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID", "D1_DATABASE_ID", "KV_NAMESPACE_ID"]
"wranglerConfig": "wrangler.jsonc"
}
```

### Wrangler placeholders

In `wrangler.jsonc` use placeholders; the workflow substitutes them from GitHub secrets:
In `wrangler.jsonc`, `ALL_CAPS_SNAKE_CASE` placeholder values in `env.production` and `env.preview` are
**auto-detected** by `substitute-wrangler-secrets.py` and substituted from GitHub Secrets. No explicit key list or
per-secret workflow wiring needed — just set the placeholder and the secret.

**Default (env.production):** `D1_DATABASE_ID`, `KV_NAMESPACE_ID` **Default (env.preview):** `D1_PREVIEW_DATABASE_ID`,
`KV_PREVIEW_NAMESPACE_ID`

- `PRODUCTION_D1_DATABASE_ID` → `D1_DATABASE_ID`
- `PRODUCTION_KV_NAMESPACE_ID` → `KV_NAMESPACE_ID`
- `YOUR_CLOUDFLARE_ACCOUNT_ID` → `CLOUDFLARE_ACCOUNT_ID`
**Multi-app:** Same placeholder name across apps → same GitHub Secret → shared resource. Different names → isolated.
Prefixing (e.g. `APP_1_D1_DATABASE_ID`) is a convention for clarity, not a requirement.

Generated file is `wrangler.production.jsonc` (or `wrangler.preview.jsonc` for PR preview); source file is not modified.
Generated files: `wrangler.production.jsonc` / `wrangler.preview.jsonc` (gitignored).

## Secrets

**Settings → Secrets and variables → Actions**

### Required (production and PR preview)
### Required for production deploy

| Secret | Where to get it |
| ----------------------- | ------------------------------------ |
| `CLOUDFLARE_API_TOKEN` | Cloudflare → My Profile → API Tokens |
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare → Workers & Pages |
| `D1_DATABASE_ID` | `pnpm cf:setup` output (ottabase-db) |
| `KV_NAMESPACE_ID` | `pnpm cf:setup` output (OBCF_KV) |

### Required for PR preview deploy

| Secret | Where to get it |
| ------------------------- | -------------------------------------------- |
| `D1_PREVIEW_DATABASE_ID` | `pnpm cf:setup` output (ottabase-db-preview) |
| `KV_PREVIEW_NAMESPACE_ID` | `pnpm cf:setup` output (OBCF_KV_preview) |

| Secret | Where to get it |
| ----------------------- | --------------------------------------------------- |
| `CLOUDFLARE_API_TOKEN` | Cloudflare → My Profile → API Tokens |
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare → Workers & Pages |
| `D1_DATABASE_ID` | `wrangler d1 create <name>` (if using D1) |
| `KV_NAMESPACE_ID` | `wrangler kv:namespace create <name>` (if using KV) |
PR preview uses isolated preview D1/KV/R2 so production data is never touched.

### Optional

Expand Down Expand Up @@ -143,9 +158,10 @@ Generated file is `wrangler.production.jsonc` (or `wrangler.preview.jsonc` for P
## PR preview (pr-preview.yml)

- **Triggers:** PR opened, synchronized, reopened, or closed.
- **Open/sync/reopen:** Builds packages, builds app(s), deploys preview worker(s) named e.g. `my-app-pr-123`. Preview
URL: `https://<preview-name>.<CF_WORKER_SUBDOMAIN>.workers.dev`.
- **Closed:** Deletes the preview worker for that PR.
- **Open/sync/reopen:** Builds packages, builds app(s), deploys preview worker(s) named e.g. `my-app-pr-123` using
**env.preview** bindings (ottabase-db-preview D1, OBCF_KV_preview, ottabase-bucket-preview). Preview URL:
`https://<preview-name>.<CF_WORKER_SUBDOMAIN>.workers.dev`.
- **Closed:** Deletes the preview worker (preview D1/KV persist; shared across PRs).
- **Skip:** If PR title or description contains `#skippr` or `#skipdeploy`, preview build and deploy are skipped. See
[Skip deployment](#skip-deployment).

Expand Down Expand Up @@ -190,7 +206,7 @@ pnpm preview # if available
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| App not discovered | `deployable: true` in `cloudflare-config.json`; `package.json` has required scripts; `wrangler.jsonc` present if no cloudflare-config |
| Build fails | Actions logs; locally: `pnpm --filter=@ottabase/my-app run build` |
| Deploy fails | Required secrets set; `wrangler.jsonc` valid; no remaining `PRODUCTION_*` in generated config |
| Deploy fails | Required secrets set; `wrangler.jsonc` valid; no unsubstituted placeholders in generated config |
| Preview not created | PR without `#skippr` / `#skipdeploy`; secrets set; app in `APPS_TO_DEPLOY` or default |

Errors in workflows include what failed, why, and how to fix (e.g. missing secrets with links to Cloudflare).
Expand All @@ -205,7 +221,8 @@ Errors in workflows include what failed, why, and how to fix (e.g. missing secre
│ ├── build-packages.yml # Reusable: build packages
│ └── ci.yml # Lint, type-check, test, build
├── scripts/
│ └── discover-deployable-apps.mjs
│ ├── discover-deployable-apps.mjs
│ └── substitute-wrangler-secrets.py # Substitutes secrets into wrangler config
├── README.md # This file
└── DEPLOYMENT.md # Full reference (config, errors, extending)
```
Expand Down
Loading