vibecarbon 0.1.7 → 0.2.1
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/.env.example +3 -2
- package/carbon/DEVELOPMENT.md +18 -3
- package/carbon/PRODUCTION.md +1 -1
- package/carbon/content/docs/cli.mdx +3 -3
- package/carbon/content/docs/deployment.mdx +1 -1
- package/carbon/content/docs/development.mdx +1 -1
- package/carbon/content/docs/getting-started.mdx +0 -1
- package/carbon/docker-compose.yml +6 -1
- package/carbon/scripts/dev-init.js +1 -1
- package/carbon/scripts/generate-dev-configs.sh +5 -7
- package/carbon/scripts/validate-dev-configs.sh +5 -7
- package/carbon/src/client/components/TechStackSection.tsx +0 -68
- package/carbon/src/client/index.css +0 -14
- package/carbon/src/client/locales/de.json +0 -18
- package/carbon/src/client/locales/en.json +86 -36
- package/carbon/src/client/locales/es.json +0 -18
- package/carbon/src/client/locales/fr.json +0 -18
- package/carbon/src/client/locales/pt.json +0 -18
- package/carbon/src/client/pages/Home.tsx +0 -3
- package/package.json +4 -2
- package/services/index.json +29 -0
- package/services/metabase/compose/docker-compose.override.yml +14 -0
- package/services/metabase/compose/docker-compose.prod.yml +29 -0
- package/services/metabase/compose/docker-compose.yml +68 -0
- package/services/metabase/db/init.sh +29 -0
- package/services/metabase/docs/integration.md +191 -0
- package/services/metabase/k8s/deployment.yaml +88 -0
- package/services/metabase/k8s/ingressroute.yaml +22 -0
- package/services/metabase/k8s/kustomization.yaml +9 -0
- package/services/metabase/k8s/network-policy.yaml +74 -0
- package/services/metabase/k8s/pvc.yaml +12 -0
- package/services/metabase/k8s/service.yaml +15 -0
- package/services/metabase/manifest.json +34 -0
- package/services/metabase/scripts/setup.sh +152 -0
- package/services/n8n/compose/docker-compose.override.yml +14 -0
- package/services/n8n/compose/docker-compose.prod.yml +32 -0
- package/services/n8n/compose/docker-compose.yml +99 -0
- package/services/n8n/db/init.sh +25 -0
- package/services/n8n/docs/SSO.md +124 -0
- package/services/n8n/docs/integration.md +155 -0
- package/services/n8n/hooks/hooks.js +131 -0
- package/services/n8n/k8s/deployment.yaml +116 -0
- package/services/n8n/k8s/ingressroute.yaml +24 -0
- package/services/n8n/k8s/kustomization.yaml +9 -0
- package/services/n8n/k8s/network-policy.yaml +82 -0
- package/services/n8n/k8s/pvc.yaml +14 -0
- package/services/n8n/k8s/service.yaml +14 -0
- package/services/n8n/manifest.json +36 -0
- package/services/n8n/scripts/setup.sh +42 -0
- package/{carbon/docker-compose.observability.prod.yml → services/observability/compose/docker-compose.prod.yml} +8 -0
- package/services/observability/docs/integration.md +31 -0
- package/services/observability/k8s/grafana-deployment.yaml +107 -0
- package/services/observability/k8s/grafana-provisioning-configmap.yaml +65 -0
- package/services/observability/k8s/grafana-pvc.yaml +14 -0
- package/services/observability/k8s/grafana-service.yaml +16 -0
- package/services/observability/k8s/ingressroute.yaml +22 -0
- package/services/observability/k8s/kustomization.yaml +24 -0
- package/services/observability/k8s/loki-configmap.yaml +74 -0
- package/services/observability/k8s/loki-deployment.yaml +72 -0
- package/services/observability/k8s/loki-pvc.yaml +14 -0
- package/services/observability/k8s/loki-service.yaml +16 -0
- package/services/observability/k8s/network-policy.yaml +141 -0
- package/services/observability/k8s/prometheus-configmap.yaml +80 -0
- package/services/observability/k8s/prometheus-deployment.yaml +78 -0
- package/services/observability/k8s/prometheus-pvc.yaml +14 -0
- package/services/observability/k8s/prometheus-rbac.yaml +55 -0
- package/services/observability/k8s/prometheus-service.yaml +16 -0
- package/services/observability/manifest.json +28 -0
- package/services/redis/compose/docker-compose.prod.yml +10 -0
- package/services/redis/compose/docker-compose.yml +24 -0
- package/services/redis/docs/integration.md +148 -0
- package/services/redis/k8s/deployment.yaml +70 -0
- package/services/redis/k8s/kustomization.yaml +8 -0
- package/services/redis/k8s/network-policy.yaml +20 -0
- package/services/redis/k8s/pvc.yaml +12 -0
- package/services/redis/k8s/service.yaml +15 -0
- package/services/redis/manifest.json +30 -0
- package/services/s3/compose/docker-compose.s3-ha.yml +86 -0
- package/services/s3/compose/docker-compose.s3.yml +35 -0
- package/services/s3/manifest.json +87 -0
- package/src/add.js +57 -89
- package/src/cli.js +1 -1
- package/src/create.js +18 -87
- package/src/destroy.js +23 -1
- package/src/lib/deploy/bundle.js +4 -2
- package/src/lib/deploy/compose/ha.js +11 -8
- package/src/lib/deploy/compose/index.js +45 -17
- package/src/lib/deploy/image.js +10 -2
- package/src/lib/deploy/k8s/k3s.js +23 -4
- package/src/lib/deploy/orchestrator.js +13 -1
- package/src/lib/deploy/remote-build.js +29 -8
- package/src/remove.js +9 -6
- package/src/scale.js +14 -1
- package/carbon/content/docs/optional-services.mdx +0 -160
- package/carbon/src/client/components/MetricsStrip.tsx +0 -48
- /package/{carbon/docker-compose.observability.override.yml → services/observability/compose/docker-compose.override.yml} +0 -0
- /package/{carbon/docker-compose.observability.yml → services/observability/compose/docker-compose.yml} +0 -0
- /package/{carbon → services/observability}/volumes/grafana/dashboards/logs.json +0 -0
- /package/{carbon → services/observability}/volumes/grafana/dashboards/overview.json +0 -0
- /package/{carbon → services/observability}/volumes/grafana/dashboards/postgresql.json +0 -0
- /package/{carbon → services/observability}/volumes/grafana/dashboards.dev/logs.json +0 -0
- /package/{carbon → services/observability}/volumes/grafana/dashboards.dev/overview.json +0 -0
- /package/{carbon → services/observability}/volumes/grafana/dashboards.dev/postgresql.json +0 -0
- /package/{carbon → services/observability}/volumes/grafana/provisioning/dashboards/dashboards.dev.yml +0 -0
- /package/{carbon → services/observability}/volumes/grafana/provisioning/dashboards/dashboards.yml +0 -0
- /package/{carbon → services/observability}/volumes/grafana/provisioning/datasources/datasources.yml +0 -0
- /package/{carbon → services/observability}/volumes/loki/loki-config.yml +0 -0
- /package/{carbon → services/observability}/volumes/prometheus/prometheus.yml +0 -0
- /package/{carbon → services/observability}/volumes/promtail/promtail-config.yml +0 -0
package/src/add.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Vibecarbon Add Command
|
|
3
3
|
*
|
|
4
|
-
* Adds optional services to an existing Vibecarbon project.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Adds optional services to an existing Vibecarbon project. Service
|
|
5
|
+
* bundles ship with the CLI and are installed from the packaged
|
|
6
|
+
* `services/` directory by default — the same way `create` scaffolds
|
|
7
|
+
* from the packaged `carbon/` template, so an `add` is version-pinned
|
|
8
|
+
* and works offline. `-online` opts into fetching the latest bundles
|
|
9
|
+
* from GitHub (single-file assets only; directory assets such as `k8s/`
|
|
10
|
+
* still come from the packaged copy).
|
|
8
11
|
*
|
|
9
12
|
* Interactive-by-default: bare `vibecarbon add` prompts for a feature
|
|
10
13
|
* to add. Positional features (`vibecarbon add observability redis`)
|
|
@@ -53,7 +56,9 @@ const GITHUB_BRANCH = 'main';
|
|
|
53
56
|
const GITHUB_RAW_BASE = `https://raw.githubusercontent.com/${GITHUB_REPO}/${GITHUB_BRANCH}`;
|
|
54
57
|
const SERVICES_PATH = 'services';
|
|
55
58
|
|
|
56
|
-
//
|
|
59
|
+
// Packaged services directory — the default install source (shipped in the
|
|
60
|
+
// npm package `files`, alongside `carbon/`). `-online` overrides this to fetch
|
|
61
|
+
// from GitHub.
|
|
57
62
|
const LOCAL_SERVICES_DIR = join(__dirname, '..', SERVICES_PATH);
|
|
58
63
|
|
|
59
64
|
// Template variable placeholders
|
|
@@ -92,7 +97,7 @@ function buildSpec() {
|
|
|
92
97
|
summary: 'Add a feature to a Vibecarbon project',
|
|
93
98
|
description: [
|
|
94
99
|
'Available features:',
|
|
95
|
-
...
|
|
100
|
+
...AVAILABLE_FEATURES.map((f) => ` ${f.name.padEnd(16)} ${f.description}`),
|
|
96
101
|
'',
|
|
97
102
|
'External services (CI/CD, Stripe, OAuth, etc.) live in `vibecarbon configure`.',
|
|
98
103
|
].join('\n'),
|
|
@@ -109,9 +114,9 @@ function buildSpec() {
|
|
|
109
114
|
{ name: 'v', boolean: true, description: 'Show version' },
|
|
110
115
|
{ name: 'y', boolean: true, description: 'Skip confirmation prompts' },
|
|
111
116
|
{
|
|
112
|
-
name: '
|
|
117
|
+
name: 'online',
|
|
113
118
|
boolean: true,
|
|
114
|
-
description: '
|
|
119
|
+
description: 'Fetch the latest bundles from GitHub instead of the packaged copy',
|
|
115
120
|
},
|
|
116
121
|
],
|
|
117
122
|
examples: [
|
|
@@ -227,9 +232,13 @@ async function installServiceDirectory(serviceName, srcDir, destDir, variables,
|
|
|
227
232
|
}
|
|
228
233
|
}
|
|
229
234
|
} else {
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
|
|
235
|
+
// Online mode can't enumerate a remote directory (GitHub raw has no
|
|
236
|
+
// listing and we don't hit the contents API). Directory assets such as
|
|
237
|
+
// `k8s/` therefore only install from the packaged copy.
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Cannot fetch directory "${srcDir}" over -online (remote directory listing is not supported). ` +
|
|
240
|
+
'Drop -online to install this feature from the packaged template.',
|
|
241
|
+
);
|
|
233
242
|
}
|
|
234
243
|
}
|
|
235
244
|
|
|
@@ -290,88 +299,27 @@ function removeFromBaseKustomization(kustomizationEntry) {
|
|
|
290
299
|
writeFileSync(kustomizationPath, content);
|
|
291
300
|
}
|
|
292
301
|
|
|
293
|
-
// ============================================================================
|
|
294
|
-
// SPECIALIZED FEATURE HANDLERS
|
|
295
|
-
// ============================================================================
|
|
296
|
-
|
|
297
|
-
// Features with specialized handlers (not manifest-based). CI/CD used to be
|
|
298
|
-
// here too; it moved to `vibecarbon configure` (CI/CD is an external service,
|
|
299
|
-
// not a local container, so it lives with the other configure features).
|
|
300
|
-
const SPECIALIZED_FEATURES = ['observability'];
|
|
301
|
-
|
|
302
|
-
async function handleSpecializedFeature(featureName, options) {
|
|
303
|
-
const { project } = options;
|
|
304
|
-
|
|
305
|
-
switch (featureName) {
|
|
306
|
-
case 'observability': {
|
|
307
|
-
return handleObservabilityFeature(project);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
default:
|
|
311
|
-
throw new Error(`Unknown specialized feature: ${featureName}`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Enable observability stack (Prometheus, Grafana, Loki)
|
|
317
|
-
* Files are already included in the project - this just enables them
|
|
318
|
-
*/
|
|
319
|
-
async function handleObservabilityFeature(_project) {
|
|
320
|
-
const manifest = loadManifest();
|
|
321
|
-
|
|
322
|
-
// Check if already enabled
|
|
323
|
-
if (manifest.services?.observability) {
|
|
324
|
-
p.log.info('Observability is already enabled.');
|
|
325
|
-
return true;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Check that the observability compose file exists
|
|
329
|
-
const observabilityCompose = join(process.cwd(), 'docker-compose.observability.yml');
|
|
330
|
-
if (!existsSync(observabilityCompose)) {
|
|
331
|
-
p.log.error('Observability files not found. Please ensure your project is up to date.');
|
|
332
|
-
return false;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const s = p.spinner();
|
|
336
|
-
s.start('Enabling observability stack');
|
|
337
|
-
|
|
338
|
-
// Set env flags for dashboard visibility (client + server)
|
|
339
|
-
setEnvVar('VITE_OBSERVABILITY_ENABLED', 'true');
|
|
340
|
-
setEnvVar('OBSERVABILITY_ENABLED', 'true');
|
|
341
|
-
|
|
342
|
-
// Update manifest
|
|
343
|
-
if (!manifest.services) manifest.services = {};
|
|
344
|
-
manifest.services.observability = { addedAt: new Date().toISOString() };
|
|
345
|
-
saveManifest(manifest);
|
|
346
|
-
|
|
347
|
-
s.stop('Observability enabled');
|
|
348
|
-
|
|
349
|
-
p.note(
|
|
350
|
-
[
|
|
351
|
-
'Observability stack enabled:',
|
|
352
|
-
` ${c.info('Prometheus')} http://prometheus.localhost`,
|
|
353
|
-
` ${c.info('Grafana')} http://grafana.localhost`,
|
|
354
|
-
` ${c.info('Loki')} http://loki.localhost`,
|
|
355
|
-
].join('\n'),
|
|
356
|
-
'Observability',
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
return true;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
302
|
// ============================================================================
|
|
363
303
|
// ADD SERVICE
|
|
364
304
|
// ============================================================================
|
|
365
305
|
|
|
366
306
|
async function addService(serviceName, options) {
|
|
367
307
|
const { project } = options;
|
|
368
|
-
let { offline } = options;
|
|
369
308
|
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
309
|
+
// Parked features ship in `services/` but are turned off for this release.
|
|
310
|
+
// Refuse here — before any manifest fetch — so `add <parked>` can't install
|
|
311
|
+
// something we've hidden from the offering. `remove` is intentionally not
|
|
312
|
+
// guarded, so an existing install can still be cleaned up.
|
|
313
|
+
if (isParked(serviceName)) {
|
|
314
|
+
p.log.warn(`"${serviceName}" is not available in this release.`);
|
|
315
|
+
return false;
|
|
373
316
|
}
|
|
374
317
|
|
|
318
|
+
// Default install source is the packaged `services/` dir (mirrors how
|
|
319
|
+
// `create` uses the packaged `carbon/` template). `-online` opts into the
|
|
320
|
+
// GitHub fetch; on network failure we fall back to the packaged copy below.
|
|
321
|
+
let offline = !options.online;
|
|
322
|
+
|
|
375
323
|
let manifest;
|
|
376
324
|
try {
|
|
377
325
|
manifest = await fetchServiceManifest(serviceName, offline);
|
|
@@ -528,11 +476,18 @@ async function addService(serviceName, options) {
|
|
|
528
476
|
// LIST SERVICES
|
|
529
477
|
// ============================================================================
|
|
530
478
|
|
|
531
|
-
// Features that work locally without external dependencies
|
|
479
|
+
// Features that work locally without external dependencies.
|
|
480
|
+
//
|
|
481
|
+
// `status: 'parked'` turns a feature off for this release: it's hidden from the
|
|
482
|
+
// offering (help body + interactive prompt) and `add` refuses to install it,
|
|
483
|
+
// but its packaged bundle under `services/` stays in place so un-parking is a
|
|
484
|
+
// one-line flip. Existing installs can still be `vibecarbon remove`d. Broad
|
|
485
|
+
// add-on discovery will move to a marketplace later — this list is not the
|
|
486
|
+
// permanent catalog.
|
|
532
487
|
const LOCAL_FEATURES = [
|
|
533
488
|
{ name: 'observability', description: 'Prometheus, Grafana, Loki monitoring stack' },
|
|
534
|
-
{ name: 'n8n', description: 'Workflow automation platform' },
|
|
535
|
-
{ name: 'metabase', description: 'Business intelligence and analytics' },
|
|
489
|
+
{ name: 'n8n', description: 'Workflow automation platform', status: 'parked' },
|
|
490
|
+
{ name: 'metabase', description: 'Business intelligence and analytics', status: 'parked' },
|
|
536
491
|
{ name: 'redis', description: 'In-memory cache and session store' },
|
|
537
492
|
];
|
|
538
493
|
|
|
@@ -542,6 +497,19 @@ const LOCAL_FEATURES = [
|
|
|
542
497
|
|
|
543
498
|
const ALL_FEATURES = [...LOCAL_FEATURES];
|
|
544
499
|
|
|
500
|
+
// Features actually offered to users. Parked features are excluded from the
|
|
501
|
+
// help body and the interactive prompt, but remain in ALL_FEATURES so typo
|
|
502
|
+
// suggestions and the parked guard can still resolve their names.
|
|
503
|
+
const AVAILABLE_FEATURES = LOCAL_FEATURES.filter((f) => f.status !== 'parked');
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Whether a feature ships but is turned off for this release.
|
|
507
|
+
* @param {string} name
|
|
508
|
+
*/
|
|
509
|
+
function isParked(name) {
|
|
510
|
+
return LOCAL_FEATURES.some((f) => f.name === name && f.status === 'parked');
|
|
511
|
+
}
|
|
512
|
+
|
|
545
513
|
/**
|
|
546
514
|
* Calculate Levenshtein distance between two strings
|
|
547
515
|
*/
|
|
@@ -652,7 +620,7 @@ async function main(cliArgs) {
|
|
|
652
620
|
if (services.length === 0) {
|
|
653
621
|
const choice = await p.select({
|
|
654
622
|
message: 'Which feature do you want to add?',
|
|
655
|
-
options:
|
|
623
|
+
options: AVAILABLE_FEATURES.map((f) => ({
|
|
656
624
|
value: f.name,
|
|
657
625
|
label: f.name,
|
|
658
626
|
hint: f.description,
|
|
@@ -668,7 +636,7 @@ async function main(cliArgs) {
|
|
|
668
636
|
let success = true;
|
|
669
637
|
for (const serviceName of services) {
|
|
670
638
|
const result = await addService(serviceName, {
|
|
671
|
-
|
|
639
|
+
online: !!values.online,
|
|
672
640
|
project,
|
|
673
641
|
cliArgs: cliArgs, // Pass raw CLI args for infrastructure services
|
|
674
642
|
});
|
package/src/cli.js
CHANGED
|
@@ -95,7 +95,7 @@ ${c.bold('DEV COMMANDS')}
|
|
|
95
95
|
${c.info('down')} Stop local development environment
|
|
96
96
|
${c.info('status')} Show project and deployment status
|
|
97
97
|
${c.info('reset')} Reset local environment (removes all data)
|
|
98
|
-
${c.info('add')} [feature] Add features (
|
|
98
|
+
${c.info('add')} [feature] Add features (observability, redis)
|
|
99
99
|
${c.info('remove')} [feature] Remove features from a project
|
|
100
100
|
${c.info('configure')} Configure external services (billing, OAuth, SMTP, etc.)
|
|
101
101
|
${c.info('upgrade')} Upgrade infrastructure files to latest template
|
package/src/create.js
CHANGED
|
@@ -188,8 +188,8 @@ const SPEC = {
|
|
|
188
188
|
description: 'non-interactive (CI/CD)',
|
|
189
189
|
},
|
|
190
190
|
{
|
|
191
|
-
command: 'vibecarbon create my-saas -
|
|
192
|
-
description: 'create +
|
|
191
|
+
command: 'vibecarbon create my-saas -install',
|
|
192
|
+
description: 'create + run pm install (git is initialized automatically)',
|
|
193
193
|
},
|
|
194
194
|
],
|
|
195
195
|
};
|
|
@@ -697,13 +697,6 @@ async function bootstrap(cliArgs) {
|
|
|
697
697
|
'volumes/kong',
|
|
698
698
|
'volumes/db',
|
|
699
699
|
'volumes/traefik',
|
|
700
|
-
'volumes/prometheus',
|
|
701
|
-
'volumes/loki',
|
|
702
|
-
'volumes/promtail',
|
|
703
|
-
'volumes/grafana/provisioning/dashboards',
|
|
704
|
-
'volumes/grafana/provisioning/datasources',
|
|
705
|
-
'volumes/grafana/dashboards',
|
|
706
|
-
'volumes/grafana/dashboards.dev',
|
|
707
700
|
'.github/workflows',
|
|
708
701
|
'k8s/base/app',
|
|
709
702
|
'k8s/base/backup',
|
|
@@ -758,21 +751,11 @@ async function bootstrap(cliArgs) {
|
|
|
758
751
|
copyTemplate('.dockerignore', join(projectDir, '.dockerignore'), variables);
|
|
759
752
|
copyTemplate('docker-compose.yml', join(projectDir, 'docker-compose.yml'), variables);
|
|
760
753
|
copyTemplate('docker-compose.prod.yml', join(projectDir, 'docker-compose.prod.yml'), variables);
|
|
761
|
-
copyTemplate(
|
|
762
|
-
'docker-compose.observability.yml',
|
|
763
|
-
join(projectDir, 'docker-compose.observability.yml'),
|
|
764
|
-
variables,
|
|
765
|
-
);
|
|
766
754
|
copyTemplate(
|
|
767
755
|
'docker-compose.override.yml',
|
|
768
756
|
join(projectDir, 'docker-compose.override.yml'),
|
|
769
757
|
variables,
|
|
770
758
|
);
|
|
771
|
-
copyTemplate(
|
|
772
|
-
'docker-compose.observability.override.yml',
|
|
773
|
-
join(projectDir, 'docker-compose.observability.override.yml'),
|
|
774
|
-
variables,
|
|
775
|
-
);
|
|
776
759
|
copyTemplate('PRODUCTION.md', join(projectDir, 'PRODUCTION.md'), variables);
|
|
777
760
|
copyTemplate('DEVELOPMENT.md', join(projectDir, 'DEVELOPMENT.md'), variables);
|
|
778
761
|
copyTemplate(
|
|
@@ -780,11 +763,6 @@ async function bootstrap(cliArgs) {
|
|
|
780
763
|
join(projectDir, 'docker-compose.dev-init.yml'),
|
|
781
764
|
variables,
|
|
782
765
|
);
|
|
783
|
-
copyTemplate(
|
|
784
|
-
'docker-compose.observability.prod.yml',
|
|
785
|
-
join(projectDir, 'docker-compose.observability.prod.yml'),
|
|
786
|
-
variables,
|
|
787
|
-
);
|
|
788
766
|
chmodSync(join(projectDir, 'docker-entrypoint.sh'), 0o755);
|
|
789
767
|
|
|
790
768
|
await tick();
|
|
@@ -802,6 +780,16 @@ async function bootstrap(cliArgs) {
|
|
|
802
780
|
join(projectDir, 'volumes/db/set-passwords.sh'),
|
|
803
781
|
variables,
|
|
804
782
|
);
|
|
783
|
+
// wal-archive.sh is postgres's archive_command. It was missing from this copy
|
|
784
|
+
// list, so scaffolds shipped no source file — Docker then bind-mounted an
|
|
785
|
+
// empty DIRECTORY at /etc/postgresql/wal-archive.sh and postgres failed
|
|
786
|
+
// archiving with exit 126 ("is a directory"), re-pinning WAL. Copy it (the
|
|
787
|
+
// chmod block below makes it +x; the script has only shell $vars, no {{}}).
|
|
788
|
+
copyTemplate(
|
|
789
|
+
'volumes/db/wal-archive.sh',
|
|
790
|
+
join(projectDir, 'volumes/db/wal-archive.sh'),
|
|
791
|
+
variables,
|
|
792
|
+
);
|
|
805
793
|
copyTemplate('volumes/db/jwt.sql', join(projectDir, 'volumes/db/jwt.sql'), variables);
|
|
806
794
|
copyTemplate('volumes/db/realtime.sql', join(projectDir, 'volumes/db/realtime.sql'), variables);
|
|
807
795
|
copyTemplate(
|
|
@@ -830,68 +818,6 @@ async function bootstrap(cliArgs) {
|
|
|
830
818
|
variables,
|
|
831
819
|
);
|
|
832
820
|
|
|
833
|
-
// Observability config files (used by deploy --observability)
|
|
834
|
-
copyTemplate(
|
|
835
|
-
'volumes/prometheus/prometheus.yml',
|
|
836
|
-
join(projectDir, 'volumes/prometheus/prometheus.yml'),
|
|
837
|
-
variables,
|
|
838
|
-
);
|
|
839
|
-
copyTemplate(
|
|
840
|
-
'volumes/loki/loki-config.yml',
|
|
841
|
-
join(projectDir, 'volumes/loki/loki-config.yml'),
|
|
842
|
-
variables,
|
|
843
|
-
);
|
|
844
|
-
copyTemplate(
|
|
845
|
-
'volumes/promtail/promtail-config.yml',
|
|
846
|
-
join(projectDir, 'volumes/promtail/promtail-config.yml'),
|
|
847
|
-
variables,
|
|
848
|
-
);
|
|
849
|
-
copyTemplate(
|
|
850
|
-
'volumes/grafana/provisioning/dashboards/dashboards.yml',
|
|
851
|
-
join(projectDir, 'volumes/grafana/provisioning/dashboards/dashboards.yml'),
|
|
852
|
-
variables,
|
|
853
|
-
);
|
|
854
|
-
copyTemplate(
|
|
855
|
-
'volumes/grafana/provisioning/dashboards/dashboards.dev.yml',
|
|
856
|
-
join(projectDir, 'volumes/grafana/provisioning/dashboards/dashboards.dev.yml'),
|
|
857
|
-
variables,
|
|
858
|
-
);
|
|
859
|
-
copyTemplate(
|
|
860
|
-
'volumes/grafana/provisioning/datasources/datasources.yml',
|
|
861
|
-
join(projectDir, 'volumes/grafana/provisioning/datasources/datasources.yml'),
|
|
862
|
-
variables,
|
|
863
|
-
);
|
|
864
|
-
copyTemplate(
|
|
865
|
-
'volumes/grafana/dashboards/logs.json',
|
|
866
|
-
join(projectDir, 'volumes/grafana/dashboards/logs.json'),
|
|
867
|
-
variables,
|
|
868
|
-
);
|
|
869
|
-
copyTemplate(
|
|
870
|
-
'volumes/grafana/dashboards/overview.json',
|
|
871
|
-
join(projectDir, 'volumes/grafana/dashboards/overview.json'),
|
|
872
|
-
variables,
|
|
873
|
-
);
|
|
874
|
-
copyTemplate(
|
|
875
|
-
'volumes/grafana/dashboards/postgresql.json',
|
|
876
|
-
join(projectDir, 'volumes/grafana/dashboards/postgresql.json'),
|
|
877
|
-
variables,
|
|
878
|
-
);
|
|
879
|
-
copyTemplate(
|
|
880
|
-
'volumes/grafana/dashboards.dev/logs.json',
|
|
881
|
-
join(projectDir, 'volumes/grafana/dashboards.dev/logs.json'),
|
|
882
|
-
variables,
|
|
883
|
-
);
|
|
884
|
-
copyTemplate(
|
|
885
|
-
'volumes/grafana/dashboards.dev/overview.json',
|
|
886
|
-
join(projectDir, 'volumes/grafana/dashboards.dev/overview.json'),
|
|
887
|
-
variables,
|
|
888
|
-
);
|
|
889
|
-
copyTemplate(
|
|
890
|
-
'volumes/grafana/dashboards.dev/postgresql.json',
|
|
891
|
-
join(projectDir, 'volumes/grafana/dashboards.dev/postgresql.json'),
|
|
892
|
-
variables,
|
|
893
|
-
);
|
|
894
|
-
|
|
895
821
|
copyTemplate('backup/backup.sh', join(projectDir, 'backup/backup.sh'), variables);
|
|
896
822
|
copyTemplate('backup/compose-backup.sh', join(projectDir, 'backup/compose-backup.sh'), variables);
|
|
897
823
|
copyTemplate('backup/Dockerfile', join(projectDir, 'backup/Dockerfile'), variables);
|
|
@@ -938,6 +864,11 @@ async function bootstrap(cliArgs) {
|
|
|
938
864
|
'docker-entrypoint.sh',
|
|
939
865
|
'volumes/kong/docker-entrypoint.sh',
|
|
940
866
|
'volumes/db/set-passwords.sh',
|
|
867
|
+
// Postgres execs this directly as its archive_command, so it MUST be +x.
|
|
868
|
+
// copyTemplate (copyFileSync) drops the source's exec bit, and omitting it
|
|
869
|
+
// here shipped a 0644 script → archive_command failed with exit 126 ("not
|
|
870
|
+
// executable") → WAL pinned → disk-fill risk the wrapper exists to prevent.
|
|
871
|
+
'volumes/db/wal-archive.sh',
|
|
941
872
|
];
|
|
942
873
|
for (const script of shellScripts) {
|
|
943
874
|
const scriptPath = join(projectDir, script);
|
|
@@ -1180,7 +1111,7 @@ fi
|
|
|
1180
1111
|
# Runs lint + unit + component + integration tests.
|
|
1181
1112
|
#
|
|
1182
1113
|
# No-ops gracefully if node_modules isn't present yet — a fresh
|
|
1183
|
-
# \`vibecarbon create
|
|
1114
|
+
# \`vibecarbon create\` lands you on a commit that's pushable
|
|
1184
1115
|
# before you've run \`pnpm install\`. Once deps land the hook gates
|
|
1185
1116
|
# every subsequent push. Bypass at will with \`git push --no-verify\`.
|
|
1186
1117
|
|
package/src/destroy.js
CHANGED
|
@@ -693,7 +693,29 @@ async function handleBackupBucket(envConfig, projectConfig, args, spinner) {
|
|
|
693
693
|
}
|
|
694
694
|
const s3Provider = new HetznerS3Provider(s3Creds.accessKey, s3Creds.secretKey, region);
|
|
695
695
|
try {
|
|
696
|
-
|
|
696
|
+
// Bound the purge so a slow/contended S3 endpoint can't consume the whole
|
|
697
|
+
// destroy budget. The Hetzner resources are already freed (Pulumi destroy
|
|
698
|
+
// ran before this), but a single S3 op can hang up to ~9 min under the
|
|
699
|
+
// SDK (maxAttempts 3 × 60s requestTimeout) × _send (maxAttempts 3) retry
|
|
700
|
+
// layers — observed on k8s teardown, where the storage bucket doubles as
|
|
701
|
+
// the Pulumi state backend (slow/contended), blowing the 600s
|
|
702
|
+
// final-destroy timeout and getting the whole step SIGKILLed. On timeout
|
|
703
|
+
// we leave the bucket for the orphan sweep / next destroy rather than hang.
|
|
704
|
+
const PURGE_TIMEOUT_MS = 180_000;
|
|
705
|
+
const result = await Promise.race([
|
|
706
|
+
s3Provider.emptyAndDeleteBucket(bucketName),
|
|
707
|
+
new Promise((_, reject) =>
|
|
708
|
+
setTimeout(
|
|
709
|
+
() =>
|
|
710
|
+
reject(
|
|
711
|
+
new Error(
|
|
712
|
+
`backup bucket purge exceeded ${PURGE_TIMEOUT_MS / 1000}s — left for the orphan sweep / a retried destroy`,
|
|
713
|
+
),
|
|
714
|
+
),
|
|
715
|
+
PURGE_TIMEOUT_MS,
|
|
716
|
+
),
|
|
717
|
+
),
|
|
718
|
+
]);
|
|
697
719
|
spinner.stop(
|
|
698
720
|
result.deleted
|
|
699
721
|
? result.objectsRemoved > 0
|
package/src/lib/deploy/bundle.js
CHANGED
|
@@ -96,7 +96,9 @@ function composeFileFlags(options = {}) {
|
|
|
96
96
|
if (options.metabase) {
|
|
97
97
|
files.push('-f', 'docker-compose.metabase.yml', '-f', 'docker-compose.metabase.prod.yml');
|
|
98
98
|
}
|
|
99
|
-
if (options.redis)
|
|
99
|
+
if (options.redis) {
|
|
100
|
+
files.push('-f', 'docker-compose.redis.yml', '-f', 'docker-compose.redis.prod.yml');
|
|
101
|
+
}
|
|
100
102
|
return files.join(' ');
|
|
101
103
|
}
|
|
102
104
|
|
|
@@ -126,7 +128,7 @@ export function renderBundle(projectName, options = {}) {
|
|
|
126
128
|
if (options.metabase) {
|
|
127
129
|
rootFiles.push('docker-compose.metabase.yml', 'docker-compose.metabase.prod.yml');
|
|
128
130
|
}
|
|
129
|
-
if (options.redis) rootFiles.push('docker-compose.redis.yml');
|
|
131
|
+
if (options.redis) rootFiles.push('docker-compose.redis.yml', 'docker-compose.redis.prod.yml');
|
|
130
132
|
|
|
131
133
|
for (const file of rootFiles) {
|
|
132
134
|
const src = join(cwd, file);
|
|
@@ -1043,15 +1043,18 @@ export async function deployComposeHA(options) {
|
|
|
1043
1043
|
]),
|
|
1044
1044
|
);
|
|
1045
1045
|
|
|
1046
|
-
// Run migrations on primary only
|
|
1046
|
+
// Run migrations on primary only. Let failures propagate — silently shipping
|
|
1047
|
+
// an empty/partial schema is far worse than a visibly-failed deploy the
|
|
1048
|
+
// operator can retry. The old swallowing try/catch ("non-fatal") masked
|
|
1049
|
+
// exactly that: HA could deploy a schema-less DB that 500s on every DB-backed
|
|
1050
|
+
// route with no error surfaced. runMigrations now waits for supabase_admin AND
|
|
1051
|
+
// the storage schema (storage.buckets.public) before applying with
|
|
1052
|
+
// ON_ERROR_STOP=1, so the first-run races that motivated the swallow are gone
|
|
1053
|
+
// and a failure here is a real one that must abort the deploy.
|
|
1047
1054
|
onProgress('Running database migrations on primary...');
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
);
|
|
1052
|
-
} catch {
|
|
1053
|
-
// Migrations may partially fail on first run, non-fatal
|
|
1054
|
-
}
|
|
1055
|
+
await perfAsync('deploy.ha.compose.migrations', () =>
|
|
1056
|
+
runMigrations(primaryIp, sshKeyPath, projectName),
|
|
1057
|
+
);
|
|
1055
1058
|
|
|
1056
1059
|
// Create admin user in production Supabase (same as single-server deploy)
|
|
1057
1060
|
onProgress('Creating admin user...');
|
|
@@ -493,7 +493,9 @@ function composeFileFlags(options = {}) {
|
|
|
493
493
|
if (options.metabase) {
|
|
494
494
|
files.push('-f', 'docker-compose.metabase.yml', '-f', 'docker-compose.metabase.prod.yml');
|
|
495
495
|
}
|
|
496
|
-
if (options.redis)
|
|
496
|
+
if (options.redis) {
|
|
497
|
+
files.push('-f', 'docker-compose.redis.yml', '-f', 'docker-compose.redis.prod.yml');
|
|
498
|
+
}
|
|
497
499
|
return files.join(' ');
|
|
498
500
|
}
|
|
499
501
|
|
|
@@ -756,6 +758,27 @@ export async function runMigrations(ip, sshKeyPath, projectName) {
|
|
|
756
758
|
{ timeout: 240_000 },
|
|
757
759
|
);
|
|
758
760
|
|
|
761
|
+
// Also wait for the storage service to finish ITS first-boot migrations
|
|
762
|
+
// before applying app migrations. supabase-storage adds columns to the
|
|
763
|
+
// `storage` schema on startup (notably storage.buckets.public); our
|
|
764
|
+
// 00001_init.sql seeds buckets referencing them. Racing ahead fails with
|
|
765
|
+
// `column "public" of relation "buckets" does not exist`, which aborts the
|
|
766
|
+
// whole migration — the core public.* tables get created first, but the
|
|
767
|
+
// deploy still fails (deployComposeHA only ever masked this by swallowing the
|
|
768
|
+
// error in a try/catch). The supabase_admin gate above is insufficient: that
|
|
769
|
+
// role exists before storage has migrated. Gate on storage.buckets.public
|
|
770
|
+
// being queryable as the proxy for "storage migrations done", polling ~3 min.
|
|
771
|
+
await sshRunAsync(
|
|
772
|
+
ip,
|
|
773
|
+
sshKeyPath,
|
|
774
|
+
`cd ${remoteDir} && ` +
|
|
775
|
+
`for i in $(seq 1 60); do ` +
|
|
776
|
+
`docker compose exec -T db psql -U supabase_admin -d postgres -c 'SELECT public FROM storage.buckets LIMIT 0' >/dev/null 2>&1 && exit 0; ` +
|
|
777
|
+
`echo "[migrate] waiting for storage schema (buckets.public) to be ready (attempt $i/60)"; sleep 3; ` +
|
|
778
|
+
`done; echo "[migrate] storage schema never became ready" >&2; exit 1`,
|
|
779
|
+
{ timeout: 240_000 },
|
|
780
|
+
);
|
|
781
|
+
|
|
759
782
|
// Apply each migration with ON_ERROR_STOP=1 and abort on the first failure.
|
|
760
783
|
// A failed migration MUST fail the deploy — silently shipping an empty schema
|
|
761
784
|
// is far worse than a deploy the operator can see failed and retry. Errors
|
|
@@ -771,6 +794,25 @@ export async function runMigrations(ip, sshKeyPath, projectName) {
|
|
|
771
794
|
`done`,
|
|
772
795
|
{ timeout: 300_000 },
|
|
773
796
|
);
|
|
797
|
+
|
|
798
|
+
// Reload PostgREST schema cache so it sees newly created tables. The old
|
|
799
|
+
// `docker compose exec rest kill -s SIGUSR1 1` never worked — the
|
|
800
|
+
// postgrest/postgrest image has no `kill` binary ("exec: kill: executable
|
|
801
|
+
// file not found in $PATH"), so the reload silently no-op'd and fresh tables
|
|
802
|
+
// stayed invisible (PGRST205) until rest happened to restart. RCA prod-1
|
|
803
|
+
// 2026-05-26. Use the canonical SQL NOTIFY on the pgrst channel instead
|
|
804
|
+
// (db-channel-enabled defaults on); fall back to restarting rest if NOTIFY
|
|
805
|
+
// doesn't land. Best-effort: a missed reload self-heals on the next deploy.
|
|
806
|
+
// Lives here (not just in deployCompose) so every caller — deployCompose,
|
|
807
|
+
// deployComposeHA, and the orchestrator's inline single-compose path — gets
|
|
808
|
+
// a queryable schema instead of PGRST205.
|
|
809
|
+
await sshRunAsync(
|
|
810
|
+
ip,
|
|
811
|
+
sshKeyPath,
|
|
812
|
+
`cd ${remoteDir} && ` +
|
|
813
|
+
`docker compose exec -T db psql -U postgres -d postgres -c "NOTIFY pgrst, 'reload schema'" 2>/dev/null || ` +
|
|
814
|
+
`docker compose restart rest 2>/dev/null || true`,
|
|
815
|
+
);
|
|
774
816
|
}
|
|
775
817
|
|
|
776
818
|
/**
|
|
@@ -837,23 +879,9 @@ export async function deployCompose(options) {
|
|
|
837
879
|
// swallowing it (RCA prod-1 2026-05-26: the previous blanket try/catch hid a
|
|
838
880
|
// fully-failed migration and shipped a schema-less prod that reported success).
|
|
839
881
|
onProgress('Running database migrations...');
|
|
840
|
-
|
|
882
|
+
// runMigrations applies supabase/migrations/* AND reloads PostgREST's schema
|
|
883
|
+
// cache, so the new tables are immediately queryable (no PGRST205).
|
|
841
884
|
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
|
-
);
|
|
857
885
|
|
|
858
886
|
return {
|
|
859
887
|
success: true,
|
package/src/lib/deploy/image.js
CHANGED
|
@@ -92,11 +92,11 @@ export function inspectGitState(cwd) {
|
|
|
92
92
|
* pod (Phase 1 wired the registries.yaml; Phase 6 wires the push).
|
|
93
93
|
*
|
|
94
94
|
* @param {string} projectDir - Project root with the Dockerfile.
|
|
95
|
-
* @param {{projectName: string, timestamp?: string, rebuild?: boolean, tagPrefix?: string}} options
|
|
95
|
+
* @param {{projectName: string, timestamp?: string, rebuild?: boolean, tagPrefix?: string, buildArgs?: Record<string,string>}} options
|
|
96
96
|
* @returns {{tag: string, gitSha: string, isDirty: boolean}}
|
|
97
97
|
*/
|
|
98
98
|
export function buildLocalImage(projectDir, options) {
|
|
99
|
-
const { projectName, rebuild = false, tagPrefix } = options;
|
|
99
|
+
const { projectName, rebuild = false, tagPrefix, buildArgs = {} } = options;
|
|
100
100
|
if (!projectName) {
|
|
101
101
|
throw new Error('buildLocalImage: projectName is required');
|
|
102
102
|
}
|
|
@@ -112,6 +112,14 @@ export function buildLocalImage(projectDir, options) {
|
|
|
112
112
|
|
|
113
113
|
const args = ['build'];
|
|
114
114
|
if (rebuild) args.push('--no-cache');
|
|
115
|
+
// Vite inlines import.meta.env.VITE_* at build time, so the browser bundle
|
|
116
|
+
// bakes whatever these ARGs resolve to. Without passing them the bundle ships
|
|
117
|
+
// empty VITE_SUPABASE_* and crashes at page load with "Missing Supabase
|
|
118
|
+
// environment variables" — k8s shipped exactly this because buildAppImage
|
|
119
|
+
// passed no build args (compose plumbs them via collectComposeBuildArgs).
|
|
120
|
+
for (const [k, v] of Object.entries(buildArgs)) {
|
|
121
|
+
if (v !== undefined && v !== null && v !== '') args.push('--build-arg', `${k}=${v}`);
|
|
122
|
+
}
|
|
115
123
|
args.push('-t', tag, projectDir);
|
|
116
124
|
|
|
117
125
|
execFileSync('docker', args, { stdio: 'inherit' });
|
|
@@ -556,9 +556,25 @@ export function loadEnvLocal(envPath) {
|
|
|
556
556
|
* @param {boolean} [rebuild]
|
|
557
557
|
* @returns {Promise<{tag: string, gitSha: string, isDirty: boolean}>}
|
|
558
558
|
*/
|
|
559
|
-
export async function buildAppImage(projectDir, projectName, rebuild = false) {
|
|
559
|
+
export async function buildAppImage(projectDir, projectName, rebuild = false, domain = undefined) {
|
|
560
560
|
const { buildLocalImage } = await import('../image.js');
|
|
561
|
-
|
|
561
|
+
// App image: collect VITE_* build args (incl. VITE_SUPABASE_URL rewritten to
|
|
562
|
+
// https://api.<domain>) so the browser bundle ships with real config instead
|
|
563
|
+
// of empty strings. buildLocalImage previously passed NO build args, so the
|
|
564
|
+
// k8s SPA crashed on load with "Missing Supabase environment variables" —
|
|
565
|
+
// compose plumbs these via collectComposeBuildArgs, k8s did not. The backup
|
|
566
|
+
// image build (no frontend) calls this without a domain → no VITE args.
|
|
567
|
+
let buildArgs = {};
|
|
568
|
+
if (domain) {
|
|
569
|
+
const { collectComposeBuildArgs } = await import('../compose/build-args.js');
|
|
570
|
+
buildArgs = collectComposeBuildArgs(projectDir, { projectName, domain });
|
|
571
|
+
}
|
|
572
|
+
return buildLocalImage(projectDir, {
|
|
573
|
+
projectName,
|
|
574
|
+
rebuild,
|
|
575
|
+
tagPrefix: '10.0.1.1:5000',
|
|
576
|
+
buildArgs,
|
|
577
|
+
});
|
|
562
578
|
}
|
|
563
579
|
|
|
564
580
|
/**
|
|
@@ -2436,11 +2452,14 @@ export async function deployK3s(options) {
|
|
|
2436
2452
|
}
|
|
2437
2453
|
|
|
2438
2454
|
// 6. Build the app image locally
|
|
2439
|
-
|
|
2455
|
+
// domain is part of the cache key + passed to the build: it's baked into the
|
|
2456
|
+
// browser bundle (VITE_SUPABASE_URL=https://api.<domain>), so a domain change
|
|
2457
|
+
// must force a rebuild rather than serving a stale-domain bundle.
|
|
2458
|
+
const buildInputs = { projectName: options.projectName, domain: options.domain };
|
|
2440
2459
|
if (!state.shouldSkip('k3s-build', buildInputs)) {
|
|
2441
2460
|
state.startStep('k3s-build', buildInputs);
|
|
2442
2461
|
s.start('Building app image (10.0.1.1:5000/...)');
|
|
2443
|
-
const built = await buildAppImage(projectDir, options.projectName);
|
|
2462
|
+
const built = await buildAppImage(projectDir, options.projectName, false, options.domain);
|
|
2444
2463
|
state.completeStep('k3s-build', {
|
|
2445
2464
|
tag: built.tag,
|
|
2446
2465
|
gitSha: built.gitSha,
|