hammerkit 1.27.0-next.2 → 1.27.0-next.20
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 +9 -0
- package/dist/cache/backends/registry-cache-backend.js +2 -2
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +1 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/event-cache.js +20 -2
- package/dist/executer/execute-work-task.js +5 -3
- package/dist/executer/execution-steps.js +5 -2
- package/dist/executer/scheduler/enqueue-next.js +7 -0
- package/dist/executer/scheduler/state-key.js +42 -5
- 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 +207 -65
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +27 -16
- package/dist/kubernetes/claim-layout.js +71 -0
- package/dist/kubernetes/ensure-http-route.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +5 -9
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
- package/dist/kubernetes/ensure-persistent-data.js +30 -20
- package/dist/kubernetes/get-service-ip.js +2 -2
- package/dist/kubernetes/job-secrets.js +33 -19
- package/dist/kubernetes/kept-claims.js +22 -10
- package/dist/kubernetes/network-policy.js +85 -27
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/resources.js +25 -3
- package/dist/kubernetes/restore-kubernetes-data.js +9 -3
- package/dist/kubernetes/sandbox-runner.js +626 -0
- package/dist/kubernetes/secrets.js +7 -11
- package/dist/kubernetes/volumes.js +6 -3
- package/dist/network/egress-proxy.js +45 -6
- 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 +51 -1
- package/dist/planner/utils/append-work-dependencies.js +10 -5
- package/dist/planner/utils/append-work-service.js +1 -0
- package/dist/planner/utils/append-work-task.js +15 -2
- 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/utils/task-arguments.js +3 -0
- 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-docker.js +2 -1
- package/dist/planner/work-runtime-kubernetes.js +97 -48
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +14 -4
- package/dist/remote/annotations.js +13 -1
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +64 -3
- package/dist/remote/remote-commands.js +41 -9
- package/dist/remote/remote-work-commands.js +246 -1
- 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 +5 -1
- package/dist/schema/build-file-container-service-schema.js +2 -0
- package/dist/schema/reference-parser.js +9 -1
- package/dist/schema/work-scope-parser.js +6 -0
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/server/accounts-api.js +3 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/definitions-api.js +4 -2
- package/dist/server/endpoints.js +612 -0
- package/dist/server/executor.js +56 -5
- package/dist/server/kubernetes-backend.js +303 -64
- package/dist/server/kubernetes-sandbox-storage.js +48 -3
- package/dist/server/oauth.js +12 -1
- package/dist/server/plan.js +126 -62
- package/dist/server/policy.js +32 -2
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-plan.js +50 -19
- package/dist/server/run-request.js +64 -3
- package/dist/server/run-worker.js +146 -10
- package/dist/server/runs.js +347 -27
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes-api.js +10 -4
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-command.js +39 -20
- package/dist/server/server-config.js +213 -14
- package/dist/server/server.js +268 -15
- package/dist/server/service-runs.js +82 -0
- package/dist/utils/abort-on-signals.js +46 -6
- package/dist/utils/tar.js +36 -0
- package/package.json +1 -1
|
@@ -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
|
|
@@ -75,24 +82,78 @@ exports.runRequestSchema = zod_1.z
|
|
|
75
82
|
return repo;
|
|
76
83
|
}),
|
|
77
84
|
ref: name,
|
|
85
|
+
// the commit the ref has to be at, such as the one a CI job checked out: a ref that
|
|
86
|
+
// moved since is refused, so the run executes the code that was asked for
|
|
87
|
+
commit: zod_1.z
|
|
88
|
+
.string()
|
|
89
|
+
.regex(/^([0-9a-f]{40}|[0-9a-f]{64})$/, 'expected a full commit hash, 40 or 64 hex digits')
|
|
90
|
+
.optional(),
|
|
78
91
|
})
|
|
79
92
|
.strict(),
|
|
80
93
|
// the name of a tree announced with POST /v1/sources
|
|
81
94
|
zod_1.z.object({ type: zod_1.z.literal('upload'), snapshot: zod_1.z.string().regex(bundle_1.HASH_PATTERN) }).strict(),
|
|
82
95
|
// a sandbox of the caller's, see POST /v1/sandboxes
|
|
83
96
|
zod_1.z
|
|
84
|
-
.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
|
+
})
|
|
85
104
|
.strict(),
|
|
86
105
|
]),
|
|
87
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([]),
|
|
88
127
|
})
|
|
89
128
|
.strict()
|
|
90
|
-
.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, {
|
|
91
130
|
message: 'name at least one task, or filter by labels',
|
|
92
131
|
path: ['tasks'],
|
|
93
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'] })
|
|
94
150
|
.refine((request) => (request.options.args ?? []).length === 0 || request.tasks.length === 1, {
|
|
95
151
|
message: 'arguments go to one task, name exactly one in tasks',
|
|
96
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'],
|
|
97
158
|
});
|
|
98
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.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,8 @@ 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 check_keys_1 = require("../optimizer/check-keys");
|
|
24
26
|
// one log line of a task is cut here, the registry caps how many events a run keeps
|
|
25
27
|
const MAX_LOG_TEXT = 8 * 1024;
|
|
26
28
|
/**
|
|
@@ -59,6 +61,17 @@ exports.workerInputSchema = zod_1.z
|
|
|
59
61
|
.string()
|
|
60
62
|
.regex(/^[a-z]+-[0-9a-f]{24}$/)
|
|
61
63
|
.optional(),
|
|
64
|
+
// a service run: the services it keeps up instead of tasks, and the ports the server
|
|
65
|
+
// reaches as endpoints
|
|
66
|
+
up: zod_1.z
|
|
67
|
+
.object({
|
|
68
|
+
services: zod_1.z.array(zod_1.z.string()).min(1),
|
|
69
|
+
endpoints: zod_1.z.array(zod_1.z.object({ service: zod_1.z.string(), port: zod_1.z.number().int() }).strict()),
|
|
70
|
+
})
|
|
71
|
+
.strict()
|
|
72
|
+
.optional(),
|
|
73
|
+
// the run's results count as checks: report each task's check key before it runs
|
|
74
|
+
check: zod_1.z.boolean().optional(),
|
|
62
75
|
})
|
|
63
76
|
.strict();
|
|
64
77
|
// The ceiling of the run, combined here and not in the server: the lower of two allow lists
|
|
@@ -68,6 +81,60 @@ function getRunNetwork(input) {
|
|
|
68
81
|
return input.sandboxNetwork ? (0, work_network_1.limitNetwork)((0, work_network_1.parseWorkNetwork)(input.sandboxNetwork), runs) : runs;
|
|
69
82
|
}
|
|
70
83
|
exports.getRunNetwork = getRunNetwork;
|
|
84
|
+
/**
|
|
85
|
+
* The ports of the services of a service run as the server publishes them: on a host the
|
|
86
|
+
* ports of endpoints go to a free port of the loopback interface, where only the server
|
|
87
|
+
* reaches them, and no other port goes to the host, whatever the build file says (`up`
|
|
88
|
+
* would publish them on every interface, where two runs collide). On a cluster nothing is
|
|
89
|
+
* published, the server reaches the Service of each.
|
|
90
|
+
*/
|
|
91
|
+
function publishEndpoints(workTree, endpoints, cluster) {
|
|
92
|
+
for (const service of Object.values(workTree.services)) {
|
|
93
|
+
if (!('ports' in service.data)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
service.data.ports = service.data.ports.map((port) => !cluster &&
|
|
97
|
+
endpoints.some((endpoint) => endpoint.service === service.name && endpoint.port === port.containerPort)
|
|
98
|
+
? { containerPort: port.containerPort, hostPort: 0, hostIp: '127.0.0.1' }
|
|
99
|
+
: { containerPort: port.containerPort, hostPort: null });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.publishEndpoints = publishEndpoints;
|
|
103
|
+
// Where the server reaches each endpoint, once every service the run keeps up is ready;
|
|
104
|
+
// null until then.
|
|
105
|
+
function getEndpointTargets(workTree, up, kubernetes) {
|
|
106
|
+
const services = Object.values(workTree.services).filter((service) => up.services.includes(service.name));
|
|
107
|
+
if (services.length < up.services.length || services.some((service) => service.state.current.type !== 'running')) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const targets = [];
|
|
111
|
+
for (const endpoint of up.endpoints) {
|
|
112
|
+
const service = services.find((candidate) => candidate.name === endpoint.service);
|
|
113
|
+
const state = service?.state.current;
|
|
114
|
+
if (!service || state?.type !== 'running') {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
if (kubernetes) {
|
|
118
|
+
targets.push({
|
|
119
|
+
service: endpoint.service,
|
|
120
|
+
port: endpoint.port,
|
|
121
|
+
address: { host: `${(0, resources_1.getResourceName)(service, kubernetes)}.${kubernetes.namespace}.svc`, port: endpoint.port },
|
|
122
|
+
});
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const published = state.published?.find((candidate) => candidate.containerPort === endpoint.port);
|
|
126
|
+
if (!published) {
|
|
127
|
+
throw new Error(`${endpoint.service} is up, but its port ${endpoint.port} was not published`);
|
|
128
|
+
}
|
|
129
|
+
targets.push({
|
|
130
|
+
service: endpoint.service,
|
|
131
|
+
port: endpoint.port,
|
|
132
|
+
address: { host: published.host, port: published.port },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return targets;
|
|
136
|
+
}
|
|
137
|
+
exports.getEndpointTargets = getEndpointTargets;
|
|
71
138
|
/**
|
|
72
139
|
* Executes the tasks of a run with the engine of `hammerkit run`. The caller says which
|
|
73
140
|
* process this is in: it reports what happens through `emit` and stops when `abortCtrl`
|
|
@@ -75,7 +142,9 @@ exports.getRunNetwork = getRunNetwork;
|
|
|
75
142
|
*/
|
|
76
143
|
async function runTasks(input, cwd, processEnvs, abortCtrl,
|
|
77
144
|
// whether the run was stopped from outside; the engine aborts too when a task fails
|
|
78
|
-
cancelled, emit
|
|
145
|
+
cancelled, emit,
|
|
146
|
+
// a service run: its services are ready, at these addresses
|
|
147
|
+
up) {
|
|
79
148
|
const secrets = (0, redact_1.createSecretRegistry)();
|
|
80
149
|
const status = (0, work_item_status_1.statusConsole)((0, empty_writable_1.emptyWritable)(), secrets);
|
|
81
150
|
// the process starts in a directory of the server, the tasks work on the run's files
|
|
@@ -117,15 +186,27 @@ cancelled, emit) {
|
|
|
117
186
|
const options = input.options;
|
|
118
187
|
const execOptions = {
|
|
119
188
|
logMode: 'live',
|
|
120
|
-
|
|
189
|
+
// a check takes a cached result only for the same files, never for the same dates
|
|
190
|
+
cacheDefault: input.check && options.cache === 'modify-date' ? 'checksum' : options.cache ?? 'checksum',
|
|
121
191
|
cacheReadOnly: options.cacheReadOnly ?? false,
|
|
122
192
|
timeout: options.timeout ? (0, units_1.parseDuration)(options.timeout) : null,
|
|
193
|
+
maxTimeout: options.maxTimeout ? (0, units_1.parseDuration)(options.maxTimeout) : null,
|
|
123
194
|
skipDeps: options.skipDeps !== false,
|
|
124
195
|
network: getRunNetwork(input),
|
|
125
196
|
// why a task was not taken from the cache goes into its result
|
|
126
197
|
explain: true,
|
|
127
198
|
};
|
|
128
|
-
|
|
199
|
+
const scopes = (0, run_plan_1.getRunScopes)(input.tasks, options, input.up?.services);
|
|
200
|
+
if (input.check) {
|
|
201
|
+
// the files as they are before any task wrote to them, a second task's too: a first
|
|
202
|
+
// one that formats or generates into src would key it by files no commit holds
|
|
203
|
+
for (const { scope } of scopes) {
|
|
204
|
+
for (const [task, { key }] of Object.entries(await (0, check_keys_1.getCheckKeys)(input.entry, environment, scope))) {
|
|
205
|
+
emit({ type: 'key', task, key });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (const { name, scope } of scopes) {
|
|
129
210
|
const { ctx, scope: parsed } = await (0, schema_parser_1.createParseContext)(input.entry, environment, {
|
|
130
211
|
directory: input.directory,
|
|
131
212
|
});
|
|
@@ -134,17 +215,44 @@ cancelled, emit) {
|
|
|
134
215
|
if (input.volumeScope && !input.kubernetes) {
|
|
135
216
|
(0, hosted_volumes_1.scopeServiceVolumes)(workTree, input.volumeScope);
|
|
136
217
|
}
|
|
218
|
+
if (input.up) {
|
|
219
|
+
publishEndpoints(workTree, input.up.endpoints, !!input.kubernetes);
|
|
220
|
+
}
|
|
137
221
|
const cli = (0, cli_1.getCli)(workTree, environment);
|
|
138
|
-
if (cli.tasks().length === 0) {
|
|
222
|
+
if (cli.tasks().length === 0 && (!input.up || cli.services().length === 0)) {
|
|
139
223
|
throw new Error(`no tasks found for ${name}`);
|
|
140
224
|
}
|
|
141
|
-
const execution =
|
|
225
|
+
const execution = input.up
|
|
226
|
+
? cli.up({ ...execOptions, workers: cli.defaultWorkers(), daemon: false, wait: 'ready' })
|
|
227
|
+
: cli.exec({ ...execOptions, workers: cli.defaultWorkers() });
|
|
142
228
|
const seen = new Map();
|
|
143
|
-
|
|
229
|
+
let ready = false;
|
|
230
|
+
const report = () => {
|
|
231
|
+
reportTasks(emit, execution.state.current, seen, (text) => secrets.redact(text));
|
|
232
|
+
if (input.up && up && !ready) {
|
|
233
|
+
const targets = getEndpointTargets(execution.state.current, input.up, input.kubernetes);
|
|
234
|
+
if (targets) {
|
|
235
|
+
ready = true;
|
|
236
|
+
up(targets);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
144
240
|
const watch = execution.state.on('run-events', report);
|
|
145
241
|
try {
|
|
146
242
|
const result = await execution.start();
|
|
147
243
|
report();
|
|
244
|
+
// on a cluster the services stay in their Deployments once they are ready, until
|
|
245
|
+
// the namespace of the run goes
|
|
246
|
+
if (input.up && input.kubernetes && ready && result.success && !abortCtrl.signal.aborted) {
|
|
247
|
+
await new Promise((resolve) => abortCtrl.signal.addEventListener('abort', () => resolve(), { once: true }));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (input.up && ready) {
|
|
251
|
+
if (!abortCtrl.signal.aborted) {
|
|
252
|
+
throw new Error('the services stopped on their own');
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
148
256
|
if (!result.success) {
|
|
149
257
|
throw new Error(cancelled() ? 'the run was cancelled' : `${name} failed`);
|
|
150
258
|
}
|
|
@@ -217,9 +325,13 @@ redact) {
|
|
|
217
325
|
// The run is the first line of the standard input. The server keeps it open and writes
|
|
218
326
|
// `cancel` on it to stop the run: that works the same on every platform, where a signal
|
|
219
327
|
// on Windows ends the process without letting it stop its tasks.
|
|
328
|
+
// `onEnd` is told when the input ends before the run does: the server is gone.
|
|
220
329
|
function readInput(stdin, cancel) {
|
|
221
330
|
const lines = (0, readline_1.createInterface)({ input: stdin, crlfDelay: Infinity });
|
|
222
331
|
let first;
|
|
332
|
+
let closing = false;
|
|
333
|
+
let ended = false;
|
|
334
|
+
let onEnd;
|
|
223
335
|
const input = new Promise((resolve) => {
|
|
224
336
|
lines.on('line', (line) => {
|
|
225
337
|
if (first === undefined) {
|
|
@@ -230,10 +342,29 @@ function readInput(stdin, cancel) {
|
|
|
230
342
|
cancel();
|
|
231
343
|
}
|
|
232
344
|
});
|
|
233
|
-
lines.on('close', () =>
|
|
345
|
+
lines.on('close', () => {
|
|
346
|
+
resolve(first ?? '');
|
|
347
|
+
if (!closing) {
|
|
348
|
+
ended = true;
|
|
349
|
+
onEnd?.();
|
|
350
|
+
}
|
|
351
|
+
});
|
|
234
352
|
});
|
|
235
|
-
return {
|
|
353
|
+
return {
|
|
354
|
+
input,
|
|
355
|
+
close: () => {
|
|
356
|
+
closing = true;
|
|
357
|
+
lines.close();
|
|
358
|
+
},
|
|
359
|
+
onEnd: (listener) => {
|
|
360
|
+
onEnd = listener;
|
|
361
|
+
if (ended) {
|
|
362
|
+
listener();
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
};
|
|
236
366
|
}
|
|
367
|
+
exports.readInput = readInput;
|
|
237
368
|
/**
|
|
238
369
|
* The process of one run: reads its input, executes the tasks in the current directory
|
|
239
370
|
* and reports as lines of JSON. It exits 0 when it could say how the run ended, and the
|
|
@@ -260,8 +391,13 @@ async function workerMain(stdin, stdout, processEnvs, cwd, abortCtrl) {
|
|
|
260
391
|
say({ kind: 'result', ok: false, error: `the run could not be read: ${e instanceof Error ? e.message : e}` });
|
|
261
392
|
return;
|
|
262
393
|
}
|
|
394
|
+
if (input.up) {
|
|
395
|
+
// services stay until they are stopped: without the server that would stop them, at
|
|
396
|
+
// their ttl or on a cancel, nothing would
|
|
397
|
+
reader.onEnd(stop);
|
|
398
|
+
}
|
|
263
399
|
try {
|
|
264
|
-
await runTasks(input, cwd, processEnvs, abortCtrl, () => cancelled, (event) => say({ kind: 'event', event }));
|
|
400
|
+
await runTasks(input, cwd, processEnvs, abortCtrl, () => cancelled, (event) => say({ kind: 'event', event }), (targets) => say({ kind: 'up', targets }));
|
|
265
401
|
say({ kind: 'result', ok: true });
|
|
266
402
|
}
|
|
267
403
|
catch (e) {
|