hammerkit 1.27.0-next.9 → 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 +49 -1
- package/dist/kubernetes/await-running-state.js +172 -13
- package/dist/kubernetes/cluster-state.js +245 -0
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +133 -9
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +2 -0
- package/dist/kubernetes/ensure-persistent-data.js +211 -6
- package/dist/kubernetes/follow-pod-logs.js +182 -0
- package/dist/kubernetes/job-placement.js +36 -12
- package/dist/kubernetes/kubernetes-instance.js +1 -0
- package/dist/kubernetes/network-policy.js +2 -1
- package/dist/kubernetes/remove-stale-items.js +46 -0
- package/dist/kubernetes/restore-kubernetes-data.js +9 -5
- package/dist/kubernetes/sandbox-runner.js +628 -0
- package/dist/kubernetes/service-rollouts.js +74 -0
- package/dist/kubernetes/store-kubernetes-data.js +5 -2
- package/dist/kubernetes/volumes.js +94 -9
- 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 +136 -13
- 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 +415 -48
- 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 +962 -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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#059669"/><path d="M9 8h4v6h6V8h4v16h-4v-6h-6v6H9z" fill="#fff"/></svg>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="robots" content="noindex" />
|
|
7
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
8
|
+
<title>hammerkit</title>
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-zPXa4klZ.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CjnKm5-3.css">
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -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 } : {}),
|
|
@@ -10,11 +10,13 @@ const work_task_cache_description_1 = require("../optimizer/work-task-cache-desc
|
|
|
10
10
|
// archives) in its staging directory, and return that directory for a backend
|
|
11
11
|
// to push from. Used after a task ran, and by `cache push` for a task whose
|
|
12
12
|
// outputs are up to date in this checkout but were never stored in a backend.
|
|
13
|
-
async function archiveTaskEntry(work, environment
|
|
13
|
+
async function archiveTaskEntry(work, environment,
|
|
14
|
+
// the state the outputs are of, by default the one the task is at
|
|
15
|
+
stateKey = work.state.current.stateKey) {
|
|
14
16
|
const cacheDir = (0, get_cache_directory_1.getCacheDirectory)((0, work_instance_id_1.getWorkInstanceId)(work));
|
|
15
17
|
const { stats } = await (0, get_work_cache_stats_1.getWorkItemCacheStats)(work, environment);
|
|
16
18
|
await (0, cache_metadata_1.writeCacheMetadata)(environment, work, stats, (0, work_task_cache_description_1.getWorkTaskCacheDescription)(work));
|
|
17
|
-
await work.runtime.archive(environment, cacheDir);
|
|
19
|
+
await work.runtime.archive(environment, cacheDir, stateKey ?? undefined);
|
|
18
20
|
return cacheDir;
|
|
19
21
|
}
|
|
20
22
|
exports.archiveTaskEntry = archiveTaskEntry;
|
|
@@ -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;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.restoreDeferred = exports.clearDeferredRestore = exports.deferRestore = void 0;
|
|
4
|
+
const abort_1 = require("./abort");
|
|
5
|
+
// Keyed by the item's state, which every run replaces (resetWorkTree): a hit
|
|
6
|
+
// deferred in one run of a Cli is never restored in a later one, over outputs
|
|
7
|
+
// that run built.
|
|
8
|
+
const deferred = new WeakMap();
|
|
9
|
+
function deferRestore(item, run) {
|
|
10
|
+
deferred.set(item.state, { run, promise: null });
|
|
11
|
+
}
|
|
12
|
+
exports.deferRestore = deferRestore;
|
|
13
|
+
// Every cache check starts from scratch: a key that changed since a hit was
|
|
14
|
+
// deferred must not restore the old entry.
|
|
15
|
+
function clearDeferredRestore(item) {
|
|
16
|
+
deferred.delete(item.state);
|
|
17
|
+
}
|
|
18
|
+
exports.clearDeferredRestore = clearDeferredRestore;
|
|
19
|
+
// Restore every given item that was a hit without its outputs, in parallel,
|
|
20
|
+
// each at most once however many items wait on it.
|
|
21
|
+
async function restoreDeferred(work, items, abort) {
|
|
22
|
+
await Promise.all(items.map((item) => {
|
|
23
|
+
const entry = deferred.get(item.state);
|
|
24
|
+
if (!entry) {
|
|
25
|
+
return Promise.resolve();
|
|
26
|
+
}
|
|
27
|
+
if (!entry.promise) {
|
|
28
|
+
work.status.write('debug', `restore ${item.name} from cache, ${work.name} needs its outputs`);
|
|
29
|
+
entry.promise = entry.run();
|
|
30
|
+
}
|
|
31
|
+
return entry.promise;
|
|
32
|
+
}));
|
|
33
|
+
(0, abort_1.checkForAbort)(abort);
|
|
34
|
+
}
|
|
35
|
+
exports.restoreDeferred = restoreDeferred;
|
|
36
|
+
//# sourceMappingURL=deferred-restore.js.map
|
|
@@ -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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.dockerTask = exports.buildCreateOptions = void 0;
|
|
3
|
+
exports.dockerTask = exports.getCapabilityHostConfig = exports.buildCreateOptions = void 0;
|
|
4
4
|
const run_labels_1 = require("../docker/run-labels");
|
|
5
5
|
const execute_docker_1 = require("./execute-docker");
|
|
6
6
|
const abort_1 = require("./abort");
|
|
@@ -47,10 +47,30 @@ function buildCreateOptions(item, stateKey, network, environment, secrets, resou
|
|
|
47
47
|
...(0, item_network_1.getNetworkHostConfig)(network),
|
|
48
48
|
AutoRemove: true,
|
|
49
49
|
...resources,
|
|
50
|
+
...getCapabilityHostConfig(item, environment),
|
|
50
51
|
},
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
54
|
exports.buildCreateOptions = buildCreateOptions;
|
|
55
|
+
/**
|
|
56
|
+
* The capabilities a task adds (`capAdd`). On a server they are only granted in the
|
|
57
|
+
* sandboxed runtime of `backend.capabilities`, never by the kernel of the docker host.
|
|
58
|
+
*/
|
|
59
|
+
function getCapabilityHostConfig(item, environment) {
|
|
60
|
+
const capAdd = item.data.capAdd;
|
|
61
|
+
if (capAdd.length === 0) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
if (!environment.hosted) {
|
|
65
|
+
return { CapAdd: capAdd };
|
|
66
|
+
}
|
|
67
|
+
const runtime = environment.hosted.capabilityRuntime;
|
|
68
|
+
if (!runtime) {
|
|
69
|
+
throw new Error(`${item.name}: adds the capabilities ${capAdd.join(', ')}, which this server grants in no sandboxed runtime`);
|
|
70
|
+
}
|
|
71
|
+
return { CapAdd: capAdd, Runtime: runtime };
|
|
72
|
+
}
|
|
73
|
+
exports.getCapabilityHostConfig = getCapabilityHostConfig;
|
|
54
74
|
async function dockerTask(docker, item, environment, options) {
|
|
55
75
|
item.status.write('info', `execute ${item.name} in container`);
|
|
56
76
|
let network = null;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.executeWorkService = exports.stopService = void 0;
|
|
4
4
|
const work_item_1 = require("../planner/work-item");
|
|
5
5
|
const await_completed_dependencies_1 = require("./await-completed-dependencies");
|
|
6
|
+
const deferred_restore_1 = require("./deferred-restore");
|
|
6
7
|
const abort_1 = require("./abort");
|
|
7
8
|
const log_1 = require("../log");
|
|
8
9
|
const watch_loop_1 = require("./watch-loop");
|
|
@@ -35,6 +36,7 @@ async function executeWorkService(work, environment, options) {
|
|
|
35
36
|
});
|
|
36
37
|
await (0, watch_loop_1.watchLoop)(work, environment, options, async (cacheState, abort, stop) => {
|
|
37
38
|
await (0, await_completed_dependencies_1.awaitCompletedDependencies)(work, work.deps, abort);
|
|
39
|
+
await (0, deferred_restore_1.restoreDeferred)(work, (0, await_completed_dependencies_1.transitiveDeps)(work.deps), abort);
|
|
38
40
|
work.state.set({
|
|
39
41
|
type: 'ready',
|
|
40
42
|
stateKey: cacheState.stateKey,
|
|
@@ -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");
|
|
@@ -10,9 +11,11 @@ const deadline_1 = require("./deadline");
|
|
|
10
11
|
const explain_1 = require("../cache/explain");
|
|
11
12
|
const archive_task_entry_1 = require("./archive-task-entry");
|
|
12
13
|
const units_1 = require("../utils/units");
|
|
14
|
+
const deferred_restore_1 = require("./deferred-restore");
|
|
13
15
|
async function pushToBackend(work, environment, cacheState, options) {
|
|
14
16
|
const { resolved, stateKey, provable } = cacheState;
|
|
15
|
-
|
|
17
|
+
// an aggregate has nothing to store, see checkCacheState
|
|
18
|
+
if (resolved.method === 'none' || !provable || (0, work_task_1.isAggregateTask)(work.data)) {
|
|
16
19
|
return;
|
|
17
20
|
}
|
|
18
21
|
if (options.cacheReadOnly) {
|
|
@@ -20,7 +23,7 @@ async function pushToBackend(work, environment, cacheState, options) {
|
|
|
20
23
|
return;
|
|
21
24
|
}
|
|
22
25
|
try {
|
|
23
|
-
const cacheDir = await (0, archive_task_entry_1.archiveTaskEntry)(work, environment);
|
|
26
|
+
const cacheDir = await (0, archive_task_entry_1.archiveTaskEntry)(work, environment, stateKey);
|
|
24
27
|
await resolved.backend.push(work.id(), stateKey, cacheDir, environment);
|
|
25
28
|
work.status.write('info', `${work.name} pushed to cache "${resolved.name}" (${resolved.backend.type})`);
|
|
26
29
|
}
|
|
@@ -29,7 +32,7 @@ async function pushToBackend(work, environment, cacheState, options) {
|
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
// A task without cmds only groups its dependencies (`ci: { deps: [test, build] }`).
|
|
32
|
-
//
|
|
35
|
+
// It is never a cache hit itself (see checkCacheState), so its dependencies are
|
|
33
36
|
// always checked and their outputs restored. When every one of them was a hit,
|
|
34
37
|
// nothing ran: report the group as cached too. Reporting only, the cache
|
|
35
38
|
// decision is untouched.
|
|
@@ -87,6 +90,10 @@ dependents = null) {
|
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
await (0, await_completed_dependencies_1.awaitCompletedDependencies)(work, work.deps, abort);
|
|
93
|
+
// an aggregate runs nothing, so it needs no outputs on disk
|
|
94
|
+
if (!(0, work_task_1.isAggregateTask)(work.data)) {
|
|
95
|
+
await (0, deferred_restore_1.restoreDeferred)(work, (0, await_completed_dependencies_1.transitiveDeps)(work.deps), abort);
|
|
96
|
+
}
|
|
90
97
|
await (0, await_completed_dependencies_1.awaitRunningNeeds)(work, work.needs.map((n) => n.service), abort);
|
|
91
98
|
await options.processManager.task(work, async () => {
|
|
92
99
|
work.state.set({
|
|
@@ -94,7 +101,7 @@ dependents = null) {
|
|
|
94
101
|
stateKey: cacheState.stateKey,
|
|
95
102
|
started,
|
|
96
103
|
});
|
|
97
|
-
const timeout = work.data.timeout
|
|
104
|
+
const timeout = (0, deadline_1.getTaskTimeout)(work.data.timeout, options);
|
|
98
105
|
const deadline = (0, deadline_1.withDeadline)(abort, timeout);
|
|
99
106
|
try {
|
|
100
107
|
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,19 +2,24 @@
|
|
|
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");
|
|
8
9
|
const log_1 = require("../../log");
|
|
9
10
|
const state_key_1 = require("./state-key");
|
|
10
11
|
const work_instance_id_1 = require("../../planner/work-instance-id");
|
|
12
|
+
const deferred_restore_1 = require("../deferred-restore");
|
|
11
13
|
// The key a task whose cache is off runs and records its outputs under. It is
|
|
12
14
|
// no hash, so a later run with its cache on never takes those outputs as its own.
|
|
13
15
|
exports.UNCACHED_STATE_KEY = 'uncached';
|
|
14
16
|
async function checkCacheState(item, defaultCacheMethod, environment,
|
|
15
17
|
// watch mode compares keys to rerun on a change, so it needs one even when
|
|
16
18
|
// the cache is off
|
|
19
|
+
// restore: false only looks a backend hit up, its outputs are downloaded once
|
|
20
|
+
// something that runs depends on them (`run --no-restore`, see deferred-restore)
|
|
17
21
|
options = {}) {
|
|
22
|
+
(0, deferred_restore_1.clearDeferredRestore)(item);
|
|
18
23
|
// A task whose cache is off is never looked up, so hashing its sources buys
|
|
19
24
|
// nothing, and a `**` over a node_modules takes most of a minute (every run
|
|
20
25
|
// with arguments). Its dependents still hash its sources for their own key.
|
|
@@ -35,18 +40,41 @@ options = {}) {
|
|
|
35
40
|
item.status.write('debug', `${item.name} always runs, it or one of its dependencies has no src files`);
|
|
36
41
|
return { cached: false, stateKey, resolved, provable };
|
|
37
42
|
}
|
|
43
|
+
// An aggregate runs nothing, so running it costs nothing, and it is never a hit itself:
|
|
44
|
+
// asked for, it still has its dependencies checked and their outputs restored. Its key
|
|
45
|
+
// counts for what depends on it, and it is reported cached when they all were.
|
|
46
|
+
if ((0, work_item_1.isWorkTaskItem)(item) && (0, work_task_1.isAggregateTask)(item.data)) {
|
|
47
|
+
return { cached: false, stateKey, resolved, provable };
|
|
48
|
+
}
|
|
38
49
|
if ((0, work_item_1.isWorkTaskItem)(item)) {
|
|
39
50
|
const runtimeStateKey = await item.runtime.currentStateKey(environment);
|
|
40
51
|
if (runtimeStateKey === stateKey) {
|
|
41
52
|
return { cached: true, stateKey, resolved, provable };
|
|
42
53
|
}
|
|
54
|
+
if (options.restore === false && options.watch) {
|
|
55
|
+
item.status.write('debug', `${item.name} restores cache hits, --no-restore has no effect in watch mode`);
|
|
56
|
+
}
|
|
57
|
+
if (options.restore === false && !options.watch) {
|
|
58
|
+
try {
|
|
59
|
+
if (await resolved.backend.has(item.id(), stateKey, environment)) {
|
|
60
|
+
item.status.write('info', `${item.name} found in cache "${resolved.name}" (${resolved.backend.type}), not restored`);
|
|
61
|
+
await touchEntry(item, resolved, stateKey, environment);
|
|
62
|
+
(0, deferred_restore_1.deferRestore)(item, async () => {
|
|
63
|
+
if (!(await restoreFromBackend(item, resolved, stateKey, currentStats, environment))) {
|
|
64
|
+
throw new Error(`${item.name} is no longer in cache "${resolved.name}", its outputs can not be restored. ` +
|
|
65
|
+
'Run again without --no-restore.');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return { cached: true, stateKey, resolved, provable };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
item.status.write('warn', `${item.name} failed to look up cache "${resolved.name}": ${(0, log_1.getErrorMessage)(e)} — continuing with local execution`);
|
|
73
|
+
}
|
|
74
|
+
return { cached: false, stateKey, resolved, provable };
|
|
75
|
+
}
|
|
43
76
|
try {
|
|
44
|
-
|
|
45
|
-
const pulled = await resolved.backend.pull(item.id(), stateKey, cacheDir, environment);
|
|
46
|
-
if (pulled) {
|
|
47
|
-
item.status.write('info', `${item.name} pulled from cache "${resolved.name}" (${resolved.backend.type})`);
|
|
48
|
-
await item.runtime.restore(environment, cacheDir, stateKey);
|
|
49
|
-
await (0, cache_metadata_1.writeCacheMetadata)(environment, item, currentStats, (0, work_task_cache_description_1.getWorkTaskCacheDescription)(item));
|
|
77
|
+
if (await restoreFromBackend(item, resolved, stateKey, currentStats, environment)) {
|
|
50
78
|
return { cached: true, stateKey, resolved, provable };
|
|
51
79
|
}
|
|
52
80
|
}
|
|
@@ -57,4 +85,27 @@ options = {}) {
|
|
|
57
85
|
return { cached: false, stateKey, resolved, provable };
|
|
58
86
|
}
|
|
59
87
|
exports.checkCacheState = checkCacheState;
|
|
88
|
+
// Pull a task's entry and restore its outputs into the checkout. False when the
|
|
89
|
+
// backend has no such entry.
|
|
90
|
+
async function restoreFromBackend(item, resolved, stateKey, currentStats, environment) {
|
|
91
|
+
const cacheDir = (0, get_cache_directory_1.getCacheDirectory)((0, work_instance_id_1.getWorkInstanceId)(item));
|
|
92
|
+
const pulled = await resolved.backend.pull(item.id(), stateKey, cacheDir, environment);
|
|
93
|
+
if (!pulled) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
item.status.write('info', `${item.name} pulled from cache "${resolved.name}" (${resolved.backend.type})`);
|
|
97
|
+
await item.runtime.restore(environment, cacheDir, stateKey);
|
|
98
|
+
await (0, cache_metadata_1.writeCacheMetadata)(environment, item, currentStats, (0, work_task_cache_description_1.getWorkTaskCacheDescription)(item));
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
// A hit that is only looked up still counts as used, so retention keeps it.
|
|
102
|
+
// Best effort: the hit stands whether or not the marker could be written.
|
|
103
|
+
async function touchEntry(item, resolved, stateKey, environment) {
|
|
104
|
+
try {
|
|
105
|
+
await resolved.backend.touch?.(item.id(), stateKey, environment);
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
item.status.write('debug', `${item.name} could not mark its cache entry as used: ${(0, log_1.getErrorMessage)(e)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
60
111
|
//# sourceMappingURL=enqueue-next.js.map
|
|
@@ -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
|
|
@@ -8,11 +8,14 @@ const state_resolver_1 = require("./state-resolver");
|
|
|
8
8
|
const abort_1 = require("./abort");
|
|
9
9
|
const enqueue_next_1 = require("./scheduler/enqueue-next");
|
|
10
10
|
async function watchLoop(work, environment, options, factory) {
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// a container service that watches its own files (`continuous`); a kubernetes-service and a
|
|
12
|
+
// task have no such field
|
|
13
13
|
const continuous = 'continuous' in work.data ? work.data.continuous : false;
|
|
14
14
|
const watchMode = options.watch && !options.daemon && !continuous;
|
|
15
|
-
const currentStats = await (0, enqueue_next_1.checkCacheState)(work, options.cacheDefault, environment, {
|
|
15
|
+
const currentStats = await (0, enqueue_next_1.checkCacheState)(work, options.cacheDefault, environment, {
|
|
16
|
+
watch: watchMode,
|
|
17
|
+
restore: options.restore,
|
|
18
|
+
});
|
|
16
19
|
(0, abort_1.checkForAbort)(environment.abortCtrl.signal);
|
|
17
20
|
const watchState = watchMode
|
|
18
21
|
? (0, watch_state_key_1.watchStateKey)(work, currentStats, environment, options)
|