hammerkit 1.27.0-next.8 → 2.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/build.schema.json +176 -6
  2. package/dist/cache/backends/local-cache-backend.js +6 -0
  3. package/dist/cache/backends/registry/registry-client.js +1 -0
  4. package/dist/cache/cache-sync.js +1 -1
  5. package/dist/cache/explain.js +17 -0
  6. package/dist/cli.js +2 -0
  7. package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
  8. package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
  9. package/dist/dashboard/favicon.svg +1 -0
  10. package/dist/dashboard/index.html +15 -0
  11. package/dist/docker/item-network.js +13 -5
  12. package/dist/executer/archive-task-entry.js +4 -2
  13. package/dist/executer/deadline.js +17 -3
  14. package/dist/executer/deferred-restore.js +36 -0
  15. package/dist/executer/docker-service.js +30 -8
  16. package/dist/executer/docker-task.js +21 -1
  17. package/dist/executer/execute-work-service.js +2 -0
  18. package/dist/executer/execute-work-task.js +11 -4
  19. package/dist/executer/execution-steps.js +25 -4
  20. package/dist/executer/reset-work-tree.js +4 -0
  21. package/dist/executer/scheduler/enqueue-next.js +57 -6
  22. package/dist/executer/scheduler/state-key.js +42 -5
  23. package/dist/executer/watch-loop.js +6 -3
  24. package/dist/harness/connect.js +20 -6
  25. package/dist/harness/mcp-command.js +22 -6
  26. package/dist/harness/mcp.js +63 -114
  27. package/dist/harness/opencode.js +158 -50
  28. package/dist/harness/pi.js +29 -0
  29. package/dist/harness/sandbox-client.js +194 -23
  30. package/dist/harness/service-tools.js +141 -0
  31. package/dist/harness/task-tools.js +221 -0
  32. package/dist/kubernetes/apply.js +76 -17
  33. package/dist/kubernetes/await-running-state.js +172 -13
  34. package/dist/kubernetes/cluster-state.js +245 -0
  35. package/dist/kubernetes/ensure-http-route.js +1 -1
  36. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +138 -18
  37. package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
  38. package/dist/kubernetes/ensure-persistent-data.js +212 -10
  39. package/dist/kubernetes/follow-pod-logs.js +182 -0
  40. package/dist/kubernetes/get-service-ip.js +2 -2
  41. package/dist/kubernetes/job-placement.js +36 -12
  42. package/dist/kubernetes/job-secrets.js +9 -6
  43. package/dist/kubernetes/kept-claims.js +22 -10
  44. package/dist/kubernetes/kubernetes-instance.js +1 -0
  45. package/dist/kubernetes/network-policy.js +16 -8
  46. package/dist/kubernetes/remove-persistent-data.js +3 -1
  47. package/dist/kubernetes/remove-stale-items.js +46 -0
  48. package/dist/kubernetes/resources.js +25 -3
  49. package/dist/kubernetes/restore-kubernetes-data.js +9 -5
  50. package/dist/kubernetes/sandbox-runner.js +628 -0
  51. package/dist/kubernetes/secrets.js +7 -11
  52. package/dist/kubernetes/service-rollouts.js +74 -0
  53. package/dist/kubernetes/store-kubernetes-data.js +5 -2
  54. package/dist/kubernetes/volumes.js +96 -11
  55. package/dist/main.js +2 -0
  56. package/dist/optimizer/calculate-checksum.js +3 -2
  57. package/dist/optimizer/check-key.js +94 -0
  58. package/dist/optimizer/check-keys.js +64 -0
  59. package/dist/optimizer/get-work-cache-stats.js +17 -3
  60. package/dist/optimizer/work-task-cache-description.js +49 -1
  61. package/dist/planner/service-definition.js +9 -4
  62. package/dist/planner/utils/append-work-dependencies.js +5 -3
  63. package/dist/planner/utils/append-work-service.js +18 -1
  64. package/dist/planner/utils/append-work-task.js +13 -3
  65. package/dist/planner/utils/get-work-runtime.js +6 -1
  66. package/dist/planner/utils/normalize-path.js +19 -1
  67. package/dist/planner/utils/parse-work-mount.js +7 -1
  68. package/dist/planner/utils/parse-work-port.js +4 -1
  69. package/dist/planner/validate.js +22 -0
  70. package/dist/planner/work-network.js +3 -1
  71. package/dist/planner/work-runtime-aggregate.js +32 -0
  72. package/dist/planner/work-runtime-kubernetes.js +153 -26
  73. package/dist/planner/work-task.js +6 -1
  74. package/dist/program.js +27 -9
  75. package/dist/remote/device-login.js +7 -2
  76. package/dist/remote/host-config.js +69 -3
  77. package/dist/remote/remote-commands.js +33 -7
  78. package/dist/remote/remote-status.js +112 -0
  79. package/dist/remote/remote-work-commands.js +562 -45
  80. package/dist/remote/server-client.js +15 -2
  81. package/dist/remote/token-source.js +64 -26
  82. package/dist/run-program.js +9 -0
  83. package/dist/schema/build-file-base-task-schema.js +3 -3
  84. package/dist/schema/build-file-container-service-schema.js +10 -0
  85. package/dist/schema/build-file-container-task-schema.js +16 -1
  86. package/dist/schema/build-file-include-schema.js +18 -4
  87. package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
  88. package/dist/schema/build-file-task-schema.js +5 -1
  89. package/dist/schema/capabilities-schema.js +56 -0
  90. package/dist/schema/reference-parser.js +119 -11
  91. package/dist/schema/removed-properties.js +20 -0
  92. package/dist/schema/schema-parser.js +94 -41
  93. package/dist/schema/work-scope-parser.js +33 -10
  94. package/dist/secrets/command-secret-provider.js +3 -1
  95. package/dist/secrets/github-app-token-command.js +3 -1
  96. package/dist/secrets/secret-catalog.js +43 -19
  97. package/dist/server/accounts-api.js +4 -1
  98. package/dist/server/approvals-api.js +1 -1
  99. package/dist/server/capacity-format.js +36 -0
  100. package/dist/server/capacity.js +300 -0
  101. package/dist/server/checks-api.js +83 -0
  102. package/dist/server/checks.js +116 -0
  103. package/dist/server/dashboard.js +196 -0
  104. package/dist/server/definitions-api.js +66 -3
  105. package/dist/server/endpoints.js +794 -0
  106. package/dist/server/executor.js +164 -9
  107. package/dist/server/kubernetes-backend.js +478 -70
  108. package/dist/server/kubernetes-sandbox-storage.js +46 -2
  109. package/dist/server/overview.js +271 -0
  110. package/dist/server/plan.js +170 -121
  111. package/dist/server/policy.js +34 -2
  112. package/dist/server/proposals-api.js +17 -8
  113. package/dist/server/repo-access.js +5 -0
  114. package/dist/server/run-logs.js +4 -0
  115. package/dist/server/run-plan.js +38 -4
  116. package/dist/server/run-request.js +58 -3
  117. package/dist/server/run-worker.js +262 -13
  118. package/dist/server/runs.js +969 -36
  119. package/dist/server/sandbox-pod.js +28 -0
  120. package/dist/server/sandbox-storage.js +23 -0
  121. package/dist/server/sandboxes.js +8 -0
  122. package/dist/server/server-config.js +246 -10
  123. package/dist/server/server.js +429 -32
  124. package/dist/server/service-runs.js +82 -0
  125. package/dist/server/timings.js +132 -0
  126. package/dist/server/wait-reason.js +21 -0
  127. package/dist/utils/tar.js +28 -1
  128. package/dist/utils/terminal-prompt.js +53 -0
  129. 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) => !!port.hostPort);
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`, [{ HostPort: `${port.hostPort}` }]])),
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) => !!p.hostPort)
63
- .reduce((map, port) => {
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
- if (resolved.method === 'none' || !provable) {
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
- // Having no src of its own it is never a cache hit, so its dependencies are
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 ?? options.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
- await environment.file.writeFile(mount.localPath, '');
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
- await environment.file.createDirectory(mount.localPath);
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
- const binds = (0, get_container_binds_1.getContainerBinds)(item).filter((bind) => !environment.hosted || !(0, path_1.isAbsolute)(bind.localPath));
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
- const cacheDir = (0, get_cache_directory_1.getCacheDirectory)((0, work_instance_id_1.getWorkInstanceId)(item));
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, crypto_1.createHash)('md5')
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
- // continuous lives on container-services and on tasks (both task types); a
12
- // kubernetes-service has no such field. Read it wherever it exists.
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, { watch: watchMode });
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)