@tdk-landscape/tdk-cli-core 1.3.99 → 1.3.101
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/cli/dist/cli.js +2 -0
- package/cli/dist/commands/maintainers.js +101 -0
- package/cli/dist/commands/ui.js +5 -4
- package/cli/package.json +1 -1
- package/engine/topologies/platform/docker/dockerfile/language_dockerfile.star +88 -9
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +2 -2
- 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/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/dist/commands/ui.js
CHANGED
|
@@ -8,11 +8,11 @@ import { findProjectRoot } from "../utils/paths.js";
|
|
|
8
8
|
import { clearMetadataCache, discoverResources, discoverStacks, getResourceMetadata, getStackMetadata, } from "../utils/services.js";
|
|
9
9
|
import { isTiltAvailable } from "../utils/tilt.js";
|
|
10
10
|
// biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
|
|
11
|
-
const HelpPanel = ({ onClose }) => (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60, children: [_jsx(Text, { bold: true, color: "cyan", children: "Keyboard Shortcuts" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, underline: true, children: "Navigation" }), _jsx(Text, { children: " \u2191/\u2193 Navigate list items" }), _jsx(Text, { children: " Enter Select item / Open detail" }), _jsx(Text, { children: " Space Toggle expand (tree view)" }), _jsx(Text, { children: " Tab Next tab" }), _jsx(Text, { children: " 1-5 Direct tab access" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, underline: true, children: "Actions" }) }), _jsx(Text, { children: " m Toggle mouse support" }), _jsx(Text, { children: " t Toggle tooltips" }), _jsx(Text, { children: " e Toggle enabled/disabled services" }), _jsx(Text, { children: " r Refresh data" }), _jsx(Text, { children: " / Search/filter" }), _jsx(Text, { children: " ? Show this help" }), _jsx(Text, { children: " q
|
|
11
|
+
const HelpPanel = ({ onClose }) => (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60, children: [_jsx(Text, { bold: true, color: "cyan", children: "Keyboard Shortcuts" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, underline: true, children: "Navigation" }), _jsx(Text, { children: " \u2191/\u2193 Navigate list items" }), _jsx(Text, { children: " Enter Select item / Open detail" }), _jsx(Text, { children: " Space Toggle expand (tree view)" }), _jsx(Text, { children: " Tab Next tab" }), _jsx(Text, { children: " 1-5 Direct tab access" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, underline: true, children: "Actions" }) }), _jsx(Text, { children: " m Toggle mouse support" }), _jsx(Text, { children: " t Toggle tooltips" }), _jsx(Text, { children: " e Toggle enabled/disabled services" }), _jsx(Text, { children: " r Refresh data" }), _jsx(Text, { children: " / Search/filter" }), _jsx(Text, { children: " ? Show this help" }), _jsx(Text, { children: " q Quit \u2502 Esc Back" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press any key to close..." }) })] }));
|
|
12
12
|
const LoadingScreen = ({ progress, message }) => (_jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "\u2593\u2592\u2591 TDK NEON EDITION \u2591\u2592\u2593" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { children: ["Loading: ", message] }), _jsxs(Box, { marginY: 1, borderStyle: "single", borderColor: "gray", width: 50, children: [_jsx(Box, { width: progress / 2, backgroundColor: "cyan", children: _jsx(Text, { children: " ".repeat(progress / 2) }) }), _jsxs(Text, { children: [" ", progress, "%"] })] })] }));
|
|
13
13
|
// biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
|
|
14
14
|
const ErrorScreen = ({ error, onRetry }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "red", children: "Could Not Load Services" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { color: "red", children: ["\u2717 ", error] }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "Troubleshooting:" }), _jsx(Text, { color: "gray", children: " 1. Check that every service.json is valid JSON" }), _jsx(Text, { color: "gray", children: " 2. Run tdk from your project (the folder with the Tiltfile)" }), _jsx(Text, { color: "gray", children: " 3. Try: tdk status --verbose" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to retry or [q] to quit" })] }));
|
|
15
|
-
const EmptyState = () => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "yellow", children: "No Services Found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "\u25C9 No service.json files found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { children: "To get started:" }), _jsx(Text, { children: " 1. Run: tdk init" }), _jsx(Text, { children: " 2. Or create services manually" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to refresh or [q] to quit" })] }));
|
|
15
|
+
const EmptyState = ({ message }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "yellow", children: "No Services Found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "\u25C9 No service.json files found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { children: "To get started:" }), _jsx(Text, { children: " 1. Run: tdk init" }), _jsx(Text, { children: " 2. Or create services manually" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to refresh or [q] to quit" }), message && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: "cyan", children: ["\u2593\u2592\u2591 ", message, " \u2591\u2592\u2593"] }) }))] }));
|
|
16
16
|
const TUIApp = () => {
|
|
17
17
|
const { exit } = useApp();
|
|
18
18
|
const { stdout } = useStdout();
|
|
@@ -297,7 +297,8 @@ const TUIApp = () => {
|
|
|
297
297
|
setSelectedStack(null);
|
|
298
298
|
return;
|
|
299
299
|
}
|
|
300
|
-
|
|
300
|
+
setMessage("Press q to quit");
|
|
301
|
+
setTimeout(() => setMessage(""), 2000);
|
|
301
302
|
return;
|
|
302
303
|
}
|
|
303
304
|
if (input === "?") {
|
|
@@ -413,7 +414,7 @@ const TUIApp = () => {
|
|
|
413
414
|
return _jsx(ErrorScreen, { error: error, onRetry: refresh });
|
|
414
415
|
}
|
|
415
416
|
if (services.length === 0) {
|
|
416
|
-
return _jsx(EmptyState, {});
|
|
417
|
+
return _jsx(EmptyState, { message: message });
|
|
417
418
|
}
|
|
418
419
|
return (_jsxs(Box, { flexDirection: "column", height: stdout.rows || 24, children: [_jsx(Box, { paddingX: 1, paddingY: 0, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u2593\u2592\u2591 TDK NEON EDITION \u2591\u2592\u2593" }), _jsx(Text, { color: "gray", children: " \u2502 " }), _jsx(Text, { color: "white", children: projectRoot }), _jsx(Text, { color: "gray", children: " \u2502 " }), _jsxs(Text, { color: "green", children: [services.length, " services ready"] })] }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "gray", children: "─".repeat(compact ? 60 : Math.min(terminalWidth - 4, 100)) }) }), isSearching && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: "yellow", children: ["Search: ", searchQuery, "_"] }) })), !isSearching && message && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: "cyan", children: ["\u2593\u2592\u2591 ", message, " \u2591\u2592\u2593"] }) })), !isSearching && !message && showTooltips && !showHelp && (_jsx(Box, { paddingX: 1, height: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: activeTab === "overview" && selectedStack
|
|
419
420
|
? `Stack "${selectedStack}" selected. [Enter] view │ [Esc] back │ [e] ${showEnabledOnly ? "show all" : "enabled only"} │ [?] help`
|
package/cli/package.json
CHANGED
|
@@ -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 [
|