hammerkit 1.6.0 → 1.8.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 (277) hide show
  1. package/README.md +7 -7
  2. package/build.schema.json +59 -15
  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 +17 -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 +18 -3
  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-docker.js +18 -2
  27. package/dist/executer/execute-work-service.js +10 -1
  28. package/dist/executer/execute-work-task.js +111 -26
  29. package/dist/executer/execute-work-tree.js +26 -1
  30. package/dist/executer/execution-steps.js +3 -8
  31. package/dist/executer/get-container-binds.js +13 -1
  32. package/dist/executer/get-scheduler-execute-result.js +4 -1
  33. package/dist/executer/lazy-resolver.js +15 -3
  34. package/dist/executer/reset-work-tree.js +2 -0
  35. package/dist/executer/run-summary.js +102 -0
  36. package/dist/executer/scheduler/enqueue-next.js +15 -60
  37. package/dist/executer/scheduler/state-key.js +78 -0
  38. package/dist/index.js +14 -1
  39. package/dist/kubernetes/apply.js +6 -7
  40. package/dist/kubernetes/await-running-state.js +100 -60
  41. package/dist/kubernetes/ensure-http-route.js +7 -10
  42. package/dist/kubernetes/ensure-namespace.js +9 -10
  43. package/dist/kubernetes/ensure-persistent-data.js +6 -5
  44. package/dist/kubernetes/get-service-ip.js +2 -2
  45. package/dist/kubernetes/kubernetes-instance.js +0 -4
  46. package/dist/kubernetes/remove-persistent-data.js +16 -6
  47. package/dist/kubernetes/resolve-pod-name.js +7 -7
  48. package/dist/log.js +16 -6
  49. package/dist/logging/live-logger.js +2 -1
  50. package/dist/optimizer/calculate-checksum.js +8 -2
  51. package/dist/optimizer/get-work-cache-stats.js +66 -16
  52. package/dist/optimizer/work-task-cache-description.js +34 -5
  53. package/dist/planner/project-root.js +26 -0
  54. package/dist/planner/utils/append-work-environment.js +1 -1
  55. package/dist/planner/utils/append-work-service.js +2 -1
  56. package/dist/planner/utils/append-work-task.js +7 -1
  57. package/dist/planner/utils/get-outputs-to-reset.js +20 -0
  58. package/dist/planner/utils/parse-work-generate.js +2 -2
  59. package/dist/planner/utils/parse-work-source.js +35 -30
  60. package/dist/planner/utils/portable-path.js +30 -0
  61. package/dist/planner/work-cache-id.js +2 -2
  62. package/dist/planner/work-graph-serializer.js +103 -0
  63. package/dist/planner/work-instance-id.js +15 -0
  64. package/dist/planner/work-runtime-docker.js +58 -15
  65. package/dist/planner/work-runtime-kubernetes.js +95 -45
  66. package/dist/planner/work-runtime-local.js +31 -8
  67. package/dist/planner/work-service-id.js +16 -4
  68. package/dist/program.js +235 -1
  69. package/dist/schema/build-file-base-task-schema.js +4 -0
  70. package/dist/schema/build-file-environment-schema-http-route.js +15 -0
  71. package/dist/schema/build-file-environment-schema.js +8 -2
  72. package/dist/schema/build-file-schema.js +1 -1
  73. package/dist/schema/cache-schema.js +21 -2
  74. package/dist/schema/duration-schema.js +18 -0
  75. package/dist/schema/envs-schema.js +1 -1
  76. package/dist/schema/reference-parser.js +3 -0
  77. package/dist/schema/size-schema.js +18 -0
  78. package/dist/schema/work-scope-parser.js +15 -2
  79. package/dist/utils/units.js +87 -0
  80. package/package.json +20 -9
  81. package/.agents/FEEDBACK.md +0 -74
  82. package/.agents/skills/before-commit/SKILL.md +0 -101
  83. package/.agents/skills/do-the-work/SKILL.md +0 -64
  84. package/.agents/skills/fix-ci/SKILL.md +0 -98
  85. package/.agents/skills/speckit-git-commit/SKILL.md +0 -53
  86. package/.agents/skills/speckit-git-feature/SKILL.md +0 -72
  87. package/.agents/skills/speckit-git-initialize/SKILL.md +0 -54
  88. package/.agents/skills/speckit-git-remote/SKILL.md +0 -50
  89. package/.agents/skills/speckit-git-validate/SKILL.md +0 -54
  90. package/.devcontainer/Dockerfile +0 -5
  91. package/.devcontainer/devcontainer.json +0 -40
  92. package/.gitbook.yaml +0 -5
  93. package/.specify/extensions/.registry +0 -37
  94. package/.specify/extensions/git/README.md +0 -100
  95. package/.specify/extensions/git/commands/speckit.git.commit.md +0 -48
  96. package/.specify/extensions/git/commands/speckit.git.feature.md +0 -67
  97. package/.specify/extensions/git/commands/speckit.git.initialize.md +0 -49
  98. package/.specify/extensions/git/commands/speckit.git.remote.md +0 -45
  99. package/.specify/extensions/git/commands/speckit.git.validate.md +0 -49
  100. package/.specify/extensions/git/config-template.yml +0 -62
  101. package/.specify/extensions/git/extension.yml +0 -140
  102. package/.specify/extensions/git/git-config.yml +0 -62
  103. package/.specify/extensions.yml +0 -149
  104. package/.specify/feature.json +0 -3
  105. package/.specify/init-options.json +0 -10
  106. package/.specify/integration.json +0 -15
  107. package/.specify/integrations/claude.manifest.json +0 -16
  108. package/.specify/integrations/speckit.manifest.json +0 -17
  109. package/.specify/memory/constitution.md +0 -162
  110. package/.specify/templates/checklist-template.md +0 -40
  111. package/.specify/templates/constitution-template.md +0 -50
  112. package/.specify/templates/plan-template.md +0 -113
  113. package/.specify/templates/spec-template.md +0 -131
  114. package/.specify/templates/tasks-template.md +0 -252
  115. package/.specify/workflows/speckit/workflow.yml +0 -77
  116. package/.specify/workflows/workflow-registry.json +0 -13
  117. package/AGENTS.md +0 -94
  118. package/Dockerfile +0 -32
  119. package/FOLLOWUPS.md +0 -34
  120. package/SUMMARY.md +0 -68
  121. package/VALUES.md +0 -53
  122. package/dist/cache/auto-pull.spec.js +0 -123
  123. package/dist/cache/backends/local-cache-backend.spec.js +0 -72
  124. package/dist/cache/backends/s3-cache-backend.spec.js +0 -118
  125. package/dist/cache/cache-backend-registry.spec.js +0 -22
  126. package/dist/cache/resolve-cache.spec.js +0 -52
  127. package/dist/cli.spec.js +0 -23
  128. package/dist/console/get-logger.spec.js +0 -23
  129. package/dist/docker/get-default-platform.spec.js +0 -29
  130. package/dist/docker/longest-common-prefix.spec.js +0 -26
  131. package/dist/docker/package.spec.js +0 -145
  132. package/dist/docker/remove-container.spec.js +0 -32
  133. package/dist/docker/using-container.spec.js +0 -90
  134. package/dist/environment/merge-environment-variables.spec.js +0 -24
  135. package/dist/environment/replace-env-variables.spec.js +0 -47
  136. package/dist/executer/abort.spec.js +0 -18
  137. package/dist/executer/environment-mock.spec.js +0 -22
  138. package/dist/executer/execute.spec.js +0 -51
  139. package/dist/executer/get-service-containers.spec.js +0 -59
  140. package/dist/executer/get-service-env-hints.spec.js +0 -65
  141. package/dist/executer/label-values.spec.js +0 -61
  142. package/dist/executer/lazy-resolver.spec.js +0 -20
  143. package/dist/executer/process-manager.spec.js +0 -54
  144. package/dist/executer/scheduler/enqueue-next.spec.js +0 -136
  145. package/dist/executer/state-resolver.spec.js +0 -37
  146. package/dist/executer/state.spec.js +0 -66
  147. package/dist/executer/states.spec.js +0 -14
  148. package/dist/file/get-file-context.spec.js +0 -178
  149. package/dist/file/move-files.spec.js +0 -48
  150. package/dist/kubernetes/ensure-http-route.spec.js +0 -70
  151. package/dist/kubernetes/ensure-ingress.js +0 -54
  152. package/dist/kubernetes/ensure-namespace.spec.js +0 -39
  153. package/dist/kubernetes/resolve-pod-name.spec.js +0 -81
  154. package/dist/log.spec.js +0 -131
  155. package/dist/logging/grouped-logger.spec.js +0 -84
  156. package/dist/logging/interactive-logger.spec.js +0 -57
  157. package/dist/logging/live-logger.spec.js +0 -37
  158. package/dist/logging/loggers.spec.js +0 -154
  159. package/dist/optimizer/calculate-checksum.spec.js +0 -34
  160. package/dist/optimizer/get-cache-directory.spec.js +0 -59
  161. package/dist/optimizer/get-work-cache-stats.spec.js +0 -79
  162. package/dist/optimizer/work-task-cache-description.spec.js +0 -45
  163. package/dist/parser/default-build-file.spec.js +0 -48
  164. package/dist/parser/parse-label-arguments.spec.js +0 -27
  165. package/dist/parser/read-build-file.spec.js +0 -89
  166. package/dist/parser/read-env-file.spec.js +0 -42
  167. package/dist/planner/utils/append-work-service.spec.js +0 -71
  168. package/dist/planner/utils/get-container-user.spec.js +0 -30
  169. package/dist/planner/utils/get-default-kube-config.spec.js +0 -11
  170. package/dist/planner/utils/normalize-path.spec.js +0 -24
  171. package/dist/planner/utils/parse-work-command.spec.js +0 -21
  172. package/dist/planner/utils/parse-work-generate.spec.js +0 -45
  173. package/dist/planner/utils/parse-work-mount.spec.js +0 -62
  174. package/dist/planner/utils/parse-work-mounts.spec.js +0 -19
  175. package/dist/planner/utils/parse-work-port.spec.js +0 -24
  176. package/dist/planner/utils/parse-work-ports.spec.js +0 -24
  177. package/dist/planner/utils/parse-work-source.spec.js +0 -58
  178. package/dist/planner/utils/parse-work-volume.spec.js +0 -18
  179. package/dist/planner/utils/plan-work-tasks.spec.js +0 -30
  180. package/dist/planner/utils/plan-work-volume.spec.js +0 -33
  181. package/dist/planner/utils/split-name.spec.js +0 -16
  182. package/dist/planner/utils/template-value.spec.js +0 -14
  183. package/dist/planner/validate.spec.js +0 -251
  184. package/dist/planner/work-cache-id.spec.js +0 -173
  185. package/dist/planner/work-runtime-local.spec.js +0 -193
  186. package/dist/program-commands.spec.js +0 -84
  187. package/dist/program.spec.js +0 -43
  188. package/dist/schema/build-file-environment-schema-ingress.js +0 -14
  189. package/dist/schema/build-file-schema.spec.js +0 -61
  190. package/dist/schema/doc-build-files.spec.js +0 -223
  191. package/dist/schema/parse-error.spec.js +0 -18
  192. package/dist/schema/reference-parser.spec.js +0 -83
  193. package/dist/schema/schema-parser.spec.js +0 -80
  194. package/dist/schema/type-guards.spec.js +0 -23
  195. package/dist/schema/work-scope-parser.spec.js +0 -66
  196. package/dist/service/update-service-status.spec.js +0 -27
  197. package/dist/testing/ensure-local-registry.js +0 -134
  198. package/dist/testing/example-test-suite.js +0 -63
  199. package/dist/testing/expect.js +0 -82
  200. package/dist/testing/get-test-suite.js +0 -9
  201. package/dist/testing/http-get.js +0 -21
  202. package/dist/testing/integration/cache.spec.js +0 -49
  203. package/dist/testing/integration/cancellation.spec.js +0 -45
  204. package/dist/testing/integration/clean.spec.js +0 -43
  205. package/dist/testing/integration/cmd.spec.js +0 -15
  206. package/dist/testing/integration/concurrency.spec.js +0 -19
  207. package/dist/testing/integration/docker-package-registry.spec.js +0 -44
  208. package/dist/testing/integration/docker-service-crash.spec.js +0 -36
  209. package/dist/testing/integration/docker.spec.js +0 -18
  210. package/dist/testing/integration/env.spec.js +0 -48
  211. package/dist/testing/integration/environment.spec.js +0 -97
  212. package/dist/testing/integration/error.spec.js +0 -19
  213. package/dist/testing/integration/export.spec.js +0 -22
  214. package/dist/testing/integration/extend.spec.js +0 -29
  215. package/dist/testing/integration/glob.spec.js +0 -40
  216. package/dist/testing/integration/include.spec.js +0 -31
  217. package/dist/testing/integration/invalid.spec.js +0 -17
  218. package/dist/testing/integration/invalid_loop.spec.js +0 -56
  219. package/dist/testing/integration/k8s-state-cache.spec.js +0 -41
  220. package/dist/testing/integration/kubernetes-service.spec.js +0 -148
  221. package/dist/testing/integration/kubernetes.spec.js +0 -99
  222. package/dist/testing/integration/labels.spec.js +0 -42
  223. package/dist/testing/integration/local.spec.js +0 -14
  224. package/dist/testing/integration/monorepo.spec.js +0 -15
  225. package/dist/testing/integration/multi-line.spec.js +0 -17
  226. package/dist/testing/integration/reference.spec.js +0 -21
  227. package/dist/testing/integration/services.spec.js +0 -38
  228. package/dist/testing/integration/store.spec.js +0 -52
  229. package/dist/testing/integration/unknown.spec.js +0 -28
  230. package/dist/testing/integration/validate.spec.js +0 -160
  231. package/dist/testing/integration/watch-dependency.spec.js +0 -33
  232. package/dist/testing/integration/watch.spec.js +0 -45
  233. package/dist/testing/requires-kubernetes.js +0 -18
  234. package/dist/testing/requires-linux-containers.js +0 -18
  235. package/dist/testing/requires-windows-containers.js +0 -18
  236. package/dist/testing/test-case.js +0 -75
  237. package/dist/testing/test-streams.js +0 -28
  238. package/dist/testing/test-suite-setup.js +0 -3
  239. package/dist/testing/test-suite.js +0 -3
  240. package/dist/testing/testing-timeout.js +0 -32
  241. package/dist/utils/abort-event.spec.js +0 -46
  242. package/dist/utils/buffer-context.spec.js +0 -38
  243. package/dist/utils/ci.spec.js +0 -53
  244. package/dist/utils/debouncer.spec.js +0 -58
  245. package/dist/utils/emitter.spec.js +0 -34
  246. package/dist/utils/empty-writable.spec.js +0 -18
  247. package/dist/utils/environment-config.spec.js +0 -26
  248. package/dist/utils/fail-never.spec.js +0 -9
  249. package/dist/utils/sleep.spec.js +0 -22
  250. package/dist/version.spec.js +0 -12
  251. package/specs/build-file/caches/spec.md +0 -114
  252. package/specs/build-file/environment-variables/spec.md +0 -82
  253. package/specs/build-file/includes/spec.md +0 -60
  254. package/specs/build-file/references/spec.md +0 -56
  255. package/specs/build-file/spec.md +0 -62
  256. package/specs/cli/clean/spec.md +0 -85
  257. package/specs/cli/down/spec.md +0 -85
  258. package/specs/cli/execute/spec.md +0 -101
  259. package/specs/cli/init/spec.md +0 -64
  260. package/specs/cli/ls/spec.md +0 -66
  261. package/specs/cli/package/spec.md +0 -110
  262. package/specs/cli/spec.md +0 -73
  263. package/specs/cli/store-restore/spec.md +0 -74
  264. package/specs/cli/up/spec.md +0 -80
  265. package/specs/cli/validate/spec.md +0 -70
  266. package/specs/labels/spec.md +0 -91
  267. package/specs/service/container/spec.md +0 -120
  268. package/specs/service/kubernetes/spec.md +0 -97
  269. package/specs/service/spec.md +0 -105
  270. package/specs/task/caching/spec.md +0 -107
  271. package/specs/task/container/spec.md +0 -118
  272. package/specs/task/dependencies/spec.md +0 -70
  273. package/specs/task/extending/spec.md +0 -74
  274. package/specs/task/kubernetes/spec.md +0 -125
  275. package/specs/task/needs/spec.md +0 -59
  276. package/specs/task/spec.md +0 -87
  277. 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,17 @@ 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");
15
+ const stream_1 = require("stream");
13
16
  function dockerTaskRuntime(task, workEnvironment) {
14
17
  const docker = (0, execute_docker_1.getContainerCli)(workEnvironment);
15
18
  return {
16
19
  async initialize(state) {
17
20
  const currentTasks = await docker.listContainers({
18
21
  filters: {
19
- label: [`hammerkit-id=${task.id()}`],
22
+ label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(task)}`],
20
23
  },
21
24
  });
22
25
  const currentTask = currentTasks[0];
@@ -40,7 +43,7 @@ function dockerTaskRuntime(task, workEnvironment) {
40
43
  const containers = await docker.listContainers({
41
44
  all: true,
42
45
  filters: {
43
- label: [`hammerkit-id=${task.id()}`],
46
+ label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(task)}`],
44
47
  },
45
48
  });
46
49
  for (const container of containers) {
@@ -66,11 +69,11 @@ function dockerTaskRuntime(task, workEnvironment) {
66
69
  }
67
70
  }
68
71
  },
69
- async currentStateKey() {
72
+ async currentStateKey(environment) {
70
73
  const containers = await docker.listContainers({
71
74
  all: true,
72
75
  filters: {
73
- label: [`hammerkit-id=${task.id()}`],
76
+ label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(task)}`],
74
77
  },
75
78
  });
76
79
  const container = containers[0];
@@ -80,11 +83,26 @@ function dockerTaskRuntime(task, workEnvironment) {
80
83
  if (!container.Labels['hammerkit-state']) {
81
84
  return null;
82
85
  }
86
+ // outputs removed since the run (a deleted export, a pruned volume)
87
+ // leave nothing to reuse
88
+ for (const generate of task.data.generates.filter((g) => !g.inherited)) {
89
+ const present = isOnHost(generate)
90
+ ? await environment.file.exists(generate.path)
91
+ : !!(await (0, get_docker_executor_1.existsVolume)(docker, generate.volumeName));
92
+ if (!present) {
93
+ return null;
94
+ }
95
+ }
83
96
  return container.Labels['hammerkit-state'];
84
97
  },
85
98
  };
86
99
  }
87
100
  exports.dockerTaskRuntime = dockerTaskRuntime;
101
+ // File outputs are bind mounts of host files and exported directories are
102
+ // copied to the host, so both must be present on the host, not only in a volume.
103
+ function isOnHost(generate) {
104
+ return generate.isFile || generate.export;
105
+ }
88
106
  async function restoreContainer(docker, environment, item, path) {
89
107
  await (0, using_container_1.usingContainer)(docker, item, {
90
108
  abortSignal: environment.abortCtrl.signal,
@@ -95,25 +113,44 @@ async function restoreContainer(docker, environment, item, path) {
95
113
  WorkingDir: (0, execute_docker_1.convertToPosixPath)(item.data.cwd),
96
114
  Labels: {
97
115
  app: 'hammerkit',
98
- 'hammerkit-id': item.id(),
116
+ 'hammerkit-id': (0, work_instance_id_1.getWorkInstanceId)(item),
99
117
  'hammerkit-pid': process.pid.toString(),
100
118
  'hammerkit-type': 'task',
101
119
  },
102
120
  HostConfig: {
103
121
  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)}`)],
122
+ // mount the same volumes archiveContainer reads from, so the restored
123
+ // archives land in the task's output volumes rather than in this
124
+ // throwaway container's filesystem
125
+ Binds: item.data.type === 'container-service'
126
+ ? item.data.volumes
127
+ .filter((v) => !v.inherited)
128
+ .map((v) => `${v.name}:${(0, execute_docker_1.convertToPosixPath)(v.containerPath)}`)
129
+ : item.data.generates
130
+ .filter((v) => !v.inherited && !v.isFile)
131
+ .map((v) => `${v.volumeName}:${(0, execute_docker_1.convertToPosixPath)(v.path)}`),
107
132
  },
108
133
  }, null, async (container) => {
109
134
  for (const generate of (0, event_cache_1.getArchivePaths)(item.data, path)) {
110
135
  if (await environment.file.exists(generate.filename)) {
136
+ // a cache hit means exactly the stored outputs: whatever a failed or
137
+ // older run left in the volume goes first
138
+ await (0, execute_docker_1.clearContainerDirectory)(item.status, environment, container, generate.path);
111
139
  await container.putArchive(environment.file.readStream(generate.filename), {
112
140
  path: (0, path_1.dirname)(generate.path),
113
141
  });
114
142
  }
115
143
  }
116
144
  });
145
+ if (item.data.type === 'container-task') {
146
+ const onHost = new Set(item.data.generates.filter((g) => !g.inherited && isOnHost(g)).map((g) => g.path));
147
+ for (const generate of (0, event_cache_1.getArchivePaths)(item.data, path)) {
148
+ if (onHost.has(generate.path) && (await environment.file.exists(generate.filename))) {
149
+ await environment.file.remove(generate.path);
150
+ await (0, tar_1.extract)({ file: generate.filename, cwd: (0, path_1.dirname)(generate.path) });
151
+ }
152
+ }
153
+ }
117
154
  }
118
155
  function dockerServiceRuntime(service, workEnvironment) {
119
156
  const docker = (0, execute_docker_1.getContainerCli)(workEnvironment);
@@ -121,7 +158,7 @@ function dockerServiceRuntime(service, workEnvironment) {
121
158
  async initialize(state) {
122
159
  const currentServices = await docker.listContainers({
123
160
  filters: {
124
- label: [`hammerkit-id=${service.id()}`],
161
+ label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(service)}`],
125
162
  },
126
163
  });
127
164
  const currentService = currentServices[0];
@@ -143,7 +180,7 @@ function dockerServiceRuntime(service, workEnvironment) {
143
180
  const containers = await docker.listContainers({
144
181
  all: true,
145
182
  filters: {
146
- label: [`hammerkit-id=${service.id()}`],
183
+ label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(service)}`],
147
184
  },
148
185
  });
149
186
  for (const container of containers) {
@@ -173,7 +210,7 @@ function dockerServiceRuntime(service, workEnvironment) {
173
210
  const containers = await docker.listContainers({
174
211
  all: true,
175
212
  filters: {
176
- label: [`hammerkit-id=${service.id()}`],
213
+ label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(service)}`],
177
214
  },
178
215
  });
179
216
  const container = containers[0];
@@ -198,7 +235,7 @@ async function archiveContainer(docker, environment, item, path) {
198
235
  WorkingDir: (0, execute_docker_1.convertToPosixPath)(item.data.cwd),
199
236
  Labels: {
200
237
  app: 'hammerkit',
201
- 'hammerkit-id': item.id(),
238
+ 'hammerkit-id': (0, work_instance_id_1.getWorkInstanceId)(item),
202
239
  'hammerkit-pid': process.pid.toString(),
203
240
  'hammerkit-type': 'task',
204
241
  },
@@ -213,10 +250,16 @@ async function archiveContainer(docker, environment, item, path) {
213
250
  .map((v) => `${v.volumeName}:${(0, execute_docker_1.convertToPosixPath)(v.path)}`),
214
251
  },
215
252
  }, null, async (container) => {
253
+ const hostFiles = new Set(item.data.type === 'container-task'
254
+ ? item.data.generates.filter((g) => !g.inherited && g.isFile).map((g) => g.path)
255
+ : []);
216
256
  for (const generatedArchive of (0, event_cache_1.getArchivePaths)(item.data, path)) {
217
- const readable = await container.getArchive({
218
- path: generatedArchive.path,
219
- });
257
+ // a file output is a bind mount of a host file, so archive it from the host
258
+ const readable = hostFiles.has(generatedArchive.path)
259
+ ? stream_1.Readable.from((0, tar_1.create)({ cwd: (0, path_1.dirname)(generatedArchive.path) }, [(0, path_1.basename)(generatedArchive.path)]))
260
+ : await container.getArchive({
261
+ path: generatedArchive.path,
262
+ });
220
263
  await environment.file.writeStream(generatedArchive.filename, readable);
221
264
  }
222
265
  });
@@ -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");
@@ -15,7 +16,6 @@ const ensure_persistent_data_1 = require("../kubernetes/ensure-persistent-data")
15
16
  const await_running_state_1 = require("../kubernetes/await-running-state");
16
17
  const volumes_1 = require("../kubernetes/volumes");
17
18
  const resources_1 = require("../kubernetes/resources");
18
- const ensure_ingress_1 = require("../kubernetes/ensure-ingress");
19
19
  const ensure_http_route_1 = require("../kubernetes/ensure-http-route");
20
20
  const find_process_1 = __importDefault(require("find-process"));
21
21
  const log_1 = require("../log");
@@ -23,9 +23,12 @@ const version_1 = require("../version");
23
23
  const restore_kubernetes_data_1 = require("../kubernetes/restore-kubernetes-data");
24
24
  const store_kubernetes_data_1 = require("../kubernetes/store-kubernetes-data");
25
25
  const remove_persistent_data_1 = require("../kubernetes/remove-persistent-data");
26
- function statusCodeOf(e) {
27
- const err = e;
28
- return err?.statusCode ?? err?.response?.statusCode ?? err?.body?.code;
26
+ async function listJobNames(instance, kubernetes, task) {
27
+ const jobs = await instance.batchApi.listNamespacedJob({
28
+ namespace: kubernetes.namespace,
29
+ labelSelector: `hammerkit.dev/id=${task.id()}`,
30
+ });
31
+ return jobs.items.flatMap((job) => (job.metadata?.name ? [job.metadata.name] : []));
29
32
  }
30
33
  function kubernetesTaskRuntime(task, kubernetes) {
31
34
  const instance = (0, kubernetes_instance_1.createKubernetesInstances)(kubernetes);
@@ -44,6 +47,11 @@ function kubernetesTaskRuntime(task, kubernetes) {
44
47
  const persistence = await (0, volumes_1.getKubernetesPersistence)(task);
45
48
  await (0, ensure_namespace_1.ensureNamespace)(instance, kubernetes.namespace);
46
49
  await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, task, persistence);
50
+ // The last job of a run stays: its state label is what currentStateKey
51
+ // reads. Jobs from an earlier run of this task go before this one starts.
52
+ for (const name of await listJobNames(instance, kubernetes, task)) {
53
+ await (0, await_running_state_1.deleteJobAndWait)(instance, kubernetes, name, options.abort);
54
+ }
47
55
  const podName = `${task.name}-${options.stateKey}`;
48
56
  let i = 0;
49
57
  for (const cmd of task.data.cmds) {
@@ -95,12 +103,19 @@ function kubernetesTaskRuntime(task, kubernetes) {
95
103
  try {
96
104
  const pod = await (0, apply_1.apply)(instance, spec);
97
105
  if (!pod.status?.succeeded) {
98
- await (0, await_running_state_1.awaitJobState)(instance, kubernetes, spec.metadata.name);
106
+ await (0, await_running_state_1.awaitJobCompletion)(instance, kubernetes, spec.metadata.name, options.abort);
99
107
  }
100
108
  task.status.write('debug', 'pod completed');
101
- await instance.batchApi.deleteNamespacedJob(podName, kubernetes.namespace);
109
+ if (i < task.data.cmds.length) {
110
+ await (0, await_running_state_1.deleteJobAndWait)(instance, kubernetes, podName, options.abort);
111
+ }
102
112
  }
103
113
  catch (e) {
114
+ if (e instanceof abort_1.AbortError) {
115
+ options.state.set({ stateKey: options.stateKey, type: 'canceled' });
116
+ return;
117
+ }
118
+ await (0, await_running_state_1.deleteJob)(instance, kubernetes, podName).catch(() => undefined);
104
119
  options.state.set({
105
120
  stateKey: options.stateKey,
106
121
  type: 'error',
@@ -111,22 +126,34 @@ function kubernetesTaskRuntime(task, kubernetes) {
111
126
  }
112
127
  },
113
128
  async stop() {
114
- const jobs = await instance.batchApi.listNamespacedJob(kubernetes.namespace, undefined, undefined, undefined, undefined, `hammerkit.dev/id=${task.id()}`);
115
- for (const pod of jobs.body.items) {
129
+ const jobs = await instance.batchApi.listNamespacedJob({
130
+ namespace: kubernetes.namespace,
131
+ labelSelector: `hammerkit.dev/id=${task.id()}`,
132
+ });
133
+ for (const pod of jobs.items) {
116
134
  if (pod.metadata?.name) {
117
- await instance.batchApi.deleteNamespacedJob(pod.metadata.name, kubernetes.namespace);
135
+ await instance.batchApi.deleteNamespacedJob({ name: pod.metadata.name, namespace: kubernetes.namespace });
118
136
  }
119
137
  }
120
- const deployments = await instance.appsApi.listNamespacedDeployment(kubernetes.namespace, undefined, undefined, undefined, undefined, `hammerkit.dev/id=${task.id()}`);
121
- for (const deploy of deployments.body.items) {
138
+ const deployments = await instance.appsApi.listNamespacedDeployment({
139
+ namespace: kubernetes.namespace,
140
+ labelSelector: `hammerkit.dev/id=${task.id()}`,
141
+ });
142
+ for (const deploy of deployments.items) {
122
143
  if (deploy.metadata?.name) {
123
- await instance.appsApi.deleteNamespacedDeployment(deploy.metadata.name, kubernetes.namespace);
144
+ await instance.appsApi.deleteNamespacedDeployment({
145
+ name: deploy.metadata.name,
146
+ namespace: kubernetes.namespace,
147
+ });
124
148
  }
125
149
  }
126
- const pods = await instance.coreApi.listNamespacedPod(kubernetes.namespace, undefined, undefined, undefined, undefined, `hammerkit.dev/id=${task.id()}`);
127
- for (const pod of pods.body.items) {
150
+ const pods = await instance.coreApi.listNamespacedPod({
151
+ namespace: kubernetes.namespace,
152
+ labelSelector: `hammerkit.dev/id=${task.id()}`,
153
+ });
154
+ for (const pod of pods.items) {
128
155
  if (pod.metadata?.name) {
129
- await instance.coreApi.deleteNamespacedPod(pod.metadata.name, kubernetes.namespace);
156
+ await instance.coreApi.deleteNamespacedPod({ name: pod.metadata.name, namespace: kubernetes.namespace });
130
157
  }
131
158
  }
132
159
  },
@@ -135,11 +162,32 @@ function kubernetesTaskRuntime(task, kubernetes) {
135
162
  await (0, remove_persistent_data_1.removePersistentData)(instance, kubernetes, task);
136
163
  },
137
164
  async currentStateKey() {
138
- const jobs = await instance.batchApi.listNamespacedJob(kubernetes.namespace, undefined, undefined, undefined, undefined, `hammerkit.dev/id=${task.id()}`);
139
- const completedStates = jobs.body.items
165
+ const jobs = await instance.batchApi.listNamespacedJob({
166
+ namespace: kubernetes.namespace,
167
+ labelSelector: `hammerkit.dev/id=${task.id()}`,
168
+ });
169
+ const completedStates = jobs.items
140
170
  .filter((j) => j.status?.succeeded && j.metadata?.labels?.['hammerkit.dev/state'])
141
171
  .map((j) => j.metadata.labels['hammerkit.dev/state']);
142
- return completedStates[completedStates.length - 1] ?? null;
172
+ const stateKey = completedStates[completedStates.length - 1] ?? null;
173
+ if (stateKey === null || !task.data.generates.some((g) => !g.inherited)) {
174
+ return stateKey;
175
+ }
176
+ // outputs live in the task's claim; without it the finished job left
177
+ // nothing to reuse
178
+ try {
179
+ const claim = await instance.coreApi.readNamespacedPersistentVolumeClaim({
180
+ name: (0, volumes_1.getVolumeName)(task),
181
+ namespace: kubernetes.namespace,
182
+ });
183
+ return claim.metadata?.deletionTimestamp ? null : stateKey;
184
+ }
185
+ catch (e) {
186
+ if ((0, apply_1.statusCodeOf)(e) === 404) {
187
+ return null;
188
+ }
189
+ throw e;
190
+ }
143
191
  },
144
192
  };
145
193
  }
@@ -157,10 +205,16 @@ function kubernetesServiceRuntime(service, kubernetes) {
157
205
  await (0, store_kubernetes_data_1.storeKubernetesData)(service, kubernetes, instance, environment, path);
158
206
  },
159
207
  async stop() {
160
- const deployments = await instance.appsApi.listNamespacedDeployment(kubernetes.namespace, undefined, undefined, undefined, undefined, `hammerkit.dev/id=${service.id()}`);
161
- for (const deployment of deployments.body.items) {
208
+ const deployments = await instance.appsApi.listNamespacedDeployment({
209
+ namespace: kubernetes.namespace,
210
+ labelSelector: `hammerkit.dev/id=${service.id()}`,
211
+ });
212
+ for (const deployment of deployments.items) {
162
213
  if (deployment.metadata?.name) {
163
- await instance.appsApi.deleteNamespacedDeployment(deployment.metadata.name, kubernetes.namespace);
214
+ await instance.appsApi.deleteNamespacedDeployment({
215
+ name: deployment.metadata.name,
216
+ namespace: kubernetes.namespace,
217
+ });
164
218
  }
165
219
  }
166
220
  },
@@ -177,29 +231,16 @@ function kubernetesServiceRuntime(service, kubernetes) {
177
231
  stateKey: options.stateKey,
178
232
  remote: null,
179
233
  });
180
- const ingresses = kubernetes.ingresses.filter((i) => i.service === service.name);
181
- for (const ingress of ingresses) {
182
- if (ingress.kind === 'httproute') {
183
- await (0, ensure_http_route_1.ensureHttpRoute)(instance, kubernetes, ingress, service);
184
- }
185
- else {
186
- await (0, ensure_ingress_1.ensureIngress)(instance, kubernetes, ingress, service);
187
- }
234
+ for (const route of kubernetes.httpRoutes.filter((r) => r.service === service.name)) {
235
+ await (0, ensure_http_route_1.ensureHttpRoute)(instance, kubernetes, route, service);
188
236
  }
189
237
  },
190
238
  async remove() {
191
- const ingresses = await instance.networkingApi.listNamespacedIngress(kubernetes.namespace, undefined, undefined, undefined, undefined, `hammerkit.dev/id=${service.id()}`);
192
- for (const ingress of ingresses.body.items) {
193
- if (ingress.metadata?.name) {
194
- await instance.networkingApi.deleteNamespacedIngress(ingress.metadata.name, kubernetes.namespace);
195
- }
196
- }
197
239
  // HTTPRoutes are a CRD, so they are deleted by name through the generic
198
240
  // objectApi rather than listed (a list would error on clusters without the
199
- // Gateway API CRDs even when none are declared). Only entries this service
200
- // declared as httproute are removed; a missing route (404) is fine.
201
- const httpRoutes = kubernetes.ingresses.filter((i) => i.kind === 'httproute' && i.service === service.name);
202
- for (const route of httpRoutes) {
241
+ // Gateway API CRDs even when none are declared). Only the routes this
242
+ // service declares are removed; a missing route (404) is fine.
243
+ for (const route of kubernetes.httpRoutes.filter((r) => r.service === service.name)) {
203
244
  try {
204
245
  await instance.objectApi.delete({
205
246
  apiVersion: ensure_http_route_1.HTTP_ROUTE_API_VERSION,
@@ -208,23 +249,32 @@ function kubernetesServiceRuntime(service, kubernetes) {
208
249
  });
209
250
  }
210
251
  catch (e) {
211
- if (statusCodeOf(e) !== 404) {
252
+ if ((0, apply_1.statusCodeOf)(e) !== 404) {
212
253
  throw e;
213
254
  }
214
255
  }
215
256
  }
216
- const services = await instance.coreApi.listNamespacedService(kubernetes.namespace, undefined, undefined, undefined, undefined, `hammerkit.dev/id=${service.id()}`);
217
- for (const service of services.body.items) {
257
+ const services = await instance.coreApi.listNamespacedService({
258
+ namespace: kubernetes.namespace,
259
+ labelSelector: `hammerkit.dev/id=${service.id()}`,
260
+ });
261
+ for (const service of services.items) {
218
262
  if (service.metadata?.name) {
219
- await instance.coreApi.deleteNamespacedService(service.metadata.name, kubernetes.namespace);
263
+ await instance.coreApi.deleteNamespacedService({
264
+ name: service.metadata.name,
265
+ namespace: kubernetes.namespace,
266
+ });
220
267
  }
221
268
  }
222
269
  await this.stop();
223
270
  await (0, remove_persistent_data_1.removePersistentData)(instance, kubernetes, service);
224
271
  },
225
272
  async currentStateKey() {
226
- const deployments = await instance.appsApi.listNamespacedDeployment(kubernetes.namespace, undefined, undefined, undefined, undefined, `hammerkit.dev/id=${service.id()}`);
227
- const deployment = deployments.body.items[0];
273
+ const deployments = await instance.appsApi.listNamespacedDeployment({
274
+ namespace: kubernetes.namespace,
275
+ labelSelector: `hammerkit.dev/id=${service.id()}`,
276
+ });
277
+ const deployment = deployments.items[0];
228
278
  return deployment?.metadata?.labels?.['hammerkit.dev/state'] ?? null;
229
279
  },
230
280
  };
@@ -8,7 +8,9 @@ const tar_1 = require("tar");
8
8
  const path_1 = require("path");
9
9
  const local_task_1 = require("../executer/local-task");
10
10
  const event_cache_1 = require("../executer/event-cache");
11
+ const get_outputs_to_reset_1 = require("./utils/get-outputs-to-reset");
11
12
  const find_process_1 = __importDefault(require("find-process"));
13
+ const stream_1 = require("stream");
12
14
  function getStateFilename(task) {
13
15
  return (0, path_1.join)(task.data.cwd, '.hammerkit', `${task.id()}`);
14
16
  }
@@ -56,8 +58,21 @@ function getLocalWorkRuntime(task) {
56
58
  const pidFile = getPidFilename(task);
57
59
  await environment.file.writeFile(pidFile, `${process.pid}`);
58
60
  try {
61
+ // The record says "the outputs belong to this state key". A run that
62
+ // fails overwrites outputs, so the previous record goes first, and only
63
+ // a run that succeeded writes a new one.
64
+ const stateFile = getStateFilename(task);
65
+ if (await environment.file.exists(stateFile)) {
66
+ await environment.file.remove(stateFile);
67
+ }
68
+ for (const generate of (0, get_outputs_to_reset_1.getOutputsToReset)(task.data)) {
69
+ await environment.file.remove(generate.path);
70
+ }
59
71
  await (0, local_task_1.localTask)(task, environment, options);
60
- await environment.file.writeFile(getStateFilename(task), options.stateKey);
72
+ const failed = ['crash', 'error', 'canceled'].includes(options.state.current.type);
73
+ if (!failed) {
74
+ await environment.file.writeFile(stateFile, options.stateKey);
75
+ }
61
76
  }
62
77
  finally {
63
78
  if (await environment.file.exists(pidFile)) {
@@ -71,10 +86,16 @@ function getLocalWorkRuntime(task) {
71
86
  },
72
87
  async currentStateKey(environment) {
73
88
  const stateFileName = getStateFilename(task);
74
- if (await environment.file.exists(stateFileName)) {
75
- return await environment.file.read(stateFileName);
89
+ if (!(await environment.file.exists(stateFileName))) {
90
+ return null;
91
+ }
92
+ // outputs deleted since the run (rm -rf dist) leave nothing to reuse
93
+ for (const generate of task.data.generates) {
94
+ if (!generate.inherited && !(await environment.file.exists(generate.path))) {
95
+ return null;
96
+ }
76
97
  }
77
- return Promise.resolve(null);
98
+ return await environment.file.read(stateFileName);
78
99
  },
79
100
  async remove(environment) {
80
101
  for (const generate of task.data.generates) {
@@ -90,15 +111,17 @@ function getLocalWorkRuntime(task) {
90
111
  exports.getLocalWorkRuntime = getLocalWorkRuntime;
91
112
  async function archiveLocal(environment, task, path) {
92
113
  for (const generatedArchive of (0, event_cache_1.getArchivePaths)(task.data, path)) {
93
- await environment.file.writeStream(generatedArchive.filename, (0, tar_1.create)({
94
- cwd: task.data.cwd,
95
- gzip: true,
96
- }, [(0, path_1.relative)(task.data.cwd, generatedArchive.path)]));
114
+ await environment.file.writeStream(generatedArchive.filename,
115
+ // tar's stream is a Minipass stream; Readable.from adapts it to a Node one
116
+ stream_1.Readable.from((0, tar_1.create)({ cwd: task.data.cwd, gzip: true }, [(0, path_1.relative)(task.data.cwd, generatedArchive.path)])));
97
117
  }
98
118
  }
99
119
  async function restoreLocal(environment, task, path) {
100
120
  for (const generate of (0, event_cache_1.getArchivePaths)(task, path)) {
101
121
  if (await environment.file.exists(generate.filename)) {
122
+ // a cache hit means exactly the stored outputs: whatever a failed or
123
+ // older run left in the folder goes first
124
+ await environment.file.remove(generate.path);
102
125
  await (0, tar_1.extract)({
103
126
  file: generate.filename,
104
127
  cwd: task.cwd,
@@ -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
  }