@tdk-landscape/tdk-cli-core 1.3.100 → 1.3.102
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/CHANGELOG.md +1 -0
- package/README.md +9 -1
- package/Tiltfile +5 -0
- package/cli/dist/cli.js +2 -0
- package/cli/dist/commands/maintainers.js +101 -0
- package/cli/package.json +1 -1
- package/cli/templates/Tiltfile.hbs +9 -2
- package/engine/Tiltfile +4 -0
- package/engine/entrypoint.star +2 -0
- package/engine/topologies/platform/docker/dockerfile/language_dockerfile.star +88 -9
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +13 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,7 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
- Go services can opt in to a live development loop with `"dev": { "liveReload": true }` in `service.json`: edits are copied into the running container and recompiled without an image rebuild, while `go.mod`, `go.sum` and the generated Dockerfile still rebuild the image. The watcher compiles into a staging binary while the old build keeps serving and swaps only on success, so a compile error shows in the Tilt log and the last good build keeps answering (Air, tried first, stopped the app before compiling and left it down for the whole compile; in 2 of 6 edits a Traefik health check then returned 503 for 10 s). Measured on one loaded Mac: median edit-to-ready 1.63 s for the scaffold. The default Go image (`golang:1.23`) now sets `GOTOOLCHAIN=auto`, so a `go.mod` that needs a newer Go, such as Gin 1.12 (`go 1.25`), downloads that toolchain instead of stopping with `requires go >= 1.25`. Offline or air-gapped builds of a go.mod newer than 1.23 still fail, but now at the toolchain download rather than at the version check. `scripts/e2e/go-dev-loop.sh` checks all of this through a real `tdk up`. Refs #369.
|
|
7
8
|
- `tdk up` keeps each smoke step's status and response body under `.tdk/smoke/<service>/<step>/` (`latest.json` and `body.txt`, body capped at 64 KiB), plus `last-success.json` and `last-success-body.txt` that a later failure never overwrites, so a regression can be diffed against the last good response. The failure line ends with the record path, and `tdk project` gitignores `.tdk/smoke/`. A new `smoke-e2e` workflow runs `scripts/verify-smoke.sh` and uploads the records. Before its first step a smoke check now waits for the service's health path (`healthCheckPath`, default `/health` for backends) with GETs until it answers something other than a connection error, 404 or 5xx, so a write is no longer the first request to hit a service that is still building; write retry rules are unchanged.
|
|
8
9
|
- Add an opt-in Python backend: `tdk resource <name> --type backend --language python` scaffolds a FastAPI service (`pyproject.toml`, `src/main.py`, a pytest smoke test) and TDK generates its Docker image (`python:3.12-slim`, with `test` and `production` stages). The choice is saved as `language` in `service.json` only when you pass the flag; omitting it keeps the existing Bun + Hono output byte for byte. Unknown ids and `--language` on non-backend types fail before any file is written. A runnable example is in `examples/one-backend-python`, and the provider contract is in `docs/backend-language-providers.md`. Node.js is not included.
|
|
9
10
|
- `tdk doctor` no longer reports "resource without a package.json" for a Python backend, and a Python service missing `pyproject.toml` or `src/main.py` now fails early with the missing file named.
|
package/README.md
CHANGED
|
@@ -10,6 +10,10 @@ Docker runs the containers. Tilt watches services and live-updates containers wh
|
|
|
10
10
|
|
|
11
11
|
[Website](https://tdk-landscape.github.io/tdk-website/) · [Quickstart](https://tdk-landscape.github.io/tdk-website/docs/quickstart/) · [Examples](https://tdk-landscape.github.io/tdk-website/docs/examples/) · [Awesome TDK](https://github.com/tdk-landscape/awesome-tdk-framework) · [Demo](https://tdk-landscape.github.io/tdk-demo-animation/) · [Report a bug](https://github.com/tdk-landscape/tdk-cli-core/issues)
|
|
12
12
|
|
|
13
|
+
## Who uses TDK
|
|
14
|
+
|
|
15
|
+
No teams are listed yet. Be the first: [tell us you use TDK](https://github.com/tdk-landscape/tdk-cli-core/issues/new?template=we-use-tdk.yml) or [add a row to ADOPTERS.md](ADOPTERS.md).
|
|
16
|
+
|
|
13
17
|
## Quick start
|
|
14
18
|
|
|
15
19
|
```bash
|
|
@@ -60,6 +64,8 @@ curl -fsSL https://tdk-landscape.github.io/install.sh | sh
|
|
|
60
64
|
## Docs
|
|
61
65
|
|
|
62
66
|
- [Documentation index](docs/README.md)
|
|
67
|
+
- [Pilot TDK on a real repository](docs/adopt-tdk.md)
|
|
68
|
+
- [FAQ for agencies and multi-client teams](docs/faq-teams.md)
|
|
63
69
|
- [Working alongside Helm](docs/with-helm.md)
|
|
64
70
|
- [Configuration and editor schemas](docs/configuration.md)
|
|
65
71
|
- [Runnable one-backend example](examples/one-backend/README.md)
|
|
@@ -103,4 +109,6 @@ Core stays free. Premium is a separate key for the extras above; no key required
|
|
|
103
109
|
|
|
104
110
|
Contributions are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md) and the [contributor guide](docs/contributing/README.md). Report security issues using [SECURITY.md](SECURITY.md).
|
|
105
111
|
|
|
106
|
-
TDK is MIT-licensed; see [LICENSE](LICENSE).
|
|
112
|
+
TDK is MIT-licensed; see [LICENSE](LICENSE). The [license boundary](GOVERNANCE.md#license-boundary-and-donation-scope) says which code is MIT and which is downloaded with a key.
|
|
113
|
+
|
|
114
|
+
Project governance: [GOVERNANCE.md](GOVERNANCE.md), [MAINTAINERS.md](MAINTAINERS.md), [ADOPTERS.md](ADOPTERS.md).
|
package/Tiltfile
CHANGED
|
@@ -35,6 +35,7 @@ load('./engine/topologies/tilt/resources/conditions.star', _Database='Database')
|
|
|
35
35
|
load('./engine/topologies/tilt/resources/declaration.star', _Orchestrator='Orchestrator')
|
|
36
36
|
load('./engine/topologies/tilt/resources/ordering.star', _Infra='Infra')
|
|
37
37
|
load('./engine/topologies/tilt/resources/triggers.star', _Watchers='Watchers')
|
|
38
|
+
load('./engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star', _resource_defers_start='resource_defers_start')
|
|
38
39
|
|
|
39
40
|
# =============================================================================
|
|
40
41
|
# SPECS (Tech stack and standards)
|
|
@@ -128,6 +129,10 @@ Orchestrator = _Orchestrator
|
|
|
128
129
|
Infra = _Infra
|
|
129
130
|
Watchers = _Watchers
|
|
130
131
|
|
|
132
|
+
# True when a manifest's deferStart is actually active (licensed); decides whether a `<name>-image` prebuild resource exists.
|
|
133
|
+
# The generated Tiltfile loads this from ext://tdk-cli, which resolves to this root Tiltfile.
|
|
134
|
+
resource_defers_start = _resource_defers_start
|
|
135
|
+
|
|
131
136
|
# Specs
|
|
132
137
|
assert_tech_stack = _assert_tech_stack
|
|
133
138
|
TECH_STACK = _TECH_STACK
|
package/cli/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { doctorCommand } from "./commands/doctor.js";
|
|
|
8
8
|
import { downCommand } from "./commands/down.js";
|
|
9
9
|
import { ejectCommand } from "./commands/eject.js";
|
|
10
10
|
import { showHelp } from "./commands/help.js";
|
|
11
|
+
import { maintainersCommand } from "./commands/maintainers.js";
|
|
11
12
|
import { networksCommand } from "./commands/networks.js";
|
|
12
13
|
import { projectCommand } from "./commands/project.js";
|
|
13
14
|
import { projectsCommand } from "./commands/projects.js";
|
|
@@ -53,6 +54,7 @@ program.addCommand(projectCommand);
|
|
|
53
54
|
program.addCommand(configCommand);
|
|
54
55
|
program.addCommand(uiCommand);
|
|
55
56
|
program.addCommand(versionCommand);
|
|
57
|
+
program.addCommand(maintainersCommand);
|
|
56
58
|
program.addCommand(doctorCommand);
|
|
57
59
|
program.addCommand(completionCommand);
|
|
58
60
|
program.addCommand(upgradeCommand);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
export const MIN_MAINTAINERS = 3;
|
|
5
|
+
export const MIN_COMPANIES = 2;
|
|
6
|
+
const REQUIRED_COLUMNS = ["name", "github id", "company"];
|
|
7
|
+
const PLACEHOLDER_COMPANIES = new Set(["", "tbd", "unknown", "tdk-landscape"]);
|
|
8
|
+
function splitRow(line) {
|
|
9
|
+
return line
|
|
10
|
+
.trim()
|
|
11
|
+
.replace(/^\|/, "")
|
|
12
|
+
.replace(/\|$/, "")
|
|
13
|
+
.split("|")
|
|
14
|
+
.map((cell) => cell.trim());
|
|
15
|
+
}
|
|
16
|
+
export function parseMaintainers(markdown) {
|
|
17
|
+
const lines = markdown.split(/\r?\n/);
|
|
18
|
+
const headerIndex = lines.findIndex((line) => {
|
|
19
|
+
if (!line.includes("|"))
|
|
20
|
+
return false;
|
|
21
|
+
const cells = splitRow(line).map((cell) => cell.toLowerCase());
|
|
22
|
+
return REQUIRED_COLUMNS.every((column) => cells.includes(column));
|
|
23
|
+
});
|
|
24
|
+
if (headerIndex === -1) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
error: "table with columns Name, GitHub id, Company not found",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const header = splitRow(lines[headerIndex]).map((cell) => cell.toLowerCase());
|
|
31
|
+
const nameAt = header.indexOf("name");
|
|
32
|
+
const githubAt = header.indexOf("github id");
|
|
33
|
+
const companyAt = header.indexOf("company");
|
|
34
|
+
const rows = [];
|
|
35
|
+
for (const line of lines.slice(headerIndex + 1)) {
|
|
36
|
+
if (!line.includes("|"))
|
|
37
|
+
break;
|
|
38
|
+
const cells = splitRow(line);
|
|
39
|
+
if (cells.every((cell) => /^:?-+:?$/.test(cell)))
|
|
40
|
+
continue;
|
|
41
|
+
rows.push({
|
|
42
|
+
name: cells[nameAt] ?? "",
|
|
43
|
+
githubId: cells[githubAt] ?? "",
|
|
44
|
+
company: cells[companyAt] ?? "",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, rows };
|
|
48
|
+
}
|
|
49
|
+
export function evaluateMaintainers(rows) {
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
const valid = rows.filter((row) => {
|
|
52
|
+
const id = row.githubId.toLowerCase();
|
|
53
|
+
if (!row.name || !id || seen.has(id) || PLACEHOLDER_COMPANIES.has(row.company.toLowerCase())) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
seen.add(id);
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
const companies = new Set(valid.map((row) => row.company.toLowerCase()));
|
|
60
|
+
const missingPeople = Math.max(0, MIN_MAINTAINERS - valid.length);
|
|
61
|
+
const missingCompanies = Math.max(0, MIN_COMPANIES - companies.size);
|
|
62
|
+
return {
|
|
63
|
+
eligible: missingPeople === 0 && missingCompanies === 0,
|
|
64
|
+
people: valid.length,
|
|
65
|
+
companies: companies.size,
|
|
66
|
+
missingPeople,
|
|
67
|
+
missingCompanies,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function runMaintainersCheck(file) {
|
|
71
|
+
const path = resolve(file);
|
|
72
|
+
if (!existsSync(path)) {
|
|
73
|
+
return { exitCode: 1, output: `${file} not found` };
|
|
74
|
+
}
|
|
75
|
+
const parsed = parseMaintainers(readFileSync(path, "utf8"));
|
|
76
|
+
if (!parsed.ok) {
|
|
77
|
+
return { exitCode: 1, output: `${file}: ${parsed.error}` };
|
|
78
|
+
}
|
|
79
|
+
const report = evaluateMaintainers(parsed.rows);
|
|
80
|
+
if (report.eligible) {
|
|
81
|
+
return {
|
|
82
|
+
exitCode: 0,
|
|
83
|
+
output: `${report.people} maintainers, ${report.companies} companies`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
exitCode: 1,
|
|
88
|
+
output: `not eligible: ${report.people} maintainers (${report.missingPeople} missing), ${report.companies} companies (${report.missingCompanies} missing)`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export const maintainersCommand = new Command("maintainers")
|
|
92
|
+
.description("Maintainer file checks")
|
|
93
|
+
.addCommand(new Command("check")
|
|
94
|
+
.description("Check MAINTAINERS.md lists 3 maintainers from 2 companies (a gate, not a claim of eligibility)")
|
|
95
|
+
.argument("[file]", "maintainers file", "MAINTAINERS.md")
|
|
96
|
+
.action((file) => {
|
|
97
|
+
const { exitCode, output } = runMaintainersCheck(file);
|
|
98
|
+
(exitCode === 0 ? console.log : console.error)(output);
|
|
99
|
+
process.exitCode = exitCode;
|
|
100
|
+
}));
|
|
101
|
+
//# sourceMappingURL=maintainers.js.map
|
package/cli/package.json
CHANGED
|
@@ -89,6 +89,7 @@ load('ext://tdk-cli',
|
|
|
89
89
|
'APP_RESOURCES',
|
|
90
90
|
'GLOBAL_CONFIG_EXPORT',
|
|
91
91
|
'Registry',
|
|
92
|
+
'resource_defers_start',
|
|
92
93
|
)
|
|
93
94
|
|
|
94
95
|
# =============================================================================
|
|
@@ -265,7 +266,9 @@ for _KNOWN_SVC in _app_resources_list:
|
|
|
265
266
|
_KNOWN_TILT_RESOURCES.add(_KNOWN_RES_NAME + '-config-gen')
|
|
266
267
|
_KNOWN_RES_MANIFEST = _KNOWN_RES.get('_manifest', {})
|
|
267
268
|
_KNOWN_RES_SABLIER = _KNOWN_RES_MANIFEST.get('sablier', {})
|
|
268
|
-
|
|
269
|
+
# Mirrors _register_single_resource: the prebuild only exists for
|
|
270
|
+
# a resource that gets a docker_build (has syncs, not a library/sdk/BYO).
|
|
271
|
+
if resource_defers_start(_KNOWN_RES_MANIFEST) and _KNOWN_RES_MANIFEST.get('appType', 'backend') not in ['library', 'sdk', 'bring-your-own'] and _KNOWN_RES_MANIFEST.get('syncs'):
|
|
269
272
|
_DEFERRED_IMAGE_RESOURCES.add(_KNOWN_RES_NAME + '-image')
|
|
270
273
|
if _KNOWN_SVC_STACK:
|
|
271
274
|
_STACK_TO_RESOURCE_NAMES.setdefault(_KNOWN_SVC_STACK, [])
|
|
@@ -291,7 +294,11 @@ if FOCUS_MODE and FOCUS_ENABLED_RESOURCES:
|
|
|
291
294
|
# local_resource because their dc_resource is not auto-init.
|
|
292
295
|
# Keep that prebuild in focus mode so `tdk up <stack>` still
|
|
293
296
|
# prepares the image before the first request wakes the service.
|
|
294
|
-
|
|
297
|
+
# Only when it exists: without a license deferStart is off and
|
|
298
|
+
# no `-image` resource is registered, and Tilt errors on an
|
|
299
|
+
# enabled name it cannot find.
|
|
300
|
+
if _STACK_RES_NAME + '-image' in _DEFERRED_IMAGE_RESOURCES:
|
|
301
|
+
_FILTERED_RESOURCES.append(_STACK_RES_NAME + '-image')
|
|
295
302
|
else:
|
|
296
303
|
_MATCHED = [kr for kr in _KNOWN_TILT_RESOURCES if kr != 'service-discovery-daemon' and kr.startswith(_R + '-')]
|
|
297
304
|
_FILTERED_RESOURCES.extend(_MATCHED)
|
package/engine/Tiltfile
CHANGED
|
@@ -39,6 +39,7 @@ load('./topologies/tilt/resources/conditions.star', _Database='Database')
|
|
|
39
39
|
load('./topologies/tilt/resources/declaration.star', _Orchestrator='Orchestrator')
|
|
40
40
|
load('./topologies/tilt/resources/ordering.star', _Infra='Infra')
|
|
41
41
|
load('./topologies/tilt/resources/triggers.star', _Watchers='Watchers')
|
|
42
|
+
load('./topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star', _resource_defers_start='resource_defers_start')
|
|
42
43
|
|
|
43
44
|
# =============================================================================
|
|
44
45
|
# RE-EXPORTS
|
|
@@ -77,4 +78,7 @@ Orchestrator = _Orchestrator
|
|
|
77
78
|
Infra = _Infra
|
|
78
79
|
Watchers = _Watchers
|
|
79
80
|
|
|
81
|
+
# True when a manifest's deferStart is actually active (licensed); decides whether a `<name>-image` prebuild resource exists.
|
|
82
|
+
resource_defers_start = _resource_defers_start
|
|
83
|
+
|
|
80
84
|
print("✅ TDK Engine loaded: orchestration, resources, and lifecycle")
|
package/engine/entrypoint.star
CHANGED
|
@@ -36,6 +36,7 @@ load("./topologies/tilt/resources/conditions.star", _Database = "Database")
|
|
|
36
36
|
load("./topologies/tilt/resources/declaration.star", _Orchestrator = "Orchestrator")
|
|
37
37
|
load("./topologies/tilt/resources/ordering.star", _Infra = "Infra")
|
|
38
38
|
load("./topologies/tilt/resources/triggers.star", _Watchers = "Watchers")
|
|
39
|
+
load("./topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star", _resource_defers_start = "resource_defers_start")
|
|
39
40
|
|
|
40
41
|
Utils = _Utils
|
|
41
42
|
Manifest = _Manifest
|
|
@@ -65,6 +66,7 @@ Database = _Database
|
|
|
65
66
|
Orchestrator = _Orchestrator
|
|
66
67
|
Infra = _Infra
|
|
67
68
|
Watchers = _Watchers
|
|
69
|
+
resource_defers_start = _resource_defers_start
|
|
68
70
|
|
|
69
71
|
# =============================================================================
|
|
70
72
|
# 🔮 AUTO-DISCOVERY DAEMON
|
|
@@ -51,6 +51,9 @@ CMD ["sh", "-c", "uvicorn main:app --app-dir src --host 0.0.0.0 --port ${{PORT:-
|
|
|
51
51
|
|
|
52
52
|
_GO = """# Generated by TDK for a Go backend. Do not edit; change service.json instead.
|
|
53
53
|
FROM golang:1.23-bookworm AS build
|
|
54
|
+
# The official image pins GOTOOLCHAIN=local, so a go.mod that needs a newer Go (Gin 1.12 needs 1.25) stops with "requires go >= 1.25".
|
|
55
|
+
# `auto` lets Go fetch the toolchain go.mod asks for; a go.mod at or below the image's Go downloads nothing.
|
|
56
|
+
ENV GOTOOLCHAIN=auto
|
|
54
57
|
|
|
55
58
|
WORKDIR /app/{res_path}
|
|
56
59
|
|
|
@@ -102,21 +105,97 @@ EXPOSE {port}
|
|
|
102
105
|
CMD ["app"]
|
|
103
106
|
"""
|
|
104
107
|
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
+
# The watcher the Go development target runs. It compiles a change into a staging binary while the running app keeps serving, and stops
|
|
109
|
+
# and swaps the app only when that build succeeds. Air was tried first and stops the app as soon as it sees a change (engine.go `start`
|
|
110
|
+
# calls stopBin before buildRun), so the service stayed down for the whole compile; a Traefik health check landing in that gap kept the
|
|
111
|
+
# route at 503 for up to 10s. A compile error is printed to the log and the last good build keeps serving.
|
|
112
|
+
# `-s -w` drops debug info, which shortens the link step; delve cannot attach to a binary built this way.
|
|
113
|
+
# `\\n` in find's -printf format is written with a doubled backslash so the shell receives a backslash and an n.
|
|
114
|
+
_GO_DEV_WATCH = """#!/bin/sh
|
|
115
|
+
app=/tmp/app
|
|
116
|
+
next=/tmp/app.next
|
|
117
|
+
pid=
|
|
118
|
+
|
|
119
|
+
# Path, mtime and size of every file a Go build can read. Dot-directories (.git, .autogenerated), tmp/, tests and docs are ignored.
|
|
120
|
+
signature() {
|
|
121
|
+
find . -type f -not -path '*/.*' -not -path './tmp/*' -not -name '*_test.go' -not -name '*.md' -printf '%p %T@ %s\\n' | sort | cksum
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# The app runs in its own session so a signal reaches everything it forked and no child keeps the listen socket.
|
|
125
|
+
start_app() {
|
|
126
|
+
setsid "$app" &
|
|
127
|
+
pid=$!
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Succeeds only when the app is really gone; a swap on top of a live process would fail (ETXTBSY) or hit address-in-use.
|
|
131
|
+
stop_app() {
|
|
132
|
+
[ -n "$pid" ] || return 0
|
|
133
|
+
kill -TERM -- "-$pid" 2>/dev/null || kill "$pid" 2>/dev/null
|
|
134
|
+
waited=0
|
|
135
|
+
while kill -0 "$pid" 2>/dev/null && [ "$waited" -lt 50 ]; do
|
|
136
|
+
sleep 0.1
|
|
137
|
+
waited=$((waited + 1))
|
|
138
|
+
done
|
|
139
|
+
kill -KILL -- "-$pid" 2>/dev/null || kill -9 "$pid" 2>/dev/null
|
|
140
|
+
wait "$pid" 2>/dev/null
|
|
141
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
142
|
+
return 1
|
|
143
|
+
fi
|
|
144
|
+
pid=
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
trap 'stop_app; exit 0' TERM INT
|
|
148
|
+
|
|
149
|
+
last=$(signature)
|
|
150
|
+
echo "[tdk] watching for Go changes; the running app keeps serving while a rebuild compiles"
|
|
151
|
+
start_app
|
|
152
|
+
|
|
153
|
+
while true; do
|
|
154
|
+
sleep 0.2 &
|
|
155
|
+
wait $!
|
|
156
|
+
if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then
|
|
157
|
+
wait "$pid"
|
|
158
|
+
echo "[tdk] the app exited with status $?; waiting for a change to rebuild"
|
|
159
|
+
pid=
|
|
160
|
+
fi
|
|
161
|
+
now=$(signature)
|
|
162
|
+
[ "$now" = "$last" ] && continue
|
|
163
|
+
last=$now
|
|
164
|
+
echo "[tdk] change detected, rebuilding..."
|
|
165
|
+
if go build -ldflags='-s -w' -o "$next" . ; then
|
|
166
|
+
if stop_app && mv "$next" "$app"; then
|
|
167
|
+
start_app
|
|
168
|
+
echo "[tdk] rebuilt and restarted"
|
|
169
|
+
else
|
|
170
|
+
echo "[tdk] could not stop the running app or install the new build; save again to retry"
|
|
171
|
+
last=
|
|
172
|
+
fi
|
|
173
|
+
else
|
|
174
|
+
echo "[tdk] build failed; the last good build keeps running. Fix the error and save again."
|
|
175
|
+
fi
|
|
176
|
+
done
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
# Opt-in Go development target (`dev.liveReload`): the toolchain image plus the watcher above, which rebuilds and restarts the app when a
|
|
180
|
+
# source file changes. Dependencies are downloaded in their own layer and the module download is its own image layer, so an ordinary
|
|
181
|
+
# source edit recompiles incrementally. `go.mod`, `go.sum` and the Dockerfile still rebuild the image. The heredoc needs BuildKit's
|
|
182
|
+
# Dockerfile frontend 1.4 (Docker Engine 23+).
|
|
108
183
|
_GO_DEVELOPMENT = """
|
|
109
184
|
FROM golang:1.23-bookworm AS development
|
|
110
|
-
|
|
185
|
+
ENV GOTOOLCHAIN=auto
|
|
111
186
|
WORKDIR /app/{res_path}
|
|
112
187
|
COPY {res_path}/go.* ./
|
|
113
|
-
RUN
|
|
188
|
+
# A plain RUN (not a cache mount) keeps the modules in a layer keyed on go.mod and go.sum, so the priming build below reuses them.
|
|
189
|
+
RUN go mod download
|
|
114
190
|
COPY {res_path}/ ./
|
|
115
|
-
ENV GOFLAGS=-buildvcs=false
|
|
191
|
+
ENV GOFLAGS=-buildvcs=false CGO_ENABLED=0
|
|
116
192
|
# Prime the build cache inside the image (a cache mount would not be kept), so the first edit recompiles only what changed.
|
|
117
|
-
|
|
193
|
+
# Same flags as the watcher's builds (and CGO off like the production stage), so the primed binary and cache match every later one.
|
|
194
|
+
RUN go build -ldflags='-s -w' -o /tmp/app .
|
|
195
|
+
COPY --chmod=755 <<'TDK_GO_DEV_WATCH' /usr/local/bin/tdk-go-watch
|
|
196
|
+
{watch_script}TDK_GO_DEV_WATCH
|
|
118
197
|
EXPOSE {port}
|
|
119
|
-
CMD ["
|
|
198
|
+
CMD ["tdk-go-watch"]
|
|
120
199
|
"""
|
|
121
200
|
|
|
122
201
|
def generate_language_dockerfile(res_path, language, port = BASE_PORT_BACKEND, dev = False):
|
|
@@ -126,7 +205,7 @@ def generate_language_dockerfile(res_path, language, port = BASE_PORT_BACKEND, d
|
|
|
126
205
|
fail("No Dockerfile generator for backend language '{}'".format(language))
|
|
127
206
|
go_development = ""
|
|
128
207
|
if dev and language == "go":
|
|
129
|
-
go_development = _GO_DEVELOPMENT.format(res_path = res_path, port = port)
|
|
208
|
+
go_development = _GO_DEVELOPMENT.format(res_path = res_path, port = port, watch_script = _GO_DEV_WATCH)
|
|
130
209
|
return template.format(
|
|
131
210
|
res_path = res_path,
|
|
132
211
|
port = port,
|
package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star
CHANGED
|
@@ -220,7 +220,7 @@ def _build_live_update_rules(res_path, full_res_path, syncs, project_root='', la
|
|
|
220
220
|
|
|
221
221
|
Python backends reload their own process after a sync (uvicorn --reload), so they have
|
|
222
222
|
no Bun install step. A changed pyproject.toml is not synced, so Tilt rebuilds the image instead.
|
|
223
|
-
|
|
223
|
+
Rust backends, and Go backends without `dev.liveReload`, have no live update (see the early return).
|
|
224
224
|
"""
|
|
225
225
|
# Go and Rust are compiled into the image and have no reload process, so syncing sources would leave the
|
|
226
226
|
# old binary running. Return before any sync() step is built (Tilt rejects sync steps that are created
|
|
@@ -229,7 +229,7 @@ def _build_live_update_rules(res_path, full_res_path, syncs, project_root='', la
|
|
|
229
229
|
return []
|
|
230
230
|
|
|
231
231
|
if go_live_reload:
|
|
232
|
-
# Opted-in Go development loop: sync the sources into the development image, where
|
|
232
|
+
# Opted-in Go development loop: sync the sources into the development image, where the tdk-go-watch script rebuilds and restarts.
|
|
233
233
|
# Dependency and Dockerfile changes cannot be picked up by a restart, so they fall back to a full image rebuild.
|
|
234
234
|
source_dir = _live_update_sync_source(project_root, full_res_path, '')
|
|
235
235
|
return [
|
|
@@ -290,6 +290,11 @@ def _wake_compose_invocation(project_root, resource_path, res_name, resource_nam
|
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
|
|
293
|
+
def _image_target(config):
|
|
294
|
+
"""The Dockerfile target Tilt builds and the deferStart prebuild must match: `development` for Go with `dev.liveReload`."""
|
|
295
|
+
return 'development' if Docker.go_live_reload(config.get('manifest', {})) else 'production'
|
|
296
|
+
|
|
297
|
+
|
|
293
298
|
def _register_docker_build(config, live_update_rules):
|
|
294
299
|
"""Register docker_build with live_update for hot-reload support."""
|
|
295
300
|
TypescriptBuilders.build_typescript_service(
|
|
@@ -298,7 +303,7 @@ def _register_docker_build(config, live_update_rules):
|
|
|
298
303
|
_resolve_dockerfile(config),
|
|
299
304
|
live_update_rules,
|
|
300
305
|
config['res_deps'],
|
|
301
|
-
target =
|
|
306
|
+
target = _image_target(config),
|
|
302
307
|
)
|
|
303
308
|
|
|
304
309
|
|
|
@@ -309,20 +314,23 @@ def _register_deferred_image_prebuild(config, auto_init_apps):
|
|
|
309
314
|
live on tdk-erp-system, task 7.2), so a fresh project's first request would
|
|
310
315
|
find no image and the wake gateway would fall back to `tilt trigger`, which
|
|
311
316
|
queues behind every other build. This local_resource builds the same image
|
|
312
|
-
(same Dockerfile
|
|
317
|
+
(same Dockerfile and target as docker_build: `production`, or `development` for Go with `dev.liveReload`; host network, and an allowlist
|
|
313
318
|
.dockerignore equivalent to docker_build's only=/ignore=) under the tag the
|
|
314
319
|
compose service declares, so the gateway can start it directly.
|
|
315
320
|
"""
|
|
316
321
|
project_root = config.get('project_root', '')
|
|
317
322
|
dockerfile = _resolve_dockerfile(config)
|
|
318
323
|
prebuild_dockerfile = dockerfile + '.prebuild'
|
|
324
|
+
# The same target _register_docker_build picks: a Go service with `dev.liveReload` runs the development image (the watcher), so
|
|
325
|
+
# prebuilding `production` under the same tag would start the compiled binary with no reload.
|
|
326
|
+
target = _image_target(config)
|
|
319
327
|
ignore_content = TypescriptBuilders.prebuild_dockerignore(dockerfile, project_root, config['res_path'])
|
|
320
328
|
# BuildKit reads `<dockerfile>.dockerignore` for `-f <dockerfile>`; the
|
|
321
329
|
# original Dockerfile already has a (denylist) one, hence the copy.
|
|
322
330
|
cmd = (
|
|
323
331
|
"cp '" + dockerfile + "' '" + prebuild_dockerfile + "'"
|
|
324
332
|
+ " && printf '%s' '" + ignore_content + "' > '" + prebuild_dockerfile + ".dockerignore'"
|
|
325
|
-
+ " && docker build --network host --target
|
|
333
|
+
+ " && docker build --network host --target " + target
|
|
326
334
|
+ " -f '" + prebuild_dockerfile + "'"
|
|
327
335
|
+ " -t '" + _app_image_name(config['resource_name'], config['res_name']) + "'"
|
|
328
336
|
+ " '" + (project_root or '.') + "'"
|