@ultimat3/cli 24.0.0 → 25.1.0
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/CLAUDE.md +15 -8
- package/README.md +30 -5
- package/package.json +29 -29
- package/src/admin-catalog-keys.ts +2 -2
- package/src/affected.ts +3 -1
- package/src/app-boundaries.ts +17 -4
- package/src/app-config-load.ts +3 -3
- package/src/app-env.ts +21 -14
- package/src/app-load.ts +38 -14
- package/src/app-manifest.ts +2 -2
- package/src/app-mcp.ts +25 -27
- package/src/app-name.ts +52 -0
- package/src/app-permissions-borrowed.ts +2 -8
- package/src/app-permissions-site.ts +31 -7
- package/src/app-root.ts +9 -5
- package/src/bin.ts +3 -7
- package/src/browser-launcher.ts +4 -2
- package/src/budgets-charged.ts +60 -0
- package/src/budgets-scan.ts +76 -0
- package/src/budgets-unmeasured.ts +91 -0
- package/src/budgets.ts +115 -121
- package/src/cdp-shot-driver.ts +17 -0
- package/src/cdp-shot-watch.ts +35 -31
- package/src/ci-log.ts +0 -0
- package/src/cmd-auth.ts +2 -2
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +89 -9
- package/src/cmd-db-backfill.ts +1 -1
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db.ts +5 -2
- package/src/cmd-deploy-compose.ts +94 -0
- package/src/cmd-deploy-drain.ts +48 -0
- package/src/cmd-deploy-start-first.ts +178 -0
- package/src/cmd-deploy.ts +158 -36
- package/src/cmd-dev.ts +18 -357
- package/src/cmd-doctor.ts +16 -66
- package/src/cmd-env.ts +29 -14
- package/src/cmd-generate.ts +12 -2
- package/src/cmd-i18n.ts +11 -8
- package/src/cmd-jobs-spec.ts +7 -22
- package/src/cmd-jobs.ts +18 -137
- package/src/cmd-mcp.ts +25 -7
- package/src/cmd-new.ts +36 -8
- package/src/cmd-planned.ts +9 -0
- package/src/cmd-routes.ts +3 -3
- package/src/cmd-shot-matrix.ts +21 -4
- package/src/cmd-shot-spec.ts +10 -2
- package/src/cmd-shot.ts +35 -39
- package/src/cmd-test.ts +1 -1
- package/src/coverage-lcov.ts +4 -2
- package/src/db-backfill.ts +2 -2
- package/src/db-replay-engine.ts +4 -10
- package/src/db-subscribes.ts +19 -16
- package/src/dev-boot.ts +290 -0
- package/src/dev-child-watch.ts +63 -12
- package/src/dev-dashboard-guard.ts +1 -1
- package/src/dev-dashboard.ts +5 -1
- package/src/dev-lock.ts +96 -24
- package/src/dev-rebuild.ts +80 -0
- package/src/dev-restart-retire.ts +46 -0
- package/src/dev-route-table.ts +30 -8
- package/src/dev-supervisor.ts +114 -22
- package/src/dev-traces.ts +53 -20
- package/src/dispatch.ts +14 -7
- package/src/doctor-auth.ts +4 -4
- package/src/doctor-offline.ts +6 -5
- package/src/document-options.ts +7 -0
- package/src/drift-replica-identity.ts +2 -2
- package/src/drift.ts +29 -6
- package/src/error-codes.ts +6 -0
- package/src/error-page-csp.ts +14 -15
- package/src/exec.ts +8 -0
- package/src/favicon.ts +2 -2
- package/src/fix-command.ts +9 -1
- package/src/framework-env.ts +148 -0
- package/src/framework-schema.ts +13 -2
- package/src/generate-catalog-locales.ts +79 -0
- package/src/generate-kinds.ts +3 -17
- package/src/generate-plural.ts +50 -0
- package/src/generate-shadow.ts +50 -1
- package/src/hold.ts +56 -1
- package/src/i18n-audit.ts +21 -8
- package/src/i18n-index.ts +5 -6
- package/src/i18n-registration.ts +7 -9
- package/src/import-scan.ts +28 -3
- package/src/index.ts +1 -3
- package/src/island-bundle.ts +54 -64
- package/src/island-capture.ts +1 -1
- package/src/island-duplicate-refusal.ts +57 -0
- package/src/island-duplicates.ts +221 -0
- package/src/island-entry-missing.ts +64 -0
- package/src/island-harness-route.ts +20 -4
- package/src/island-harness-script.ts +23 -2
- package/src/island-identity.ts +17 -14
- package/src/island-package-dedupe.ts +150 -0
- package/src/island-realtime.ts +36 -16
- package/src/island-routes.ts +2 -2
- package/src/island-runtime.ts +241 -0
- package/src/island-sources.ts +105 -0
- package/src/island-states-load.ts +6 -0
- package/src/island-store.ts +90 -6
- package/src/jobs-json.ts +0 -26
- package/src/jobs-report.ts +32 -16
- package/src/local-cli-handoff.ts +90 -0
- package/src/mcp-errors.ts +8 -1
- package/src/mcp-host.ts +6 -5
- package/src/mcp-ui-inspect.ts +3 -2
- package/src/mcp-ui-interact.ts +3 -2
- package/src/mcp-ui.ts +8 -7
- package/src/measure-scope.ts +15 -10
- package/src/messages.ts +27 -16
- package/src/metrics-endpoint.ts +3 -2
- package/src/migration-extensions.ts +1 -1
- package/src/otlp-export.ts +2 -2
- package/src/output.ts +18 -0
- package/src/page-speculation.ts +14 -11
- package/src/parse.ts +0 -8
- package/src/permission-grants.ts +3 -3
- package/src/posix-path.ts +30 -0
- package/src/prerender-locales.ts +6 -4
- package/src/prerender-report.ts +58 -0
- package/src/prerender-served.ts +52 -0
- package/src/prerender.ts +63 -98
- package/src/pwa-artifacts.ts +8 -1
- package/src/pwa-manifest-locales.ts +9 -9
- package/src/retry-memo.ts +1 -1
- package/src/role-realtime.ts +7 -1
- package/src/role-replicator.ts +110 -31
- package/src/role-start.ts +66 -87
- package/src/role-sync.ts +65 -6
- package/src/role-wake.ts +2 -3
- package/src/root-env.ts +157 -28
- package/src/route-islands.ts +69 -0
- package/src/runtime-admin.ts +4 -1
- package/src/runtime-assets.ts +15 -2
- package/src/runtime-bindings.ts +23 -3
- package/src/runtime-cache.ts +8 -8
- package/src/runtime-hooks.ts +2 -1
- package/src/runtime-isr-outcome.ts +100 -0
- package/src/runtime-isr.ts +2 -2
- package/src/runtime-jobs.ts +78 -18
- package/src/runtime-live-feed.ts +1 -1
- package/src/runtime-mail.ts +26 -0
- package/src/runtime-mfa-warning.ts +2 -2
- package/src/runtime-overrides.ts +3 -3
- package/src/runtime-purge.ts +1 -1
- package/src/runtime-queue.ts +19 -30
- package/src/runtime-realtime.ts +5 -3
- package/src/runtime-render.ts +77 -82
- package/src/runtime-replica.ts +3 -8
- package/src/runtime-services.ts +36 -15
- package/src/runtime-storage-upload.ts +2 -2
- package/src/runtime-storage.ts +3 -3
- package/src/schema-diff.ts +1 -1
- package/src/secrets-rotation.ts +8 -9
- package/src/seo-routes.ts +2 -2
- package/src/serve-boot.ts +22 -11
- package/src/serve-drain.ts +3 -1
- package/src/serve-retire.ts +95 -0
- package/src/serve-web.ts +19 -8
- package/src/serve.ts +27 -10
- package/src/server-barrels.ts +6 -3
- package/src/shot-browser.ts +1 -1
- package/src/shot-locale.ts +6 -15
- package/src/shot-settle.ts +3 -3
- package/src/shot-theme.ts +1 -1
- package/src/shot-viewport.ts +152 -0
- package/src/signal-shred.ts +26 -12
- package/src/site-asset-routes.ts +2 -2
- package/src/site-seo.ts +6 -4
- package/src/source-files.ts +13 -3
- package/src/static-report.ts +5 -0
- package/src/style-routes.ts +2 -2
- package/src/sw-artifacts.ts +8 -8
- package/src/sw-precache-plan.ts +34 -4
- package/src/sync-authenticator.ts +3 -3
- package/src/sync-url.ts +1 -1
- package/src/templates/action.ts +4 -4
- package/src/templates/backfill.ts +4 -4
- package/src/templates/entity.ts +2 -2
- package/src/templates/github/ci.yml.ts +28 -18
- package/src/templates/github/image.yml.ts +92 -0
- package/src/templates/github/runner.ts +18 -0
- package/src/templates/guard-bare-error.ts +16 -2
- package/src/templates/guard-unzoned-date.ts +56 -6
- package/src/templates/index.ts +1 -1
- package/src/templates/job-body-test.ts +2 -2
- package/src/templates/job.ts +6 -6
- package/src/templates/query.ts +9 -8
- package/src/templates/resource-page.ts +9 -7
- package/src/templates/resource-service.ts +2 -2
- package/src/templates/route.ts +23 -8
- package/src/templates/scaffold-api.ts +2 -3
- package/src/templates/scaffold-app.ts +2 -1
- package/src/templates/scaffold-auth.ts +205 -70
- package/src/templates/scaffold-bin.ts +122 -0
- package/src/templates/scaffold-claude-commands.ts +11 -11
- package/src/templates/scaffold-container.ts +12 -6
- package/src/templates/scaffold-dashboard-bare.ts +8 -4
- package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
- package/src/templates/scaffold-dashboard-example.ts +112 -172
- package/src/templates/scaffold-dashboard-shared.ts +62 -1
- package/src/templates/scaffold-dashboard.ts +9 -5
- package/src/templates/scaffold-db-package.ts +2 -2
- package/src/templates/scaffold-docs.ts +20 -77
- package/src/templates/scaffold-domain-package.ts +0 -4
- package/src/templates/scaffold-entries.ts +4 -3
- package/src/templates/scaffold-env.ts +6 -3
- package/src/templates/scaffold-gitattributes.ts +59 -0
- package/src/templates/scaffold-helm-templates.ts +74 -18
- package/src/templates/scaffold-helm.ts +31 -4
- package/src/templates/scaffold-http.ts +1 -1
- package/src/templates/scaffold-i18n.ts +15 -2
- package/src/templates/scaffold-icon.ts +2 -2
- package/src/templates/scaffold-repo.ts +18 -15
- package/src/templates/scaffold-shell.ts +17 -2
- package/src/test-only-shipped.ts +193 -0
- package/src/test-select.ts +2 -1
- package/src/test-shards.ts +1 -4
- package/src/theme-boot.ts +2 -2
- package/src/theme-brand.ts +66 -0
- package/src/trusted-hops.ts +49 -0
- package/src/unscanned-admin.ts +2 -2
- package/src/verify-checks.ts +12 -2
- package/src/verify-coverage-run.ts +3 -2
- package/src/verify-deadline.ts +87 -10
- package/src/verify-merge.ts +48 -16
- package/src/verify-part-step.ts +10 -2
- package/src/verify-role-load-probe.ts +12 -2
- package/src/verify-role-load.ts +9 -4
- package/src/verify-run.ts +3 -0
- package/src/verify-step.ts +2 -0
- package/src/verify-test-run.ts +3 -2
- package/src/verify-tests.ts +0 -1
- package/src/worker-bundle.ts +2 -2
- package/src/workspace-checks.ts +53 -7
- package/src/workspace-graph.ts +3 -2
- package/src/write-line.ts +82 -7
- package/src/browser-launcher-fake-html.ts +0 -80
- package/src/browser-launcher-fake.ts +0 -168
- package/src/jobs-drain.ts +0 -230
- package/src/scaffold-typecheck.ts +0 -227
- package/src/thrown-by.ts +0 -24
|
@@ -22,7 +22,7 @@ export const routeConfig = (load: string): string => `export const config = defi
|
|
|
22
22
|
// toggle, provider, the ui runtime they share and the error registry are the rest. It was
|
|
23
23
|
// 60.9kb before the framework stopped shipping solid-js twice and the i18n catalog with it
|
|
24
24
|
// (issue #490), which is what put this at 64kb; 60kb is the figure the scaffold budgets held
|
|
25
|
-
// before that, kept rather than tightened so a Bun patch cannot red a first \`
|
|
25
|
+
// before that, kept rather than tightened so a Bun patch cannot red a first \`bun run check\`.
|
|
26
26
|
budget: { js: '60kb' },${load}
|
|
27
27
|
meta: ({ t }) => ({
|
|
28
28
|
title: t('app.dashboard.title'),
|
|
@@ -55,4 +55,65 @@ export const dashboardStyle = (): string => `@use '@ultimat3/ui/tokens' as token
|
|
|
55
55
|
margin-inline: auto;
|
|
56
56
|
color: tokens.role('fg');
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
// The trend beside the ring and the dial: side by side once the main column is wide enough for the
|
|
60
|
+
// area chart to keep its 14 day labels legible, stacked before that.
|
|
61
|
+
.charts {
|
|
62
|
+
display: grid;
|
|
63
|
+
gap: tokens.space(6);
|
|
64
|
+
min-inline-size: 0;
|
|
65
|
+
|
|
66
|
+
@include tokens.respond-to('lg') {
|
|
67
|
+
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
|
|
68
|
+
align-items: start;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The ring and the dial share a column: two across on a tablet, where one alone would leave half
|
|
73
|
+
// the row empty, one above the other on a phone and in the narrow side column of a monitor.
|
|
74
|
+
.side {
|
|
75
|
+
display: grid;
|
|
76
|
+
gap: tokens.space(4);
|
|
77
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* `apps/web/shared/theme.ts`: the app's brand, which the boot reads by this path and inlines in every
|
|
83
|
+
* document (`theme-brand.ts`). Emitted as the default theme — `defineTheme({})` renders no CSS, so
|
|
84
|
+
* no tag and no CSP entry — with the preset one edit away: the look is the app's choice (axiom 8).
|
|
85
|
+
*/
|
|
86
|
+
export const themeFile =
|
|
87
|
+
(): string => `// This app's brand: the boot reads \`brand\` from this file and inlines it in every document,
|
|
88
|
+
// after global.scss, with its hash in the CSP. \`defineTheme({})\` is @ultimat3/ui's default theme
|
|
89
|
+
// and renders nothing, so today no tag is emitted.
|
|
90
|
+
//
|
|
91
|
+
// \`defineTheme()\` is the one seam for restyling — colours, radii, fonts and shadows by role,
|
|
92
|
+
// validated at the call. The dashboard look (near-black panels, a cyan accent, glowing edges, its
|
|
93
|
+
// own eight chart series) is one edit:
|
|
94
|
+
//
|
|
95
|
+
// export const brand = defineTheme({ preset: 'scifi' });
|
|
96
|
+
//
|
|
97
|
+
// Every other slot layers onto the preset role by role:
|
|
98
|
+
// defineTheme({ preset: 'scifi', colors: { dark: { 'chart-1': '120 220 255' } } })
|
|
99
|
+
// A preset never forces a theme: to open dark whatever the OS says, set \`theme.defaultMode\` in
|
|
100
|
+
// app.config.ts.
|
|
101
|
+
import { defineTheme } from '@ultimat3/ui';
|
|
102
|
+
|
|
103
|
+
export const brand = defineTheme({});
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
/** Its test: what the brand renders is a fact the app pins, so opting in is a visible diff. */
|
|
107
|
+
export const themeTest =
|
|
108
|
+
(): string => `// The brand the boot inlines. The default theme renders no CSS — no \`<style>\`, no CSP entry —
|
|
109
|
+
// and the preset is a whole palette; either way \`defineTheme()\` validated it at import.
|
|
110
|
+
import { expect, unitTest } from '@ultimat3/testing';
|
|
111
|
+
import { defineTheme } from '@ultimat3/ui';
|
|
112
|
+
import { brand } from './theme';
|
|
113
|
+
|
|
114
|
+
unitTest('the app ships the default theme until it opts into a preset', () => {
|
|
115
|
+
expect(brand.css).toBe('');
|
|
116
|
+
// The opt-in is one edit, and what it would ship is real CSS the boot inlines.
|
|
117
|
+
expect(defineTheme({ preset: 'scifi' }).css).toContain('--color-');
|
|
118
|
+
});
|
|
58
119
|
`;
|
|
@@ -2,25 +2,29 @@
|
|
|
2
2
|
// stopped being an `<h1>` in a panel. Two shapes, one per `x new` invocation, and both show only
|
|
3
3
|
// what is true of the app that was just written:
|
|
4
4
|
//
|
|
5
|
-
// --example the seeded `post` slice: a stat row,
|
|
6
|
-
//
|
|
7
|
-
// module this file
|
|
5
|
+
// --example the seeded `post` slice: a stat row, an area chart of posts per day, a ring of
|
|
6
|
+
// free against paid, a dial of this week's share, the latest posts in a table.
|
|
7
|
+
// Real rows from the app's own repo, aggregated by a pure view module this file
|
|
8
|
+
// also emits.
|
|
8
9
|
// --no-example no entity, so no rows to count and nothing honest to chart. Framework facts
|
|
9
10
|
// instead — routes, locales, roles, version — and the route table. A chart of
|
|
10
11
|
// invented numbers would teach the wrong thing to every app that starts here.
|
|
11
12
|
//
|
|
12
|
-
// One island on the page, the theme toggle (`scaffold-shell.ts`); the tiles,
|
|
13
|
+
// One island on the page, the theme toggle (`scaffold-shell.ts`); the tiles, charts and table are
|
|
13
14
|
// server markup, which is what keeps the route inside its 60kb budget.
|
|
14
15
|
|
|
15
16
|
import type { GeneratedFile, NameSet } from './naming';
|
|
16
17
|
import { bareDashboardFiles } from './scaffold-dashboard-bare';
|
|
17
18
|
import { exampleDashboardFiles } from './scaffold-dashboard-example';
|
|
18
|
-
import { DASHBOARD_DIR, dashboardStyle } from './scaffold-dashboard-shared';
|
|
19
|
+
import { DASHBOARD_DIR, dashboardStyle, themeFile, themeTest } from './scaffold-dashboard-shared';
|
|
19
20
|
|
|
20
21
|
/** `apps/web/app/dashboard/`, in the shape the invocation earns. */
|
|
21
22
|
export function dashboardFiles(app: NameSet, example: boolean): readonly GeneratedFile[] {
|
|
22
23
|
return [
|
|
23
24
|
...(example ? exampleDashboardFiles(app) : bareDashboardFiles(app)),
|
|
24
25
|
{ path: `${DASHBOARD_DIR}/page.module.scss`, contents: dashboardStyle() },
|
|
26
|
+
// Both shapes: the look is the app's to choose whether or not it has an entity yet.
|
|
27
|
+
{ path: 'apps/web/shared/theme.ts', contents: themeFile() },
|
|
28
|
+
{ path: 'apps/web/shared/theme.test.ts', contents: themeTest() },
|
|
25
29
|
];
|
|
26
30
|
}
|
|
@@ -165,7 +165,7 @@ const dbSeedTest = (app: NameSet, example: boolean): string =>
|
|
|
165
165
|
// development viewer's tenant policy refuses, and the dashboard then counts zero.
|
|
166
166
|
${sortedImports([
|
|
167
167
|
`import { DEMO_ORG_ID } from '@${app.kebab}/web/shared/demo-org';`,
|
|
168
|
-
"import {
|
|
168
|
+
"import { ctxOf, runWithContext } from '@ultimat3/core';",
|
|
169
169
|
"import { testActor } from '@ultimat3/policy';",
|
|
170
170
|
"import { afterEach, expect, unitTest } from '@ultimat3/testing';",
|
|
171
171
|
])}
|
|
@@ -174,7 +174,7 @@ import { ${app.camel}Seed } from './seed';
|
|
|
174
174
|
|
|
175
175
|
/** The development viewer's org: every read below runs as an actor inside it. */
|
|
176
176
|
const asViewer = <T>(run: () => Promise<T>): Promise<T> =>
|
|
177
|
-
runWithContext(
|
|
177
|
+
runWithContext(ctxOf({ actor: testActor('viewer', { orgId: DEMO_ORG_ID }).actor }), run);
|
|
178
178
|
|
|
179
179
|
// One store per process: without this, one test's rows are the next test's fixtures.
|
|
180
180
|
afterEach(() => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The human-authored half of what `x new` writes: the READMEs, the agent-facing convention files,
|
|
2
|
-
// the bin/
|
|
2
|
+
// the bin/ scripts and the optional dev compose. Separated from the config half so neither file has
|
|
3
3
|
// to be scrolled to find the other — one file, one job applies to templates too. The image, its
|
|
4
4
|
// ignore file, the production topology and the deploy page are `scaffold-container.ts`; the
|
|
5
5
|
// `.claude/` harness that reads AGENTS.md is `scaffold-claude.ts`; the one workflow is
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { LINE_CEILING } from '../workspace-checks';
|
|
9
9
|
import { githubFiles } from './github/ci.yml';
|
|
10
10
|
import type { GeneratedFile, NameSet } from './naming';
|
|
11
|
+
import { binFiles } from './scaffold-bin';
|
|
11
12
|
import { claudeFiles } from './scaffold-claude';
|
|
12
13
|
import { containerFiles } from './scaffold-container';
|
|
13
14
|
|
|
@@ -21,7 +22,7 @@ not exist — five of these had an empty column and were each measured green on
|
|
|
21
22
|
|
|
22
23
|
| Rule | Detail | Refused by |
|
|
23
24
|
|---|---|---|
|
|
24
|
-
| One gate | \`
|
|
25
|
+
| One gate | \`bun run check\` — \`x build\` and then \`x verify\`. Green means shippable; never merge red. | the gate itself, and \`.github/workflows/ci.yml\` on every push and pull request; \`.github/workflows/image.yml\` publishes only a commit it passed |
|
|
25
26
|
| Coverage | the unit suite covers 95% of the lines and functions of this app's own source — every file under \`apps/\` and \`packages/\`, one no test loads counted at 0%. A floor, not the goal: a test added to raise it is proven by mutation — break the source, watch it go red, restore | \`X_COVERAGE_BELOW_FLOOR\` on the \`unit\` step; \`coverage\` in \`x.verify.json\` states the floor, and it only rises |
|
|
26
27
|
| One way | generators, not hand-rolled files: \`x g resource\`, \`x g action\`, \`x g route\` | review |
|
|
27
28
|
| Surfaces | \`site/\` is 0kb JS and may not import \`app/\`; \`shared/\` is a leaf | \`X_BOUNDARY_SITE_TO_APP\` |
|
|
@@ -75,14 +76,14 @@ Every test the scaffold and the generators write is the idiom to copy. All of th
|
|
|
75
76
|
| an island | \`mountIsland\`, once per state its \`*.island.states.ts\` declares | \`apps/web/shared/theme-toggle.island.test.ts\` |
|
|
76
77
|
| an action or a query | \`target.as(actor, input)\` — the refusal first, then what it answers | \`apps/web/api/health.test.ts\` |
|
|
77
78
|
| a job, a task, a backfill | \`test('…', async ({ runJobs }) => …)\` — a worker in this process; assert on \`trace.steps\` and each run's \`outcome\` | what \`x g job\` writes |
|
|
78
|
-
| a repo, a seed | as an actor, \`runWithContext(
|
|
79
|
+
| a repo, a seed | as an actor, \`runWithContext(ctxOf({ actor }), …)\`, with \`driver.reset?.()\` after each | \`packages/db/src/seed.test.ts\` |
|
|
79
80
|
|
|
80
81
|
Money is the one row with no guard, deliberately: a float has no static signature a text rule can
|
|
81
82
|
see, and the type already fires — measured, \`price: 19.99\` in a seed is
|
|
82
83
|
\`TS2322: Type 'number' is not assignable to type 'MoneyInput'\`. A guard that pretended to check
|
|
83
84
|
it would be worse than the type that really does.
|
|
84
85
|
|
|
85
|
-
\`
|
|
86
|
+
\`bun run check\` is the gate and \`x verify\` is only its second half: the first is
|
|
86
87
|
\`x build --target static\`, and the build is what writes the \`.x/build-stats.json\` the
|
|
87
88
|
\`budgets\` step measures. Run \`x verify\` on a tree nobody has built and \`budgets\` is red with
|
|
88
89
|
X_BUDGET_UNMEASURED — the gate reporting on a file that does not exist, not on your code.
|
|
@@ -93,8 +94,9 @@ here is generated from it. So the scaffold never clobbers a maintainer policy, t
|
|
|
93
94
|
clobbers \`AGENTS.md\`, \`bin/\`, \`.claude/\` or \`.github/\`, and deleting either side leaves the
|
|
94
95
|
other exactly as it was.
|
|
95
96
|
|
|
96
|
-
Commands: \`
|
|
97
|
-
\`x db branch create <name>\`, \`x doctor\`.
|
|
97
|
+
Commands: \`bun run setup\`, \`bun run dev\`, \`bun run check\`, \`x g <primitive>\`,
|
|
98
|
+
\`x g guard <name>\`, \`x db branch create <name>\`, \`x doctor\`. \`setup\` and \`check\` are
|
|
99
|
+
\`bin/*.ts\` files Bun runs, so all three are the same command in PowerShell, cmd and bash.
|
|
98
100
|
|
|
99
101
|
Project notes for ${app.kebab}: replace this line with the conventions a newcomer could not guess.
|
|
100
102
|
`;
|
|
@@ -103,10 +105,10 @@ const claude = (app: NameSet): string => `# CLAUDE.md
|
|
|
103
105
|
|
|
104
106
|
${app.kebab} — Ultimate app. Read AGENTS.md first; it is the same content in the same order.
|
|
105
107
|
|
|
106
|
-
- Gate: \`
|
|
108
|
+
- Gate: \`bun run check\` (add \`--json\` for machine output — it reaches both halves). It is
|
|
107
109
|
\`x build --target static\` and then \`x verify\`; \`x verify\` alone leaves \`budgets\` with
|
|
108
|
-
nothing to measure. \`.github/workflows/ci.yml\` runs \`
|
|
109
|
-
and pull request, so CI and your terminal run the same two commands.
|
|
110
|
+
nothing to measure. \`.github/workflows/ci.yml\` runs \`bun run setup && bun run check\` on
|
|
111
|
+
every push and pull request, so CI and your terminal run the same two commands.
|
|
110
112
|
- \`.dz/\` belongs to the developerz.ai platform and is additive both ways: the scaffold writes
|
|
111
113
|
nothing there, and nothing there is generated from this repo. Neither side clobbers the other.
|
|
112
114
|
- Scaffold, do not hand-write: \`x g <kind> <name>\` — \`x g --help\` lists every kind, and is the
|
|
@@ -129,12 +131,12 @@ Built with [Ultimate](https://github.com/developerz-ai/ultimate). Bun-only, Post
|
|
|
129
131
|
## 🚀 Start
|
|
130
132
|
|
|
131
133
|
\`\`\`sh
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
bun run setup # prerequisites, deps, env, the first migration, migrate, seed, the manifest
|
|
135
|
+
bun run dev # all roles in one process, embedded Postgres, /_x mounted
|
|
136
|
+
bun run check # the gate: a static build, then typecheck, lint, boundaries, tests, drift, budgets
|
|
135
137
|
\`\`\`
|
|
136
138
|
|
|
137
|
-
\`packages/db/migrations\` starts empty and \`x db gen\` is its only writer — \`
|
|
139
|
+
\`packages/db/migrations\` starts empty and \`x db gen\` is its only writer — \`bun run setup\` runs
|
|
138
140
|
\`x db gen "initial"\` for you on a fresh clone. Until it has, \`x verify\`'s \`drift\` step is red
|
|
139
141
|
with \`X_DB_DRIFT\`, and that is the fix it names.
|
|
140
142
|
|
|
@@ -150,63 +152,8 @@ with \`X_DB_DRIFT\`, and that is the fix it names.
|
|
|
150
152
|
| \`packages/*\` | domain, db, i18n, ui, mcp |
|
|
151
153
|
| \`app.config.ts\` | the one config file |
|
|
152
154
|
| \`x.manifest.json\` | generated facts: routes, actions, jobs, policies |
|
|
153
|
-
| \`.github/workflows/ci.yml\` | \`
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const binSetup = (): string => `#!/usr/bin/env bash
|
|
157
|
-
# Fresh clone to running. Idempotent: safe to re-run.
|
|
158
|
-
set -euo pipefail
|
|
159
|
-
cd "$(dirname "$0")/.."
|
|
160
|
-
command -v bun >/dev/null || { echo "X_BUN_MISSING: install bun — https://bun.sh"; exit 1; }
|
|
161
|
-
bun install
|
|
162
|
-
[ -f .env.development.local ] || printf '# per-box secrets, gitignored, wins over .env.development\\n' > .env.development.local
|
|
163
|
-
# \`x db gen\` is the ONE writer of packages/db/migrations — the scaffold no longer hand-writes a
|
|
164
|
-
# 0000_initial.sql, because a second writer is how the source and the ledger ended up disagreeing
|
|
165
|
-
# about what "initial" meant. Guarded on the directory rather than on the generator being a no-op:
|
|
166
|
-
# this script is documented idempotent, and the guard is what makes that true here.
|
|
167
|
-
ls packages/db/migrations/*.sql >/dev/null 2>&1 || bunx x db gen "initial"
|
|
168
|
-
bunx x db migrate "$@"
|
|
169
|
-
# \`x db seed\`, never \`bun run\`: the CLI owns the connection, so this reaches the same embedded
|
|
170
|
-
# PGlite the migration above just wrote to. A plain script goes through \`db()\`, which needs a
|
|
171
|
-
# \`postgres:\` DATABASE_URL and so dies on a clone with no Postgres — one line after reporting a
|
|
172
|
-
# successful migration.
|
|
173
|
-
bunx x db seed
|
|
174
|
-
# The file \`AGENTS.md\` line 3 tells an agent facts live in, and \`x dev\` prints the path of. It
|
|
175
|
-
# is a projection of the loaded app, so \`x new\` cannot write it — node_modules does not exist
|
|
176
|
-
# yet — and nothing else ever ran the command: after \`x new\`, \`bin/setup\` and all 13
|
|
177
|
-
# generators, \`find . -name '*.manifest.json'\` returned nothing while \`x verify\` reported
|
|
178
|
-
# \`\u2713 manifest\`. \`x verify\`'s manifest step now refuses its absence (X_MANIFEST_MISSING).
|
|
179
|
-
bunx x manifest
|
|
180
|
-
echo "setup complete — next: bin/dev"
|
|
181
|
-
`;
|
|
182
|
-
|
|
183
|
-
const binDev = (): string => `#!/usr/bin/env bash
|
|
184
|
-
# Every role in one process: embedded Postgres, in-process NATS, S3 to a local dir.
|
|
185
|
-
set -euo pipefail
|
|
186
|
-
cd "$(dirname "$0")/.."
|
|
187
|
-
exec bunx x dev "$@"
|
|
188
|
-
`;
|
|
189
|
-
|
|
190
|
-
const binCheck = (): string => `#!/usr/bin/env bash
|
|
191
|
-
# The gate. Same steps as CI, because a check that lives only in CI cannot be run locally.
|
|
192
|
-
set -euo pipefail
|
|
193
|
-
cd "$(dirname "$0")/.."
|
|
194
|
-
# The build FIRST, and not as a convenience: \`x verify\`'s budgets step compares declared limits
|
|
195
|
-
# against measured bytes in .x/build-stats.json, so with no build it reports X_BUDGET_UNMEASURED and
|
|
196
|
-
# the very first gate anyone runs on a brand-new app is red for a reason that has nothing to do with
|
|
197
|
-
# their code. Cheap on a warm tree, and it makes "green" reachable from a fresh clone.
|
|
198
|
-
#
|
|
199
|
-
# \`--json\` is forwarded to BOTH, or the contract breaks: \`bin/check --json\` would otherwise print
|
|
200
|
-
# the build's human renderer to stdout and then the gate's JSON, and a machine consumer reading one
|
|
201
|
-
# document off stdout gets neither. Both commands emit one object; a reader takes the last line.
|
|
202
|
-
build_flags=""
|
|
203
|
-
for arg in "$@"; do
|
|
204
|
-
case "$arg" in --json|-j) build_flags="--json" ;; esac
|
|
205
|
-
done
|
|
206
|
-
# \`--no-preflight\`: the build's own preflight is x verify's first six steps (typecheck, lint,
|
|
207
|
-
# boundaries, filesize, package-shape, errors), and the gate below runs them anyway — once is enough.
|
|
208
|
-
bunx x build --target static --no-preflight $build_flags
|
|
209
|
-
exec bunx x verify "$@"
|
|
155
|
+
| \`.github/workflows/ci.yml\` | \`bun run setup\` then \`bun run check\`, on push and pull request |
|
|
156
|
+
| \`.github/workflows/image.yml\` | after a green \`ci\` on the default branch: \`docker/Dockerfile\` to \`ghcr.io\`, tagged \`sha-<7>\`, always on \`ubuntu-latest\` (bash and Docker). \`ci.yml\` runs on \`vars.CI_RUNNER\`, default \`ubuntu-latest\` |
|
|
210
157
|
`;
|
|
211
158
|
|
|
212
159
|
const composeDev = (
|
|
@@ -243,7 +190,7 @@ services:
|
|
|
243
190
|
# the region a request was signed for, and an app that sets no S3_REGION signs for \`auto\` — so
|
|
244
191
|
# that is this gateway's region. A server for a laptop; production is any S3-compatible endpoint.
|
|
245
192
|
s3:
|
|
246
|
-
image: versity/versitygw:v1.8.0
|
|
193
|
+
image: versity/versitygw:v1.8.0@sha256:30292fc2eeacc67a36993b01f7a7a5e3361a19cced0e80c1d71cfa2a4b0a2499
|
|
247
194
|
environment:
|
|
248
195
|
VGW_BACKEND: posix
|
|
249
196
|
VGW_BACKEND_ARG: /data
|
|
@@ -264,9 +211,8 @@ export function docsFiles(app: NameSet): readonly GeneratedFile[] {
|
|
|
264
211
|
{ path: 'README.md', contents: readme(app) },
|
|
265
212
|
{ path: 'AGENTS.md', contents: agents(app) },
|
|
266
213
|
{ path: 'CLAUDE.md', contents: claude(app) },
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
{ path: 'bin/check', contents: binCheck() },
|
|
214
|
+
// The scripts `bun run setup` and `bun run check` run — TypeScript, so one file per OS.
|
|
215
|
+
...binFiles(),
|
|
270
216
|
{ path: 'docker/docker-compose.dev.yml', contents: composeDev(app) },
|
|
271
217
|
// The harness half of the same job AGENTS.md does. It lands in the app's own repo rather than
|
|
272
218
|
// in a global config, so it is visible in the scaffold's diff and deletable in one line.
|
|
@@ -278,6 +224,3 @@ export function docsFiles(app: NameSet): readonly GeneratedFile[] {
|
|
|
278
224
|
...containerFiles(app),
|
|
279
225
|
];
|
|
280
226
|
}
|
|
281
|
-
|
|
282
|
-
/** Files that must be executable after `x new` writes them. */
|
|
283
|
-
export const EXECUTABLE_FILES: readonly string[] = ['bin/setup', 'bin/dev', 'bin/check'];
|
|
@@ -23,10 +23,6 @@ import type { MoneyValue as Money } from '@ultimat3/schema';
|
|
|
23
23
|
|
|
24
24
|
export type { MoneyValue as Money } from '@ultimat3/schema';
|
|
25
25
|
|
|
26
|
-
export const ROLES = ['owner', 'member', 'viewer'] as const;
|
|
27
|
-
|
|
28
|
-
export type Role = (typeof ROLES)[number];
|
|
29
|
-
|
|
30
26
|
/**
|
|
31
27
|
* Never a bare Error: an agent reading the failure needs the code, the cause and the exact command
|
|
32
28
|
* that resolves it. Adding across currencies is a bug in the caller's data, not a runtime hiccup,
|
|
@@ -16,6 +16,7 @@ const server =
|
|
|
16
16
|
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
import { runRole } from '@ultimat3/cli/serve';
|
|
19
|
+
import { isCompiledBundle } from '@ultimat3/core';
|
|
19
20
|
|
|
20
21
|
// MORE THAN ONE REPLICA? Add these two lines, above \`runRole\`:
|
|
21
22
|
//
|
|
@@ -36,10 +37,10 @@ import { runRole } from '@ultimat3/cli/serve';
|
|
|
36
37
|
* holds this module's bundled imports and none of the app's source, and the framework's registries
|
|
37
38
|
* are filled by scanning that source at boot. So a binary reads its root from the directory it is
|
|
38
39
|
* started in — it is a launcher for an app tree, not a self-contained copy of one.
|
|
40
|
+
* \`isCompiledBundle\` knows both spellings of that filesystem: \`/$bunfs/\` on Linux and macOS,
|
|
41
|
+
* \`B:\\~BUN\\\` on Windows.
|
|
39
42
|
*/
|
|
40
|
-
const root = import.meta.dir.
|
|
41
|
-
? process.cwd()
|
|
42
|
-
: join(import.meta.dir, '..', '..');
|
|
43
|
+
const root = isCompiledBundle(import.meta.dir) ? process.cwd() : join(import.meta.dir, '..', '..');
|
|
43
44
|
|
|
44
45
|
// Guarded, because the framework's module scan imports every file under apps/*/ to fill its
|
|
45
46
|
// registries — an unguarded boot would start a server inside \`x verify\`.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// app whose own `x verify` fails on the drift the framework exists to prevent.
|
|
5
5
|
|
|
6
6
|
import type { EnvSchema, EnvVarDecl } from '@ultimat3/core';
|
|
7
|
-
import {
|
|
7
|
+
import { appEnvExample } from '../framework-env';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Committed defaults only, so a fresh clone boots with `x dev` and no scavenger hunt. Every
|
|
@@ -90,9 +90,12 @@ export function envSchemaSource(schema: EnvSchema = SCAFFOLD_ENV_SCHEMA): string
|
|
|
90
90
|
return lines.join('\n');
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Byte-identical to what `x env example` writes, because both call the one renderer — the app's
|
|
95
|
+
* declaration, then the framework's deploy-required keys (`framework-env.ts`).
|
|
96
|
+
*/
|
|
94
97
|
export const envExampleSource = (schema: EnvSchema = SCAFFOLD_ENV_SCHEMA): string =>
|
|
95
|
-
|
|
98
|
+
appEnvExample(schema);
|
|
96
99
|
|
|
97
100
|
/** Named so a reader of `repoFiles` can see the two projections come from one declaration. */
|
|
98
101
|
export const scaffoldEnvVarNames = (): readonly string[] =>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The root `.gitattributes` `x new` writes: every text file LF in the index and the checkout, every
|
|
2
|
+
// binary type left byte-for-byte. The framework repo's own root file opens with the same block —
|
|
3
|
+
// `scaffold-gitattributes.test.ts` refuses the two drifting apart.
|
|
4
|
+
|
|
5
|
+
/** The path, relative to the app root — git reads attributes from here for the whole tree. */
|
|
6
|
+
export const ROOT_GITATTRIBUTES_PATH = '.gitattributes';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Binary types an app or the framework plausibly tracks: images, icons, fonts, WebAssembly and the
|
|
10
|
+
* archives fixtures arrive in. `text=auto` would usually detect them, but "usually" is a heuristic
|
|
11
|
+
* over the first 8000 bytes — `binary` is the statement, and it also stops a textual diff.
|
|
12
|
+
*/
|
|
13
|
+
export const BINARY_EXTENSIONS: readonly string[] = [
|
|
14
|
+
'png',
|
|
15
|
+
'jpg',
|
|
16
|
+
'jpeg',
|
|
17
|
+
'gif',
|
|
18
|
+
'webp',
|
|
19
|
+
'avif',
|
|
20
|
+
'ico',
|
|
21
|
+
'woff',
|
|
22
|
+
'woff2',
|
|
23
|
+
'ttf',
|
|
24
|
+
'otf',
|
|
25
|
+
'wasm',
|
|
26
|
+
'pdf',
|
|
27
|
+
'zip',
|
|
28
|
+
'gz',
|
|
29
|
+
'tgz',
|
|
30
|
+
'tar',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `* text=auto eol=lf` is not cosmetic. Git for Windows installs with `core.autocrlf=true`, so a
|
|
35
|
+
* checkout without it rewrites every file to CRLF: Biome (`lineEnding: "lf"`) then flags every
|
|
36
|
+
* file, every `split('\n')` scanner sees a trailing `\r`, and every byte-compared artifact — the
|
|
37
|
+
* manifest, the schema dump, a migration's checksum — reads as drift on a correct tree. Pinned
|
|
38
|
+
* here, it holds for every clone whatever the contributor's git config says.
|
|
39
|
+
*/
|
|
40
|
+
export const LINE_ENDING_ATTRIBUTES = `# Every text file is LF, in the index and in the checkout, whatever \`core.autocrlf\` says. Git for
|
|
41
|
+
# Windows defaults it to true: without this line a Windows clone is CRLF, Biome flags every file and
|
|
42
|
+
# every byte-compared artifact (manifest, schema dump, migration checksum) reads as drift.
|
|
43
|
+
* text=auto eol=lf
|
|
44
|
+
|
|
45
|
+
# Binary, stated rather than detected: never line-ending converted, never textually diffed.
|
|
46
|
+
${BINARY_EXTENSIONS.map((extension) => `*.${extension} binary`).join('\n')}
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
/** The app's root file: the shared block, then the app's one generated file git should collapse. */
|
|
50
|
+
export const rootGitattributes = (): string => `${LINE_ENDING_ATTRIBUTES}
|
|
51
|
+
# Bun's lockfile. Regenerated by \`bun install\`, never edited by hand.
|
|
52
|
+
bun.lock -diff linguist-generated=true
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
/** The file `x new` writes at the app root — one entry in `repoFiles`' list. */
|
|
56
|
+
export const rootGitattributesFile = (): { readonly path: string; readonly contents: string } => ({
|
|
57
|
+
path: ROOT_GITATTRIBUTES_PATH,
|
|
58
|
+
contents: rootGitattributes(),
|
|
59
|
+
});
|
|
@@ -29,11 +29,13 @@ every object would be ${app.kebab}-${app.kebab}-web.
|
|
|
29
29
|
|
|
30
30
|
{{/*
|
|
31
31
|
repository:tag, and NOT a single "image" string: x deploy --method helm passes
|
|
32
|
-
--set image.repository=... --set image.tag=..., because replacing the map with a
|
|
33
|
-
every workload below fail on .repository and deploy nothing.
|
|
32
|
+
--set-string image.repository=... --set-string image.tag=..., because replacing the map with a
|
|
33
|
+
string makes every workload below fail on .repository and deploy nothing. toString on both halves:
|
|
34
|
+
a plain --set image.tag=1234567 is an int64, which printf renders as %!s(int64=1234567).
|
|
34
35
|
*/}}
|
|
35
36
|
{{- define "${app.kebab}.image" -}}
|
|
36
|
-
{{-
|
|
37
|
+
{{- $repository := required "image.repository is empty — set it to the registry path your CI pushes the app image to: --set-string image.repository=ghcr.io/<org>/<app>, or x deploy --method helm --image ghcr.io/<org>/<app>:<tag>" .Values.image.repository -}}
|
|
38
|
+
{{- printf "%s:%s" (toString $repository) (toString (default .Chart.AppVersion .Values.image.tag)) -}}
|
|
37
39
|
{{- end -}}
|
|
38
40
|
|
|
39
41
|
{{- define "${app.kebab}.labels" -}}
|
|
@@ -66,7 +68,7 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
66
68
|
{{- $envPort = sub (int $cfg.port) 1 -}}
|
|
67
69
|
{{- end -}}
|
|
68
70
|
- name: {{ $role }}
|
|
69
|
-
image: {{ include "${app.kebab}.image" $root }}
|
|
71
|
+
image: {{ include "${app.kebab}.image" $root | quote }}
|
|
70
72
|
imagePullPolicy: {{ $root.Values.image.pullPolicy }}
|
|
71
73
|
securityContext: {{- toYaml $root.Values.securityContext | nindent 4 }}
|
|
72
74
|
env:
|
|
@@ -116,10 +118,12 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
116
118
|
{{/*
|
|
117
119
|
Probes follow the role, because the roles do not agree on what they open. web and sync serve
|
|
118
120
|
HTTP and get both. worker, scheduler and replicator open no HTTP socket at all — the scrape
|
|
119
|
-
listener is their only port — so they take liveness on it
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
listener is their only port — so they take liveness on it, and readiness on /readyz of that same
|
|
122
|
+
listener: 503 until the role has started and from the first instant of a drain, so a rollout
|
|
123
|
+
never counts a worker that has not started its role yet. The replicator reads /readyz?deep=1:
|
|
124
|
+
503 while the replication stream is down, so the pod shows 0/1 instead of a healthy pod feeding
|
|
125
|
+
nothing. Their headless Service publishes not-ready addresses, so a not-ready pod is still
|
|
126
|
+
scraped.
|
|
123
127
|
|
|
124
128
|
Every one of them takes a startupProbe too, because no listener opens early: the server builds
|
|
125
129
|
the app's islands before any role binds a port, and a liveness probe counting from container
|
|
@@ -152,27 +156,69 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
152
156
|
httpGet: { path: /metrics, port: metrics }
|
|
153
157
|
periodSeconds: 5
|
|
154
158
|
failureThreshold: 30
|
|
155
|
-
{{- if eq $role "replicator" }}
|
|
156
159
|
readinessProbe:
|
|
160
|
+
{{- if eq $role "replicator" }}
|
|
157
161
|
httpGet: { path: '/readyz?deep=1', port: metrics }
|
|
158
162
|
periodSeconds: 10
|
|
159
|
-
|
|
163
|
+
{{- else }}
|
|
164
|
+
httpGet: { path: /readyz, port: metrics }
|
|
165
|
+
periodSeconds: 5
|
|
166
|
+
{{- end }}
|
|
160
167
|
livenessProbe:
|
|
161
168
|
httpGet: { path: /metrics, port: metrics }
|
|
162
169
|
periodSeconds: 15
|
|
163
170
|
failureThreshold: 4
|
|
171
|
+
{{- /*
|
|
172
|
+
The worker's retire, opt-in: roles.worker.retireSeconds > 0. The preStop sends PID 1 SIGUSR2 —
|
|
173
|
+
the framework's retire: stop claiming, finish every held job however long it takes, abort
|
|
174
|
+
nothing, then drain and exit 0 — and waits until PID 1 is gone, so the SIGTERM that follows finds
|
|
175
|
+
nothing left to cut off. bun -e, never kill/sleep: bun is the image's ENTRYPOINT (exec form, so it
|
|
176
|
+
IS PID 1). The kubelet counts the preStop against the grace period, so retireSeconds is added to
|
|
177
|
+
it and is the longest a retire may take.
|
|
178
|
+
*/}}
|
|
179
|
+
{{- if and (eq $role "worker") (gt (int $cfg.retireSeconds) 0) }}
|
|
180
|
+
lifecycle:
|
|
181
|
+
preStop:
|
|
182
|
+
exec:
|
|
183
|
+
command:
|
|
184
|
+
- bun
|
|
185
|
+
- -e
|
|
186
|
+
- "process.kill(1, 'SIGUSR2'); for (;;) { try { process.kill(1, 0); } catch { break; } await Bun.sleep(1000); }"
|
|
187
|
+
{{- end }}
|
|
164
188
|
{{- end }}
|
|
165
189
|
resources: {{- toYaml $cfg.resources | nindent 4 }}
|
|
166
190
|
volumeMounts:
|
|
167
191
|
- name: tmp
|
|
168
192
|
mountPath: /tmp
|
|
169
193
|
{{- end -}}
|
|
194
|
+
|
|
195
|
+
{{/*
|
|
196
|
+
A role's terminationGracePeriodSeconds, in the order the kubelet's budget is spent: the preStop
|
|
197
|
+
sleep (web and sync, 1.30+), the worker's opt-in retire (roles.worker.retireSeconds), the
|
|
198
|
+
readiness grace (web and sync — every other role drains with none), the drain budget (app.config.ts drain.deadlineMs, every role) and the teardown margin.
|
|
199
|
+
Defaults: 45s web/sync (40s below 1.30), 35s the rest. x deploy --method helm sets
|
|
200
|
+
drain.deadlineSeconds and drain.readinessGraceSeconds from app.config.ts.
|
|
201
|
+
*/}}
|
|
202
|
+
{{- define "${app.kebab}.terminationGracePeriodSeconds" -}}
|
|
203
|
+
{{- $drain := .root.Values.drain -}}
|
|
204
|
+
{{- $total := add (int $drain.deadlineSeconds) (int $drain.teardownMarginSeconds) -}}
|
|
205
|
+
{{- if eq .role "worker" -}}
|
|
206
|
+
{{- $total = add $total (int .cfg.retireSeconds) -}}
|
|
207
|
+
{{- end -}}
|
|
208
|
+
{{- if .cfg.port -}}
|
|
209
|
+
{{- $total = add $total (int $drain.readinessGraceSeconds) -}}
|
|
210
|
+
{{- if semverCompare ">=1.30-0" .root.Capabilities.KubeVersion.Version -}}
|
|
211
|
+
{{- $total = add $total (int $drain.preStopSleepSeconds) -}}
|
|
212
|
+
{{- end -}}
|
|
213
|
+
{{- end -}}
|
|
214
|
+
{{- $total -}}
|
|
215
|
+
{{- end -}}
|
|
170
216
|
`;
|
|
171
217
|
|
|
172
218
|
const deployments = (app: NameSet): string => `{{/*
|
|
173
219
|
One Deployment per enabled role, from one image. terminationGracePeriodSeconds covers the preStop
|
|
174
|
-
sleep, the framework's readiness grace and its SIGTERM drain
|
|
175
|
-
and running job steps finish.
|
|
220
|
+
sleep, the framework's readiness grace and its SIGTERM drain — in-flight requests, open websockets
|
|
221
|
+
and running job steps finish — derived per role from drain in values.yaml.
|
|
176
222
|
*/}}
|
|
177
223
|
{{- range $role, $cfg := .Values.roles }}
|
|
178
224
|
{{- if $cfg.enabled }}
|
|
@@ -194,9 +240,21 @@ spec:
|
|
|
194
240
|
matchLabels:
|
|
195
241
|
app.kubernetes.io/instance: {{ $.Release.Name }}
|
|
196
242
|
app.kubernetes.io/component: {{ $role }}
|
|
243
|
+
{{/*
|
|
244
|
+
The replicator is Recreate, never a rolling update: one pod per database holds the replication
|
|
245
|
+
slot's advisory lock, so a surged pod can only stand by, unready, and maxUnavailable: 0 never
|
|
246
|
+
terminates the holder it is waiting on — every rollout stalled. Recreate lets the holder go first.
|
|
247
|
+
*/}}
|
|
248
|
+
{{/* A new pod must stay Ready this long before the rollout counts it and stops an old one, so a
|
|
249
|
+
role that crashes seconds after its first probe never replaces a working pod. */}}
|
|
250
|
+
minReadySeconds: {{ $.Values.minReadySeconds | int }}
|
|
197
251
|
strategy:
|
|
252
|
+
{{- if eq $role "replicator" }}
|
|
253
|
+
type: Recreate
|
|
254
|
+
{{- else }}
|
|
198
255
|
type: RollingUpdate
|
|
199
256
|
rollingUpdate: { maxUnavailable: 0, maxSurge: 1 }
|
|
257
|
+
{{- end }}
|
|
200
258
|
template:
|
|
201
259
|
metadata:
|
|
202
260
|
labels:
|
|
@@ -208,12 +266,10 @@ spec:
|
|
|
208
266
|
code that reaches the filesystem, which for a web role is one path traversal. */}}
|
|
209
267
|
automountServiceAccountToken: false
|
|
210
268
|
securityContext: {{- toYaml $.Values.podSecurityContext | nindent 8 }}
|
|
211
|
-
{{/*
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
abandons its hooks at its own deadline. */}}
|
|
216
|
-
terminationGracePeriodSeconds: 45
|
|
269
|
+
{{/* Derived per role from the drain values — see the helper in _helpers.tpl. Raise
|
|
270
|
+
drain.deadlineMs in app.config.ts, never this: the drain abandons its hooks at its own
|
|
271
|
+
deadline, so a longer grace period alone buys nothing. */}}
|
|
272
|
+
terminationGracePeriodSeconds: {{ include "${app.kebab}.terminationGracePeriodSeconds" (dict "role" $role "cfg" $cfg "root" $) }}
|
|
217
273
|
containers:
|
|
218
274
|
{{- include "${app.kebab}.container" (dict "role" $role "cfg" $cfg "root" $) | nindent 8 }}
|
|
219
275
|
volumes:
|