hammerkit 1.27.0-next.8 → 2.0.0-next.1
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/build.schema.json +176 -6
- package/dist/cache/backends/local-cache-backend.js +6 -0
- package/dist/cache/backends/registry/registry-client.js +1 -0
- package/dist/cache/cache-sync.js +1 -1
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +2 -0
- package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
- package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
- package/dist/dashboard/favicon.svg +1 -0
- package/dist/dashboard/index.html +15 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/archive-task-entry.js +4 -2
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/deferred-restore.js +36 -0
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/docker-task.js +21 -1
- package/dist/executer/execute-work-service.js +2 -0
- package/dist/executer/execute-work-task.js +11 -4
- package/dist/executer/execution-steps.js +25 -4
- package/dist/executer/reset-work-tree.js +4 -0
- package/dist/executer/scheduler/enqueue-next.js +57 -6
- package/dist/executer/scheduler/state-key.js +42 -5
- package/dist/executer/watch-loop.js +6 -3
- package/dist/harness/connect.js +20 -6
- package/dist/harness/mcp-command.js +22 -6
- package/dist/harness/mcp.js +63 -114
- package/dist/harness/opencode.js +158 -50
- package/dist/harness/pi.js +29 -0
- package/dist/harness/sandbox-client.js +194 -23
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +76 -17
- package/dist/kubernetes/await-running-state.js +172 -13
- package/dist/kubernetes/cluster-state.js +245 -0
- package/dist/kubernetes/ensure-http-route.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +138 -18
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
- package/dist/kubernetes/ensure-persistent-data.js +212 -10
- package/dist/kubernetes/follow-pod-logs.js +182 -0
- package/dist/kubernetes/get-service-ip.js +2 -2
- package/dist/kubernetes/job-placement.js +36 -12
- package/dist/kubernetes/job-secrets.js +9 -6
- package/dist/kubernetes/kept-claims.js +22 -10
- package/dist/kubernetes/kubernetes-instance.js +1 -0
- package/dist/kubernetes/network-policy.js +16 -8
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/remove-stale-items.js +46 -0
- package/dist/kubernetes/resources.js +25 -3
- package/dist/kubernetes/restore-kubernetes-data.js +9 -5
- package/dist/kubernetes/sandbox-runner.js +628 -0
- package/dist/kubernetes/secrets.js +7 -11
- package/dist/kubernetes/service-rollouts.js +74 -0
- package/dist/kubernetes/store-kubernetes-data.js +5 -2
- package/dist/kubernetes/volumes.js +96 -11
- package/dist/main.js +2 -0
- package/dist/optimizer/calculate-checksum.js +3 -2
- package/dist/optimizer/check-key.js +94 -0
- package/dist/optimizer/check-keys.js +64 -0
- package/dist/optimizer/get-work-cache-stats.js +17 -3
- package/dist/optimizer/work-task-cache-description.js +49 -1
- package/dist/planner/service-definition.js +9 -4
- package/dist/planner/utils/append-work-dependencies.js +5 -3
- package/dist/planner/utils/append-work-service.js +18 -1
- package/dist/planner/utils/append-work-task.js +13 -3
- package/dist/planner/utils/get-work-runtime.js +6 -1
- package/dist/planner/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/planner/validate.js +22 -0
- package/dist/planner/work-network.js +3 -1
- package/dist/planner/work-runtime-aggregate.js +32 -0
- package/dist/planner/work-runtime-kubernetes.js +153 -26
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +27 -9
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +69 -3
- package/dist/remote/remote-commands.js +33 -7
- package/dist/remote/remote-status.js +112 -0
- package/dist/remote/remote-work-commands.js +562 -45
- package/dist/remote/server-client.js +15 -2
- package/dist/remote/token-source.js +64 -26
- package/dist/run-program.js +9 -0
- package/dist/schema/build-file-base-task-schema.js +3 -3
- package/dist/schema/build-file-container-service-schema.js +10 -0
- package/dist/schema/build-file-container-task-schema.js +16 -1
- package/dist/schema/build-file-include-schema.js +18 -4
- package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
- package/dist/schema/build-file-task-schema.js +5 -1
- package/dist/schema/capabilities-schema.js +56 -0
- package/dist/schema/reference-parser.js +119 -11
- package/dist/schema/removed-properties.js +20 -0
- package/dist/schema/schema-parser.js +94 -41
- package/dist/schema/work-scope-parser.js +33 -10
- package/dist/secrets/command-secret-provider.js +3 -1
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/secrets/secret-catalog.js +43 -19
- package/dist/server/accounts-api.js +4 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/capacity-format.js +36 -0
- package/dist/server/capacity.js +300 -0
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/dashboard.js +196 -0
- package/dist/server/definitions-api.js +66 -3
- package/dist/server/endpoints.js +794 -0
- package/dist/server/executor.js +164 -9
- package/dist/server/kubernetes-backend.js +478 -70
- package/dist/server/kubernetes-sandbox-storage.js +46 -2
- package/dist/server/overview.js +271 -0
- package/dist/server/plan.js +170 -121
- package/dist/server/policy.js +34 -2
- package/dist/server/proposals-api.js +17 -8
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-logs.js +4 -0
- package/dist/server/run-plan.js +38 -4
- package/dist/server/run-request.js +58 -3
- package/dist/server/run-worker.js +262 -13
- package/dist/server/runs.js +969 -36
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-config.js +246 -10
- package/dist/server/server.js +429 -32
- package/dist/server/service-runs.js +82 -0
- package/dist/server/timings.js +132 -0
- package/dist/server/wait-reason.js +21 -0
- package/dist/utils/tar.js +28 -1
- package/dist/utils/terminal-prompt.js +53 -0
- package/package.json +8 -2
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runRequestSchema = exports.runOptionsSchema = exports.MAX_ARGS_BYTES = exports.MAX_ARGS = void 0;
|
|
3
|
+
exports.runRequestSchema = exports.runOptionsSchema = exports.MAX_ARGS_BYTES = exports.MAX_ARGS = exports.MAX_ENDPOINTS = exports.SERVICE_RUN_NAME_PATTERN = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const units_1 = require("../utils/units");
|
|
6
6
|
const bundle_1 = require("../remote/bundle");
|
|
7
7
|
const repo_1 = require("./repo");
|
|
8
8
|
const runs_1 = require("./runs");
|
|
9
9
|
const name = zod_1.z.string().min(1).max(200);
|
|
10
|
+
// The name of a service run, one DNS label: the hosts of its endpoints carry it, and a run of
|
|
11
|
+
// the same caller with the same name replaces it
|
|
12
|
+
exports.SERVICE_RUN_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$/;
|
|
13
|
+
// endpoints one service run may publish
|
|
14
|
+
exports.MAX_ENDPOINTS = 5;
|
|
10
15
|
// what one run may pass after `--`
|
|
11
16
|
exports.MAX_ARGS = 100;
|
|
12
17
|
exports.MAX_ARGS_BYTES = 16 * 1024;
|
|
@@ -38,6 +43,8 @@ exports.runOptionsSchema = zod_1.z
|
|
|
38
43
|
cacheReadOnly: zod_1.z.boolean().optional(),
|
|
39
44
|
// as --timeout
|
|
40
45
|
timeout: duration.optional(),
|
|
46
|
+
// as --max-timeout: caps every task, also one with a longer timeout of its own
|
|
47
|
+
maxTimeout: duration.optional(),
|
|
41
48
|
// as --no-skip-deps when false
|
|
42
49
|
skipDeps: zod_1.z.boolean().optional(),
|
|
43
50
|
// as the text after `--`: appended to the last command of the one task the run
|
|
@@ -87,18 +94,66 @@ exports.runRequestSchema = zod_1.z
|
|
|
87
94
|
zod_1.z.object({ type: zod_1.z.literal('upload'), snapshot: zod_1.z.string().regex(bundle_1.HASH_PATTERN) }).strict(),
|
|
88
95
|
// a sandbox of the caller's, see POST /v1/sandboxes
|
|
89
96
|
zod_1.z
|
|
90
|
-
.object({
|
|
97
|
+
.object({
|
|
98
|
+
type: zod_1.z.literal('sandbox'),
|
|
99
|
+
id: zod_1.z.string().regex(runs_1.SANDBOX_ID_PATTERN, 'expected a sandbox id'),
|
|
100
|
+
// a check: run on a copy of the files a push of the sandbox would commit, so what
|
|
101
|
+
// passes counts for that commit (remote verify)
|
|
102
|
+
check: zod_1.z.literal(true).optional(),
|
|
103
|
+
})
|
|
91
104
|
.strict(),
|
|
92
105
|
]),
|
|
93
106
|
options: exports.runOptionsSchema.default({}),
|
|
107
|
+
// a service run: these services of the build file, started as `hammerkit up` starts
|
|
108
|
+
// them and kept up until `ttl` ends or the run is cancelled, instead of tasks
|
|
109
|
+
services: zod_1.z.array(name).max(20).default([]),
|
|
110
|
+
// how long the services stay once they are ready, whether they sleep in between or not;
|
|
111
|
+
// `runs.up.ttl` when left out. At its end they are removed.
|
|
112
|
+
ttl: duration.optional(),
|
|
113
|
+
// how long the services stay up without a request to one of the endpoints, then they
|
|
114
|
+
// sleep until the next request wakes them; `runs.up.sleepTtl` when left out
|
|
115
|
+
sleepTtl: duration.optional(),
|
|
116
|
+
// a second service run of the caller with the same name replaces the first; the branch
|
|
117
|
+
// of the source when left out
|
|
118
|
+
name: zod_1.z
|
|
119
|
+
.string()
|
|
120
|
+
.regex(exports.SERVICE_RUN_NAME_PATTERN, 'expected lower case letters, digits and -, at most 40, as a DNS label')
|
|
121
|
+
.optional(),
|
|
122
|
+
// ports of the run's services published at hosts of their own, behind the server's sign-in
|
|
123
|
+
endpoints: zod_1.z
|
|
124
|
+
.array(zod_1.z.object({ service: name, port: zod_1.z.number().int().min(1).max(65535) }).strict())
|
|
125
|
+
.max(exports.MAX_ENDPOINTS)
|
|
126
|
+
.default([]),
|
|
94
127
|
})
|
|
95
128
|
.strict()
|
|
96
|
-
.refine((request) => request.tasks.length > 0 || (request.options.filter ?? []).length > 0, {
|
|
129
|
+
.refine((request) => request.services.length > 0 || request.tasks.length > 0 || (request.options.filter ?? []).length > 0, {
|
|
97
130
|
message: 'name at least one task, or filter by labels',
|
|
98
131
|
path: ['tasks'],
|
|
99
132
|
})
|
|
133
|
+
.refine((request) => request.services.length === 0 || request.tasks.length === 0, {
|
|
134
|
+
message: 'a run names tasks or services, not both',
|
|
135
|
+
path: ['services'],
|
|
136
|
+
})
|
|
137
|
+
.refine((request) => request.services.length === 0 ||
|
|
138
|
+
((request.options.filter ?? []).length === 0 && (request.options.exclude ?? []).length === 0), { message: 'a service run names its services, labels do not select them', path: ['options', 'filter'] })
|
|
139
|
+
.refine((request) => request.services.length === 0 || (request.options.args ?? []).length === 0, {
|
|
140
|
+
message: 'a service run takes no arguments',
|
|
141
|
+
path: ['options', 'args'],
|
|
142
|
+
})
|
|
143
|
+
.refine((request) => request.services.length > 0 ||
|
|
144
|
+
(request.ttl === undefined &&
|
|
145
|
+
request.sleepTtl === undefined &&
|
|
146
|
+
request.name === undefined &&
|
|
147
|
+
request.endpoints.length === 0), { message: 'ttl, sleepTtl, name and endpoints are for a run of services', path: ['services'] })
|
|
148
|
+
.refine((request) => new Set(request.endpoints.map((endpoint) => `${endpoint.service}:${endpoint.port}`)).size ===
|
|
149
|
+
request.endpoints.length, { message: 'an endpoint is named twice', path: ['endpoints'] })
|
|
100
150
|
.refine((request) => (request.options.args ?? []).length === 0 || request.tasks.length === 1, {
|
|
101
151
|
message: 'arguments go to one task, name exactly one in tasks',
|
|
102
152
|
path: ['options', 'args'],
|
|
153
|
+
})
|
|
154
|
+
.refine((request) => request.source.type !== 'sandbox' || !request.source.check || (request.options.args ?? []).length === 0, { message: 'a check runs the task as the build file says, it takes no arguments', path: ['options', 'args'] })
|
|
155
|
+
.refine((request) => request.source.type !== 'sandbox' || !request.source.check || request.services.length === 0, {
|
|
156
|
+
message: 'a check runs tasks, a run of services keeps them up and passes nothing',
|
|
157
|
+
path: ['services'],
|
|
103
158
|
});
|
|
104
159
|
//# sourceMappingURL=run-request.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.workerMain = exports.taskCauses = exports.runTasks = exports.getRunNetwork = exports.workerInputSchema = void 0;
|
|
3
|
+
exports.workerMain = exports.readInput = exports.taskCauses = exports.describeFailures = exports.runTasks = exports.getEndpointTargets = exports.publishEndpoints = exports.getRunNetwork = exports.workerInputSchema = void 0;
|
|
4
4
|
const readline_1 = require("readline");
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const cli_1 = require("../cli");
|
|
@@ -21,6 +21,12 @@ const network_schema_1 = require("../schema/network-schema");
|
|
|
21
21
|
const work_network_1 = require("../planner/work-network");
|
|
22
22
|
const run_plan_1 = require("./run-plan");
|
|
23
23
|
const runs_1 = require("./runs");
|
|
24
|
+
const resources_1 = require("../kubernetes/resources");
|
|
25
|
+
const remove_stale_items_1 = require("../kubernetes/remove-stale-items");
|
|
26
|
+
const service_rollouts_1 = require("../kubernetes/service-rollouts");
|
|
27
|
+
const cluster_state_1 = require("../kubernetes/cluster-state");
|
|
28
|
+
const kubernetes_instance_1 = require("../kubernetes/kubernetes-instance");
|
|
29
|
+
const check_keys_1 = require("../optimizer/check-keys");
|
|
24
30
|
// one log line of a task is cut here, the registry caps how many events a run keeps
|
|
25
31
|
const MAX_LOG_TEXT = 8 * 1024;
|
|
26
32
|
/**
|
|
@@ -49,6 +55,9 @@ exports.workerInputSchema = zod_1.z
|
|
|
49
55
|
kubernetes: zod_1.z
|
|
50
56
|
.custom((value) => typeof value === 'object' && value !== null && value.type === 'kubernetes')
|
|
51
57
|
.optional(),
|
|
58
|
+
// on the server's docker host, the runtime a container that adds capabilities runs in
|
|
59
|
+
// (`backend.capabilities.runtime`)
|
|
60
|
+
capabilityRuntime: zod_1.z.string().min(1).optional(),
|
|
52
61
|
// the most any task or service reaches besides its needs, as `--network`: `runs.network`
|
|
53
62
|
network: network_schema_1.networkSchema.optional(),
|
|
54
63
|
// for a run on a sandbox, `sandboxes.network`: the run gets the lower of the two
|
|
@@ -57,8 +66,19 @@ exports.workerInputSchema = zod_1.z
|
|
|
57
66
|
// volumes (see scopeServiceVolumes)
|
|
58
67
|
volumeScope: zod_1.z
|
|
59
68
|
.string()
|
|
60
|
-
.regex(/^[a-z]
|
|
69
|
+
.regex(/^(?:[a-z]+|up-[0-9a-f]{8})-[0-9a-f]{24}$/)
|
|
61
70
|
.optional(),
|
|
71
|
+
// a service run: the services it keeps up instead of tasks, and the ports the server
|
|
72
|
+
// reaches as endpoints
|
|
73
|
+
up: zod_1.z
|
|
74
|
+
.object({
|
|
75
|
+
services: zod_1.z.array(zod_1.z.string()).min(1),
|
|
76
|
+
endpoints: zod_1.z.array(zod_1.z.object({ service: zod_1.z.string(), port: zod_1.z.number().int() }).strict()),
|
|
77
|
+
})
|
|
78
|
+
.strict()
|
|
79
|
+
.optional(),
|
|
80
|
+
// the run's results count as checks: report each task's check key before it runs
|
|
81
|
+
check: zod_1.z.boolean().optional(),
|
|
62
82
|
})
|
|
63
83
|
.strict();
|
|
64
84
|
// The ceiling of the run, combined here and not in the server: the lower of two allow lists
|
|
@@ -68,6 +88,60 @@ function getRunNetwork(input) {
|
|
|
68
88
|
return input.sandboxNetwork ? (0, work_network_1.limitNetwork)((0, work_network_1.parseWorkNetwork)(input.sandboxNetwork), runs) : runs;
|
|
69
89
|
}
|
|
70
90
|
exports.getRunNetwork = getRunNetwork;
|
|
91
|
+
/**
|
|
92
|
+
* The ports of the services of a service run as the server publishes them: on a host the
|
|
93
|
+
* ports of endpoints go to a free port of the loopback interface, where only the server
|
|
94
|
+
* reaches them, and no other port goes to the host, whatever the build file says (`up`
|
|
95
|
+
* would publish them on every interface, where two runs collide). On a cluster nothing is
|
|
96
|
+
* published, the server reaches the Service of each.
|
|
97
|
+
*/
|
|
98
|
+
function publishEndpoints(workTree, endpoints, cluster) {
|
|
99
|
+
for (const service of Object.values(workTree.services)) {
|
|
100
|
+
if (!('ports' in service.data)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
service.data.ports = service.data.ports.map((port) => !cluster &&
|
|
104
|
+
endpoints.some((endpoint) => endpoint.service === service.name && endpoint.port === port.containerPort)
|
|
105
|
+
? { containerPort: port.containerPort, hostPort: 0, hostIp: '127.0.0.1' }
|
|
106
|
+
: { containerPort: port.containerPort, hostPort: null });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.publishEndpoints = publishEndpoints;
|
|
110
|
+
// Where the server reaches each endpoint, once every service the run keeps up is ready;
|
|
111
|
+
// null until then.
|
|
112
|
+
function getEndpointTargets(workTree, up, kubernetes) {
|
|
113
|
+
const services = Object.values(workTree.services).filter((service) => up.services.includes(service.name));
|
|
114
|
+
if (services.length < up.services.length || services.some((service) => service.state.current.type !== 'running')) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const targets = [];
|
|
118
|
+
for (const endpoint of up.endpoints) {
|
|
119
|
+
const service = services.find((candidate) => candidate.name === endpoint.service);
|
|
120
|
+
const state = service?.state.current;
|
|
121
|
+
if (!service || state?.type !== 'running') {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (kubernetes) {
|
|
125
|
+
targets.push({
|
|
126
|
+
service: endpoint.service,
|
|
127
|
+
port: endpoint.port,
|
|
128
|
+
address: { host: `${(0, resources_1.getResourceName)(service, kubernetes)}.${kubernetes.namespace}.svc`, port: endpoint.port },
|
|
129
|
+
});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const published = state.published?.find((candidate) => candidate.containerPort === endpoint.port);
|
|
133
|
+
if (!published) {
|
|
134
|
+
throw new Error(`${endpoint.service} is up, but its port ${endpoint.port} was not published`);
|
|
135
|
+
}
|
|
136
|
+
targets.push({
|
|
137
|
+
service: endpoint.service,
|
|
138
|
+
port: endpoint.port,
|
|
139
|
+
address: { host: published.host, port: published.port },
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return targets;
|
|
143
|
+
}
|
|
144
|
+
exports.getEndpointTargets = getEndpointTargets;
|
|
71
145
|
/**
|
|
72
146
|
* Executes the tasks of a run with the engine of `hammerkit run`. The caller says which
|
|
73
147
|
* process this is in: it reports what happens through `emit` and stops when `abortCtrl`
|
|
@@ -75,7 +149,9 @@ exports.getRunNetwork = getRunNetwork;
|
|
|
75
149
|
*/
|
|
76
150
|
async function runTasks(input, cwd, processEnvs, abortCtrl,
|
|
77
151
|
// whether the run was stopped from outside; the engine aborts too when a task fails
|
|
78
|
-
cancelled, emit
|
|
152
|
+
cancelled, emit,
|
|
153
|
+
// a service run: its services are ready, at these addresses
|
|
154
|
+
up) {
|
|
79
155
|
const secrets = (0, redact_1.createSecretRegistry)();
|
|
80
156
|
const status = (0, work_item_status_1.statusConsole)((0, empty_writable_1.emptyWritable)(), secrets);
|
|
81
157
|
// the process starts in a directory of the server, the tasks work on the run's files
|
|
@@ -84,7 +160,12 @@ cancelled, emit) {
|
|
|
84
160
|
// is what is streamed: it is redacted with the secrets of this run
|
|
85
161
|
const environment = {
|
|
86
162
|
processEnvs,
|
|
87
|
-
hosted: {
|
|
163
|
+
hosted: {
|
|
164
|
+
catalog: input.catalog,
|
|
165
|
+
accounts: input.accounts,
|
|
166
|
+
kubernetes: input.kubernetes,
|
|
167
|
+
capabilityRuntime: input.capabilityRuntime,
|
|
168
|
+
},
|
|
88
169
|
abortCtrl,
|
|
89
170
|
cwd: work,
|
|
90
171
|
file: (0, get_file_context_1.getFileContext)(work),
|
|
@@ -117,15 +198,27 @@ cancelled, emit) {
|
|
|
117
198
|
const options = input.options;
|
|
118
199
|
const execOptions = {
|
|
119
200
|
logMode: 'live',
|
|
120
|
-
|
|
201
|
+
// a check takes a cached result only for the same files, never for the same dates
|
|
202
|
+
cacheDefault: input.check && options.cache === 'modify-date' ? 'checksum' : options.cache ?? 'checksum',
|
|
121
203
|
cacheReadOnly: options.cacheReadOnly ?? false,
|
|
122
204
|
timeout: options.timeout ? (0, units_1.parseDuration)(options.timeout) : null,
|
|
205
|
+
maxTimeout: options.maxTimeout ? (0, units_1.parseDuration)(options.maxTimeout) : null,
|
|
123
206
|
skipDeps: options.skipDeps !== false,
|
|
124
207
|
network: getRunNetwork(input),
|
|
125
208
|
// why a task was not taken from the cache goes into its result
|
|
126
209
|
explain: true,
|
|
127
210
|
};
|
|
128
|
-
|
|
211
|
+
const scopes = (0, run_plan_1.getRunScopes)(input.tasks, options, input.up?.services);
|
|
212
|
+
if (input.check) {
|
|
213
|
+
// the files as they are before any task wrote to them, a second task's too: a first
|
|
214
|
+
// one that formats or generates into src would key it by files no commit holds
|
|
215
|
+
for (const { scope } of scopes) {
|
|
216
|
+
for (const [task, { key }] of Object.entries(await (0, check_keys_1.getCheckKeys)(input.entry, environment, scope))) {
|
|
217
|
+
emit({ type: 'key', task, key });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const { name, scope } of scopes) {
|
|
129
222
|
const { ctx, scope: parsed } = await (0, schema_parser_1.createParseContext)(input.entry, environment, {
|
|
130
223
|
directory: input.directory,
|
|
131
224
|
});
|
|
@@ -134,23 +227,75 @@ cancelled, emit) {
|
|
|
134
227
|
if (input.volumeScope && !input.kubernetes) {
|
|
135
228
|
(0, hosted_volumes_1.scopeServiceVolumes)(workTree, input.volumeScope);
|
|
136
229
|
}
|
|
230
|
+
if (input.up) {
|
|
231
|
+
publishEndpoints(workTree, input.up.endpoints, !!input.kubernetes);
|
|
232
|
+
}
|
|
137
233
|
const cli = (0, cli_1.getCli)(workTree, environment);
|
|
138
|
-
if (cli.tasks().length === 0) {
|
|
234
|
+
if (cli.tasks().length === 0 && (!input.up || cli.services().length === 0)) {
|
|
139
235
|
throw new Error(`no tasks found for ${name}`);
|
|
140
236
|
}
|
|
141
|
-
const execution =
|
|
237
|
+
const execution = input.up
|
|
238
|
+
? cli.up({ ...execOptions, workers: cli.defaultWorkers(), daemon: false, wait: 'ready' })
|
|
239
|
+
: cli.exec({ ...execOptions, workers: cli.defaultWorkers() });
|
|
142
240
|
const seen = new Map();
|
|
143
|
-
const
|
|
241
|
+
const failedServices = new Set();
|
|
242
|
+
let ready = false;
|
|
243
|
+
const report = () => {
|
|
244
|
+
reportTasks(emit, execution.state.current, seen, (text) => secrets.redact(text));
|
|
245
|
+
reportServiceFailures(emit, execution.state.current, failedServices, (text) => secrets.redact(text));
|
|
246
|
+
if (input.up && up && !ready) {
|
|
247
|
+
const targets = getEndpointTargets(execution.state.current, input.up, input.kubernetes);
|
|
248
|
+
if (targets) {
|
|
249
|
+
ready = true;
|
|
250
|
+
up(targets);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
144
254
|
const watch = execution.state.on('run-events', report);
|
|
145
255
|
try {
|
|
146
256
|
const result = await execution.start();
|
|
147
257
|
report();
|
|
258
|
+
// on a cluster the services stay in their Deployments once they are ready, until
|
|
259
|
+
// the namespace of the run goes
|
|
260
|
+
if (input.up && input.kubernetes && ready && result.success && !abortCtrl.signal.aborted) {
|
|
261
|
+
if (input.kubernetes.server?.serviceRun) {
|
|
262
|
+
// a run that moved here from another commit: what it made for items it has no more
|
|
263
|
+
const ids = [...Object.values(workTree.tasks), ...Object.values(workTree.services)].map((item) => item.id());
|
|
264
|
+
await (0, remove_stale_items_1.removeStaleItems)((0, kubernetes_instance_1.createKubernetesInstances)(input.kubernetes), input.kubernetes, ids).catch((e) => emit({
|
|
265
|
+
type: 'log',
|
|
266
|
+
task: 'hammerkit',
|
|
267
|
+
stream: 'stderr',
|
|
268
|
+
text: `what the run made at its previous commit could not be removed: ${(0, log_1.getErrorMessage)(e)}`,
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
await new Promise((resolve) => abortCtrl.signal.addEventListener('abort', () => resolve(), { once: true }));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (input.up && ready) {
|
|
275
|
+
if (!abortCtrl.signal.aborted) {
|
|
276
|
+
throw new Error('the services stopped on their own');
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
148
280
|
if (!result.success) {
|
|
149
|
-
|
|
281
|
+
if (cancelled()) {
|
|
282
|
+
throw new Error('the run was cancelled');
|
|
283
|
+
}
|
|
284
|
+
if (input.kubernetes) {
|
|
285
|
+
// what the cluster said, before the server removes what the run made
|
|
286
|
+
await reportClusterState(input.kubernetes, execution.state.current, !!input.up, emit, (text) => secrets.redact(text));
|
|
287
|
+
}
|
|
288
|
+
// a service run names what failed and why: its services have no results of their own
|
|
289
|
+
const failures = input.up ? describeFailures(execution.state.current, (text) => secrets.redact(text)) : [];
|
|
290
|
+
throw new Error(failures.length > 0 ? failures.join('; ').slice(0, runs_1.MAX_TASK_ERROR) : `${name} failed`);
|
|
150
291
|
}
|
|
151
292
|
}
|
|
152
293
|
finally {
|
|
153
294
|
watch.close();
|
|
295
|
+
if (input.up && input.kubernetes?.server?.serviceRun && !ready && service_rollouts_1.serviceRollouts.size > 0) {
|
|
296
|
+
// a move (or a start) whose services did not all get up: the run stays where it was
|
|
297
|
+
await service_rollouts_1.serviceRollouts.undo((0, kubernetes_instance_1.createKubernetesInstances)(input.kubernetes), (text) => emit({ type: 'log', task: 'hammerkit', stream: 'stderr', text }));
|
|
298
|
+
}
|
|
154
299
|
}
|
|
155
300
|
}
|
|
156
301
|
}
|
|
@@ -159,6 +304,82 @@ cancelled, emit) {
|
|
|
159
304
|
}
|
|
160
305
|
}
|
|
161
306
|
exports.runTasks = runTasks;
|
|
307
|
+
// What the cluster said of a run that failed, into its log: the server removes its pods
|
|
308
|
+
// once it ended.
|
|
309
|
+
async function reportClusterState(kubernetes, workTree, ownNamespace, emit, redact) {
|
|
310
|
+
let lines;
|
|
311
|
+
try {
|
|
312
|
+
lines = await (0, cluster_state_1.readClusterState)((0, kubernetes_instance_1.createKubernetesInstances)(kubernetes), kubernetes, workTree, ownNamespace);
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
lines = [`it could not be read: ${(0, log_1.getErrorMessage)(e)}`];
|
|
316
|
+
}
|
|
317
|
+
if (lines.length === 0) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
for (const text of [`what the cluster said in ${kubernetes.namespace}:`, ...lines.map((line) => ` ${line}`)]) {
|
|
321
|
+
emit({ type: 'log', task: 'hammerkit', stream: 'stderr', text: redact(text).slice(0, MAX_LOG_TEXT) });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// A service has no result of its own in the run: why it failed goes into the log, under
|
|
325
|
+
// its name, once.
|
|
326
|
+
function reportServiceFailures(emit, workTree, said, redact) {
|
|
327
|
+
for (const service of (0, plan_work_tasks_1.iterateWorkServices)(workTree)) {
|
|
328
|
+
const state = service.state.current;
|
|
329
|
+
if (state.type === 'error' && !said.has(service.name)) {
|
|
330
|
+
said.add(service.name);
|
|
331
|
+
if (lastError(service) === state.errorMessage) {
|
|
332
|
+
// the runtime said it already
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
emit({
|
|
336
|
+
type: 'log',
|
|
337
|
+
task: service.name,
|
|
338
|
+
stream: 'stderr',
|
|
339
|
+
text: redact(state.errorMessage).slice(0, MAX_LOG_TEXT),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// The last error the runtime of an item wrote to its status.
|
|
345
|
+
function lastError(item) {
|
|
346
|
+
let last;
|
|
347
|
+
for (const message of item.status.read()) {
|
|
348
|
+
if (message.level === 'error') {
|
|
349
|
+
last = message.message;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return last;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* What failed in a run, one line each, for its error: the services and tasks that failed,
|
|
356
|
+
* with their reason. Those that only stopped because another failed are left out.
|
|
357
|
+
*/
|
|
358
|
+
function describeFailures(workTree, redact) {
|
|
359
|
+
const failures = [];
|
|
360
|
+
for (const service of (0, plan_work_tasks_1.iterateWorkServices)(workTree)) {
|
|
361
|
+
const state = service.state.current;
|
|
362
|
+
if (state.type === 'error') {
|
|
363
|
+
failures.push(`${service.name}: ${redact(state.errorMessage)}`);
|
|
364
|
+
}
|
|
365
|
+
else if (state.type === 'end' && state.reason === 'crash') {
|
|
366
|
+
// on Docker a service that does not get ready ends, and its runtime says why
|
|
367
|
+
const why = lastError(service);
|
|
368
|
+
failures.push(`${service.name}: ${why ? redact(why) : 'it stopped on its own'}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
for (const task of (0, plan_work_tasks_1.iterateWorkTasks)(workTree)) {
|
|
372
|
+
const state = task.state.current;
|
|
373
|
+
if (state.type === 'error') {
|
|
374
|
+
failures.push(`${task.name}: ${redact(state.errorMessage)}`);
|
|
375
|
+
}
|
|
376
|
+
else if (state.type === 'crash') {
|
|
377
|
+
failures.push(`${task.name}: exit code ${state.exitCode}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return failures;
|
|
381
|
+
}
|
|
382
|
+
exports.describeFailures = describeFailures;
|
|
162
383
|
// Why a task ran, as a task event carries it: the first causes, masked and cut short.
|
|
163
384
|
function taskCauses(causes, redact) {
|
|
164
385
|
return causes?.length
|
|
@@ -217,9 +438,13 @@ redact) {
|
|
|
217
438
|
// The run is the first line of the standard input. The server keeps it open and writes
|
|
218
439
|
// `cancel` on it to stop the run: that works the same on every platform, where a signal
|
|
219
440
|
// on Windows ends the process without letting it stop its tasks.
|
|
441
|
+
// `onEnd` is told when the input ends before the run does: the server is gone.
|
|
220
442
|
function readInput(stdin, cancel) {
|
|
221
443
|
const lines = (0, readline_1.createInterface)({ input: stdin, crlfDelay: Infinity });
|
|
222
444
|
let first;
|
|
445
|
+
let closing = false;
|
|
446
|
+
let ended = false;
|
|
447
|
+
let onEnd;
|
|
223
448
|
const input = new Promise((resolve) => {
|
|
224
449
|
lines.on('line', (line) => {
|
|
225
450
|
if (first === undefined) {
|
|
@@ -230,10 +455,29 @@ function readInput(stdin, cancel) {
|
|
|
230
455
|
cancel();
|
|
231
456
|
}
|
|
232
457
|
});
|
|
233
|
-
lines.on('close', () =>
|
|
458
|
+
lines.on('close', () => {
|
|
459
|
+
resolve(first ?? '');
|
|
460
|
+
if (!closing) {
|
|
461
|
+
ended = true;
|
|
462
|
+
onEnd?.();
|
|
463
|
+
}
|
|
464
|
+
});
|
|
234
465
|
});
|
|
235
|
-
return {
|
|
466
|
+
return {
|
|
467
|
+
input,
|
|
468
|
+
close: () => {
|
|
469
|
+
closing = true;
|
|
470
|
+
lines.close();
|
|
471
|
+
},
|
|
472
|
+
onEnd: (listener) => {
|
|
473
|
+
onEnd = listener;
|
|
474
|
+
if (ended) {
|
|
475
|
+
listener();
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
};
|
|
236
479
|
}
|
|
480
|
+
exports.readInput = readInput;
|
|
237
481
|
/**
|
|
238
482
|
* The process of one run: reads its input, executes the tasks in the current directory
|
|
239
483
|
* and reports as lines of JSON. It exits 0 when it could say how the run ended, and the
|
|
@@ -260,8 +504,13 @@ async function workerMain(stdin, stdout, processEnvs, cwd, abortCtrl) {
|
|
|
260
504
|
say({ kind: 'result', ok: false, error: `the run could not be read: ${e instanceof Error ? e.message : e}` });
|
|
261
505
|
return;
|
|
262
506
|
}
|
|
507
|
+
if (input.up) {
|
|
508
|
+
// services stay until they are stopped: without the server that would stop them, at
|
|
509
|
+
// their ttl or on a cancel, nothing would
|
|
510
|
+
reader.onEnd(stop);
|
|
511
|
+
}
|
|
263
512
|
try {
|
|
264
|
-
await runTasks(input, cwd, processEnvs, abortCtrl, () => cancelled, (event) => say({ kind: 'event', event }));
|
|
513
|
+
await runTasks(input, cwd, processEnvs, abortCtrl, () => cancelled, (event) => say({ kind: 'event', event }), (targets) => say({ kind: 'up', targets }));
|
|
265
514
|
say({ kind: 'result', ok: true });
|
|
266
515
|
}
|
|
267
516
|
catch (e) {
|