hammerkit 1.6.0-alpha.3 → 1.7.0

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 (256) hide show
  1. package/README.md +4 -4
  2. package/build.schema.json +49 -2
  3. package/dist/cache/backends/local-cache-backend.js +58 -1
  4. package/dist/cache/backends/registry/registry-client.js +264 -0
  5. package/dist/cache/backends/registry/registry-credentials.js +81 -0
  6. package/dist/cache/backends/registry/registry-reference.js +36 -0
  7. package/dist/cache/backends/registry-cache-backend.js +145 -0
  8. package/dist/cache/backends/s3-cache-backend.js +99 -46
  9. package/dist/cache/cache-backend-registry.js +2 -0
  10. package/dist/cache/cache-inventory.js +84 -0
  11. package/dist/cache/cache-sync.js +77 -0
  12. package/dist/cache/explain.js +214 -0
  13. package/dist/cache/last-resolved.js +48 -0
  14. package/dist/cache/read-only.js +15 -0
  15. package/dist/cache/retention.js +61 -0
  16. package/dist/cli.js +43 -0
  17. package/dist/docker/using-container.js +5 -1
  18. package/dist/executer/archive-task-entry.js +21 -0
  19. package/dist/executer/await-completed-dependencies.js +32 -14
  20. package/dist/executer/cache-metadata.js +9 -2
  21. package/dist/executer/docker-service.js +2 -1
  22. package/dist/executer/docker-task.js +2 -1
  23. package/dist/executer/dry-run.js +73 -0
  24. package/dist/executer/event-cache.js +6 -5
  25. package/dist/executer/execute-command.js +21 -3
  26. package/dist/executer/execute-work-service.js +10 -1
  27. package/dist/executer/execute-work-task.js +95 -25
  28. package/dist/executer/execute-work-tree.js +26 -1
  29. package/dist/executer/get-scheduler-execute-result.js +4 -1
  30. package/dist/executer/lazy-resolver.js +15 -3
  31. package/dist/executer/reset-work-tree.js +2 -0
  32. package/dist/executer/run-summary.js +102 -0
  33. package/dist/executer/scheduler/enqueue-next.js +15 -60
  34. package/dist/executer/scheduler/state-key.js +78 -0
  35. package/dist/kubernetes/await-running-state.js +73 -19
  36. package/dist/log.js +16 -6
  37. package/dist/logging/live-logger.js +2 -1
  38. package/dist/optimizer/calculate-checksum.js +8 -2
  39. package/dist/optimizer/get-work-cache-stats.js +66 -16
  40. package/dist/optimizer/work-task-cache-description.js +34 -5
  41. package/dist/planner/project-root.js +26 -0
  42. package/dist/planner/utils/append-work-service.js +2 -1
  43. package/dist/planner/utils/append-work-task.js +7 -1
  44. package/dist/planner/utils/parse-work-source.js +35 -30
  45. package/dist/planner/utils/portable-path.js +30 -0
  46. package/dist/planner/work-cache-id.js +2 -2
  47. package/dist/planner/work-graph-serializer.js +103 -0
  48. package/dist/planner/work-instance-id.js +15 -0
  49. package/dist/planner/work-runtime-docker.js +53 -15
  50. package/dist/planner/work-runtime-kubernetes.js +35 -3
  51. package/dist/planner/work-runtime-local.js +9 -3
  52. package/dist/planner/work-service-id.js +16 -4
  53. package/dist/program.js +235 -1
  54. package/dist/schema/build-file-base-task-schema.js +4 -0
  55. package/dist/schema/cache-schema.js +21 -2
  56. package/dist/schema/duration-schema.js +18 -0
  57. package/dist/schema/reference-parser.js +3 -0
  58. package/dist/schema/size-schema.js +18 -0
  59. package/dist/schema/work-scope-parser.js +15 -2
  60. package/dist/utils/units.js +87 -0
  61. package/package.json +15 -4
  62. package/.agents/FEEDBACK.md +0 -74
  63. package/.agents/skills/before-commit/SKILL.md +0 -101
  64. package/.agents/skills/do-the-work/SKILL.md +0 -64
  65. package/.agents/skills/fix-ci/SKILL.md +0 -98
  66. package/.agents/skills/speckit-git-commit/SKILL.md +0 -53
  67. package/.agents/skills/speckit-git-feature/SKILL.md +0 -72
  68. package/.agents/skills/speckit-git-initialize/SKILL.md +0 -54
  69. package/.agents/skills/speckit-git-remote/SKILL.md +0 -50
  70. package/.agents/skills/speckit-git-validate/SKILL.md +0 -54
  71. package/.devcontainer/Dockerfile +0 -5
  72. package/.devcontainer/devcontainer.json +0 -40
  73. package/.gitbook.yaml +0 -5
  74. package/.specify/extensions/.registry +0 -37
  75. package/.specify/extensions/git/README.md +0 -100
  76. package/.specify/extensions/git/commands/speckit.git.commit.md +0 -48
  77. package/.specify/extensions/git/commands/speckit.git.feature.md +0 -67
  78. package/.specify/extensions/git/commands/speckit.git.initialize.md +0 -49
  79. package/.specify/extensions/git/commands/speckit.git.remote.md +0 -45
  80. package/.specify/extensions/git/commands/speckit.git.validate.md +0 -49
  81. package/.specify/extensions/git/config-template.yml +0 -62
  82. package/.specify/extensions/git/extension.yml +0 -140
  83. package/.specify/extensions/git/git-config.yml +0 -62
  84. package/.specify/extensions.yml +0 -149
  85. package/.specify/feature.json +0 -3
  86. package/.specify/init-options.json +0 -10
  87. package/.specify/integration.json +0 -15
  88. package/.specify/integrations/claude.manifest.json +0 -16
  89. package/.specify/integrations/speckit.manifest.json +0 -17
  90. package/.specify/memory/constitution.md +0 -162
  91. package/.specify/templates/checklist-template.md +0 -40
  92. package/.specify/templates/constitution-template.md +0 -50
  93. package/.specify/templates/plan-template.md +0 -113
  94. package/.specify/templates/spec-template.md +0 -131
  95. package/.specify/templates/tasks-template.md +0 -252
  96. package/.specify/workflows/speckit/workflow.yml +0 -77
  97. package/.specify/workflows/workflow-registry.json +0 -13
  98. package/AGENTS.md +0 -94
  99. package/Dockerfile +0 -25
  100. package/FOLLOWUPS.md +0 -34
  101. package/SUMMARY.md +0 -68
  102. package/VALUES.md +0 -53
  103. package/dist/cache/auto-pull.spec.js +0 -123
  104. package/dist/cache/backends/local-cache-backend.spec.js +0 -72
  105. package/dist/cache/backends/s3-cache-backend.spec.js +0 -118
  106. package/dist/cache/cache-backend-registry.spec.js +0 -22
  107. package/dist/cache/resolve-cache.spec.js +0 -52
  108. package/dist/cli.spec.js +0 -23
  109. package/dist/console/get-logger.spec.js +0 -23
  110. package/dist/docker/get-default-platform.spec.js +0 -29
  111. package/dist/docker/longest-common-prefix.spec.js +0 -26
  112. package/dist/docker/package.spec.js +0 -145
  113. package/dist/docker/remove-container.spec.js +0 -32
  114. package/dist/docker/using-container.spec.js +0 -90
  115. package/dist/environment/merge-environment-variables.spec.js +0 -24
  116. package/dist/environment/replace-env-variables.spec.js +0 -47
  117. package/dist/executer/abort.spec.js +0 -18
  118. package/dist/executer/environment-mock.spec.js +0 -22
  119. package/dist/executer/execute.spec.js +0 -51
  120. package/dist/executer/get-service-containers.spec.js +0 -59
  121. package/dist/executer/get-service-env-hints.spec.js +0 -65
  122. package/dist/executer/label-values.spec.js +0 -61
  123. package/dist/executer/lazy-resolver.spec.js +0 -20
  124. package/dist/executer/process-manager.spec.js +0 -54
  125. package/dist/executer/scheduler/enqueue-next.spec.js +0 -136
  126. package/dist/executer/state-resolver.spec.js +0 -37
  127. package/dist/executer/state.spec.js +0 -66
  128. package/dist/executer/states.spec.js +0 -14
  129. package/dist/file/get-file-context.spec.js +0 -178
  130. package/dist/file/move-files.spec.js +0 -48
  131. package/dist/kubernetes/ensure-http-route.spec.js +0 -70
  132. package/dist/kubernetes/ensure-namespace.spec.js +0 -39
  133. package/dist/kubernetes/resolve-pod-name.spec.js +0 -81
  134. package/dist/log.spec.js +0 -131
  135. package/dist/logging/grouped-logger.spec.js +0 -84
  136. package/dist/logging/interactive-logger.spec.js +0 -57
  137. package/dist/logging/live-logger.spec.js +0 -37
  138. package/dist/logging/loggers.spec.js +0 -154
  139. package/dist/optimizer/calculate-checksum.spec.js +0 -34
  140. package/dist/optimizer/get-cache-directory.spec.js +0 -59
  141. package/dist/optimizer/get-work-cache-stats.spec.js +0 -79
  142. package/dist/optimizer/work-task-cache-description.spec.js +0 -45
  143. package/dist/parser/default-build-file.spec.js +0 -48
  144. package/dist/parser/parse-label-arguments.spec.js +0 -27
  145. package/dist/parser/read-build-file.spec.js +0 -89
  146. package/dist/parser/read-env-file.spec.js +0 -42
  147. package/dist/planner/utils/append-work-service.spec.js +0 -71
  148. package/dist/planner/utils/get-container-user.spec.js +0 -30
  149. package/dist/planner/utils/get-default-kube-config.spec.js +0 -11
  150. package/dist/planner/utils/normalize-path.spec.js +0 -24
  151. package/dist/planner/utils/parse-work-command.spec.js +0 -21
  152. package/dist/planner/utils/parse-work-generate.spec.js +0 -45
  153. package/dist/planner/utils/parse-work-mount.spec.js +0 -62
  154. package/dist/planner/utils/parse-work-mounts.spec.js +0 -19
  155. package/dist/planner/utils/parse-work-port.spec.js +0 -24
  156. package/dist/planner/utils/parse-work-ports.spec.js +0 -24
  157. package/dist/planner/utils/parse-work-source.spec.js +0 -58
  158. package/dist/planner/utils/parse-work-volume.spec.js +0 -18
  159. package/dist/planner/utils/plan-work-tasks.spec.js +0 -30
  160. package/dist/planner/utils/plan-work-volume.spec.js +0 -33
  161. package/dist/planner/utils/split-name.spec.js +0 -16
  162. package/dist/planner/utils/template-value.spec.js +0 -14
  163. package/dist/planner/validate.spec.js +0 -251
  164. package/dist/planner/work-cache-id.spec.js +0 -173
  165. package/dist/planner/work-runtime-local.spec.js +0 -193
  166. package/dist/program-commands.spec.js +0 -84
  167. package/dist/program.spec.js +0 -43
  168. package/dist/schema/build-file-schema.spec.js +0 -61
  169. package/dist/schema/doc-build-files.spec.js +0 -223
  170. package/dist/schema/parse-error.spec.js +0 -18
  171. package/dist/schema/reference-parser.spec.js +0 -83
  172. package/dist/schema/schema-parser.spec.js +0 -80
  173. package/dist/schema/type-guards.spec.js +0 -23
  174. package/dist/schema/work-scope-parser.spec.js +0 -66
  175. package/dist/service/update-service-status.spec.js +0 -27
  176. package/dist/testing/ensure-local-registry.js +0 -134
  177. package/dist/testing/example-test-suite.js +0 -63
  178. package/dist/testing/expect.js +0 -82
  179. package/dist/testing/get-test-suite.js +0 -9
  180. package/dist/testing/http-get.js +0 -21
  181. package/dist/testing/integration/cache.spec.js +0 -49
  182. package/dist/testing/integration/cancellation.spec.js +0 -45
  183. package/dist/testing/integration/clean.spec.js +0 -43
  184. package/dist/testing/integration/cmd.spec.js +0 -15
  185. package/dist/testing/integration/concurrency.spec.js +0 -19
  186. package/dist/testing/integration/docker-package-registry.spec.js +0 -44
  187. package/dist/testing/integration/docker-service-crash.spec.js +0 -36
  188. package/dist/testing/integration/docker.spec.js +0 -17
  189. package/dist/testing/integration/env.spec.js +0 -48
  190. package/dist/testing/integration/environment.spec.js +0 -97
  191. package/dist/testing/integration/error.spec.js +0 -19
  192. package/dist/testing/integration/export.spec.js +0 -22
  193. package/dist/testing/integration/extend.spec.js +0 -29
  194. package/dist/testing/integration/glob.spec.js +0 -40
  195. package/dist/testing/integration/include.spec.js +0 -31
  196. package/dist/testing/integration/invalid.spec.js +0 -17
  197. package/dist/testing/integration/invalid_loop.spec.js +0 -56
  198. package/dist/testing/integration/k8s-state-cache.spec.js +0 -41
  199. package/dist/testing/integration/kubernetes-service.spec.js +0 -148
  200. package/dist/testing/integration/kubernetes.spec.js +0 -99
  201. package/dist/testing/integration/labels.spec.js +0 -42
  202. package/dist/testing/integration/local.spec.js +0 -14
  203. package/dist/testing/integration/monorepo.spec.js +0 -15
  204. package/dist/testing/integration/multi-line.spec.js +0 -17
  205. package/dist/testing/integration/reference.spec.js +0 -21
  206. package/dist/testing/integration/services.spec.js +0 -38
  207. package/dist/testing/integration/store.spec.js +0 -52
  208. package/dist/testing/integration/unknown.spec.js +0 -28
  209. package/dist/testing/integration/validate.spec.js +0 -160
  210. package/dist/testing/integration/watch-dependency.spec.js +0 -33
  211. package/dist/testing/integration/watch.spec.js +0 -45
  212. package/dist/testing/requires-kubernetes.js +0 -18
  213. package/dist/testing/requires-linux-containers.js +0 -18
  214. package/dist/testing/requires-windows-containers.js +0 -18
  215. package/dist/testing/test-case.js +0 -75
  216. package/dist/testing/test-streams.js +0 -28
  217. package/dist/testing/test-suite-setup.js +0 -3
  218. package/dist/testing/test-suite.js +0 -3
  219. package/dist/testing/testing-timeout.js +0 -32
  220. package/dist/utils/abort-event.spec.js +0 -46
  221. package/dist/utils/buffer-context.spec.js +0 -38
  222. package/dist/utils/ci.spec.js +0 -53
  223. package/dist/utils/debouncer.spec.js +0 -58
  224. package/dist/utils/emitter.spec.js +0 -34
  225. package/dist/utils/empty-writable.spec.js +0 -18
  226. package/dist/utils/environment-config.spec.js +0 -26
  227. package/dist/utils/fail-never.spec.js +0 -9
  228. package/dist/utils/sleep.spec.js +0 -22
  229. package/dist/version.spec.js +0 -12
  230. package/specs/build-file/caches/spec.md +0 -114
  231. package/specs/build-file/environment-variables/spec.md +0 -82
  232. package/specs/build-file/includes/spec.md +0 -60
  233. package/specs/build-file/references/spec.md +0 -56
  234. package/specs/build-file/spec.md +0 -62
  235. package/specs/cli/clean/spec.md +0 -85
  236. package/specs/cli/down/spec.md +0 -85
  237. package/specs/cli/execute/spec.md +0 -101
  238. package/specs/cli/init/spec.md +0 -64
  239. package/specs/cli/ls/spec.md +0 -66
  240. package/specs/cli/package/spec.md +0 -110
  241. package/specs/cli/spec.md +0 -73
  242. package/specs/cli/store-restore/spec.md +0 -74
  243. package/specs/cli/up/spec.md +0 -80
  244. package/specs/cli/validate/spec.md +0 -70
  245. package/specs/labels/spec.md +0 -91
  246. package/specs/service/container/spec.md +0 -120
  247. package/specs/service/kubernetes/spec.md +0 -97
  248. package/specs/service/spec.md +0 -105
  249. package/specs/task/caching/spec.md +0 -107
  250. package/specs/task/container/spec.md +0 -118
  251. package/specs/task/dependencies/spec.md +0 -70
  252. package/specs/task/extending/spec.md +0 -74
  253. package/specs/task/kubernetes/spec.md +0 -125
  254. package/specs/task/needs/spec.md +0 -59
  255. package/specs/task/spec.md +0 -87
  256. package/specs/task/watching/spec.md +0 -69
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.serializeWorkGraph = void 0;
4
+ const plan_work_tasks_1 = require("./utils/plan-work-tasks");
5
+ const validate_1 = require("./validate");
6
+ function buildGraph(workTree) {
7
+ const taskItems = Array.from((0, plan_work_tasks_1.iterateWorkTasks)(workTree)).sort((a, b) => a.name.localeCompare(b.name));
8
+ const serviceItems = Array.from((0, plan_work_tasks_1.iterateWorkServices)(workTree)).sort((a, b) => a.name.localeCompare(b.name));
9
+ const nodes = [];
10
+ const taskNodeId = new Map();
11
+ const serviceNodeId = new Map();
12
+ taskItems.forEach((task, index) => {
13
+ const id = `t${index}`;
14
+ taskNodeId.set(task.name, id);
15
+ nodes.push({ id, name: task.name, kind: 'task' });
16
+ });
17
+ serviceItems.forEach((service, index) => {
18
+ const id = `s${index}`;
19
+ serviceNodeId.set(service.name, id);
20
+ nodes.push({ id, name: service.name, kind: 'service' });
21
+ });
22
+ const edges = [];
23
+ for (const task of taskItems) {
24
+ const from = taskNodeId.get(task.name);
25
+ if (!from) {
26
+ continue;
27
+ }
28
+ for (const dep of task.deps) {
29
+ const to = taskNodeId.get(dep.name);
30
+ if (to) {
31
+ edges.push({ from, to, kind: 'deps' });
32
+ }
33
+ }
34
+ for (const need of task.needs) {
35
+ const to = serviceNodeId.get(need.service.name);
36
+ if (to) {
37
+ edges.push({ from, to, kind: 'needs' });
38
+ }
39
+ }
40
+ }
41
+ edges.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || a.kind.localeCompare(b.kind));
42
+ let cycle = null;
43
+ for (const task of taskItems) {
44
+ const path = (0, validate_1.hasDependencyCycle)(task, []);
45
+ if (path && path.length > 0) {
46
+ cycle = path.map((n) => n.name);
47
+ break;
48
+ }
49
+ }
50
+ return { nodes, edges, cycle };
51
+ }
52
+ function mermaidLabel(name) {
53
+ return name.replace(/"/g, "'");
54
+ }
55
+ function renderMermaid(nodes, edges, cycle) {
56
+ const lines = ['graph TD'];
57
+ for (const node of nodes) {
58
+ // tasks are rectangles, services are stadium-shaped, so the two are visually distinct
59
+ lines.push(node.kind === 'task'
60
+ ? ` ${node.id}["${mermaidLabel(node.name)}"]`
61
+ : ` ${node.id}(["${mermaidLabel(node.name)}"])`);
62
+ }
63
+ for (const edge of edges) {
64
+ // deps are solid arrows, needs are dotted — distinct edge types
65
+ lines.push(edge.kind === 'deps' ? ` ${edge.from} --> ${edge.to}` : ` ${edge.from} -.-> ${edge.to}`);
66
+ }
67
+ if (cycle) {
68
+ lines.push(` %% cycle detected: ${cycle.join(' -> ')}`);
69
+ }
70
+ return lines.join('\n');
71
+ }
72
+ function dotId(id) {
73
+ return `"${id}"`;
74
+ }
75
+ function dotLabel(name) {
76
+ return name.replace(/"/g, '\\"');
77
+ }
78
+ function renderDot(nodes, edges, cycle) {
79
+ const lines = ['digraph hammerkit {'];
80
+ if (cycle) {
81
+ lines.push(` // cycle detected: ${cycle.join(' -> ')}`);
82
+ }
83
+ for (const node of nodes) {
84
+ const shape = node.kind === 'task' ? 'box' : 'ellipse';
85
+ lines.push(` ${dotId(node.id)} [label="${dotLabel(node.name)}" shape=${shape}];`);
86
+ }
87
+ for (const edge of edges) {
88
+ const style = edge.kind === 'needs' ? ' [style=dotted]' : '';
89
+ lines.push(` ${dotId(edge.from)} -> ${dotId(edge.to)}${style};`);
90
+ }
91
+ lines.push('}');
92
+ return lines.join('\n');
93
+ }
94
+ // Serialize the planner's already-built work graph (tasks, services, deps and
95
+ // needs edges) into a renderable format. A cyclic graph still renders and marks
96
+ // the cycle rather than crashing.
97
+ function serializeWorkGraph(workTree, format) {
98
+ const { nodes, edges, cycle } = buildGraph(workTree);
99
+ const output = format === 'dot' ? renderDot(nodes, edges, cycle) : renderMermaid(nodes, edges, cycle);
100
+ return { output, cycle };
101
+ }
102
+ exports.serializeWorkGraph = serializeWorkGraph;
103
+ //# sourceMappingURL=work-graph-serializer.js.map
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getWorkInstanceId = void 0;
4
+ const crypto_1 = require("crypto");
5
+ // `id()` is the portable cache identity: identical for the same task in every
6
+ // checkout, so a shared cache hits across machines. Machine-local runtime state
7
+ // (docker container labels, the cache staging directory) must instead stay
8
+ // scoped to one checkout — two worktrees of the same repo on one host would
9
+ // otherwise see each other's paused containers and staging files. The instance
10
+ // id folds the absolute project root into the portable id for that purpose.
11
+ function getWorkInstanceId(item) {
12
+ return (0, crypto_1.createHash)('sha1').update(`${item.id()}\0${item.data.projectRoot}`).digest('hex');
13
+ }
14
+ exports.getWorkInstanceId = getWorkInstanceId;
15
+ //# sourceMappingURL=work-instance-id.js.map
@@ -9,14 +9,16 @@ const using_container_1 = require("../docker/using-container");
9
9
  const event_cache_1 = require("../executer/event-cache");
10
10
  const get_docker_executor_1 = require("../executer/get-docker-executor");
11
11
  const path_1 = require("path");
12
+ const tar_1 = require("tar");
12
13
  const plan_work_volume_1 = require("./utils/plan-work-volume");
14
+ const work_instance_id_1 = require("./work-instance-id");
13
15
  function dockerTaskRuntime(task, workEnvironment) {
14
16
  const docker = (0, execute_docker_1.getContainerCli)(workEnvironment);
15
17
  return {
16
18
  async initialize(state) {
17
19
  const currentTasks = await docker.listContainers({
18
20
  filters: {
19
- label: [`hammerkit-id=${task.id()}`],
21
+ label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(task)}`],
20
22
  },
21
23
  });
22
24
  const currentTask = currentTasks[0];
@@ -40,7 +42,7 @@ function dockerTaskRuntime(task, workEnvironment) {
40
42
  const containers = await docker.listContainers({
41
43
  all: true,
42
44
  filters: {
43
- label: [`hammerkit-id=${task.id()}`],
45
+ label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(task)}`],
44
46
  },
45
47
  });
46
48
  for (const container of containers) {
@@ -66,11 +68,11 @@ function dockerTaskRuntime(task, workEnvironment) {
66
68
  }
67
69
  }
68
70
  },
69
- async currentStateKey() {
71
+ async currentStateKey(environment) {
70
72
  const containers = await docker.listContainers({
71
73
  all: true,
72
74
  filters: {
73
- label: [`hammerkit-id=${task.id()}`],
75
+ label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(task)}`],
74
76
  },
75
77
  });
76
78
  const container = containers[0];
@@ -80,11 +82,26 @@ function dockerTaskRuntime(task, workEnvironment) {
80
82
  if (!container.Labels['hammerkit-state']) {
81
83
  return null;
82
84
  }
85
+ // outputs removed since the run (a deleted export, a pruned volume)
86
+ // leave nothing to reuse
87
+ for (const generate of task.data.generates.filter((g) => !g.inherited)) {
88
+ const present = isOnHost(generate)
89
+ ? await environment.file.exists(generate.path)
90
+ : !!(await (0, get_docker_executor_1.existsVolume)(docker, generate.volumeName));
91
+ if (!present) {
92
+ return null;
93
+ }
94
+ }
83
95
  return container.Labels['hammerkit-state'];
84
96
  },
85
97
  };
86
98
  }
87
99
  exports.dockerTaskRuntime = dockerTaskRuntime;
100
+ // File outputs are bind mounts of host files and exported directories are
101
+ // copied to the host, so both must be present on the host, not only in a volume.
102
+ function isOnHost(generate) {
103
+ return generate.isFile || generate.export;
104
+ }
88
105
  async function restoreContainer(docker, environment, item, path) {
89
106
  await (0, using_container_1.usingContainer)(docker, item, {
90
107
  abortSignal: environment.abortCtrl.signal,
@@ -95,15 +112,22 @@ async function restoreContainer(docker, environment, item, path) {
95
112
  WorkingDir: (0, execute_docker_1.convertToPosixPath)(item.data.cwd),
96
113
  Labels: {
97
114
  app: 'hammerkit',
98
- 'hammerkit-id': item.id(),
115
+ 'hammerkit-id': (0, work_instance_id_1.getWorkInstanceId)(item),
99
116
  'hammerkit-pid': process.pid.toString(),
100
117
  'hammerkit-type': 'task',
101
118
  },
102
119
  HostConfig: {
103
120
  AutoRemove: true,
104
- Binds: item.data.type === 'container-task'
105
- ? []
106
- : [...item.data.volumes.map((v) => `${v.name}:${(0, execute_docker_1.convertToPosixPath)(v.containerPath)}`)],
121
+ // mount the same volumes archiveContainer reads from, so the restored
122
+ // archives land in the task's output volumes rather than in this
123
+ // throwaway container's filesystem
124
+ Binds: item.data.type === 'container-service'
125
+ ? item.data.volumes
126
+ .filter((v) => !v.inherited)
127
+ .map((v) => `${v.name}:${(0, execute_docker_1.convertToPosixPath)(v.containerPath)}`)
128
+ : item.data.generates
129
+ .filter((v) => !v.inherited && !v.isFile)
130
+ .map((v) => `${v.volumeName}:${(0, execute_docker_1.convertToPosixPath)(v.path)}`),
107
131
  },
108
132
  }, null, async (container) => {
109
133
  for (const generate of (0, event_cache_1.getArchivePaths)(item.data, path)) {
@@ -114,6 +138,14 @@ async function restoreContainer(docker, environment, item, path) {
114
138
  }
115
139
  }
116
140
  });
141
+ if (item.data.type === 'container-task') {
142
+ const onHost = new Set(item.data.generates.filter((g) => !g.inherited && isOnHost(g)).map((g) => g.path));
143
+ for (const generate of (0, event_cache_1.getArchivePaths)(item.data, path)) {
144
+ if (onHost.has(generate.path) && (await environment.file.exists(generate.filename))) {
145
+ await (0, tar_1.extract)({ file: generate.filename, cwd: (0, path_1.dirname)(generate.path) });
146
+ }
147
+ }
148
+ }
117
149
  }
118
150
  function dockerServiceRuntime(service, workEnvironment) {
119
151
  const docker = (0, execute_docker_1.getContainerCli)(workEnvironment);
@@ -121,7 +153,7 @@ function dockerServiceRuntime(service, workEnvironment) {
121
153
  async initialize(state) {
122
154
  const currentServices = await docker.listContainers({
123
155
  filters: {
124
- label: [`hammerkit-id=${service.id()}`],
156
+ label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(service)}`],
125
157
  },
126
158
  });
127
159
  const currentService = currentServices[0];
@@ -143,7 +175,7 @@ function dockerServiceRuntime(service, workEnvironment) {
143
175
  const containers = await docker.listContainers({
144
176
  all: true,
145
177
  filters: {
146
- label: [`hammerkit-id=${service.id()}`],
178
+ label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(service)}`],
147
179
  },
148
180
  });
149
181
  for (const container of containers) {
@@ -173,7 +205,7 @@ function dockerServiceRuntime(service, workEnvironment) {
173
205
  const containers = await docker.listContainers({
174
206
  all: true,
175
207
  filters: {
176
- label: [`hammerkit-id=${service.id()}`],
208
+ label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(service)}`],
177
209
  },
178
210
  });
179
211
  const container = containers[0];
@@ -198,7 +230,7 @@ async function archiveContainer(docker, environment, item, path) {
198
230
  WorkingDir: (0, execute_docker_1.convertToPosixPath)(item.data.cwd),
199
231
  Labels: {
200
232
  app: 'hammerkit',
201
- 'hammerkit-id': item.id(),
233
+ 'hammerkit-id': (0, work_instance_id_1.getWorkInstanceId)(item),
202
234
  'hammerkit-pid': process.pid.toString(),
203
235
  'hammerkit-type': 'task',
204
236
  },
@@ -213,10 +245,16 @@ async function archiveContainer(docker, environment, item, path) {
213
245
  .map((v) => `${v.volumeName}:${(0, execute_docker_1.convertToPosixPath)(v.path)}`),
214
246
  },
215
247
  }, null, async (container) => {
248
+ const hostFiles = new Set(item.data.type === 'container-task'
249
+ ? item.data.generates.filter((g) => !g.inherited && g.isFile).map((g) => g.path)
250
+ : []);
216
251
  for (const generatedArchive of (0, event_cache_1.getArchivePaths)(item.data, path)) {
217
- const readable = await container.getArchive({
218
- path: generatedArchive.path,
219
- });
252
+ // a file output is a bind mount of a host file, so archive it from the host
253
+ const readable = hostFiles.has(generatedArchive.path)
254
+ ? (0, tar_1.create)({ cwd: (0, path_1.dirname)(generatedArchive.path) }, [(0, path_1.basename)(generatedArchive.path)])
255
+ : await container.getArchive({
256
+ path: generatedArchive.path,
257
+ });
220
258
  await environment.file.writeStream(generatedArchive.filename, readable);
221
259
  }
222
260
  });
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.kubernetesForwardRuntime = exports.kubernetesServiceRuntime = exports.kubernetesTaskRuntime = void 0;
7
+ const abort_1 = require("../executer/abort");
7
8
  const kubernetes_service_1 = require("../executer/kubernetes-service");
8
9
  const kubernetes_instance_1 = require("../kubernetes/kubernetes-instance");
9
10
  const replace_env_variables_1 = require("../environment/replace-env-variables");
@@ -27,6 +28,10 @@ function statusCodeOf(e) {
27
28
  const err = e;
28
29
  return err?.statusCode ?? err?.response?.statusCode ?? err?.body?.code;
29
30
  }
31
+ async function listJobNames(instance, kubernetes, task) {
32
+ const jobs = await instance.batchApi.listNamespacedJob(kubernetes.namespace, undefined, undefined, undefined, undefined, `hammerkit.dev/id=${task.id()}`);
33
+ return jobs.body.items.flatMap((job) => (job.metadata?.name ? [job.metadata.name] : []));
34
+ }
30
35
  function kubernetesTaskRuntime(task, kubernetes) {
31
36
  const instance = (0, kubernetes_instance_1.createKubernetesInstances)(kubernetes);
32
37
  return {
@@ -44,6 +49,11 @@ function kubernetesTaskRuntime(task, kubernetes) {
44
49
  const persistence = await (0, volumes_1.getKubernetesPersistence)(task);
45
50
  await (0, ensure_namespace_1.ensureNamespace)(instance, kubernetes.namespace);
46
51
  await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, task, persistence);
52
+ // The last job of a run stays: its state label is what currentStateKey
53
+ // reads. Jobs from an earlier run of this task go before this one starts.
54
+ for (const name of await listJobNames(instance, kubernetes, task)) {
55
+ await (0, await_running_state_1.deleteJobAndWait)(instance, kubernetes, name, options.abort);
56
+ }
47
57
  const podName = `${task.name}-${options.stateKey}`;
48
58
  let i = 0;
49
59
  for (const cmd of task.data.cmds) {
@@ -95,12 +105,19 @@ function kubernetesTaskRuntime(task, kubernetes) {
95
105
  try {
96
106
  const pod = await (0, apply_1.apply)(instance, spec);
97
107
  if (!pod.status?.succeeded) {
98
- await (0, await_running_state_1.awaitJobState)(instance, kubernetes, spec.metadata.name);
108
+ await (0, await_running_state_1.awaitJobCompletion)(instance, kubernetes, spec.metadata.name, options.abort);
99
109
  }
100
110
  task.status.write('debug', 'pod completed');
101
- await instance.batchApi.deleteNamespacedJob(podName, kubernetes.namespace);
111
+ if (i < task.data.cmds.length) {
112
+ await (0, await_running_state_1.deleteJobAndWait)(instance, kubernetes, podName, options.abort);
113
+ }
102
114
  }
103
115
  catch (e) {
116
+ if (e instanceof abort_1.AbortError) {
117
+ options.state.set({ stateKey: options.stateKey, type: 'canceled' });
118
+ return;
119
+ }
120
+ await (0, await_running_state_1.deleteJob)(instance, kubernetes, podName).catch(() => undefined);
104
121
  options.state.set({
105
122
  stateKey: options.stateKey,
106
123
  type: 'error',
@@ -139,7 +156,22 @@ function kubernetesTaskRuntime(task, kubernetes) {
139
156
  const completedStates = jobs.body.items
140
157
  .filter((j) => j.status?.succeeded && j.metadata?.labels?.['hammerkit.dev/state'])
141
158
  .map((j) => j.metadata.labels['hammerkit.dev/state']);
142
- return completedStates[completedStates.length - 1] ?? null;
159
+ const stateKey = completedStates[completedStates.length - 1] ?? null;
160
+ if (stateKey === null || !task.data.generates.some((g) => !g.inherited)) {
161
+ return stateKey;
162
+ }
163
+ // outputs live in the task's claim; without it the finished job left
164
+ // nothing to reuse
165
+ try {
166
+ const claim = await instance.coreApi.readNamespacedPersistentVolumeClaim((0, volumes_1.getVolumeName)(task), kubernetes.namespace);
167
+ return claim.body.metadata?.deletionTimestamp ? null : stateKey;
168
+ }
169
+ catch (e) {
170
+ if (statusCodeOf(e) === 404) {
171
+ return null;
172
+ }
173
+ throw e;
174
+ }
143
175
  },
144
176
  };
145
177
  }
@@ -71,10 +71,16 @@ function getLocalWorkRuntime(task) {
71
71
  },
72
72
  async currentStateKey(environment) {
73
73
  const stateFileName = getStateFilename(task);
74
- if (await environment.file.exists(stateFileName)) {
75
- return await environment.file.read(stateFileName);
74
+ if (!(await environment.file.exists(stateFileName))) {
75
+ return null;
76
76
  }
77
- return Promise.resolve(null);
77
+ // outputs deleted since the run (rm -rf dist) leave nothing to reuse
78
+ for (const generate of task.data.generates) {
79
+ if (!generate.inherited && !(await environment.file.exists(generate.path))) {
80
+ return null;
81
+ }
82
+ }
83
+ return await environment.file.read(stateFileName);
78
84
  },
79
85
  async remove(environment) {
80
86
  for (const generate of task.data.generates) {
@@ -3,19 +3,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getWorkServiceId = void 0;
4
4
  const crypto_1 = require("crypto");
5
5
  const work_service_1 = require("./work-service");
6
+ const portable_path_1 = require("./utils/portable-path");
7
+ const plan_work_volume_1 = require("./utils/plan-work-volume");
8
+ // Paths are made project-relative (portablePath) so a service id is identical
9
+ // wherever the project is checked out, like a task id.
6
10
  function getWorkServiceId(service) {
11
+ const portable = (path) => (0, portable_path_1.portablePath)(service.projectRoot, path);
7
12
  const jsonData = JSON.stringify((0, work_service_1.isContainerWorkService)(service)
8
13
  ? {
9
- cwd: service.cwd ?? undefined,
14
+ cwd: portable(service.cwd),
10
15
  image: service.image,
11
- volumes: service.volumes.map((v) => `${v.name}:${v.containerPath}`).sort(),
12
- src: service.src.map((s) => s.absolutePath).sort(),
16
+ volumes: service.volumes
17
+ .map((v) =>
18
+ // a derived volume name hashes the absolute path, so only an
19
+ // explicitly declared name participates
20
+ v.name === (0, plan_work_volume_1.getVolumeName)(v.containerPath)
21
+ ? portable(v.containerPath)
22
+ : `${v.name}:${portable(v.containerPath)}`)
23
+ .sort(),
24
+ src: service.src.map((s) => portable(s.absolutePath)).sort(),
13
25
  mounts: service.mounts.map((m) => m.mount).sort(),
14
26
  }
15
27
  : {
16
28
  context: service.context,
17
29
  selector: service.selector,
18
- kubeconfig: service.kubeconfig,
30
+ kubeconfig: portable(service.kubeconfig),
19
31
  });
20
32
  return (0, crypto_1.createHash)('sha1').update(jsonData).digest('hex');
21
33
  }