@ultimat3/cli 23.0.0 → 25.0.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 +28 -13
- package/README.md +32 -7
- package/package.json +29 -29
- package/src/admin-catalog-keys.ts +2 -2
- package/src/affected.ts +30 -36
- package/src/api-registration.ts +89 -0
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +17 -4
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +26 -25
- package/src/app-load.ts +38 -14
- package/src/app-manifest.ts +4 -10
- package/src/app-mcp.ts +31 -46
- package/src/app-name.ts +52 -0
- package/src/app-permissions-borrowed.ts +123 -0
- package/src/app-permissions-site.ts +48 -0
- package/src/app-permissions.ts +7 -17
- 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 +117 -154
- package/src/cdp-shot-driver.ts +20 -1
- package/src/cdp-shot-watch.ts +35 -31
- package/src/ci-log.ts +0 -0
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +151 -22
- package/src/cmd-db-backfill.ts +26 -13
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db.ts +10 -6
- package/src/cmd-deploy-compose.ts +94 -0
- package/src/cmd-deploy-drain.ts +48 -0
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy-start-first.ts +178 -0
- package/src/cmd-deploy.ts +158 -36
- package/src/cmd-dev.ts +18 -356
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +57 -80
- package/src/cmd-env.ts +55 -16
- package/src/cmd-generate.ts +89 -32
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +21 -17
- package/src/cmd-jobs-spec.ts +7 -22
- package/src/cmd-jobs.ts +33 -140
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +71 -22
- package/src/cmd-new.ts +36 -8
- package/src/cmd-planned.ts +24 -8
- package/src/cmd-routes.ts +15 -5
- package/src/cmd-secrets.ts +47 -6
- package/src/cmd-shot-matrix.ts +22 -7
- package/src/cmd-shot-spec.ts +10 -2
- package/src/cmd-shot.ts +46 -41
- package/src/cmd-tasks.ts +2 -2
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +14 -30
- package/src/cmd-verify.ts +4 -4
- 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 +27 -16
- package/src/dev-boot.ts +290 -0
- package/src/dev-child-watch.ts +63 -12
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +56 -30
- package/src/dev-lock.ts +100 -31
- 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 +17 -13
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/doctor-offline.ts +6 -5
- package/src/document-options.ts +7 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/drift.ts +29 -6
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +29 -3
- package/src/error-page-csp.ts +14 -15
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- 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-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +25 -3
- package/src/generate-catalog-locales.ts +79 -0
- package/src/generate-files.ts +4 -3
- package/src/generate-kinds.ts +18 -0
- package/src/generate-plural.ts +50 -0
- package/src/generate-shadow.ts +95 -0
- package/src/generate-write.ts +47 -13
- package/src/gh-target.ts +12 -7
- package/src/hold.ts +56 -1
- package/src/i18n-audit.ts +25 -9
- 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 +2 -6
- package/src/island-bundle.ts +57 -79
- 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 +36 -20
- package/src/local-cli-handoff.ts +90 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +20 -4
- package/src/mcp-host.ts +22 -14
- package/src/mcp-test-run.ts +38 -0
- 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 +29 -16
- package/src/metrics-endpoint.ts +28 -3
- package/src/migration-extensions.ts +1 -1
- package/src/otlp-export.ts +2 -2
- package/src/output.ts +24 -0
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +19 -31
- 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-out.ts +34 -7
- package/src/prerender-report.ts +58 -0
- package/src/prerender-served.ts +52 -0
- package/src/prerender.ts +71 -103
- package/src/pwa-artifacts.ts +33 -94
- package/src/pwa-manifest-locales.ts +9 -9
- package/src/registry.ts +2 -0
- package/src/retry-memo.ts +1 -1
- package/src/revalidated-response.ts +4 -3
- package/src/role-realtime.ts +7 -1
- package/src/role-replicator.ts +124 -29
- package/src/role-start.ts +69 -85
- package/src/role-sync.ts +119 -25
- 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 +26 -9
- package/src/runtime-bindings.ts +27 -7
- package/src/runtime-cache.ts +46 -55
- package/src/runtime-hooks.ts +2 -1
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr-outcome.ts +100 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +76 -41
- package/src/runtime-live-feed.ts +1 -1
- package/src/runtime-mail.ts +26 -0
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +4 -4
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +43 -22
- package/src/runtime-realtime.ts +11 -45
- package/src/runtime-render.ts +136 -114
- package/src/runtime-replica.ts +3 -8
- package/src/runtime-services.ts +49 -26
- package/src/runtime-storage-upload.ts +2 -2
- package/src/runtime-storage.ts +13 -8
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/secrets-rotation.ts +8 -9
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +51 -12
- package/src/serve-drain.ts +11 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-retire.ts +95 -0
- package/src/serve-web.ts +31 -15
- 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 -33
- 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-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +7 -5
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-files.ts +19 -3
- package/src/static-report.ts +5 -0
- package/src/stored-object-headers.ts +43 -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 +3 -3
- package/src/tasks-facts.ts +3 -29
- package/src/templates/action.ts +7 -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/naming.ts +28 -0
- 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 +26 -14
- package/src/templates/scaffold-api.ts +2 -3
- package/src/templates/scaffold-app.ts +8 -3
- 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-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +16 -8
- 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-client.ts +4 -2
- 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-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +100 -19
- package/src/templates/scaffold-helm.ts +62 -5
- 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 +27 -16
- package/src/templates/scaffold-shell.ts +55 -37
- package/src/test-counts.ts +2 -6
- package/src/test-ignores.ts +25 -0
- package/src/test-only-shipped.ts +193 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +7 -15
- package/src/test-shards.ts +40 -95
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +8 -33
- package/src/theme-brand.ts +66 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/trusted-hops.ts +49 -0
- package/src/unscanned-admin.ts +2 -2
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +29 -48
- package/src/verify-coverage-run.ts +3 -2
- package/src/verify-deadline.ts +207 -33
- package/src/verify-merge.ts +108 -43
- package/src/verify-part-step.ts +98 -0
- package/src/verify-role-load-probe.ts +12 -2
- package/src/verify-role-load.ts +9 -4
- package/src/verify-run.ts +32 -8
- package/src/verify-shard.ts +13 -2
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +2 -0
- package/src/verify-test-run.ts +3 -2
- package/src/verify-tests.ts +5 -3
- 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/build-errors.ts +0 -54
- package/src/jobs-drain.ts +0 -163
- package/src/scaffold-typecheck.ts +0 -227
- package/src/thrown-by.ts +0 -24
- package/src/verify-errors.ts +0 -58
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// What a compromised pod of the scaffolded chart can reach: its `/tmp`, bounded, and its ports,
|
|
2
|
+
// fenced by one NetworkPolicy per role. The framework chart's own (`docker/helm/`), mirrored —
|
|
3
|
+
// `scaffold-helm-parity.test.ts` holds the two to each other.
|
|
4
|
+
|
|
5
|
+
import type { GeneratedFile, NameSet } from './naming';
|
|
6
|
+
|
|
7
|
+
const volumes = (app: NameSet): string => `{{/*
|
|
8
|
+
The writable scratch every pod mounts at /tmp — the root filesystem is read-only. Bounded, because
|
|
9
|
+
an emptyDir with no sizeLimit is node disk: one role filling /tmp evicts its NEIGHBOURS for disk
|
|
10
|
+
pressure, where with a limit the kubelet evicts that pod alone.
|
|
11
|
+
*/}}
|
|
12
|
+
{{- define "${app.kebab}.tmpVolume" -}}
|
|
13
|
+
- name: tmp
|
|
14
|
+
emptyDir:
|
|
15
|
+
sizeLimit: {{ required "tmp.sizeLimit is unset — set it in your values (e.g. 512Mi): an unbounded /tmp is node disk" (default dict .Values.tmp).sizeLimit | quote }}
|
|
16
|
+
{{- end -}}
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const networkPolicy = (app: NameSet): string => `{{/*
|
|
20
|
+
One NetworkPolicy per enabled role: \`http\` (web, sync) from networkPolicy.httpFrom, \`metrics\`
|
|
21
|
+
(every role) from networkPolicy.metricsFrom, nothing else. A \`from\` that renders empty is omitted,
|
|
22
|
+
which Kubernetes reads as "any source". Egress is filtered only when networkPolicy.egress lists
|
|
23
|
+
rules — the database, NATS and object store are your addresses, so list DNS too. Serving roles
|
|
24
|
+
only: the migrate Job is a pre-install hook that runs before this exists, and opens no port.
|
|
25
|
+
*/}}
|
|
26
|
+
{{- if .Values.networkPolicy.enabled }}
|
|
27
|
+
{{- range $role, $cfg := .Values.roles }}
|
|
28
|
+
{{- if $cfg.enabled }}
|
|
29
|
+
---
|
|
30
|
+
apiVersion: networking.k8s.io/v1
|
|
31
|
+
kind: NetworkPolicy
|
|
32
|
+
metadata:
|
|
33
|
+
name: {{ include "${app.kebab}.fullname" $ }}-{{ $role }}
|
|
34
|
+
labels:
|
|
35
|
+
{{- include "${app.kebab}.labels" $ | nindent 4 }}
|
|
36
|
+
app.kubernetes.io/component: {{ $role }}
|
|
37
|
+
spec:
|
|
38
|
+
podSelector:
|
|
39
|
+
matchLabels:
|
|
40
|
+
app.kubernetes.io/instance: {{ $.Release.Name }}
|
|
41
|
+
app.kubernetes.io/component: {{ $role }}
|
|
42
|
+
policyTypes:
|
|
43
|
+
- Ingress
|
|
44
|
+
{{- if $.Values.networkPolicy.egress }}
|
|
45
|
+
- Egress
|
|
46
|
+
{{- end }}
|
|
47
|
+
ingress:
|
|
48
|
+
{{- if $cfg.port }}
|
|
49
|
+
- ports:
|
|
50
|
+
- port: http
|
|
51
|
+
protocol: TCP
|
|
52
|
+
{{- with $.Values.networkPolicy.httpFrom }}
|
|
53
|
+
from: {{- toYaml . | nindent 8 }}
|
|
54
|
+
{{- end }}
|
|
55
|
+
{{- end }}
|
|
56
|
+
- ports:
|
|
57
|
+
- port: metrics
|
|
58
|
+
protocol: TCP
|
|
59
|
+
{{- with $.Values.networkPolicy.metricsFrom }}
|
|
60
|
+
from: {{- toYaml . | nindent 8 }}
|
|
61
|
+
{{- end }}
|
|
62
|
+
{{- with $.Values.networkPolicy.egress }}
|
|
63
|
+
egress: {{- toYaml . | nindent 4 }}
|
|
64
|
+
{{- end }}
|
|
65
|
+
{{- end }}
|
|
66
|
+
{{- end }}
|
|
67
|
+
{{- end }}
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
/** The bounded /tmp helper and the per-role NetworkPolicy, as two files of the chart's templates. */
|
|
71
|
+
export function helmFenceFiles(app: NameSet): readonly GeneratedFile[] {
|
|
72
|
+
return [
|
|
73
|
+
{ path: 'docker/helm/templates/_volumes.tpl', contents: volumes(app) },
|
|
74
|
+
{ path: 'docker/helm/templates/networkpolicy.yaml', contents: networkPolicy(app) },
|
|
75
|
+
];
|
|
76
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Split from scaffold-helm.ts, which holds the chart's INPUTS (Chart.yaml, values.yaml).
|
|
4
4
|
|
|
5
5
|
import type { GeneratedFile, NameSet } from './naming';
|
|
6
|
+
import { helmFenceFiles } from './scaffold-helm-fences';
|
|
6
7
|
|
|
7
8
|
const helpers = (
|
|
8
9
|
app: NameSet,
|
|
@@ -28,11 +29,13 @@ every object would be ${app.kebab}-${app.kebab}-web.
|
|
|
28
29
|
|
|
29
30
|
{{/*
|
|
30
31
|
repository:tag, and NOT a single "image" string: x deploy --method helm passes
|
|
31
|
-
--set image.repository=... --set image.tag=..., because replacing the map with a
|
|
32
|
-
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).
|
|
33
35
|
*/}}
|
|
34
36
|
{{- define "${app.kebab}.image" -}}
|
|
35
|
-
{{-
|
|
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)) -}}
|
|
36
39
|
{{- end -}}
|
|
37
40
|
|
|
38
41
|
{{- define "${app.kebab}.labels" -}}
|
|
@@ -65,7 +68,7 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
65
68
|
{{- $envPort = sub (int $cfg.port) 1 -}}
|
|
66
69
|
{{- end -}}
|
|
67
70
|
- name: {{ $role }}
|
|
68
|
-
image: {{ include "${app.kebab}.image" $root }}
|
|
71
|
+
image: {{ include "${app.kebab}.image" $root | quote }}
|
|
69
72
|
imagePullPolicy: {{ $root.Values.image.pullPolicy }}
|
|
70
73
|
securityContext: {{- toYaml $root.Values.securityContext | nindent 4 }}
|
|
71
74
|
env:
|
|
@@ -79,13 +82,28 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
79
82
|
- name: METRICS_PORT
|
|
80
83
|
value: {{ $root.Values.metricsPort | quote }}
|
|
81
84
|
{{- end }}
|
|
85
|
+
{{- /*
|
|
86
|
+
The sync node's page origin, from the one place this chart already knows it. Undeclared, a node
|
|
87
|
+
reached over plain http behind this Ingress admits BOTH spellings of its host — the plain-http
|
|
88
|
+
page included. env.APP_URL wins when set; an APP_URL kept in the Secret is shadowed for this
|
|
89
|
+
role, so pages served from another host than the Ingress's must say so in env.
|
|
90
|
+
*/}}
|
|
91
|
+
{{- if and (eq $role "sync") $root.Values.ingress.enabled (not (hasKey $root.Values.env "APP_URL")) }}
|
|
92
|
+
- name: APP_URL
|
|
93
|
+
value: {{ printf "%s://%s" (ternary "https" "http" $root.Values.ingress.tls) $root.Values.ingress.host | quote }}
|
|
94
|
+
{{- end }}
|
|
82
95
|
{{- range $key, $value := $root.Values.env }}
|
|
83
96
|
- name: {{ $key }}
|
|
84
97
|
value: {{ $value | quote }}
|
|
85
98
|
{{- end }}
|
|
99
|
+
{{- /*
|
|
100
|
+
The Secret a role reads, per role: roles.<role>.existingSecret (or migrate.existingSecret) names a
|
|
101
|
+
narrower one than the release-wide existingSecret, so the migrate Job's schema-owning credential
|
|
102
|
+
need not sit on a web pod. Both unset is a render error, not a pod with no configuration.
|
|
103
|
+
*/}}
|
|
86
104
|
envFrom:
|
|
87
105
|
- secretRef:
|
|
88
|
-
name: {{ $root.Values.existingSecret }}
|
|
106
|
+
name: {{ required (printf "no Secret for role %s — set existingSecret, or roles.%s.existingSecret, in your values" $role $role) (default $root.Values.existingSecret $cfg.existingSecret) }}
|
|
89
107
|
{{- if or $cfg.port $scraped }}
|
|
90
108
|
ports:
|
|
91
109
|
{{- if $cfg.port }}
|
|
@@ -100,8 +118,12 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
100
118
|
{{/*
|
|
101
119
|
Probes follow the role, because the roles do not agree on what they open. web and sync serve
|
|
102
120
|
HTTP and get both. worker, scheduler and replicator open no HTTP socket at all — the scrape
|
|
103
|
-
listener is their only port — so they take liveness on it and
|
|
104
|
-
|
|
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.
|
|
105
127
|
|
|
106
128
|
Every one of them takes a startupProbe too, because no listener opens early: the server builds
|
|
107
129
|
the app's islands before any role binds a port, and a liveness probe counting from container
|
|
@@ -134,22 +156,69 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
134
156
|
httpGet: { path: /metrics, port: metrics }
|
|
135
157
|
periodSeconds: 5
|
|
136
158
|
failureThreshold: 30
|
|
159
|
+
readinessProbe:
|
|
160
|
+
{{- if eq $role "replicator" }}
|
|
161
|
+
httpGet: { path: '/readyz?deep=1', port: metrics }
|
|
162
|
+
periodSeconds: 10
|
|
163
|
+
{{- else }}
|
|
164
|
+
httpGet: { path: /readyz, port: metrics }
|
|
165
|
+
periodSeconds: 5
|
|
166
|
+
{{- end }}
|
|
137
167
|
livenessProbe:
|
|
138
168
|
httpGet: { path: /metrics, port: metrics }
|
|
139
169
|
periodSeconds: 15
|
|
140
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 }}
|
|
141
188
|
{{- end }}
|
|
142
189
|
resources: {{- toYaml $cfg.resources | nindent 4 }}
|
|
143
190
|
volumeMounts:
|
|
144
191
|
- name: tmp
|
|
145
192
|
mountPath: /tmp
|
|
146
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 -}}
|
|
147
216
|
`;
|
|
148
217
|
|
|
149
218
|
const deployments = (app: NameSet): string => `{{/*
|
|
150
219
|
One Deployment per enabled role, from one image. terminationGracePeriodSeconds covers the preStop
|
|
151
|
-
sleep, the framework's readiness grace and its SIGTERM drain
|
|
152
|
-
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.
|
|
153
222
|
*/}}
|
|
154
223
|
{{- range $role, $cfg := .Values.roles }}
|
|
155
224
|
{{- if $cfg.enabled }}
|
|
@@ -171,9 +240,21 @@ spec:
|
|
|
171
240
|
matchLabels:
|
|
172
241
|
app.kubernetes.io/instance: {{ $.Release.Name }}
|
|
173
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 }}
|
|
174
251
|
strategy:
|
|
252
|
+
{{- if eq $role "replicator" }}
|
|
253
|
+
type: Recreate
|
|
254
|
+
{{- else }}
|
|
175
255
|
type: RollingUpdate
|
|
176
256
|
rollingUpdate: { maxUnavailable: 0, maxSurge: 1 }
|
|
257
|
+
{{- end }}
|
|
177
258
|
template:
|
|
178
259
|
metadata:
|
|
179
260
|
labels:
|
|
@@ -185,17 +266,14 @@ spec:
|
|
|
185
266
|
code that reaches the filesystem, which for a web role is one path traversal. */}}
|
|
186
267
|
automountServiceAccountToken: false
|
|
187
268
|
securityContext: {{- toYaml $.Values.podSecurityContext | nindent 8 }}
|
|
188
|
-
{{/*
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
abandons its hooks at its own deadline. */}}
|
|
193
|
-
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" $) }}
|
|
194
273
|
containers:
|
|
195
274
|
{{- include "${app.kebab}.container" (dict "role" $role "cfg" $cfg "root" $) | nindent 8 }}
|
|
196
275
|
volumes:
|
|
197
|
-
-
|
|
198
|
-
emptyDir: {}
|
|
276
|
+
{{- include "${app.kebab}.tmpVolume" $ | nindent 8 }}
|
|
199
277
|
{{- end }}
|
|
200
278
|
{{- end }}
|
|
201
279
|
`;
|
|
@@ -219,6 +297,9 @@ spec:
|
|
|
219
297
|
type: ClusterIP
|
|
220
298
|
{{- if not $cfg.port }}
|
|
221
299
|
clusterIP: None
|
|
300
|
+
# Scraped whether or not ready: a replicator whose stream is down (readiness 503) is the pod
|
|
301
|
+
# whose metrics matter most, and nothing routes to these addresses anyway.
|
|
302
|
+
publishNotReadyAddresses: true
|
|
222
303
|
{{- end }}
|
|
223
304
|
selector:
|
|
224
305
|
app.kubernetes.io/instance: {{ $.Release.Name }}
|
|
@@ -267,8 +348,7 @@ spec:
|
|
|
267
348
|
containers:
|
|
268
349
|
{{- include "${app.kebab}.container" (dict "role" "migrate" "cfg" .Values.migrate "root" .) | nindent 8 }}
|
|
269
350
|
volumes:
|
|
270
|
-
-
|
|
271
|
-
emptyDir: {}
|
|
351
|
+
{{- include "${app.kebab}.tmpVolume" . | nindent 8 }}
|
|
272
352
|
{{- end }}
|
|
273
353
|
`;
|
|
274
354
|
|
|
@@ -382,5 +462,6 @@ export function helmTemplateFiles(app: NameSet): readonly GeneratedFile[] {
|
|
|
382
462
|
{ path: 'docker/helm/templates/migrate-job.yaml', contents: migrateJob(app) },
|
|
383
463
|
{ path: 'docker/helm/templates/ingress.yaml', contents: ingress(app) },
|
|
384
464
|
{ path: 'docker/helm/templates/hpa.yaml', contents: hpa(app) },
|
|
465
|
+
...helmFenceFiles(app),
|
|
385
466
|
];
|
|
386
467
|
}
|