vibecarbon 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/carbon/package.json
CHANGED
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@base-ui/react": "^1.5.0",
|
|
54
54
|
"@fontsource-variable/noto-sans": "^5.2.10",
|
|
55
55
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
|
56
|
-
"@hono/node-server": "^
|
|
56
|
+
"@hono/node-server": "^2.0.4",
|
|
57
57
|
"@hono/zod-openapi": "^1.4.0",
|
|
58
58
|
"@hookform/resolvers": "^5.4.0",
|
|
59
59
|
"@mdx-js/react": "^3.1.1",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibecarbon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Create and manage production-ready Vibecarbon applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./src/cli.js",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"@pulumi/pulumi": "^3.231.0",
|
|
62
62
|
"bcryptjs": "^3.0.3",
|
|
63
63
|
"undici": "^8.1.0",
|
|
64
|
-
"which": "^
|
|
64
|
+
"which": "^7.0.0"
|
|
65
65
|
},
|
|
66
66
|
"pnpm": {
|
|
67
67
|
"onlyBuiltDependencies": [
|
|
@@ -87,9 +87,9 @@
|
|
|
87
87
|
"better-sqlite3": "^12.8.0",
|
|
88
88
|
"conventional-changelog-conventionalcommits": "^9.3.1",
|
|
89
89
|
"msw": "^2.12.14",
|
|
90
|
-
"semantic-release": "^
|
|
90
|
+
"semantic-release": "^25.0.3",
|
|
91
91
|
"tsx": "^4.21.0",
|
|
92
|
-
"vite": "^
|
|
92
|
+
"vite": "^8.0.14",
|
|
93
93
|
"vitest": "^4.1.1",
|
|
94
94
|
"zod": "^4.3.6"
|
|
95
95
|
}
|
package/src/lib/deploy/bundle.js
CHANGED
|
@@ -207,6 +207,12 @@ export function renderBundle(projectName, options = {}) {
|
|
|
207
207
|
copyDirIfExist('volumes/db', 'volumes/db');
|
|
208
208
|
copyDirIfExist('supabase/migrations', 'supabase/migrations');
|
|
209
209
|
copyDirIfExist('volumes/kong', 'volumes/kong');
|
|
210
|
+
// Edge functions: docker-compose.prod.yml bind-mounts ./functions and starts
|
|
211
|
+
// the edge-runtime with `--main-service /home/deno/functions/main`. If this
|
|
212
|
+
// isn't bundled, the mount auto-creates an empty dir on the server and the
|
|
213
|
+
// runtime crash-loops with "could not find an appropriate entrypoint". The
|
|
214
|
+
// template ships a stub at functions/main/index.ts. RCA: prod-1 2026-05-26.
|
|
215
|
+
copyDirIfExist('functions', 'functions');
|
|
210
216
|
|
|
211
217
|
// volumes/traefik/middlewares.yml
|
|
212
218
|
const traefikMiddlewares = join(cwd, 'volumes', 'traefik', 'middlewares.yml');
|
|
@@ -734,17 +734,40 @@ export async function pullComposeImages(ip, sshKeyPath, projectName, options = {
|
|
|
734
734
|
export async function runMigrations(ip, sshKeyPath, projectName) {
|
|
735
735
|
const remoteDir = `/opt/${projectName}`;
|
|
736
736
|
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
737
|
+
// On a fresh volume, supabase/postgres runs its own first-boot initdb scripts
|
|
738
|
+
// (creating the supabase_admin role + auth/storage/realtime schemas) before
|
|
739
|
+
// our app migrations can apply. `pg_isready` flips true as soon as PG accepts
|
|
740
|
+
// connections — which can be *during* that init, before supabase_admin
|
|
741
|
+
// exists. Running migrations then errors (missing role/extension). Previously
|
|
742
|
+
// those errors were piped to /dev/null with `|| true`, so a fully-failed
|
|
743
|
+
// migration was indistinguishable from success and an empty schema shipped to
|
|
744
|
+
// prod (RCA prod-1 2026-05-26: 0 public tables, every DB feature 500'd).
|
|
745
|
+
//
|
|
746
|
+
// Gate on supabase_admin actually being able to run a query (not just
|
|
747
|
+
// pg_isready), polling up to ~3 min.
|
|
742
748
|
await sshRunAsync(
|
|
743
749
|
ip,
|
|
744
750
|
sshKeyPath,
|
|
745
|
-
`cd ${remoteDir} &&
|
|
751
|
+
`cd ${remoteDir} && ` +
|
|
752
|
+
`for i in $(seq 1 60); do ` +
|
|
753
|
+
`docker compose exec -T db psql -U supabase_admin -d postgres -c 'SELECT 1' >/dev/null 2>&1 && exit 0; ` +
|
|
754
|
+
`echo "[migrate] waiting for supabase_admin to accept queries (attempt $i/60)"; sleep 3; ` +
|
|
755
|
+
`done; echo "[migrate] supabase_admin never became ready" >&2; exit 1`,
|
|
756
|
+
{ timeout: 240_000 },
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
// Apply each migration with ON_ERROR_STOP=1 and abort on the first failure.
|
|
760
|
+
// A failed migration MUST fail the deploy — silently shipping an empty schema
|
|
761
|
+
// is far worse than a deploy the operator can see failed and retry. Errors
|
|
762
|
+
// propagate (no 2>/dev/null, no `|| true`); sshRunAsync throws on non-zero.
|
|
763
|
+
await sshRunAsync(
|
|
764
|
+
ip,
|
|
765
|
+
sshKeyPath,
|
|
766
|
+
`cd ${remoteDir} && ` +
|
|
746
767
|
`for f in $(ls supabase/migrations/ 2>/dev/null | sort); do ` +
|
|
747
|
-
`
|
|
768
|
+
`echo "[migrate] applying $f"; ` +
|
|
769
|
+
`cat "supabase/migrations/$f" | docker compose exec -T db psql -U supabase_admin -d postgres -v ON_ERROR_STOP=1 || ` +
|
|
770
|
+
`{ echo "[migrate] FAILED applying $f" >&2; exit 1; }; ` +
|
|
748
771
|
`done`,
|
|
749
772
|
{ timeout: 300_000 },
|
|
750
773
|
);
|
|
@@ -809,20 +832,28 @@ export async function deployCompose(options) {
|
|
|
809
832
|
onProgress('Starting services...');
|
|
810
833
|
await startComposeStack(ip, sshKeyPath, projectName, serviceOpts);
|
|
811
834
|
|
|
812
|
-
// Step 5: Run migrations
|
|
835
|
+
// Step 5: Run migrations. A failure here means an empty/partial schema, which
|
|
836
|
+
// makes every DB-backed feature 500 — so let it abort the deploy rather than
|
|
837
|
+
// swallowing it (RCA prod-1 2026-05-26: the previous blanket try/catch hid a
|
|
838
|
+
// fully-failed migration and shipped a schema-less prod that reported success).
|
|
813
839
|
onProgress('Running database migrations...');
|
|
814
840
|
const remoteDir = `/opt/${projectName}`;
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
841
|
+
await runMigrations(ip, sshKeyPath, projectName);
|
|
842
|
+
// Reload PostgREST schema cache so it sees newly created tables. The old
|
|
843
|
+
// `docker compose exec rest kill -s SIGUSR1 1` never worked — the
|
|
844
|
+
// postgrest/postgrest image has no `kill` binary ("exec: kill: executable
|
|
845
|
+
// file not found in $PATH"), so the reload silently no-op'd and fresh tables
|
|
846
|
+
// stayed invisible (PGRST205) until rest happened to restart. RCA prod-1
|
|
847
|
+
// 2026-05-26. Use the canonical SQL NOTIFY on the pgrst channel instead
|
|
848
|
+
// (db-channel-enabled defaults on); fall back to restarting rest if NOTIFY
|
|
849
|
+
// doesn't land. Best-effort: a missed reload self-heals on the next deploy.
|
|
850
|
+
await sshRunAsync(
|
|
851
|
+
ip,
|
|
852
|
+
sshKeyPath,
|
|
853
|
+
`cd ${remoteDir} && ` +
|
|
854
|
+
`docker compose exec -T db psql -U postgres -d postgres -c "NOTIFY pgrst, 'reload schema'" 2>/dev/null || ` +
|
|
855
|
+
`docker compose restart rest 2>/dev/null || true`,
|
|
856
|
+
);
|
|
826
857
|
|
|
827
858
|
return {
|
|
828
859
|
success: true,
|
|
@@ -844,6 +844,24 @@ export async function setupReplication(options) {
|
|
|
844
844
|
),
|
|
845
845
|
);
|
|
846
846
|
|
|
847
|
+
// Mirror of reloadPostgrest() in the base k8s path. The rollout-restart of
|
|
848
|
+
// supabase-supabase-rest above already refreshes its schema cache, but issue
|
|
849
|
+
// the canonical NOTIFY explicitly so the app tables stay visible to PostgREST
|
|
850
|
+
// even if that restart list ever changes — same PGRST205 class the base
|
|
851
|
+
// deploy guards against. Best-effort (ignoreError): a missed reload
|
|
852
|
+
// self-heals on the next rest restart.
|
|
853
|
+
await perfAsync('deploy.ha.replication.reloadPostgrest', () =>
|
|
854
|
+
runCommandAsync(
|
|
855
|
+
[
|
|
856
|
+
'ssh',
|
|
857
|
+
...sshOpts,
|
|
858
|
+
`root@${primaryIp}`,
|
|
859
|
+
`kubectl exec -i -n vibecarbon supabase-supabase-db-0 -- psql -U supabase_admin -d postgres -c "NOTIFY pgrst, 'reload schema'"`,
|
|
860
|
+
],
|
|
861
|
+
{ silent: true, ignoreError: true },
|
|
862
|
+
),
|
|
863
|
+
);
|
|
864
|
+
|
|
847
865
|
// 4. Initialize standby as a streaming replica of the primary.
|
|
848
866
|
// Modeled after standby-init.sh — runs pg_basebackup directly inside the standby pod
|
|
849
867
|
// connecting to the primary's supabase node via hostPort 5432 (the Hetzner Cloud firewall
|
|
@@ -1525,6 +1525,52 @@ export async function applyMigrations({ kubeconfig, projectDir }) {
|
|
|
1525
1525
|
}
|
|
1526
1526
|
}
|
|
1527
1527
|
|
|
1528
|
+
/**
|
|
1529
|
+
* Reload PostgREST's schema cache after applyMigrations.
|
|
1530
|
+
*
|
|
1531
|
+
* The chart's `rest` (supabase-supabase-rest) pod comes up during helm
|
|
1532
|
+
* `--wait`, BEFORE applyMigrations runs — so its in-memory schema cache
|
|
1533
|
+
* predates the app tables. Reads of /rest/v1/<table> then 404 with PGRST205
|
|
1534
|
+
* ("Could not find the table ... in the schema cache") until PostgREST
|
|
1535
|
+
* reloads. No DDL-watch event trigger exists in the app migrations or db init
|
|
1536
|
+
* to auto-NOTIFY (verified), so issue the canonical reload explicitly.
|
|
1537
|
+
*
|
|
1538
|
+
* This is the k8s mirror of the compose fix (compose/index.js): same intent —
|
|
1539
|
+
* `NOTIFY pgrst, 'reload schema'` on the db — via `kubectl exec` instead of
|
|
1540
|
+
* `docker compose exec`. Best-effort: a missed reload self-heals whenever the
|
|
1541
|
+
* rest pod next restarts (e.g. the k8s-ha replication path rollout-restarts it
|
|
1542
|
+
* anyway), so a failure here is logged, not fatal.
|
|
1543
|
+
*
|
|
1544
|
+
* @param {{kubeconfig: string}} args
|
|
1545
|
+
*/
|
|
1546
|
+
export async function reloadPostgrest({ kubeconfig }) {
|
|
1547
|
+
const env = { ...process.env, KUBECONFIG: kubeconfig };
|
|
1548
|
+
const { status, stderr } = await execKubectlOnce(
|
|
1549
|
+
[
|
|
1550
|
+
'-n',
|
|
1551
|
+
'vibecarbon',
|
|
1552
|
+
'exec',
|
|
1553
|
+
'-i',
|
|
1554
|
+
'--request-timeout=15s',
|
|
1555
|
+
'supabase-supabase-db-0',
|
|
1556
|
+
'--',
|
|
1557
|
+
'psql',
|
|
1558
|
+
'-U',
|
|
1559
|
+
'supabase_admin',
|
|
1560
|
+
'-d',
|
|
1561
|
+
'postgres',
|
|
1562
|
+
'-c',
|
|
1563
|
+
"NOTIFY pgrst, 'reload schema'",
|
|
1564
|
+
],
|
|
1565
|
+
{ env, killAfterMs: 20_000 },
|
|
1566
|
+
);
|
|
1567
|
+
if (status !== 0) {
|
|
1568
|
+
console.log(
|
|
1569
|
+
`[migrate] PostgREST schema reload NOTIFY failed (non-fatal, self-heals on rest restart): ${(stderr || '').trim().slice(-200)}`,
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1528
1574
|
/**
|
|
1529
1575
|
* Apply cert-manager + vibecarbon-secrets + the project's k8s/infra +
|
|
1530
1576
|
* k8s/base kustomizations, install supabase via helm, patch the app
|
|
@@ -2133,6 +2179,13 @@ export async function applyK3sManifests({
|
|
|
2133
2179
|
await perfAsync(`deploy.${perfPrefix}.applyMigrations`, async () =>
|
|
2134
2180
|
applyMigrations({ kubeconfig, projectDir }),
|
|
2135
2181
|
);
|
|
2182
|
+
// 8b. Reload PostgREST's schema cache so it sees the tables applyMigrations
|
|
2183
|
+
// just created. rest came up during helm --wait (before migrations) and
|
|
2184
|
+
// no DDL-watch trigger exists to auto-NOTIFY — without this, /rest/v1/*
|
|
2185
|
+
// 404s with PGRST205 until rest restarts. Mirrors the compose fix.
|
|
2186
|
+
await perfAsync(`deploy.${perfPrefix}.reloadPostgrest`, async () =>
|
|
2187
|
+
reloadPostgrest({ kubeconfig }),
|
|
2188
|
+
);
|
|
2136
2189
|
// 9. Wait for app rollout. The deployment template already has
|
|
2137
2190
|
// imagePullPolicy: IfNotPresent, so kubelet uses the sideloaded image
|
|
2138
2191
|
// without pulling. 300s wasn't enough on a cold deploy where the app
|