hammerkit 1.27.0-next.2 → 1.27.0-next.21
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 +27 -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 +25 -4
- package/dist/executer/reset-work-tree.js +4 -0
- 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 +35 -13
- 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/build-file-include-schema.js +10 -4
- package/dist/schema/reference-parser.js +9 -1
- package/dist/schema/schema-parser.js +36 -4
- package/dist/schema/work-scope-parser.js +33 -10
- 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
package/build.schema.json
CHANGED
|
@@ -289,6 +289,9 @@
|
|
|
289
289
|
],
|
|
290
290
|
"description": "network the task or service reaches besides its needs: open (default), none or { allow: [hosts or host:port] }"
|
|
291
291
|
},
|
|
292
|
+
"args": {
|
|
293
|
+
"type": "boolean"
|
|
294
|
+
},
|
|
292
295
|
"runner": {
|
|
293
296
|
"type": "string",
|
|
294
297
|
"minLength": 1
|
|
@@ -370,6 +373,9 @@
|
|
|
370
373
|
"network": {
|
|
371
374
|
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/network"
|
|
372
375
|
},
|
|
376
|
+
"args": {
|
|
377
|
+
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/args"
|
|
378
|
+
},
|
|
373
379
|
"runner": {
|
|
374
380
|
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/runner"
|
|
375
381
|
},
|
|
@@ -511,6 +517,9 @@
|
|
|
511
517
|
"network": {
|
|
512
518
|
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/network",
|
|
513
519
|
"description": "network the task or service reaches besides its needs: open (default), none or { allow: [hosts or host:port] }"
|
|
520
|
+
},
|
|
521
|
+
"args": {
|
|
522
|
+
"type": "boolean"
|
|
514
523
|
}
|
|
515
524
|
},
|
|
516
525
|
"required": [
|
|
@@ -586,6 +595,24 @@
|
|
|
586
595
|
{
|
|
587
596
|
"type": "string"
|
|
588
597
|
},
|
|
598
|
+
{
|
|
599
|
+
"type": "object",
|
|
600
|
+
"properties": {
|
|
601
|
+
"path": {
|
|
602
|
+
"type": "string",
|
|
603
|
+
"minLength": 1,
|
|
604
|
+
"description": "Build file or directory, relative to the including file"
|
|
605
|
+
},
|
|
606
|
+
"with": {
|
|
607
|
+
"$ref": "#/definitions/hammerkit/properties/envs",
|
|
608
|
+
"description": "Environment values for the current build task\nhttps://hammerkit.dev/docs/build-file/environment-variables"
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
"required": [
|
|
612
|
+
"path"
|
|
613
|
+
],
|
|
614
|
+
"additionalProperties": false
|
|
615
|
+
},
|
|
589
616
|
{
|
|
590
617
|
"type": "object",
|
|
591
618
|
"properties": {
|
|
@@ -5,6 +5,7 @@ const crypto_1 = require("crypto");
|
|
|
5
5
|
const os_1 = require("os");
|
|
6
6
|
const path_1 = require("path");
|
|
7
7
|
const tar_1 = require("tar");
|
|
8
|
+
const tar_2 = require("../../utils/tar");
|
|
8
9
|
const registry_reference_1 = require("./registry/registry-reference");
|
|
9
10
|
const registry_client_1 = require("./registry/registry-client");
|
|
10
11
|
const MAX_TAG_LENGTH = 128;
|
|
@@ -74,8 +75,7 @@ function createRegistryCacheBackend(spec) {
|
|
|
74
75
|
const layerFile = (0, path_1.join)(dir, 'layer.tar');
|
|
75
76
|
const files = (await environment.file.listFiles(from)).sort();
|
|
76
77
|
// portable + mtime-free so identical entries produce identical digests
|
|
77
|
-
|
|
78
|
-
await (0, tar_1.create)({ file: layerFile, cwd: from, portable: true, noMtime: true }, files);
|
|
78
|
+
await (0, tar_2.packArchive)({ file: layerFile, cwd: from, portable: true, noMtime: true }, files);
|
|
79
79
|
const layer = await (0, registry_client_1.digestFile)(layerFile);
|
|
80
80
|
const config = Buffer.from(JSON.stringify({
|
|
81
81
|
architecture: 'amd64',
|
package/dist/cache/explain.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.explainWorkTree = exports.explainTask = exports.describeCause = void 0;
|
|
4
|
+
const work_task_1 = require("../planner/work-task");
|
|
4
5
|
const state_key_1 = require("../executer/scheduler/state-key");
|
|
5
6
|
const work_task_cache_description_1 = require("../optimizer/work-task-cache-description");
|
|
6
7
|
const get_cache_directory_1 = require("../optimizer/get-cache-directory");
|
|
@@ -157,6 +158,22 @@ async function explainTask(item, defaultCacheMethod, environment, memo = new Map
|
|
|
157
158
|
visiting.delete(item.id());
|
|
158
159
|
return explanation;
|
|
159
160
|
}
|
|
161
|
+
// Mirrors executeWorkTask: an aggregate is never looked up itself, and is reported
|
|
162
|
+
// cached when all its dependencies were. What changed is in its dependencies.
|
|
163
|
+
if ((0, work_task_1.isAggregateTask)(item.data)) {
|
|
164
|
+
const causes = [];
|
|
165
|
+
for (const dep of item.deps) {
|
|
166
|
+
const depExplanation = await explainTask(dep, defaultCacheMethod, environment, memo, visiting);
|
|
167
|
+
if (depExplanation.status !== 'hit') {
|
|
168
|
+
causes.push({ kind: 'dependency-changed', identifier: dep.name });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const status = causes.length === 0 ? 'hit' : 'miss';
|
|
172
|
+
const explanation = { taskId: item.id(), taskName: item.name, status, causes };
|
|
173
|
+
memo.set(item.id(), explanation);
|
|
174
|
+
visiting.delete(item.id());
|
|
175
|
+
return explanation;
|
|
176
|
+
}
|
|
160
177
|
const runtimeStateKey = await safeCurrentStateKey(item, environment);
|
|
161
178
|
const hasEntry = await safeHas(item, stateKey, environment);
|
|
162
179
|
const predictedHit = runtimeStateKey === stateKey || hasEntry;
|
package/dist/cli.js
CHANGED
|
@@ -79,6 +79,7 @@ class Cli {
|
|
|
79
79
|
explain: options?.explain ?? false,
|
|
80
80
|
cacheReadOnly: options?.cacheReadOnly ?? false,
|
|
81
81
|
timeout: options?.timeout ?? null,
|
|
82
|
+
maxTimeout: options?.maxTimeout ?? null,
|
|
82
83
|
skipDeps: options?.skipDeps ?? true,
|
|
83
84
|
wait: options?.wait ?? 'ready',
|
|
84
85
|
resourceCheck: options?.resourceCheck ?? true,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.removeOrphanedNetworks = exports.removeItemNetworks = exports.getNetworkHostConfig = exports.removeItemNetwork = exports.prepareItemNetwork = exports.checkNetworkLimitSupport = exports.getProxyNetworkName = exports.getRunNetworkName = exports.getItemNetworkName = void 0;
|
|
3
|
+
exports.removeOrphanedNetworks = exports.removeItemNetworks = exports.getNetworkHostConfig = exports.getPortBinding = exports.removeItemNetwork = exports.prepareItemNetwork = exports.checkNetworkLimitSupport = exports.getProxyNetworkName = exports.getRunNetworkName = exports.getItemNetworkName = void 0;
|
|
4
4
|
const readline_1 = require("readline");
|
|
5
5
|
const stream_1 = require("stream");
|
|
6
6
|
const work_service_1 = require("../planner/work-service");
|
|
@@ -258,7 +258,7 @@ options) {
|
|
|
258
258
|
await removeItemNetwork(docker, name);
|
|
259
259
|
throw e;
|
|
260
260
|
}
|
|
261
|
-
const publish = (options.publish ?? []).filter((port) =>
|
|
261
|
+
const publish = (options.publish ?? []).filter((port) => port.hostPort !== null);
|
|
262
262
|
return {
|
|
263
263
|
options: {
|
|
264
264
|
mode: name,
|
|
@@ -272,8 +272,9 @@ options) {
|
|
|
272
272
|
await docker.getNetwork(getProxyNetworkName(name)).connect({ Container: container.id });
|
|
273
273
|
}
|
|
274
274
|
if (publish.length > 0) {
|
|
275
|
-
await startPortRelay(docker, item, environment, name, container, publish, options);
|
|
275
|
+
return await startPortRelay(docker, item, environment, name, container, publish, options);
|
|
276
276
|
}
|
|
277
|
+
return undefined;
|
|
277
278
|
}
|
|
278
279
|
catch (e) {
|
|
279
280
|
await removeItemNetwork(docker, name);
|
|
@@ -374,12 +375,13 @@ async function startPortRelay(docker, item, environment, networkName, service, p
|
|
|
374
375
|
ExposedPorts: Object.fromEntries(ports.map((port) => [`${port.containerPort}/tcp`, {}])),
|
|
375
376
|
HostConfig: {
|
|
376
377
|
...HELPER_HOST_CONFIG,
|
|
377
|
-
PortBindings: Object.fromEntries(ports.map((port) => [`${port.containerPort}/tcp`, [
|
|
378
|
+
PortBindings: Object.fromEntries(ports.map((port) => [`${port.containerPort}/tcp`, [getPortBinding(port)]])),
|
|
378
379
|
},
|
|
379
380
|
});
|
|
380
381
|
await docker.getNetwork(networkName).connect({ Container: container.id });
|
|
381
382
|
await container.start();
|
|
382
|
-
item.status.write('debug', `ports ${ports.map((port) => port.hostPort).join(', ')} published by a relay`);
|
|
383
|
+
item.status.write('debug', `ports ${ports.map((port) => port.hostPort || 'any').join(', ')} published by a relay`);
|
|
384
|
+
return container;
|
|
383
385
|
}
|
|
384
386
|
// The network, its proxy network, its proxy and relay, and every endpoint on
|
|
385
387
|
// them: what an earlier run left behind, or what the item used.
|
|
@@ -429,6 +431,12 @@ async function removeNetwork(docker, name) {
|
|
|
429
431
|
}
|
|
430
432
|
}
|
|
431
433
|
}
|
|
434
|
+
// A port of the host for a port of a container: a free one for 0, on one address of it
|
|
435
|
+
// when the port names one.
|
|
436
|
+
function getPortBinding(port) {
|
|
437
|
+
return { HostPort: port.hostPort ? `${port.hostPort}` : '', ...(port.hostIp ? { HostIp: port.hostIp } : {}) };
|
|
438
|
+
}
|
|
439
|
+
exports.getPortBinding = getPortBinding;
|
|
432
440
|
function getNetworkHostConfig(network) {
|
|
433
441
|
return {
|
|
434
442
|
...(network.mode ? { NetworkMode: network.mode } : {}),
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.withDeadline = void 0;
|
|
3
|
+
exports.withDeadline = exports.getTaskTimeout = void 0;
|
|
4
4
|
const abort_event_1 = require("../utils/abort-event");
|
|
5
|
+
// How long a task may run: its own `timeout`, else the run's default (`--timeout`), and
|
|
6
|
+
// never longer than the run's cap (`--max-timeout`), which also covers tasks with a
|
|
7
|
+
// longer timeout of their own. A caller can shorten a task this way, never extend it.
|
|
8
|
+
function getTaskTimeout(own, run) {
|
|
9
|
+
const timeout = own ?? run.timeout;
|
|
10
|
+
if (run.maxTimeout === null) {
|
|
11
|
+
return timeout;
|
|
12
|
+
}
|
|
13
|
+
return timeout === null ? run.maxTimeout : Math.min(timeout, run.maxTimeout);
|
|
14
|
+
}
|
|
15
|
+
exports.getTaskTimeout = getTaskTimeout;
|
|
16
|
+
// the longest delay of setTimeout: Node fires a longer one after 1 ms
|
|
17
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
5
18
|
// An abort signal that follows `parent` and additionally fires after `timeout`
|
|
6
|
-
// ms, so a task deadline reuses the runtimes' existing abort/cleanup path.
|
|
19
|
+
// ms, so a task deadline reuses the runtimes' existing abort/cleanup path. A
|
|
20
|
+
// timeout beyond what a timer can wait (about 24.8 days) sets no deadline.
|
|
7
21
|
function withDeadline(parent, timeout) {
|
|
8
22
|
const controller = new AbortController();
|
|
9
23
|
let expired = false;
|
|
10
24
|
const parentListener = (0, abort_event_1.listenOnAbort)(parent, () => controller.abort());
|
|
11
|
-
const timer = timeout === null
|
|
25
|
+
const timer = timeout === null || timeout > MAX_TIMER_MS
|
|
12
26
|
? null
|
|
13
27
|
: setTimeout(() => {
|
|
14
28
|
expired = true;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.dockerService = exports.buildServiceCreateOptions = void 0;
|
|
3
|
+
exports.dockerService = exports.buildServiceCreateOptions = exports.readPublishedPorts = void 0;
|
|
4
4
|
const work_service_1 = require("../planner/work-service");
|
|
5
5
|
const run_labels_1 = require("../docker/run-labels");
|
|
6
6
|
const service_hostname_1 = require("../planner/utils/service-hostname");
|
|
@@ -22,6 +22,28 @@ const work_healthcheck_1 = require("../planner/work-healthcheck");
|
|
|
22
22
|
const docker_resources_1 = require("./docker-resources");
|
|
23
23
|
const egress_proxy_1 = require("../network/egress-proxy");
|
|
24
24
|
const HEALTHCHECK_INTERVAL_MS = 1000;
|
|
25
|
+
// Where docker published the ports of a container (or of its relay), for those that asked
|
|
26
|
+
// for a free one.
|
|
27
|
+
async function readPublishedPorts(container, ports) {
|
|
28
|
+
const wanted = ports.filter((port) => port.hostPort === 0);
|
|
29
|
+
if (wanted.length === 0) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const bound = (await container.inspect()).NetworkSettings.Ports ?? {};
|
|
33
|
+
return wanted.flatMap((port) => {
|
|
34
|
+
const binding = bound[`${port.containerPort}/tcp`]?.find((candidate) => candidate.HostPort);
|
|
35
|
+
return binding
|
|
36
|
+
? [
|
|
37
|
+
{
|
|
38
|
+
containerPort: port.containerPort,
|
|
39
|
+
host: port.hostIp ?? (binding.HostIp && binding.HostIp !== '0.0.0.0' ? binding.HostIp : '127.0.0.1'),
|
|
40
|
+
port: Number(binding.HostPort),
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
: [];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
exports.readPublishedPorts = readPublishedPorts;
|
|
25
47
|
function buildServiceCreateOptions(item, options, network, secrets, resources = {}) {
|
|
26
48
|
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(item.data.envs);
|
|
27
49
|
const command = (0, work_service_1.getServiceCommand)(item.data);
|
|
@@ -58,12 +80,9 @@ function buildServiceCreateOptions(item, options, network, secrets, resources =
|
|
|
58
80
|
// collide with another run on the same host. A limited service's are
|
|
59
81
|
// published by its relay.
|
|
60
82
|
PortBindings: options.publishPorts && !network.relayPorts
|
|
61
|
-
? item.data.ports
|
|
62
|
-
.filter((p) =>
|
|
63
|
-
.
|
|
64
|
-
map[`${port.containerPort}/tcp`] = [{ HostPort: `${port.hostPort}` }];
|
|
65
|
-
return map;
|
|
66
|
-
}, {})
|
|
83
|
+
? Object.fromEntries(item.data.ports
|
|
84
|
+
.filter((p) => p.hostPort !== null)
|
|
85
|
+
.map((port) => [`${port.containerPort}/tcp`, [(0, item_network_1.getPortBinding)(port)]]))
|
|
67
86
|
: {},
|
|
68
87
|
...resources,
|
|
69
88
|
},
|
|
@@ -90,16 +109,18 @@ async function dockerService(docker, item, options, environment) {
|
|
|
90
109
|
const resources = await (0, docker_resources_1.resolveDockerResources)(docker, item.data.resources, item.status);
|
|
91
110
|
await (0, execution_steps_1.refuseHostLinks)(item, environment);
|
|
92
111
|
container = await docker.createContainer(buildServiceCreateOptions(item, options, network.options, secrets, resources));
|
|
93
|
-
await network.connect(container);
|
|
112
|
+
const relay = await network.connect(container);
|
|
94
113
|
const stream = await container.attach({ stream: true, stdout: true, stderr: true });
|
|
95
114
|
(0, stream_1.logStream)(item.status, stream);
|
|
96
115
|
await container.start();
|
|
116
|
+
const published = options.publishPorts ? await readPublishedPorts(relay ?? container, item.data.ports) : [];
|
|
97
117
|
if (!item.data.healthcheck || !options.waitForReady) {
|
|
98
118
|
options.state.set({
|
|
99
119
|
type: 'running',
|
|
100
120
|
dns: { containerId: container.id },
|
|
101
121
|
stateKey: options.stateKey,
|
|
102
122
|
remote: null,
|
|
123
|
+
...(published.length > 0 ? { published } : {}),
|
|
103
124
|
});
|
|
104
125
|
}
|
|
105
126
|
else {
|
|
@@ -125,6 +146,7 @@ async function dockerService(docker, item, options, environment) {
|
|
|
125
146
|
dns: { containerId: container.id },
|
|
126
147
|
stateKey: options.stateKey,
|
|
127
148
|
remote: null,
|
|
149
|
+
...(published.length > 0 ? { published } : {}),
|
|
128
150
|
});
|
|
129
151
|
}
|
|
130
152
|
}
|
|
@@ -8,6 +8,8 @@ const execute_docker_1 = require("./execute-docker");
|
|
|
8
8
|
const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
|
|
9
9
|
const get_cache_directory_1 = require("../optimizer/get-cache-directory");
|
|
10
10
|
const path_1 = require("path");
|
|
11
|
+
const crypto_1 = require("crypto");
|
|
12
|
+
const claim_layout_1 = require("../kubernetes/claim-layout");
|
|
11
13
|
const move_files_1 = require("../file/move-files");
|
|
12
14
|
const work_instance_id_1 = require("../planner/work-instance-id");
|
|
13
15
|
async function restoreCache(environment, path, workTree) {
|
|
@@ -43,8 +45,24 @@ function* getArchivePaths(task, path) {
|
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
else {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
// `a-b` and `a/b` flatten alike: of those, the least deep (then the first in sorted
|
|
49
|
+
// order) keeps the name, the others' names carry a hash of their relative path, so a
|
|
50
|
+
// store and a restore in another directory or order find the same archive
|
|
51
|
+
const generates = task.generates
|
|
52
|
+
.filter((g) => !g.inherited)
|
|
53
|
+
.map((generate) => ({ generate, rel: (0, path_1.relative)(task.cwd, generate.path).split(path_1.sep).join(path_1.posix.sep) }));
|
|
54
|
+
const holders = new Map();
|
|
55
|
+
for (const { rel } of [...generates].sort((a, b) => (0, claim_layout_1.byDepth)(a.rel, b.rel))) {
|
|
56
|
+
const id = rel.split(path_1.posix.sep).join('-');
|
|
57
|
+
if (!holders.has(id)) {
|
|
58
|
+
holders.set(id, rel);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const { generate, rel } of generates) {
|
|
62
|
+
let generateId = rel.split(path_1.posix.sep).join('-');
|
|
63
|
+
if (holders.get(generateId) !== rel) {
|
|
64
|
+
generateId += '-' + (0, crypto_1.createHash)('sha256').update(rel).digest('hex').slice(0, 12);
|
|
65
|
+
}
|
|
48
66
|
yield {
|
|
49
67
|
filename: (0, path_1.join)(path, generateId + '-generates.tgz'),
|
|
50
68
|
path: generate.path,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.executeWorkTask = void 0;
|
|
4
|
+
const work_task_1 = require("../planner/work-task");
|
|
4
5
|
const states_1 = require("./states");
|
|
5
6
|
const abort_1 = require("./abort");
|
|
6
7
|
const log_1 = require("../log");
|
|
@@ -12,7 +13,8 @@ const archive_task_entry_1 = require("./archive-task-entry");
|
|
|
12
13
|
const units_1 = require("../utils/units");
|
|
13
14
|
async function pushToBackend(work, environment, cacheState, options) {
|
|
14
15
|
const { resolved, stateKey, provable } = cacheState;
|
|
15
|
-
|
|
16
|
+
// an aggregate has nothing to store, see checkCacheState
|
|
17
|
+
if (resolved.method === 'none' || !provable || (0, work_task_1.isAggregateTask)(work.data)) {
|
|
16
18
|
return;
|
|
17
19
|
}
|
|
18
20
|
if (options.cacheReadOnly) {
|
|
@@ -29,7 +31,7 @@ async function pushToBackend(work, environment, cacheState, options) {
|
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
// A task without cmds only groups its dependencies (`ci: { deps: [test, build] }`).
|
|
32
|
-
//
|
|
34
|
+
// It is never a cache hit itself (see checkCacheState), so its dependencies are
|
|
33
35
|
// always checked and their outputs restored. When every one of them was a hit,
|
|
34
36
|
// nothing ran: report the group as cached too. Reporting only, the cache
|
|
35
37
|
// decision is untouched.
|
|
@@ -94,7 +96,7 @@ dependents = null) {
|
|
|
94
96
|
stateKey: cacheState.stateKey,
|
|
95
97
|
started,
|
|
96
98
|
});
|
|
97
|
-
const timeout = work.data.timeout
|
|
99
|
+
const timeout = (0, deadline_1.getTaskTimeout)(work.data.timeout, options);
|
|
98
100
|
const deadline = (0, deadline_1.withDeadline)(abort, timeout);
|
|
99
101
|
try {
|
|
100
102
|
await work.runtime.execute(environment, {
|
|
@@ -9,6 +9,7 @@ const work_task_1 = require("../planner/work-task");
|
|
|
9
9
|
const work_service_1 = require("../planner/work-service");
|
|
10
10
|
const get_container_binds_1 = require("./get-container-binds");
|
|
11
11
|
const assert_item_paths_1 = require("../planner/utils/assert-item-paths");
|
|
12
|
+
const log_1 = require("../log");
|
|
12
13
|
// The files a server run works on: its checkout, or a sandbox. Somebody else wrote them.
|
|
13
14
|
function hostedRoot(environment) {
|
|
14
15
|
return environment.hosted ? environment.cwd : undefined;
|
|
@@ -66,10 +67,27 @@ async function prepareMounts(item, environment) {
|
|
|
66
67
|
continue;
|
|
67
68
|
}
|
|
68
69
|
if ((0, path_1.extname)(mount.localPath)) {
|
|
69
|
-
|
|
70
|
+
try {
|
|
71
|
+
await environment.file.writeFile(mount.localPath, '');
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
// Docker would make a directory there
|
|
75
|
+
throw new Error(`mount ${mount.localPath} does not exist and can't be created (${(0, log_1.getErrorMessage)(e)})`);
|
|
76
|
+
}
|
|
70
77
|
}
|
|
71
78
|
else {
|
|
72
|
-
|
|
79
|
+
try {
|
|
80
|
+
await environment.file.createDirectory(mount.localPath);
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
// on a server a root-owned directory in the run's files could break its cleanup
|
|
84
|
+
if (environment.hosted) {
|
|
85
|
+
throw e;
|
|
86
|
+
}
|
|
87
|
+
// hammerkit may run in a container whose file system is not the one of the Docker
|
|
88
|
+
// host: Docker creates the missing directory of a bind on its own
|
|
89
|
+
item.status.write('warn', `can't create ${mount.localPath} (${(0, log_1.getErrorMessage)(e)}), Docker creates it on its host as root`);
|
|
90
|
+
}
|
|
73
91
|
}
|
|
74
92
|
}
|
|
75
93
|
if ((0, work_task_1.isContainerWorkTask)(item.data)) {
|
|
@@ -89,8 +107,11 @@ exports.prepareMounts = prepareMounts;
|
|
|
89
107
|
async function setUserPermissions(item, container, environment) {
|
|
90
108
|
if (item.data.user) {
|
|
91
109
|
// On a server only the volumes: a chown as the container's root on a path of the host
|
|
92
|
-
// would follow a link a task put there after the binds were checked
|
|
93
|
-
|
|
110
|
+
// would follow a link a task put there after the binds were checked. Never a mount: it
|
|
111
|
+
// is a path of the host the build file names, such as /var/run/docker.sock, whose owner
|
|
112
|
+
// is not hammerkit's to change
|
|
113
|
+
const mounted = new Set(item.data.mounts.map((mount) => mount.containerPath));
|
|
114
|
+
const binds = (0, get_container_binds_1.getContainerBinds)(item).filter((bind) => !mounted.has(bind.containerPath) && (!environment.hosted || !(0, path_1.isAbsolute)(bind.localPath)));
|
|
94
115
|
if (!environment.hosted) {
|
|
95
116
|
await (0, set_user_permission_1.setUserPermission)(item.data.cwd, item.status, environment, container, item.data.user);
|
|
96
117
|
}
|
|
@@ -5,6 +5,10 @@ const state_1 = require("./state");
|
|
|
5
5
|
function resetWorkTree(workTree, type) {
|
|
6
6
|
return {
|
|
7
7
|
tasks: Object.entries(workTree.tasks).reduce((tasks, [key, task]) => {
|
|
8
|
+
// down only stops services: a task it selects names the services it needs
|
|
9
|
+
if (type === 'down') {
|
|
10
|
+
return tasks;
|
|
11
|
+
}
|
|
8
12
|
task.state = new state_1.State({
|
|
9
13
|
type: 'pending',
|
|
10
14
|
stateKey: null,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.checkCacheState = exports.UNCACHED_STATE_KEY = void 0;
|
|
4
4
|
const work_item_1 = require("../../planner/work-item");
|
|
5
|
+
const work_task_1 = require("../../planner/work-task");
|
|
5
6
|
const get_cache_directory_1 = require("../../optimizer/get-cache-directory");
|
|
6
7
|
const cache_metadata_1 = require("../cache-metadata");
|
|
7
8
|
const work_task_cache_description_1 = require("../../optimizer/work-task-cache-description");
|
|
@@ -35,6 +36,12 @@ options = {}) {
|
|
|
35
36
|
item.status.write('debug', `${item.name} always runs, it or one of its dependencies has no src files`);
|
|
36
37
|
return { cached: false, stateKey, resolved, provable };
|
|
37
38
|
}
|
|
39
|
+
// An aggregate runs nothing, so running it costs nothing, and it is never a hit itself:
|
|
40
|
+
// asked for, it still has its dependencies checked and their outputs restored. Its key
|
|
41
|
+
// counts for what depends on it, and it is reported cached when they all were.
|
|
42
|
+
if ((0, work_item_1.isWorkTaskItem)(item) && (0, work_task_1.isAggregateTask)(item.data)) {
|
|
43
|
+
return { cached: false, stateKey, resolved, provable };
|
|
44
|
+
}
|
|
38
45
|
if ((0, work_item_1.isWorkTaskItem)(item)) {
|
|
39
46
|
const runtimeStateKey = await item.runtime.currentStateKey(environment);
|
|
40
47
|
if (runtimeStateKey === stateKey) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.computeStateKey = exports.combineStateKeys = exports.resolveEffective = void 0;
|
|
4
|
-
const crypto_1 = require("crypto");
|
|
3
|
+
exports.computeStateKey = exports.combineStateKeys = exports.isOwnProvable = exports.resolveEffective = void 0;
|
|
5
4
|
const get_work_cache_stats_1 = require("../../optimizer/get-work-cache-stats");
|
|
5
|
+
const work_task_1 = require("../../planner/work-task");
|
|
6
6
|
// The state-key engine is shared by the scheduler's hit/miss decision
|
|
7
7
|
// (`checkCacheState`) and the read-only cache-explain engine, so a prediction
|
|
8
8
|
// can never diverge from the decision it predicts — there is one implementation.
|
|
@@ -21,9 +21,14 @@ exports.resolveEffective = resolveEffective;
|
|
|
21
21
|
// whose `src` entries all match no file (a typo, a moved path). Services are not
|
|
22
22
|
// affected.
|
|
23
23
|
function isOwnProvable(item, unmatched) {
|
|
24
|
+
if ((0, work_task_1.isAggregateTask)(item.data)) {
|
|
25
|
+
// it runs nothing and reads nothing: what its dependencies prove is all there is
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
24
28
|
// unmatched ⊆ declared, so this is also false when nothing is declared
|
|
25
29
|
return unmatched.length < item.data.src.filter((src) => !src.inherited).length;
|
|
26
30
|
}
|
|
31
|
+
exports.isOwnProvable = isOwnProvable;
|
|
27
32
|
// Fold a task's own state key together with the state keys of its dependencies.
|
|
28
33
|
// A change anywhere in the dependency subtree must change the resulting key, so
|
|
29
34
|
// that a downstream task is invalidated when an upstream source changes — even
|
|
@@ -34,9 +39,7 @@ function combineStateKeys(ownKey, depKeys) {
|
|
|
34
39
|
}
|
|
35
40
|
// Sort so the key is independent of dependency declaration order.
|
|
36
41
|
const sorted = [...depKeys].sort();
|
|
37
|
-
return (0,
|
|
38
|
-
.update([ownKey, ...sorted].join(','))
|
|
39
|
-
.digest('hex');
|
|
42
|
+
return (0, get_work_cache_stats_1.stateKeyDigest)([ownKey, ...sorted].join(','));
|
|
40
43
|
}
|
|
41
44
|
exports.combineStateKeys = combineStateKeys;
|
|
42
45
|
// `none` only disables the cache *check* for a task; its sources still influence
|
|
@@ -72,7 +75,41 @@ async function computeStateKey(item, defaultCacheMethod, environment, memo = new
|
|
|
72
75
|
depKeys.push(effective.stateKey);
|
|
73
76
|
provable = provable && effective.provable;
|
|
74
77
|
}
|
|
78
|
+
// what a task that needs a service passed against: the sources of the service and of the
|
|
79
|
+
// tasks it depends on (its definition is in the task id, getWorkServiceCacheIdentity)
|
|
80
|
+
for (const need of item.needs) {
|
|
81
|
+
const effective = await resolveNeedStateKey(need.service, defaultCacheMethod, environment, memo);
|
|
82
|
+
depKeys.push(`${need.name}=${effective.stateKey}`);
|
|
83
|
+
}
|
|
75
84
|
return { stateKey: combineStateKeys(ownKey, depKeys), stats, resolved, provable, unmatched };
|
|
76
85
|
}
|
|
77
86
|
exports.computeStateKey = computeStateKey;
|
|
87
|
+
// A needed service's sources folded with the state keys of its dependencies and needs. It
|
|
88
|
+
// does not make the task that needs it unprovable: a dependency without `src` (resetting the
|
|
89
|
+
// service's data, say) is skipped with the service when the task is a cache hit.
|
|
90
|
+
async function resolveNeedStateKey(service, defaultCacheMethod, environment, memo) {
|
|
91
|
+
const memoKey = `need\0${service.name}`;
|
|
92
|
+
const existing = memo.get(memoKey);
|
|
93
|
+
if (existing !== undefined) {
|
|
94
|
+
return existing;
|
|
95
|
+
}
|
|
96
|
+
// services that need each other in a loop fail to plan (checkForLoop): this only has to end
|
|
97
|
+
memo.set(memoKey, { stateKey: 'loop', provable: true });
|
|
98
|
+
const resolved = resolveEffective(service, defaultCacheMethod);
|
|
99
|
+
const { stats } = await (0, get_work_cache_stats_1.getWorkItemCacheStats)(service, environment);
|
|
100
|
+
const keys = [];
|
|
101
|
+
for (const dep of service.deps) {
|
|
102
|
+
keys.push((await resolveEffectiveStateKey(dep, defaultCacheMethod, environment, memo)).stateKey);
|
|
103
|
+
}
|
|
104
|
+
for (const need of service.needs) {
|
|
105
|
+
const effective = await resolveNeedStateKey(need.service, defaultCacheMethod, environment, memo);
|
|
106
|
+
keys.push(`${need.name}=${effective.stateKey}`);
|
|
107
|
+
}
|
|
108
|
+
const effective = {
|
|
109
|
+
stateKey: combineStateKeys((0, get_work_cache_stats_1.getStateKey)(stats, concreteMethod(resolved, defaultCacheMethod)), keys),
|
|
110
|
+
provable: true,
|
|
111
|
+
};
|
|
112
|
+
memo.set(memoKey, effective);
|
|
113
|
+
return effective;
|
|
114
|
+
}
|
|
78
115
|
//# sourceMappingURL=state-key.js.map
|
package/dist/harness/connect.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getHarnessEnvironment = exports.connectSandbox = exports.harnessOptionsFromEnv = exports.DEFAULT_SHELL_TASK = void 0;
|
|
3
|
+
exports.getHarnessEnvironment = exports.connectSandbox = exports.connectServer = exports.hasShell = exports.harnessOptionsFromEnv = exports.DEFAULT_SHELL_TASK = void 0;
|
|
4
4
|
const get_file_context_1 = require("../file/get-file-context");
|
|
5
5
|
const log_1 = require("../log");
|
|
6
6
|
const work_item_status_1 = require("../planner/work-item-status");
|
|
@@ -20,11 +20,21 @@ function harnessOptionsFromEnv(env) {
|
|
|
20
20
|
repo: value('HAMMERKIT_REPO'),
|
|
21
21
|
ref: value('HAMMERKIT_REF'),
|
|
22
22
|
build: value('HAMMERKIT_BUILD'),
|
|
23
|
-
shell
|
|
23
|
+
// set but empty is no shell, unset the default one
|
|
24
|
+
shell: env.HAMMERKIT_SHELL !== undefined ? env.HAMMERKIT_SHELL.trim() : undefined,
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
exports.harnessOptionsFromEnv = harnessOptionsFromEnv;
|
|
27
|
-
|
|
28
|
+
// whether the clients offer a shell tool
|
|
29
|
+
function hasShell(options) {
|
|
30
|
+
return options.shell !== '';
|
|
31
|
+
}
|
|
32
|
+
exports.hasShell = hasShell;
|
|
33
|
+
/**
|
|
34
|
+
* The registered server a harness works on and the published version it runs, checked:
|
|
35
|
+
* what the clients need to list the tasks of the version before they take a sandbox.
|
|
36
|
+
*/
|
|
37
|
+
async function connectServer(environment, options) {
|
|
28
38
|
const config = await (0, host_config_1.readHostConfig)(environment);
|
|
29
39
|
const name = options.server ?? config.run.on;
|
|
30
40
|
if (!name || !(0, host_config_1.hasServer)(config.servers, name)) {
|
|
@@ -39,8 +49,12 @@ async function connectSandbox(environment, options, log = () => undefined) {
|
|
|
39
49
|
if (!bundle_1.HASH_PATTERN.test(options.build)) {
|
|
40
50
|
throw new Error(`${options.build} is not the hash of a published version (sha256:...), name it as hammerkit remote publish printed it (HAMMERKIT_BUILD or --build)`);
|
|
41
51
|
}
|
|
42
|
-
const server = config
|
|
43
|
-
|
|
52
|
+
const server = (0, host_config_1.getRegisteredServer)(config, name);
|
|
53
|
+
return { name, connection: { url: server.url, ...(0, token_source_1.getTokenSource)(environment, name, server) }, build: options.build };
|
|
54
|
+
}
|
|
55
|
+
exports.connectServer = connectServer;
|
|
56
|
+
async function connectSandbox(environment, options, log = () => undefined) {
|
|
57
|
+
const { name, connection, build } = await connectServer(environment, options);
|
|
44
58
|
let sandbox;
|
|
45
59
|
if (options.sandbox) {
|
|
46
60
|
sandbox = await sandbox_client_1.SandboxClient.get(connection, options.sandbox);
|
|
@@ -59,7 +73,7 @@ async function connectSandbox(environment, options, log = () => undefined) {
|
|
|
59
73
|
else {
|
|
60
74
|
throw new Error('name a sandbox (HAMMERKIT_SANDBOX or --sandbox), or a repository and ref to create one from (HAMMERKIT_REPO and HAMMERKIT_REF, or --repo and --ref)');
|
|
61
75
|
}
|
|
62
|
-
return new sandbox_client_1.SandboxClient(connection, sandbox, { hash:
|
|
76
|
+
return new sandbox_client_1.SandboxClient(connection, sandbox, { hash: build, shell: options.shell ?? exports.DEFAULT_SHELL_TASK });
|
|
63
77
|
}
|
|
64
78
|
exports.connectSandbox = connectSandbox;
|
|
65
79
|
// A client that starts with a repository and ref takes a sandbox of the caller's of the same
|
|
@@ -6,6 +6,15 @@ const log_1 = require("../log");
|
|
|
6
6
|
const version_1 = require("../version");
|
|
7
7
|
const connect_1 = require("./connect");
|
|
8
8
|
const mcp_1 = require("./mcp");
|
|
9
|
+
const task_tools_1 = require("./task-tools");
|
|
10
|
+
// the tasks run_task lists; a server that does not answer leaves them out, the tools still work
|
|
11
|
+
async function listTasks(sandbox, log) {
|
|
12
|
+
const listing = await (0, task_tools_1.readTaskListing)(() => sandbox.tasks());
|
|
13
|
+
if (!Array.isArray(listing)) {
|
|
14
|
+
log(`the tasks of ${sandbox.build.hash} could not be listed: ${listing.error}`);
|
|
15
|
+
}
|
|
16
|
+
return listing;
|
|
17
|
+
}
|
|
9
18
|
// `hammerkit remote mcp`: a Model Context Protocol server on stdio for one sandbox of a
|
|
10
19
|
// server. It lives under `remote`, which already is a command: a top-level `mcp` would hide a
|
|
11
20
|
// task named `mcp`, since `hammerkit <task>` runs a task. Like the other `remote` commands it
|
|
@@ -21,20 +30,27 @@ function registerMcpCommand(remote, environment, input = process.stdin) {
|
|
|
21
30
|
.addOption(new commander_1.Option('--repo <repo>', 'create a sandbox of this repository').env('HAMMERKIT_REPO'))
|
|
22
31
|
.addOption(new commander_1.Option('--ref <ref>', 'at this branch').env('HAMMERKIT_REF'))
|
|
23
32
|
.addOption(new commander_1.Option('--build <hash>', 'hash of the published version of the build file whose tasks run on it').env('HAMMERKIT_BUILD'))
|
|
24
|
-
.addOption(new commander_1.Option('--shell <task>',
|
|
33
|
+
.addOption(new commander_1.Option('--shell <task>', "its task whose last command is a shell, like sh -c; '' for no bash tool")
|
|
25
34
|
.env('HAMMERKIT_SHELL')
|
|
26
35
|
.default(connect_1.DEFAULT_SHELL_TASK))
|
|
27
36
|
.action(async (options) => {
|
|
28
37
|
try {
|
|
29
38
|
// stdout carries the protocol only, everything else goes to stderr
|
|
30
39
|
const log = (line) => environment.stderr.write(`hammerkit remote mcp: ${line}\n`);
|
|
31
|
-
|
|
40
|
+
// as the plugins read it: blank is no shell
|
|
41
|
+
const sandbox = await (0, connect_1.connectSandbox)(environment, { ...options, shell: options.shell.trim() }, log);
|
|
32
42
|
log(`sandbox ${sandbox.id} of ${sandbox.sandbox.repo} at ${sandbox.sandbox.commit}`);
|
|
43
|
+
const shell = sandbox.build.shell;
|
|
33
44
|
const instructions = `The tools work on hammerkit sandbox ${sandbox.id}, a checkout of ${sandbox.sandbox.repo} at ` +
|
|
34
|
-
`${sandbox.sandbox.commit} on a server. Paths are relative to its root.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
`${sandbox.sandbox.commit} on a server. Paths are relative to its root. ` +
|
|
46
|
+
(shell
|
|
47
|
+
? `bash runs a command as the task ${shell} of the published build file ${sandbox.build.hash}, ` +
|
|
48
|
+
'run_task runs its other tasks; only the files are kept between commands. '
|
|
49
|
+
: `run_task runs the tasks of the published build file ${sandbox.build.hash}; only the files are ` +
|
|
50
|
+
'kept between runs. ') +
|
|
51
|
+
'changes shows what changed since the fetch.';
|
|
52
|
+
const tasks = await listTasks(sandbox, log);
|
|
53
|
+
const served = (0, mcp_1.serveMcp)(input, environment.stdout, (0, mcp_1.createMcpTools)(async () => sandbox, { tasks, shell: shell || false }), {
|
|
38
54
|
name: 'hammerkit',
|
|
39
55
|
version: (0, version_1.getVersion)(),
|
|
40
56
|
instructions,
|