@tdk-landscape/tdk-cli-core 1.3.49 → 1.3.51
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/skills/layer-autoresearch/SKILL.md +48 -0
- package/.claude/skills/tdk-troubleshoot/SKILL.md +222 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +40 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +18 -0
- package/.github/dependabot.yml +33 -0
- package/.github/pull_request_template.md +5 -0
- package/.github/workflows/ci.yml +187 -0
- package/.github/workflows/release-binaries.yml +19 -9
- package/.github/zizmor.yml +9 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +42 -0
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/SECURITY.md +17 -0
- package/benchmarks/results/container-scale-2026-09-25T20-54-49-736Z.json +45 -0
- package/benchmarks/results/container-scale-2026-09-25T21-01-20-540Z.json +150 -0
- package/benchmarks/results/container-scale-2026-09-25T21-15-01-441Z.json +73 -0
- package/benchmarks/results/container-scale-2026-09-25T22-09-01-630Z.json +77 -0
- package/benchmarks/results/container-scale-2026-09-25T22-18-28-371Z.json +202 -0
- package/cli/README.md +25 -0
- package/cli/biome.json +1 -1
- package/cli/dist/cli.d.ts +3 -0
- package/cli/dist/cli.d.ts.map +1 -0
- package/cli/dist/cli.js +64 -0
- package/cli/dist/cli.js.map +1 -0
- package/cli/dist/commands/completion.d.ts +3 -0
- package/cli/dist/commands/completion.d.ts.map +1 -0
- package/cli/dist/commands/completion.js +294 -0
- package/cli/dist/commands/completion.js.map +1 -0
- package/cli/dist/commands/config.d.ts +3 -0
- package/cli/dist/commands/config.d.ts.map +1 -0
- package/cli/dist/commands/config.js +231 -0
- package/cli/dist/commands/config.js.map +1 -0
- package/cli/dist/commands/doctor.d.ts +19 -0
- package/cli/dist/commands/doctor.d.ts.map +1 -0
- package/cli/dist/commands/doctor.js +745 -0
- package/cli/dist/commands/doctor.js.map +1 -0
- package/cli/dist/commands/down.d.ts +3 -0
- package/cli/dist/commands/down.d.ts.map +1 -0
- package/cli/dist/commands/down.js +32 -0
- package/cli/dist/commands/down.js.map +1 -0
- package/cli/dist/commands/help.d.ts +2 -0
- package/cli/dist/commands/help.d.ts.map +1 -0
- package/cli/dist/commands/help.js +138 -0
- package/cli/dist/commands/help.js.map +1 -0
- package/cli/dist/commands/networks.d.ts +3 -0
- package/cli/dist/commands/networks.d.ts.map +1 -0
- package/cli/dist/commands/networks.js +260 -0
- package/cli/dist/commands/networks.js.map +1 -0
- package/cli/dist/commands/project.d.ts +3 -0
- package/cli/dist/commands/project.d.ts.map +1 -0
- package/cli/dist/commands/project.js +370 -0
- package/cli/dist/commands/project.js.map +1 -0
- package/cli/dist/commands/projects.d.ts +3 -0
- package/cli/dist/commands/projects.d.ts.map +1 -0
- package/cli/dist/commands/projects.js +71 -0
- package/cli/dist/commands/projects.js.map +1 -0
- package/cli/dist/commands/resource.d.ts +69 -0
- package/cli/dist/commands/resource.d.ts.map +1 -0
- package/cli/dist/commands/resource.js +579 -0
- package/cli/dist/commands/resource.js.map +1 -0
- package/cli/dist/commands/resources.d.ts +3 -0
- package/cli/dist/commands/resources.d.ts.map +1 -0
- package/cli/dist/commands/resources.js +73 -0
- package/cli/dist/commands/resources.js.map +1 -0
- package/cli/dist/commands/stack.d.ts +3 -0
- package/cli/dist/commands/stack.d.ts.map +1 -0
- package/cli/dist/commands/stack.js +97 -0
- package/cli/dist/commands/stack.js.map +1 -0
- package/cli/dist/commands/stacks.d.ts +3 -0
- package/cli/dist/commands/stacks.d.ts.map +1 -0
- package/cli/dist/commands/stacks.js +43 -0
- package/cli/dist/commands/stacks.js.map +1 -0
- package/cli/dist/commands/status.d.ts +3 -0
- package/cli/dist/commands/status.d.ts.map +1 -0
- package/cli/dist/commands/status.js +71 -0
- package/cli/dist/commands/status.js.map +1 -0
- package/cli/dist/commands/ui.d.ts +3 -0
- package/cli/dist/commands/ui.d.ts.map +1 -0
- package/cli/dist/commands/ui.js +445 -0
- package/cli/dist/commands/ui.js.map +1 -0
- package/cli/dist/commands/up.d.ts +3 -0
- package/cli/dist/commands/up.d.ts.map +1 -0
- package/cli/dist/commands/up.js +124 -0
- package/cli/dist/commands/up.js.map +1 -0
- package/cli/dist/commands/upgrade.d.ts +11 -0
- package/cli/dist/commands/upgrade.d.ts.map +1 -0
- package/cli/dist/commands/upgrade.js +496 -0
- package/cli/dist/commands/upgrade.js.map +1 -0
- package/cli/dist/commands/version.d.ts +3 -0
- package/cli/dist/commands/version.d.ts.map +1 -0
- package/cli/dist/commands/version.js +9 -0
- package/cli/dist/commands/version.js.map +1 -0
- package/cli/dist/components/Accessible.d.ts +8 -0
- package/cli/dist/components/Accessible.d.ts.map +1 -0
- package/cli/dist/components/Accessible.js +10 -0
- package/cli/dist/components/Accessible.js.map +1 -0
- package/cli/dist/components/BaseTooltip.d.ts +4 -0
- package/cli/dist/components/BaseTooltip.d.ts.map +1 -0
- package/cli/dist/components/BaseTooltip.js +28 -0
- package/cli/dist/components/BaseTooltip.js.map +1 -0
- package/cli/dist/components/DetailPanel.d.ts +4 -0
- package/cli/dist/components/DetailPanel.d.ts.map +1 -0
- package/cli/dist/components/DetailPanel.js +18 -0
- package/cli/dist/components/DetailPanel.js.map +1 -0
- package/cli/dist/components/FileTree.d.ts +4 -0
- package/cli/dist/components/FileTree.d.ts.map +1 -0
- package/cli/dist/components/FileTree.js +47 -0
- package/cli/dist/components/FileTree.js.map +1 -0
- package/cli/dist/components/ResourceSelectInput.d.ts +4 -0
- package/cli/dist/components/ResourceSelectInput.d.ts.map +1 -0
- package/cli/dist/components/ResourceSelectInput.js +7 -0
- package/cli/dist/components/ResourceSelectInput.js.map +1 -0
- package/cli/dist/components/ResourceTable.d.ts +4 -0
- package/cli/dist/components/ResourceTable.d.ts.map +1 -0
- package/cli/dist/components/ResourceTable.js +18 -0
- package/cli/dist/components/ResourceTable.js.map +1 -0
- package/cli/dist/components/TabBar.d.ts +4 -0
- package/cli/dist/components/TabBar.d.ts.map +1 -0
- package/cli/dist/components/TabBar.js +17 -0
- package/cli/dist/components/TabBar.js.map +1 -0
- package/cli/dist/components/Tooltip.d.ts +12 -0
- package/cli/dist/components/Tooltip.d.ts.map +1 -0
- package/cli/dist/components/Tooltip.js +12 -0
- package/cli/dist/components/Tooltip.js.map +1 -0
- package/cli/dist/components/index.d.ts +8 -0
- package/cli/dist/components/index.d.ts.map +1 -0
- package/cli/dist/components/index.js +8 -0
- package/cli/dist/components/index.js.map +1 -0
- package/cli/dist/config/platform-standards.d.ts +159 -0
- package/cli/dist/config/platform-standards.d.ts.map +1 -0
- package/cli/dist/config/platform-standards.js +181 -0
- package/cli/dist/config/platform-standards.js.map +1 -0
- package/cli/dist/generator/extension-fetch.d.ts +25 -0
- package/cli/dist/generator/extension-fetch.d.ts.map +1 -0
- package/cli/dist/generator/extension-fetch.js +259 -0
- package/cli/dist/generator/extension-fetch.js.map +1 -0
- package/cli/dist/generator/template-engine.d.ts +62 -0
- package/cli/dist/generator/template-engine.d.ts.map +1 -0
- package/cli/dist/generator/template-engine.js +623 -0
- package/cli/dist/generator/template-engine.js.map +1 -0
- package/cli/dist/index.d.ts +11 -0
- package/cli/dist/index.d.ts.map +1 -0
- package/cli/dist/index.js +11 -0
- package/cli/dist/index.js.map +1 -0
- package/cli/dist/types/index.d.ts +257 -0
- package/cli/dist/types/index.d.ts.map +1 -0
- package/cli/dist/types/index.js +17 -0
- package/cli/dist/types/index.js.map +1 -0
- package/cli/dist/utils/cache.d.ts +19 -0
- package/cli/dist/utils/cache.d.ts.map +1 -0
- package/cli/dist/utils/cache.js +64 -0
- package/cli/dist/utils/cache.js.map +1 -0
- package/cli/dist/utils/command-helpers.d.ts +17 -0
- package/cli/dist/utils/command-helpers.d.ts.map +1 -0
- package/cli/dist/utils/command-helpers.js +41 -0
- package/cli/dist/utils/command-helpers.js.map +1 -0
- package/cli/dist/utils/constants.d.ts +53 -0
- package/cli/dist/utils/constants.d.ts.map +1 -0
- package/cli/dist/utils/constants.js +115 -0
- package/cli/dist/utils/constants.js.map +1 -0
- package/cli/dist/utils/discovery-context.d.ts +4 -0
- package/cli/dist/utils/discovery-context.d.ts.map +1 -0
- package/cli/dist/utils/discovery-context.js +39 -0
- package/cli/dist/utils/discovery-context.js.map +1 -0
- package/cli/dist/utils/docker.d.ts +4 -0
- package/cli/dist/utils/docker.d.ts.map +1 -0
- package/cli/dist/utils/docker.js +19 -0
- package/cli/dist/utils/docker.js.map +1 -0
- package/cli/dist/utils/doctor-runtime.d.ts +81 -0
- package/cli/dist/utils/doctor-runtime.d.ts.map +1 -0
- package/cli/dist/utils/doctor-runtime.js +498 -0
- package/cli/dist/utils/doctor-runtime.js.map +1 -0
- package/cli/dist/utils/env-validator.d.ts +15 -0
- package/cli/dist/utils/env-validator.d.ts.map +1 -0
- package/cli/dist/utils/env-validator.js +109 -0
- package/cli/dist/utils/env-validator.js.map +1 -0
- package/cli/dist/utils/errors.d.ts +31 -0
- package/cli/dist/utils/errors.d.ts.map +1 -0
- package/cli/dist/utils/errors.js +140 -0
- package/cli/dist/utils/errors.js.map +1 -0
- package/cli/dist/utils/file-helpers.d.ts +14 -0
- package/cli/dist/utils/file-helpers.d.ts.map +1 -0
- package/cli/dist/utils/file-helpers.js +45 -0
- package/cli/dist/utils/file-helpers.js.map +1 -0
- package/cli/dist/utils/formatting.d.ts +30 -0
- package/cli/dist/utils/formatting.d.ts.map +1 -0
- package/cli/dist/utils/formatting.js +164 -0
- package/cli/dist/utils/formatting.js.map +1 -0
- package/cli/dist/utils/paths.d.ts +3 -0
- package/cli/dist/utils/paths.d.ts.map +1 -0
- package/cli/dist/utils/paths.js +41 -0
- package/cli/dist/utils/paths.js.map +1 -0
- package/cli/dist/utils/port-assignment.d.ts +6 -0
- package/cli/dist/utils/port-assignment.d.ts.map +1 -0
- package/cli/dist/utils/port-assignment.js +81 -0
- package/cli/dist/utils/port-assignment.js.map +1 -0
- package/cli/dist/utils/project-features.d.ts +23 -0
- package/cli/dist/utils/project-features.d.ts.map +1 -0
- package/cli/dist/utils/project-features.js +98 -0
- package/cli/dist/utils/project-features.js.map +1 -0
- package/cli/dist/utils/project-templates.d.ts +7 -0
- package/cli/dist/utils/project-templates.d.ts.map +1 -0
- package/cli/dist/utils/project-templates.js +24 -0
- package/cli/dist/utils/project-templates.js.map +1 -0
- package/cli/dist/utils/prompt.d.ts +28 -0
- package/cli/dist/utils/prompt.d.ts.map +1 -0
- package/cli/dist/utils/prompt.js +79 -0
- package/cli/dist/utils/prompt.js.map +1 -0
- package/cli/dist/utils/resource-features.d.ts +29 -0
- package/cli/dist/utils/resource-features.d.ts.map +1 -0
- package/cli/dist/utils/resource-features.js +175 -0
- package/cli/dist/utils/resource-features.js.map +1 -0
- package/cli/dist/utils/service-urls.d.ts +30 -0
- package/cli/dist/utils/service-urls.d.ts.map +1 -0
- package/cli/dist/utils/service-urls.js +93 -0
- package/cli/dist/utils/service-urls.js.map +1 -0
- package/cli/dist/utils/services.d.ts +18 -0
- package/cli/dist/utils/services.d.ts.map +1 -0
- package/cli/dist/utils/services.js +292 -0
- package/cli/dist/utils/services.js.map +1 -0
- package/cli/dist/utils/stack-features.d.ts +22 -0
- package/cli/dist/utils/stack-features.d.ts.map +1 -0
- package/cli/dist/utils/stack-features.js +13 -0
- package/cli/dist/utils/stack-features.js.map +1 -0
- package/cli/dist/utils/tar.d.ts +7 -0
- package/cli/dist/utils/tar.d.ts.map +1 -0
- package/cli/dist/utils/tar.js +21 -0
- package/cli/dist/utils/tar.js.map +1 -0
- package/cli/dist/utils/tilt.d.ts +20 -0
- package/cli/dist/utils/tilt.d.ts.map +1 -0
- package/cli/dist/utils/tilt.js +95 -0
- package/cli/dist/utils/tilt.js.map +1 -0
- package/cli/dist/utils/validation.d.ts +16 -0
- package/cli/dist/utils/validation.d.ts.map +1 -0
- package/cli/dist/utils/validation.js +58 -0
- package/cli/dist/utils/validation.js.map +1 -0
- package/cli/package.json +11 -12
- package/cli/src/commands/__tests__/always-enabled-infra.test.ts +2 -8
- package/cli/src/commands/__tests__/doctor.test.ts +181 -8
- package/cli/src/commands/__tests__/phases-model.test.ts +2 -4
- package/cli/src/commands/__tests__/project-resource-features-e2e.test.ts +4 -24
- package/cli/src/commands/__tests__/saas-starter-integration.test.ts +8 -10
- package/cli/src/commands/__tests__/stack-features-e2e.test.ts +10 -4
- package/cli/src/commands/__tests__/upgrade.test.ts +23 -13
- package/cli/src/commands/doctor.ts +136 -0
- package/cli/src/commands/help.ts +13 -3
- package/cli/src/commands/project.ts +11 -14
- package/cli/src/commands/projects.ts +2 -2
- package/cli/src/commands/up.ts +13 -4
- package/cli/src/commands/upgrade.ts +11 -4
- package/cli/src/config/platform-standards.test.ts +6 -6
- package/cli/src/generator/__tests__/extension-fetch.test.ts +36 -14
- package/cli/src/generator/extension-fetch.ts +12 -5
- package/cli/src/generator/template-engine.ts +19 -21
- package/cli/src/types/index.ts +2 -0
- package/cli/src/utils/__tests__/tilt.test.ts +29 -0
- package/cli/src/utils/constants.ts +4 -0
- package/cli/src/utils/doctor-runtime.ts +67 -27
- package/cli/src/utils/env-validator.ts +2 -2
- package/cli/src/utils/project-templates.ts +2 -1
- package/cli/src/utils/prompt.ts +2 -4
- package/cli/src/utils/resource-features.ts +10 -4
- package/cli/src/utils/services.ts +2 -1
- package/cli/src/utils/tilt.ts +20 -0
- package/docs/demo.svg +217 -0
- package/engine/assets/docker/wake-gateway/Dockerfile +27 -0
- package/engine/assets/docker/wake-gateway/decide.test.ts +133 -0
- package/engine/assets/docker/wake-gateway/decide.ts +105 -0
- package/engine/assets/docker/wake-gateway/server.ts +311 -0
- package/engine/topologies/platform/docker/build/golden_image_dockerfile.star +13 -23
- package/engine/topologies/platform/docker/compose/compose.star +7 -14
- package/engine/topologies/platform/docker/compose/compose_helpers.star +1 -1
- package/engine/topologies/platform/docker/compose/traefik_standalone.star +66 -6
- package/engine/topologies/platform/docker/config/healthcheck.star +56 -0
- package/engine/topologies/platform/docker/constants.star +10 -0
- package/engine/topologies/platform/docker/layers/l1_base_layers.star +1 -1
- package/engine/topologies/platform/docker/layers/l4_runtime_layers.star +55 -20
- package/engine/topologies/platform/docker/networking/traefik_static_routes.star +161 -0
- package/engine/topologies/tilt/generators/typescript/backend_tsconfig.star +2 -2
- package/engine/topologies/tilt/generators/typescript/frontend_tsconfig.star +1 -1
- package/engine/topologies/tilt/manifest/schema.star +6 -0
- package/engine/topologies/tilt/manifest/validator.star +16 -0
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +143 -9
- package/engine/topologies/tilt/resources/orchestrator/builders/typescript.star +12 -6
- package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +2 -1
- package/knip.json +1 -1
- package/openspec/changes/optimize-runtime-docker-layers/.openspec.yaml +3 -0
- package/openspec/changes/optimize-runtime-docker-layers/design.md +81 -0
- package/openspec/changes/optimize-runtime-docker-layers/proposal.md +44 -0
- package/openspec/changes/optimize-runtime-docker-layers/specs/runtime-container-footprint/spec.md +79 -0
- package/openspec/changes/optimize-runtime-docker-layers/tasks.md +36 -0
- package/openspec/changes/prioritized-cold-start/.openspec.yaml +4 -0
- package/openspec/changes/prioritized-cold-start/design.md +133 -0
- package/openspec/changes/prioritized-cold-start/proposal.md +41 -0
- package/openspec/changes/prioritized-cold-start/specs/on-demand-resource-startup/spec.md +92 -0
- package/openspec/changes/prioritized-cold-start/tasks.md +56 -0
- package/openspec/config.yaml +1 -0
- package/package.json +35 -5
- package/scripts/autoresearch/layer-eval.sh +65 -0
- package/scripts/benchmark/README.md +25 -0
- package/scripts/benchmark/container-scale.ts +503 -0
- package/scripts/benchmark/pre-release-gate.sh +9 -0
- package/scripts/measure-idle-footprint.sh +58 -0
- package/scripts/record-demo.sh +43 -0
- package/tests/tilt-engine/test_runtime_container_footprint.py +162 -0
- package/tests/tilt-engine/test_sablier_defer_start.py +196 -0
- package/tests/tilt-engine/test_standalone_traefik.py +52 -0
- package/tests/tilt-engine/test_traefik_static_wake_route.py +187 -0
- package/bun.lock +0 -538
package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star
CHANGED
|
@@ -9,7 +9,9 @@ OUTPUT = struct(AUTOGENERATED_FOLDER = ".autogenerated")
|
|
|
9
9
|
|
|
10
10
|
load('../../../platform/docker/index.star', 'Docker')
|
|
11
11
|
load('../../../platform/docker/constants.star', 'PlatformDockerConstants')
|
|
12
|
-
load('../../../tilt/manifest/constants.star', 'BASE_PORT_FRONTEND')
|
|
12
|
+
load('../../../tilt/manifest/constants.star', 'BASE_PORT_FRONTEND', 'BASE_PORT_BACKEND')
|
|
13
|
+
load('../../../platform/docker/networking/traefik_static_routes.star', 'generate_static_wake_route', 'normalize_abs_path')
|
|
14
|
+
load('../../../platform/docker/networking/sablier_container_cycle.star', 'sablier_middleware_suffix')
|
|
13
15
|
|
|
14
16
|
load('./builders/typescript.star', 'TypescriptBuilders')
|
|
15
17
|
|
|
@@ -114,6 +116,71 @@ def _build_resource_deps(res, res_name, manifest, resource_config, infra_deps, c
|
|
|
114
116
|
return res_deps
|
|
115
117
|
|
|
116
118
|
|
|
119
|
+
def resource_defers_start(manifest):
|
|
120
|
+
"""True when a manifest opts into `sablier.enable` + `sablier.deferStart`
|
|
121
|
+
AND the licensed sablier module is actually active.
|
|
122
|
+
|
|
123
|
+
deferStart stays behind the same TDK_LICENSE_KEY gate as the rest of
|
|
124
|
+
`sablier` (an explicit product decision, not just the opt-in manifest
|
|
125
|
+
flag): the free-tier stub's sablier_middleware_suffix() always reports
|
|
126
|
+
`enabled=False` regardless of what the manifest says, so checking through
|
|
127
|
+
it here -- rather than reading manifest['sablier']['enable'] directly --
|
|
128
|
+
is what keeps deferStart off for unlicensed projects. Without this check,
|
|
129
|
+
a project with no license would still get "no container created or
|
|
130
|
+
started," silently bypassing the license boundary for the disable-the-
|
|
131
|
+
container half of the feature even though the Docker-label wake half
|
|
132
|
+
stays correctly disabled.
|
|
133
|
+
|
|
134
|
+
A deferred resource's image still builds (docker_build has no auto_init
|
|
135
|
+
gate), but its dc_resource/run-only/replica resources are held back with
|
|
136
|
+
auto_init=False: no container is created or started until the Traefik
|
|
137
|
+
static route wakes it on first request (see openspec/changes/
|
|
138
|
+
prioritized-cold-start)."""
|
|
139
|
+
_, sablier_licensed_and_enabled = sablier_middleware_suffix(manifest, '')
|
|
140
|
+
if not sablier_licensed_and_enabled:
|
|
141
|
+
return False
|
|
142
|
+
sablier = manifest.get('sablier', {}) if manifest else {}
|
|
143
|
+
return bool(sablier.get('deferStart', False))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def resolve_manifest_dependency_names(manifest, all_services_map):
|
|
147
|
+
"""Resolve a manifest's `dependsOn` short names to full resource names,
|
|
148
|
+
reusing the same `_resolve_dependency_to_resource` that `_build_resource_deps`
|
|
149
|
+
already trusts for Tilt's own resource_deps ordering -- an ambiguous name
|
|
150
|
+
behaves identically here as it already does there."""
|
|
151
|
+
resolved = []
|
|
152
|
+
for dep in (manifest.get('dependsOn', []) if manifest else []):
|
|
153
|
+
resolved_dep = _resolve_dependency_to_resource(dep, all_services_map)
|
|
154
|
+
if resolved_dep and resolved_dep not in resolved:
|
|
155
|
+
resolved.append(resolved_dep)
|
|
156
|
+
return resolved
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def resolve_defer_start_dependencies(manifest, resource_manifests, all_services_map):
|
|
160
|
+
"""Of a deferStart resource's manifest dependencies, resolve the subset
|
|
161
|
+
that are themselves deferStart resources, for the wake gateway's
|
|
162
|
+
X-Wake-Deps (see generate_static_wake_route's `dependencies` param and
|
|
163
|
+
openspec/changes/prioritized-cold-start design.md D5).
|
|
164
|
+
|
|
165
|
+
A dependsOn name `_resolve_dependency_to_resource` can't map to an exact
|
|
166
|
+
resource (its own fallback branches -- see that function) won't be a key
|
|
167
|
+
in `resource_manifests` either, so it's quietly excluded here rather than
|
|
168
|
+
guessed at -- consistent with D5's existing stance. Always-on infra
|
|
169
|
+
(database, NATS) is never itself a deferStart resource (D5), so in
|
|
170
|
+
practice this only ever picks up the one case D5 flags as mattering: a
|
|
171
|
+
deferStart resource depending on another."""
|
|
172
|
+
dependencies = []
|
|
173
|
+
for resolved_dep in resolve_manifest_dependency_names(manifest, all_services_map):
|
|
174
|
+
dep_manifest = resource_manifests.get(resolved_dep)
|
|
175
|
+
if not dep_manifest or not resource_defers_start(dep_manifest):
|
|
176
|
+
continue
|
|
177
|
+
dep_service_name = all_services_map.get(resolved_dep, {}).get('name', '')
|
|
178
|
+
if not dep_service_name:
|
|
179
|
+
continue
|
|
180
|
+
dependencies.append((resolved_dep, dep_service_name + '-autogenerated'))
|
|
181
|
+
return dependencies
|
|
182
|
+
|
|
183
|
+
|
|
117
184
|
def _register_replicas(config, auto_init_apps):
|
|
118
185
|
"""Register additional replica resources for high-availability services."""
|
|
119
186
|
replicas = config.get('replicas', 1)
|
|
@@ -146,9 +213,38 @@ def _build_live_update_rules(res_path, full_res_path, syncs):
|
|
|
146
213
|
return live_update_rules
|
|
147
214
|
|
|
148
215
|
|
|
216
|
+
def _app_image_name(resource_name, res_name):
|
|
217
|
+
"""The image ref docker_build builds and the app compose service declares."""
|
|
218
|
+
return resource_name + '_' + res_name + ':dev'
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _wake_compose_invocation(project_root, resource_path, res_name, resource_name):
|
|
222
|
+
"""Compose files, env file and image for the wake gateway's direct start.
|
|
223
|
+
|
|
224
|
+
The same files/--env-file the `-run-only` resource passes, as absolute
|
|
225
|
+
normalized paths: the gateway mounts the project at the same normalized
|
|
226
|
+
root (traefik_standalone.star), so these resolve identically there.
|
|
227
|
+
Returns None without a project root (paths would be relative to Tilt's cwd).
|
|
228
|
+
"""
|
|
229
|
+
if not project_root:
|
|
230
|
+
return None
|
|
231
|
+
root = normalize_abs_path(project_root)
|
|
232
|
+
compose_dir = root + '/' + resource_path + '/' + OUTPUT.AUTOGENERATED_FOLDER
|
|
233
|
+
files = [compose_dir + '/docker-compose.app.autogenerated.yml']
|
|
234
|
+
port_override = compose_dir + '/docker-compose.port-override.yml'
|
|
235
|
+
if os.path.exists(port_override):
|
|
236
|
+
files.append(port_override)
|
|
237
|
+
env_file = root + '/.env'
|
|
238
|
+
return {
|
|
239
|
+
'compose_files': files,
|
|
240
|
+
'env_file': env_file if os.path.exists(env_file) else '',
|
|
241
|
+
'image': _app_image_name(resource_name, res_name),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
149
245
|
def _register_docker_build(config, live_update_rules):
|
|
150
246
|
"""Register docker_build with live_update for hot-reload support."""
|
|
151
|
-
image_name = config['resource_name']
|
|
247
|
+
image_name = _app_image_name(config['resource_name'], config['res_name'])
|
|
152
248
|
project_root = config.get('project_root', '')
|
|
153
249
|
# Use custom dockerfile from config if specified, otherwise use autogenerated default
|
|
154
250
|
# The res_path is relative to project_root (e.g., 'services/product/...')
|
|
@@ -219,9 +315,15 @@ def _register_single_resource(config, resource_path, compose_project_name, auto_
|
|
|
219
315
|
"""Register a single dc_resource with optional docker_build and run-only resource."""
|
|
220
316
|
app_type = config.get('app_type', 'backend')
|
|
221
317
|
res_name = config['res_name']
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
318
|
+
|
|
319
|
+
# A sablier.enable + sablier.deferStart resource is held back from tdk up's
|
|
320
|
+
# normal bring-up: its container is never created or started here, only via
|
|
321
|
+
# the Traefik wake path on first request. docker_build below is unaffected
|
|
322
|
+
# (it has no auto_init gate), so the image still builds either way.
|
|
323
|
+
effective_auto_init = auto_init_apps and not resource_defers_start(config.get('manifest', {}))
|
|
324
|
+
|
|
325
|
+
print("DEBUG REGISTER: Registering '{}' app_type='{}' auto_init={}".format(res_name, app_type, effective_auto_init))
|
|
326
|
+
|
|
225
327
|
# Skip docker_build for libraries and SDKs - they don't have Dockerfiles
|
|
226
328
|
if app_type not in ['library', 'sdk'] and config.get('syncs'):
|
|
227
329
|
print("DEBUG REGISTER: '{}' has syncs, calling docker_build".format(res_name))
|
|
@@ -239,12 +341,12 @@ def _register_single_resource(config, resource_path, compose_project_name, auto_
|
|
|
239
341
|
config['res_name'],
|
|
240
342
|
labels=config['labels'],
|
|
241
343
|
resource_deps=config['res_deps'],
|
|
242
|
-
auto_init=
|
|
344
|
+
auto_init=effective_auto_init,
|
|
243
345
|
)
|
|
244
346
|
print("DEBUG REGISTER: dc_resource completed for '{}'".format(res_name))
|
|
245
347
|
|
|
246
|
-
_register_run_only_resource(config, resource_path, compose_project_name,
|
|
247
|
-
_register_replicas(config,
|
|
348
|
+
_register_run_only_resource(config, resource_path, compose_project_name, effective_auto_init)
|
|
349
|
+
_register_replicas(config, effective_auto_init)
|
|
248
350
|
|
|
249
351
|
|
|
250
352
|
def _register_dc_resources(resource_configs, resource_path, compose_project_name, auto_init_apps):
|
|
@@ -322,6 +424,7 @@ def _build_resource_config(res, manifest, resource_config, full_res_path, infra_
|
|
|
322
424
|
'res_path': full_res_path,
|
|
323
425
|
'env': env_vars,
|
|
324
426
|
'project_root': project_root,
|
|
427
|
+
'manifest': manifest,
|
|
325
428
|
}
|
|
326
429
|
|
|
327
430
|
|
|
@@ -388,7 +491,38 @@ def register_compose_resources(resource_config, ctx, runtime_flags, manifest_sta
|
|
|
388
491
|
entry = _process_frontend_resource(res, manifest, full_res_path, resource_path, resource_name, should_enable)
|
|
389
492
|
else:
|
|
390
493
|
entry = _process_backend_resource(res, manifest, full_res_path, resource_path, resource_name, should_enable)
|
|
391
|
-
|
|
494
|
+
# A deferStart resource has no container Traefik can discover via
|
|
495
|
+
# labels until its first request creates one (D2/D4) -- give it a
|
|
496
|
+
# static file-provider route instead, pointed at the wake gateway.
|
|
497
|
+
# Scoped to backends for now, matching the proposal's motivating
|
|
498
|
+
# case; frontend support is a natural follow-up, not implemented
|
|
499
|
+
# here. Dependency waking (D5): TDK's real infra dependencies
|
|
500
|
+
# (database, NATS, a resource's own migrator) live in
|
|
501
|
+
# separately-registered compose files that `depends_on` cannot
|
|
502
|
+
# reference (confirmed empirically, see design.md D5), and they're
|
|
503
|
+
# always-on infra that's essentially never actually cold, so they
|
|
504
|
+
# never show up here regardless. The one real case D5 flags as
|
|
505
|
+
# mattering -- a peer resource that is itself deferStart -- is
|
|
506
|
+
# resolved via the manifest's own `dependsOn` below.
|
|
507
|
+
if resource_defers_start(manifest) and write_file:
|
|
508
|
+
internal_port = res.get('port', manifest.get('port', BASE_PORT_BACKEND))
|
|
509
|
+
static_route_path = PlatformDockerConstants.TRAEFIK_DYNAMIC_DIR_REL + '/' + res['name'] + '.yml'
|
|
510
|
+
compose_invocation = _wake_compose_invocation(
|
|
511
|
+
ctx.get('project_root', ''), resource_path, res['name'], resource_name,
|
|
512
|
+
) or {}
|
|
513
|
+
write_file(static_route_path, generate_static_wake_route(
|
|
514
|
+
resource_entry_name=res['name'],
|
|
515
|
+
compose_project_name=compose_project_name,
|
|
516
|
+
manifest=manifest,
|
|
517
|
+
internal_port=internal_port,
|
|
518
|
+
gateway_host=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_CONTAINER,
|
|
519
|
+
gateway_port=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_PORT,
|
|
520
|
+
dependencies=resolve_defer_start_dependencies(manifest, resource_manifests, all_services_map),
|
|
521
|
+
compose_files=compose_invocation.get('compose_files'),
|
|
522
|
+
env_file=compose_invocation.get('env_file', ''),
|
|
523
|
+
image=compose_invocation.get('image', ''),
|
|
524
|
+
))
|
|
525
|
+
|
|
392
526
|
resource_entries.append(entry)
|
|
393
527
|
resource_configs.append(_build_resource_config(
|
|
394
528
|
res, manifest, resource_config, full_res_path, infra_deps,
|
|
@@ -84,14 +84,20 @@ def _get_resource_only_paths(resource_path):
|
|
|
84
84
|
|
|
85
85
|
# Shared runtime scripts copied in generated Dockerfiles.
|
|
86
86
|
'shared-platform-engineering/docker-templates',
|
|
87
|
-
|
|
88
|
-
# Common DDD layers used by backend Dockerfiles.
|
|
89
|
-
'shared-ddd-layers/domain',
|
|
90
|
-
'shared-ddd-layers/infrastructure',
|
|
91
87
|
]
|
|
92
88
|
|
|
93
|
-
# Include only internal libraries referenced by this service package.json
|
|
94
|
-
#
|
|
89
|
+
# Include only internal libraries referenced by this service package.json
|
|
90
|
+
# (covers shared-ddd-layers/<lib> too, via resolve_lib_path's 'ddd' root -
|
|
91
|
+
# no Dockerfile ever COPYs a bare shared-ddd-layers/domain or /infrastructure
|
|
92
|
+
# path, so hardcoding those two here was pure dead weight: every one of a
|
|
93
|
+
# large landscape's docker_build resources got its own Tilt watch
|
|
94
|
+
# registration on paths most projects don't even have on disk. At 107
|
|
95
|
+
# resources that's 107 duplicate watchers on the same couple of
|
|
96
|
+
# nonexistent paths - real amplification if anything ever touches them,
|
|
97
|
+
# and a plausible contributor to "fsnotify: queue or buffer overflow" at
|
|
98
|
+
# this scale, on top of being wasted watch overhead regardless).
|
|
99
|
+
# This also prevents unrelated library changes from triggering rebuilds
|
|
100
|
+
# of services that don't use them.
|
|
95
101
|
deps_map = Utils.build_deps_mapping(resource_path)
|
|
96
102
|
for dep_path in deps_map.values():
|
|
97
103
|
if dep_path not in only_paths:
|
|
@@ -361,7 +361,8 @@ def _generate_all_configs_for_resource(
|
|
|
361
361
|
res_path=resource_path,
|
|
362
362
|
resource_name=resource_name,
|
|
363
363
|
use_golden=should_enable('golden-image'),
|
|
364
|
-
use_prisma='prisma' in manifest.get('featuresEnabled', [])
|
|
364
|
+
use_prisma='prisma' in manifest.get('featuresEnabled', []),
|
|
365
|
+
manifest=manifest,
|
|
365
366
|
)
|
|
366
367
|
|
|
367
368
|
write_file(resource_path + "/" + OUTPUT.AUTOGENERATED_FOLDER + "/" + GENERATED_CONFIG_FILENAMES['dockerfile_app'], dockerfile_content)
|
package/knip.json
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Design
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
See `proposal.md` (Why) for the measurements. Current runtime layer behaviour relevant to the approach:
|
|
6
|
+
|
|
7
|
+
- The backend runtime Dockerfile is `ENTRYPOINT ["/entrypoint.sh"]` + `CMD ["bun", "run", "start"]`. `infisical-entrypoint.sh` ends in `exec "$@"`, so the `bun run start` wrapper becomes PID 1 and spawns the real `bun run dist/index.js` child. The wrapper is currently what receives `SIGTERM` from `docker stop` and passes it on to the child.
|
|
8
|
+
- The start command is set in several places: `RUNTIME_CONFIG.bun.backend_start_command` (`l3_builder_layers.star`), default `cmd` parameters in `dockerfile.star`, `golden_docker_generator_v2.star` and `l4_runtime_layers.star`, the `_runtime_setup` default (`infisical_docker.star`), and a hard-coded `CMD` for the Bun-static frontend (`l4_runtime_layers.star:213`). The Node runtime already uses `node dist/index.js` directly.
|
|
9
|
+
- Healthcheck timing lives in `get_docker_healthcheck_config` (`golden_image_dockerfile.star:6`), but `compose.star` (lines 141, 264) hard-codes `interval: 10s` with `retries: 6`.
|
|
10
|
+
- The per-service L4 layer already installs hugo only when a service's `featuresEnabled` contains `hugo`. The golden `l4_backend_bun` stage installs it for every backend anyway.
|
|
11
|
+
|
|
12
|
+
## Goals / Non-Goals
|
|
13
|
+
|
|
14
|
+
**Goals:**
|
|
15
|
+
- Meet every requirement in `specs/runtime-container-footprint/spec.md` without changing any service's source files or `package.json`.
|
|
16
|
+
- Keep a single source of truth for healthcheck timing.
|
|
17
|
+
|
|
18
|
+
**Non-Goals:**
|
|
19
|
+
- Reworking the entrypoint script beyond what signal handling needs.
|
|
20
|
+
- Default startup scope and Sablier scale-to-zero (see proposal Non-goals).
|
|
21
|
+
|
|
22
|
+
## Decisions
|
|
23
|
+
|
|
24
|
+
### D1: Resolve the start command at generation time, exec the entry file directly
|
|
25
|
+
|
|
26
|
+
When generating a Bun backend or Bun-static frontend runtime, the generator reads the service's `package.json` `scripts.start`. If it is exactly `bun run <file>` or `bun <file>` (one file path, no `&&`, `;`, pipes or env assignments), the runtime gets `CMD ["bun", "<file>"]`. In every other case it falls back to `CMD ["bun", "run", "start"]`. The resolver (`resolve_bun_start_command` in `l4_runtime_layers.star`) runs at the single point where the backend and Bun-static frontend `CMD` are emitted, both on the plain path and the Infisical-entrypoint path. Upstream defaults (`cmd='bun run start'` in `dockerfile.star`, `golden_docker_generator_v2.star` and `infisical_docker.star`) stay as they are and act as the "no explicit override" value. An explicit different `cmd` passed by a caller is always kept.
|
|
27
|
+
|
|
28
|
+
*Alternatives considered:*
|
|
29
|
+
- Rewriting `start` in each service's `package.json`: rejected because it edits user-owned files and breaks `bun run start` locally.
|
|
30
|
+
- Always emitting `bun <file>` from a fixed path: rejected because it silently ignores custom start scripts.
|
|
31
|
+
- `bun run --bun start`: rejected because it still keeps the wrapper process.
|
|
32
|
+
|
|
33
|
+
### D2: Add `init: true` to generated runtime services
|
|
34
|
+
|
|
35
|
+
With direct exec, Bun becomes PID 1. The kernel does not apply default signal actions to PID 1, so an app without a `SIGTERM` handler would ignore `docker stop` and be killed only after the stop timeout. Generated compose services get `init: true` (Docker's bundled tini, about 1 MB resident). Tini passes signals on and reaps zombies. It is not a Bun process, so the "exactly one Bun process" requirement still holds.
|
|
36
|
+
|
|
37
|
+
*Alternative:* inject a `SIGTERM` handler into generated service templates. Rejected because it doesn't cover existing services or custom servers.
|
|
38
|
+
|
|
39
|
+
### D3: One healthcheck config, 30 s interval with a 2 s start interval
|
|
40
|
+
|
|
41
|
+
A new shared module, `engine/topologies/platform/docker/config/healthcheck.star`, replaces the per-file inlined `get_docker_healthcheck_config` copies. The copies had drifted: `l4_runtime_layers.star`'s copy lacked keys its own code read. Defaults: `interval 30s`, `start_period 30s` (unchanged), `start_interval 2s`, `timeout 5s`, `retries 3`. Golden images, the L4 runtime layer, `compose.star` and `traefik_standalone.star` all read it. Each value can be overridden per run with `TDK_HEALTHCHECK_INTERVAL_SECONDS`, `TDK_HEALTHCHECK_START_INTERVAL_SECONDS`, `TDK_HEALTHCHECK_TIMEOUT_SECONDS`, `TDK_HEALTHCHECK_START_PERIOD_SECONDS` and `TDK_HEALTHCHECK_RETRIES`. Environment variables are used because there is no per-project healthcheck setting today, and `tdk up` does not pass the project `.env` through to Tilt.
|
|
42
|
+
|
|
43
|
+
During the start period Docker probes every 2 s, so a service is marked healthy right after it starts answering (see the spec's "Startup readiness is not delayed" scenario). After that, probes drop to every 30 s. With `retries 3`, a failing service is marked unhealthy within about 90 s, compared with about 60 s today.
|
|
44
|
+
|
|
45
|
+
*Alternative:* 30 s interval with no start interval. Rejected because first-healthy time would move from about 10 s to about 30 s per container, which slows every dependency chain in `tdk up`.
|
|
46
|
+
|
|
47
|
+
`start_interval` needs Docker Engine 25+ and Compose 2.20.2+. `tdk doctor` gets a check that fails with an upgrade hint on older versions.
|
|
48
|
+
|
|
49
|
+
### D4: Golden migrator stage declares `HEALTHCHECK NONE`
|
|
50
|
+
|
|
51
|
+
Resolved by task 1.2. Migrators are one-shot jobs: every migrator container is built from the per-service `l4_migrator_runtime` stage (`prisma_runtime.star`), which already sets `HEALTHCHECK NONE` and runs `ENTRYPOINT ["/app/migrate.sh"]` to completion. The golden `l4_migrator_golden` stage's `bunx prisma --version` healthcheck is therefore always overridden and never protects anything. It only runs if someone starts the bare golden image, and then it starts a bunx/Prisma process every interval. The golden stage now sets `HEALTHCHECK NONE` too, so both layers agree.
|
|
52
|
+
|
|
53
|
+
Evidence (2026-09-25): `grep -rn L4_generate_migrator_runtime engine/` shows a single caller (`golden_docker_generator_v2.star:57`), and that stage emits `HEALTHCHECK NONE` before `ENTRYPOINT`. No project in the workspace declares `"appType": "migrator"`.
|
|
54
|
+
|
|
55
|
+
*Alternative (original plan):* a `test -x` check on a symlinked Prisma binary. Dropped because a healthcheck on a run-to-completion container has no meaning.
|
|
56
|
+
|
|
57
|
+
### D5: Remove hugo from the golden backend stage only
|
|
58
|
+
|
|
59
|
+
Delete `RUN apk add --no-cache hugo` from `l4_backend_bun` in `golden_image_dockerfile.star`. The per-service feature-gated install stays, but it never actually ran: `manifest_resource.star` called `Docker.backend(...)` without `manifest`, so `featuresEnabled` was always empty in the L4 layer. The manifest is now passed through, which makes the per-service install work and makes removing hugo from the golden stage safe.
|
|
60
|
+
|
|
61
|
+
Evidence (task 1.1, 2026-09-25): no service in `tdk-erp-system`, `tdk-saas-starter`, `tdk-restaurant-example` or `tdk-user-management` lists `"hugo"` in `service.json`, and no source, script or config file mentions hugo (search excluded `node_modules`, `.autogenerated`, `dist` and `.tdk`). Removal affects no existing project.
|
|
62
|
+
|
|
63
|
+
### D6: Verifying the idle budget
|
|
64
|
+
|
|
65
|
+
`tests/tilt-engine/test_runtime_container_footprint.py` executes the real Starlark generators through `tilt alpha tiltfile-result` (skipped when Tilt isn't installed) rather than inspecting source text. Those tests assert the generated `CMD`, `init: true` and healthcheck values. The idle budget is checked by a smoke script that generates a template backend, runs it, waits for healthy, samples `docker stats` for 60 s, and fails if memory is over 64 MiB or CPU averages 2% or more. It's run manually and in the release checklist rather than on every CI run, because it needs Docker.
|
|
66
|
+
|
|
67
|
+
## Risks / Trade-offs
|
|
68
|
+
|
|
69
|
+
- [The start-script parser misreads a script, e.g. `bun run dist/index.js --port 4000`] → Only a single file argument qualifies. Anything with extra tokens falls back to `bun run start`, so the fallback is always the current behaviour.
|
|
70
|
+
- [Docker Engine / Compose is older than 25 / 2.20.2 and rejects `start_interval`] → New `tdk doctor` version check; the release notes state the minimum version.
|
|
71
|
+
- [Unhealthy detection is slower (about 90 s vs 60 s)] → Acceptable for local dev. `retries` stays configurable.
|
|
72
|
+
- [A project relied on hugo without declaring the feature] → The build fails loudly (`hugo: not found`). Migration note: add `"hugo"` to `featuresEnabled`. No project in this workspace is affected (see D5 evidence).
|
|
73
|
+
- [Background auto-commit tooling in this workspace pushes partial engine edits] → Make each decision's edits in one sitting and run the snapshot tests before moving on, so every intermediate push is still consistent.
|
|
74
|
+
|
|
75
|
+
## Migration Plan
|
|
76
|
+
|
|
77
|
+
1. Ship the engine changes in a TDK release.
|
|
78
|
+
2. In each project: run `tdk project --yes` to re-vendor `.tdk/.tdk-out/tdk-cli-ext/` and regenerate compose files and Dockerfiles, touch the Tiltfile, then rebuild golden layers with the `golden-layers-build` resource or a fresh `tdk up`.
|
|
79
|
+
3. Existing images keep working until they are rebuilt, and nothing needs to change inside services.
|
|
80
|
+
|
|
81
|
+
**Rollback:** revert the engine change, re-run `tdk project --yes`, and rebuild the golden layers.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Proposal
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
|
|
5
|
+
A full-landscape `tdk up` of `tdk-erp-system` (107 resources, 110 containers) saturated a 16 GB MacBook Air on 2026-09-25. Load average reached 30–43, only 99 MB of memory was free with 8 GB compressed, and `docker ps` took 6–13 s, which made `tdk doctor` and `tdk down` fail their own 10 s Docker check. A single idle backend is cheap (0.3–1.2% CPU, about 20 MiB in its cgroup). The overload comes from per-container overhead that the generated runtime layers multiply by 110: a redundant second Bun process, a 10 s exec-based healthcheck, and tooling nobody uses baked into the golden images. Cutting that overhead at the layer level makes large landscapes usable on the laptops TDK targets.
|
|
6
|
+
|
|
7
|
+
## What Changes
|
|
8
|
+
|
|
9
|
+
- **Start the backend with one process.** The generated L4 backend runtime (and the Bun-static frontend runtime) now start the compiled entrypoint directly, e.g. `CMD ["bun", "dist/index.js"]`, instead of `bun run start`. `bun run start` launches a Bun wrapper that then spawns a second Bun runtime (about 10 MB + 52 MB resident per container). A service whose `start` script does more than run a single file keeps `bun run start`.
|
|
10
|
+
- **Add an init process for signals.** Generated runtime services run under Docker's minimal init (`init: true`) so `docker stop` still reaches the app once the wrapper is gone.
|
|
11
|
+
- **Slow down healthchecks.** The default steady-state healthcheck interval goes from 10 s to 30 s for generated compose services and golden images. A fast start interval keeps first-healthy time where it is today. The value stays configurable through the existing healthcheck config, and compose files now read that config instead of hard-coding 10 s.
|
|
12
|
+
- **BREAKING (environment):** the fast start interval needs Docker Engine 25+ and Compose 2.20.2+. `tdk doctor` gets a check for this.
|
|
13
|
+
- **Make the migrator healthcheck cheap.** The `l4_migrator_golden` healthcheck no longer runs `bunx prisma --version`, which starts a full bunx/Prisma process on every check. It checks the locally installed Prisma binary instead.
|
|
14
|
+
- **Remove hugo from the golden backend image.** `apk add hugo` is removed from the golden `l4_backend_bun` stage (about 57 MB per backend image). The per-service L4 layer already installs hugo only when a manifest lists the `hugo` feature, and that stays as is.
|
|
15
|
+
- **Document a per-container budget.** An idle generated backend has a documented footprint target, so regressions like these are visible.
|
|
16
|
+
|
|
17
|
+
Nothing here is breaking for service code. Services that relied on hugo being present without declaring the `hugo` feature must add it to `featuresEnabled`.
|
|
18
|
+
|
|
19
|
+
## Capabilities
|
|
20
|
+
|
|
21
|
+
### New Capabilities
|
|
22
|
+
- `runtime-container-footprint`: Requirements on generated runtime images and containers: one application process per container, healthcheck cadence and cost, golden image contents limited to what every service of that type needs, and an idle footprint budget.
|
|
23
|
+
|
|
24
|
+
### Modified Capabilities
|
|
25
|
+
- (none)
|
|
26
|
+
|
|
27
|
+
## Non-goals
|
|
28
|
+
|
|
29
|
+
- Changing which resources `tdk up` starts by default, or putting backends under Sablier scale-to-zero. Even with these fixes, starting all 107 services at once stays heavy on 16 GB machines. That is a separate product decision with its own change.
|
|
30
|
+
- Changing the Bun version, base OS, or Prisma version.
|
|
31
|
+
|
|
32
|
+
## Impact
|
|
33
|
+
|
|
34
|
+
- **Engine (Starlark):**
|
|
35
|
+
- `engine/topologies/platform/docker/layers/l4_runtime_layers.star`: backend and Bun-static frontend `CMD`.
|
|
36
|
+
- `engine/topologies/platform/docker/layers/infisical/infisical_docker.star`: `_runtime_setup` default command.
|
|
37
|
+
- `engine/topologies/platform/docker/layers/l3_builder_layers.star`: `RUNTIME_CONFIG.bun.backend_start_command`.
|
|
38
|
+
- `engine/topologies/platform/docker/dockerfile/dockerfile.star` and `generators/golden_docker_generator_v2.star`: default `cmd` parameters.
|
|
39
|
+
- `engine/topologies/platform/docker/build/golden_image_dockerfile.star`: hugo removal, healthcheck interval, migrator healthcheck.
|
|
40
|
+
- `engine/topologies/platform/docker/compose/compose.star`: compose healthcheck (lines 141, 264) reads the shared config; `init: true`.
|
|
41
|
+
- **CLI:** `cli/src/commands/doctor.ts` gets a Docker Engine / Compose minimum-version check.
|
|
42
|
+
- **Generated projects:** every project needs `tdk project --yes` (re-vendoring `.tdk/.tdk-out/tdk-cli-ext/`) and a golden image rebuild to pick up the change. Existing images keep working until then.
|
|
43
|
+
- **Behaviour:** after a failure, Docker marks a container unhealthy up to about 20 s later than today. Tilt readiness and dependency ordering still use the start period.
|
|
44
|
+
- **Tests:** starlark and generated-Dockerfile snapshot tests covering these files need updating.
|
package/openspec/changes/optimize-runtime-docker-layers/specs/runtime-container-footprint/spec.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Spec Delta
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Keeps the per-container CPU, memory and image-size cost of generated TDK runtimes small enough that a large landscape (100+ resources) can run on a 16 GB developer laptop without starving the container runtime.
|
|
6
|
+
|
|
7
|
+
## ADDED Requirements
|
|
8
|
+
|
|
9
|
+
### Requirement: One Application Process Per Runtime Container
|
|
10
|
+
|
|
11
|
+
A generated Bun backend or Bun-static frontend runtime container SHALL run its application as a single Bun process. The container entrypoint MUST hand off to that process with `exec` rather than keep a wrapper process alive. This applies when the service's `start` script runs a single entry file.
|
|
12
|
+
|
|
13
|
+
#### Scenario: Default backend start script
|
|
14
|
+
|
|
15
|
+
- **WHEN** a generated backend's `package.json` has `"start": "bun run dist/index.js"` and its runtime container has finished starting
|
|
16
|
+
- **THEN** exactly one Bun process is running in the container (as listed by `docker top`)
|
|
17
|
+
- **AND** that process is running `dist/index.js`
|
|
18
|
+
|
|
19
|
+
#### Scenario: Custom start script is preserved
|
|
20
|
+
|
|
21
|
+
- **WHEN** a service's `start` script does more than run a single entry file (for example it chains commands or sets environment variables)
|
|
22
|
+
- **THEN** the runtime starts the service through `bun run start`
|
|
23
|
+
- **AND** the service behaves as it did before this change
|
|
24
|
+
|
|
25
|
+
#### Scenario: Signals reach the application
|
|
26
|
+
|
|
27
|
+
- **WHEN** a running backend container is stopped with `docker stop`
|
|
28
|
+
- **THEN** the application process receives SIGTERM directly and the container exits within the stop timeout
|
|
29
|
+
|
|
30
|
+
### Requirement: Healthchecks Are Low-Cost And Low-Frequency
|
|
31
|
+
|
|
32
|
+
Container healthchecks generated for services and golden images SHALL default to an interval of 30 seconds, and the timing MUST be overridable per run through `TDK_HEALTHCHECK_*` environment variables. A healthcheck command MUST NOT download packages or start a package-runner process (such as `bunx` or `npx`).
|
|
33
|
+
|
|
34
|
+
#### Scenario: Default service healthcheck interval
|
|
35
|
+
|
|
36
|
+
- **WHEN** a project is generated with default healthcheck configuration
|
|
37
|
+
- **THEN** every generated compose service healthcheck has `interval: 30s`
|
|
38
|
+
- **AND** the start period is at least as long as it was before this change
|
|
39
|
+
|
|
40
|
+
#### Scenario: Startup readiness is not delayed
|
|
41
|
+
|
|
42
|
+
- **WHEN** a generated service container starts and begins answering its health endpoint
|
|
43
|
+
- **THEN** Docker reports it healthy within 5 seconds of the first successful answer, even with a 30-second steady-state interval
|
|
44
|
+
|
|
45
|
+
#### Scenario: Configured interval is honoured
|
|
46
|
+
|
|
47
|
+
- **WHEN** `tdk up` runs with `TDK_HEALTHCHECK_INTERVAL_SECONDS` set (e.g. `15`)
|
|
48
|
+
- **THEN** the generated compose files and golden images use that interval
|
|
49
|
+
|
|
50
|
+
#### Scenario: One-shot migrator images carry no healthcheck
|
|
51
|
+
|
|
52
|
+
- **WHEN** the golden migrator image or a generated migrator runtime image is inspected
|
|
53
|
+
- **THEN** it declares no healthcheck (`HEALTHCHECK NONE`)
|
|
54
|
+
- **AND** no healthcheck anywhere in the generated images invokes `bunx` or `npx`
|
|
55
|
+
|
|
56
|
+
### Requirement: Golden Images Contain Only Universally Required Tooling
|
|
57
|
+
|
|
58
|
+
A golden runtime image SHALL contain only tooling that every service of that runtime type needs. Tooling that only some services use MUST be installed in the per-service layer, and only when that service's manifest declares the matching feature.
|
|
59
|
+
|
|
60
|
+
#### Scenario: Backend golden image has no hugo
|
|
61
|
+
|
|
62
|
+
- **WHEN** the golden L4 backend Bun image is built
|
|
63
|
+
- **THEN** the `hugo` binary is not present in the image
|
|
64
|
+
|
|
65
|
+
#### Scenario: Service declaring the hugo feature
|
|
66
|
+
|
|
67
|
+
- **WHEN** a service's manifest lists `hugo` in `featuresEnabled`
|
|
68
|
+
- **THEN** that service's runtime image contains the `hugo` binary
|
|
69
|
+
- **AND** services that do not declare it do not
|
|
70
|
+
|
|
71
|
+
### Requirement: Idle Backend Footprint Budget
|
|
72
|
+
|
|
73
|
+
A backend generated from the default TDK backend template SHALL, when idle (running and healthy, with no incoming requests), stay within 64 MiB container memory and average under 2% of one CPU core over 60 seconds, as reported by `docker stats`.
|
|
74
|
+
|
|
75
|
+
#### Scenario: Measuring an idle template backend
|
|
76
|
+
|
|
77
|
+
- **WHEN** a backend generated with `tdk resource <name> --type backend` has been running and healthy for 60 seconds with no incoming requests
|
|
78
|
+
- **THEN** its container memory usage is at most 64 MiB
|
|
79
|
+
- **AND** its average CPU usage over the following 60 seconds is below 2% of one core
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Tasks
|
|
2
|
+
|
|
3
|
+
## 1. Preflight checks
|
|
4
|
+
|
|
5
|
+
- [x] 1.1 Search every TDK project in the workspace (`tdk-erp-system`, `tdk-saas-starter`, `tdk-restaurant-example`, `tdk-user-management`) for services that use `hugo` without listing it in `featuresEnabled`. Record the result under design D5. Verify by attaching the search command and its (expected empty) output to the design note.
|
|
6
|
+
- [x] 1.2 Determine whether any `l4_migrator_golden`-based container runs long-lived or all are one-shot jobs. Record the answer under design D4 and keep or switch the D4 decision accordingly. Verify that design.md is updated with the evidence (compose `restart` policy and Tilt resource type).
|
|
7
|
+
|
|
8
|
+
## 2. Single-process runtime (D1, D2)
|
|
9
|
+
|
|
10
|
+
- [x] 2.1 Add one start-command resolver to the engine. It returns `bun <file>` for a `start` script of exactly `bun run <file>` or `bun <file>`, and `bun run start` otherwise. Verify with a new pytest in `tests/tilt-engine/` that generates Dockerfiles for fixture services with these start scripts: `bun run dist/index.js`, `bun dist/index.js`, `bun run dist/index.js --port 4000`, `NODE_ENV=x bun run dist/index.js`, `tsc && bun dist/index.js`, and no `start`. Assert the resulting `CMD` for each.
|
|
11
|
+
- [x] 2.2 Apply the resolver where every Bun `CMD` is emitted: the backend runtime (plain and Infisical-entrypoint paths) and the Bun-static frontend in `l4_runtime_layers.star`. Upstream `cmd` defaults stay as the no-override value and explicit overrides are kept (design D1). Verify with `test_backend_cmd_follows_start_script` (both paths) and `test_explicit_cmd_override_is_kept`.
|
|
12
|
+
- [x] 2.3 Emit `init: true` for generated backend and frontend runtime services in `compose.star`. Verify with a pytest asserting `init: true` on both service kinds in a generated compose file.
|
|
13
|
+
- [ ] 2.4 Build one real backend (`cash-management-api` in `tdk-erp-system`) from the new layers. Verify that `docker top` lists exactly one Bun process running `dist/index.js`, and that `docker stop` finishes in under 3 s.
|
|
14
|
+
|
|
15
|
+
## 3. Healthchecks (D3, D4)
|
|
16
|
+
|
|
17
|
+
- [x] 3.1 Set `get_docker_healthcheck_config` to `interval 30`, `start_interval 2`, `start_period 30`, `retries 3`, and add `--start-interval` to every golden `HEALTHCHECK`. Verify with a pytest on the generated `golden-layers.Dockerfile` asserting `--interval=30s --start-interval=2s` on each stage.
|
|
18
|
+
- [x] 3.2 Make `compose.star` (lines 141, 264) and `traefik_standalone.star` read the shared healthcheck config instead of hard-coding `10s`. Verify with a pytest asserting that generated compose files contain `interval: 30s` and `start_interval: 2s` and no `interval: 10s` for app services.
|
|
19
|
+
- [ ] 3.3 Replace the golden migrator `bunx prisma --version` healthcheck with `HEALTHCHECK NONE` (design D4). Verify with a pytest asserting that no generated golden stage or migrator runtime contains `bunx` or `npx` in a `HEALTHCHECK`, and that after a golden rebuild `docker inspect <prefix>-l4-migrator:latest` shows `Healthcheck.Test` = `["NONE"]`.
|
|
20
|
+
- [x] 3.4 Add a `tdk doctor` check for Docker Engine 25+ and Compose 2.20.2+ in `cli/src/commands/doctor.ts`, with vitest cases in `cli/src/commands/__tests__/doctor.test.ts` for an old version (fails, with an upgrade hint) and a current one (passes). Verify with `bun run test -- doctor.test.ts`.
|
|
21
|
+
- [ ] 3.5 Verify startup readiness with `docker events`: a freshly started backend is marked `healthy` within 5 s of its first successful `/health` response.
|
|
22
|
+
|
|
23
|
+
## 4. Golden image contents (D5)
|
|
24
|
+
|
|
25
|
+
- [ ] 4.1 Remove `RUN apk add --no-cache hugo` from the `l4_backend_bun` stage in `golden_image_dockerfile.star`. Verify that `docker run --rm <prefix>-l4-backend:latest which hugo` exits non-zero, and that `docker image inspect` shows the image about 57 MB smaller than before.
|
|
26
|
+
- [x] 4.2 Pass the manifest from `manifest_resource.star` to `Docker.backend` so the feature-gated install actually runs (design D5), then verify: a service with `"featuresEnabled": ["hugo"]` gets `apk add --no-cache hugo` in its generated Dockerfile and a service without it does not (`test_hugo_installed_only_for_services_declaring_it`).
|
|
27
|
+
|
|
28
|
+
## 5. Footprint budget and docs (D6)
|
|
29
|
+
|
|
30
|
+
- [ ] 5.1 Add `scripts/measure-idle-footprint.sh <container>`. It waits for the running backend container to be `healthy`, counts Bun processes, samples `docker stats` for 60 s, and exits non-zero if it finds more than one Bun process, memory over 64 MiB, or average CPU of 2% or more. (It measures an existing container instead of generating a template backend, because generation needs a whole project. The ERP backends are generated from the default template.) Verify it passes against a finance backend built from the new layers.
|
|
31
|
+
- [ ] 5.2 Document the minimum Docker Engine / Compose versions, the `TDK_HEALTHCHECK_*` overrides, the `featuresEnabled: ["hugo"]` migration note, and how to run the footprint script in `cli/README.md` (the repo has no root README). Verify the documented commands run as written.
|
|
32
|
+
|
|
33
|
+
## 6. Integration
|
|
34
|
+
|
|
35
|
+
- [ ] 6.1 Re-vendor into `tdk-erp-system` (`tdk project --yes`, touch the Tiltfile, rebuild golden layers) and run `tdk up finance`. Verify that every finance service is healthy and answers HTTP 200, and record per-container idle memory next to the baseline in proposal.md.
|
|
36
|
+
- [ ] 6.2 Measurement only: start the full `tdk-erp-system` landscape on the 16 GB reference machine. Record load average, free memory, and `docker ps` latency against the 2026-09-25 baseline, then add the numbers to proposal.md. This informs the separate default-startup-scope change and is not a pass/fail gate.
|