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,214 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.explainWorkTree = exports.explainTask = exports.describeCause = void 0;
4
+ const state_key_1 = require("../executer/scheduler/state-key");
5
+ const work_task_cache_description_1 = require("../optimizer/work-task-cache-description");
6
+ const get_cache_directory_1 = require("../optimizer/get-cache-directory");
7
+ const last_resolved_1 = require("./last-resolved");
8
+ const work_instance_id_1 = require("../planner/work-instance-id");
9
+ function describeCause(cause) {
10
+ switch (cause.kind) {
11
+ case 'no-src':
12
+ return 'no src declared (uncacheable)';
13
+ case 'no-src-files':
14
+ return 'src matches no files (uncacheable)';
15
+ case 'dependency-uncacheable':
16
+ return `dependency always runs: ${cause.identifier}`;
17
+ case 'caching-disabled':
18
+ return 'caching disabled';
19
+ case 'never-cached':
20
+ return 'never cached';
21
+ case 'cache-format-changed':
22
+ return 'cache format changed';
23
+ case 'source-changed':
24
+ return `source changed: ${cause.identifier}`;
25
+ case 'source-added':
26
+ return `source added: ${cause.identifier}`;
27
+ case 'source-removed':
28
+ return `source removed: ${cause.identifier}`;
29
+ case 'env-changed':
30
+ return `environment variable changed: ${cause.identifier}`;
31
+ case 'command-changed':
32
+ return 'command changed';
33
+ case 'image-changed':
34
+ return 'image changed';
35
+ case 'dependency-changed':
36
+ return `dependency changed: ${cause.identifier}`;
37
+ }
38
+ }
39
+ exports.describeCause = describeCause;
40
+ async function safeCurrentStateKey(item, environment) {
41
+ // currentStateKey is read-only (a file read for local, a container *list* for
42
+ // docker/k8s — never a start). If the runtime is unreachable (offline, no
43
+ // daemon) explain must still answer, so failures degrade to "no runtime state".
44
+ try {
45
+ return await item.runtime.currentStateKey(environment);
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ async function safeHas(item, stateKey, environment) {
52
+ try {
53
+ return await item.data.caching.backend.has(item.id(), stateKey, environment);
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ function diffDescription(current, previous, causes) {
60
+ if (JSON.stringify(current.cmds) !== JSON.stringify(previous.cmds)) {
61
+ causes.push({ kind: 'command-changed' });
62
+ }
63
+ if (current.image !== previous.image) {
64
+ causes.push({ kind: 'image-changed' });
65
+ }
66
+ // shell / mounts / cwd / generates all alter how the task runs; the closed
67
+ // cause vocabulary folds them into "command changed".
68
+ if (current.shell !== previous.shell ||
69
+ JSON.stringify(current.mounts) !== JSON.stringify(previous.mounts) ||
70
+ current.cwd !== previous.cwd ||
71
+ JSON.stringify(current.generates) !== JSON.stringify(previous.generates)) {
72
+ if (!causes.some((c) => c.kind === 'command-changed')) {
73
+ causes.push({ kind: 'command-changed' });
74
+ }
75
+ }
76
+ const currentEnvs = current.envs ?? {};
77
+ const previousEnvs = previous.envs ?? {};
78
+ const envKeys = Array.from(new Set([...Object.keys(currentEnvs), ...Object.keys(previousEnvs)])).sort();
79
+ for (const key of envKeys) {
80
+ if (currentEnvs[key] !== previousEnvs[key]) {
81
+ causes.push({ kind: 'env-changed', identifier: key });
82
+ }
83
+ }
84
+ }
85
+ function diffStats(current, previous, method, causes) {
86
+ const currentFiles = current.files ?? {};
87
+ const previousFiles = previous.files ?? {};
88
+ const paths = Array.from(new Set([...Object.keys(currentFiles), ...Object.keys(previousFiles)])).sort();
89
+ for (const path of paths) {
90
+ const now = currentFiles[path];
91
+ const before = previousFiles[path];
92
+ if (now && !before) {
93
+ causes.push({ kind: 'source-added', identifier: path });
94
+ }
95
+ else if (!now && before) {
96
+ causes.push({ kind: 'source-removed', identifier: path });
97
+ }
98
+ else if (now && before) {
99
+ const changed = method === 'modify-date' ? now.lastModified !== before.lastModified : now.checksum !== before.checksum;
100
+ if (changed) {
101
+ causes.push({ kind: 'source-changed', identifier: path });
102
+ }
103
+ }
104
+ }
105
+ }
106
+ function isAlwaysRun(explanation) {
107
+ return (explanation.status === 'uncacheable' &&
108
+ explanation.causes.some((c) => c.kind === 'no-src' || c.kind === 'no-src-files' || c.kind === 'dependency-uncacheable'));
109
+ }
110
+ // Explain a single task, recursively explaining its dependencies so a downstream
111
+ // task whose own inputs are unchanged is attributed to the upstream dependency
112
+ // that actually changed. Memoized by task id; `visiting` guards against a cyclic
113
+ // graph (already rejected by the planner, but explain runs before scheduling).
114
+ async function explainTask(item, defaultCacheMethod, environment, memo = new Map(), visiting = new Set()) {
115
+ const existing = memo.get(item.id());
116
+ if (existing) {
117
+ return existing;
118
+ }
119
+ if (visiting.has(item.id())) {
120
+ // Cycle: report neutrally rather than recursing forever.
121
+ return { taskId: item.id(), taskName: item.name, status: 'miss', causes: [{ kind: 'never-cached' }] };
122
+ }
123
+ visiting.add(item.id());
124
+ const { stateKey, stats, resolved, provable, unmatched } = await (0, state_key_1.computeStateKey)(item, defaultCacheMethod, environment);
125
+ // Mirrors the scheduler: a task that cannot be proven up to date always runs
126
+ // (see computeStateKey), whatever a cache holds for it.
127
+ if (!provable) {
128
+ const causes = [];
129
+ const declared = item.data.src.filter((src) => !src.inherited);
130
+ if (declared.length === 0) {
131
+ causes.push({ kind: 'no-src' });
132
+ }
133
+ else if (unmatched.length === declared.length) {
134
+ causes.push({ kind: 'no-src-files' });
135
+ }
136
+ for (const dep of item.deps) {
137
+ const depExplanation = await explainTask(dep, defaultCacheMethod, environment, memo, visiting);
138
+ if (isAlwaysRun(depExplanation)) {
139
+ causes.push({ kind: 'dependency-uncacheable', identifier: dep.name });
140
+ }
141
+ }
142
+ const explanation = { taskId: item.id(), taskName: item.name, status: 'uncacheable', causes };
143
+ memo.set(item.id(), explanation);
144
+ visiting.delete(item.id());
145
+ return explanation;
146
+ }
147
+ if (resolved.method === 'none') {
148
+ const explanation = {
149
+ taskId: item.id(),
150
+ taskName: item.name,
151
+ status: 'uncacheable',
152
+ causes: [{ kind: 'caching-disabled' }],
153
+ };
154
+ memo.set(item.id(), explanation);
155
+ visiting.delete(item.id());
156
+ return explanation;
157
+ }
158
+ const runtimeStateKey = await safeCurrentStateKey(item, environment);
159
+ const hasEntry = await safeHas(item, stateKey, environment);
160
+ const predictedHit = runtimeStateKey === stateKey || hasEntry;
161
+ if (predictedHit) {
162
+ const explanation = { taskId: item.id(), taskName: item.name, status: 'hit', causes: [] };
163
+ memo.set(item.id(), explanation);
164
+ visiting.delete(item.id());
165
+ return explanation;
166
+ }
167
+ const causes = [];
168
+ const record = await (0, last_resolved_1.readLastResolvedRecord)(environment, item);
169
+ if (record === null) {
170
+ const legacyDesc = await environment.file.exists((0, get_cache_directory_1.getCacheDescriptionFile)((0, work_instance_id_1.getWorkInstanceId)(item)));
171
+ if (runtimeStateKey === null && !hasEntry && !legacyDesc) {
172
+ causes.push({ kind: 'never-cached' });
173
+ }
174
+ else {
175
+ // Prior evidence of a run, but no diffable breakdown (pre-feature entry).
176
+ causes.push({ kind: 'cache-format-changed' });
177
+ }
178
+ }
179
+ else {
180
+ // records store env values as digests, so compare in the stored form
181
+ diffDescription((0, work_task_cache_description_1.storedCacheDescription)((0, work_task_cache_description_1.getWorkTaskCacheDescription)(item)), record.description, causes);
182
+ diffStats(stats, record.stats, resolved.method, causes);
183
+ }
184
+ for (const dep of item.deps) {
185
+ const depExplanation = await explainTask(dep, defaultCacheMethod, environment, memo, visiting);
186
+ if (depExplanation.status !== 'hit') {
187
+ causes.push({ kind: 'dependency-changed', identifier: dep.name });
188
+ }
189
+ }
190
+ if (causes.length === 0) {
191
+ // A miss we could not attribute (e.g. the artifact was pruned/evicted though
192
+ // metadata records a prior run) — treat as no usable cache entry.
193
+ causes.push({ kind: 'never-cached' });
194
+ }
195
+ const explanation = { taskId: item.id(), taskName: item.name, status: 'miss', causes };
196
+ memo.set(item.id(), explanation);
197
+ visiting.delete(item.id());
198
+ return explanation;
199
+ }
200
+ exports.explainTask = explainTask;
201
+ // Explain every task in scope (the work tree already includes dependencies),
202
+ // deterministically ordered by name. Read-only: executes no command, starts no
203
+ // container/service, performs no cache push/pull (only metadata `has` lookups).
204
+ async function explainWorkTree(workTree, defaultCacheMethod, environment) {
205
+ const memo = new Map();
206
+ const tasks = Object.values(workTree.tasks).sort((a, b) => a.name.localeCompare(b.name));
207
+ const results = [];
208
+ for (const task of tasks) {
209
+ results.push(await explainTask(task, defaultCacheMethod, environment, memo));
210
+ }
211
+ return results;
212
+ }
213
+ exports.explainWorkTree = explainWorkTree;
214
+ //# sourceMappingURL=explain.js.map
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readLastResolvedRecord = exports.writeLastResolvedRecord = exports.getLastResolvedFile = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const path_1 = require("path");
6
+ const work_task_cache_description_1 = require("../optimizer/work-task-cache-description");
7
+ // Stored in the project's `.hammerkit` dir (local-first, alongside the local
8
+ // runtime state file) keyed by task name. The name is sanitized for the
9
+ // filesystem and suffixed with a hash of the full name to avoid collisions
10
+ // between names that sanitize to the same string (e.g. referenced `lib:build`).
11
+ function getLastResolvedFile(task) {
12
+ const sanitized = task.name.replace(/[^a-z0-9-_]+/gi, '_').slice(0, 80);
13
+ const suffix = (0, crypto_1.createHash)('sha1').update(task.name).digest('hex').slice(0, 8);
14
+ return (0, path_1.join)(task.data.cwd, '.hammerkit', 'last-resolved', `${sanitized}-${suffix}.json`);
15
+ }
16
+ exports.getLastResolvedFile = getLastResolvedFile;
17
+ async function writeLastResolvedRecord(environment, task, record) {
18
+ const file = getLastResolvedFile(task);
19
+ await environment.file.createDirectory((0, path_1.join)(task.data.cwd, '.hammerkit', 'last-resolved'));
20
+ const payload = {
21
+ taskId: task.id(),
22
+ taskName: task.name,
23
+ description: (0, work_task_cache_description_1.storedCacheDescription)(record.description),
24
+ stats: record.stats,
25
+ };
26
+ await environment.file.writeFile(file, JSON.stringify(payload));
27
+ }
28
+ exports.writeLastResolvedRecord = writeLastResolvedRecord;
29
+ async function readLastResolvedRecord(environment, task) {
30
+ const file = getLastResolvedFile(task);
31
+ if (!(await environment.file.exists(file))) {
32
+ return null;
33
+ }
34
+ try {
35
+ const parsed = JSON.parse(await environment.file.read(file));
36
+ // A record written by an older hammerkit (or otherwise truncated) lacks the
37
+ // component breakdown explain needs — signal that rather than crashing.
38
+ if (!parsed || typeof parsed !== 'object' || !parsed.description || !parsed.stats) {
39
+ return null;
40
+ }
41
+ return parsed;
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ exports.readLastResolvedRecord = readLastResolvedRecord;
48
+ //# sourceMappingURL=last-resolved.js.map
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isCacheReadOnly = exports.CACHE_READ_ONLY_ENV = void 0;
4
+ exports.CACHE_READ_ONLY_ENV = 'HAMMERKIT_CACHE_READ_ONLY';
5
+ // Read-only is on when the flag is passed or the env var is set to a truthy
6
+ // value, so a sandbox image can enforce it once for every invocation.
7
+ function isCacheReadOnly(flag, processEnvs) {
8
+ if (flag) {
9
+ return true;
10
+ }
11
+ const value = (processEnvs[exports.CACHE_READ_ONLY_ENV] ?? '').trim().toLowerCase();
12
+ return value !== '' && value !== '0' && value !== 'false' && value !== 'no';
13
+ }
14
+ exports.isCacheReadOnly = isCacheReadOnly;
15
+ //# sourceMappingURL=read-only.js.map
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.planRetention = void 0;
4
+ function recency(entry) {
5
+ return entry.lastAccessedAt ?? entry.createdAt;
6
+ }
7
+ // most recent first; entries without a timestamp count as the oldest
8
+ function byRecencyDesc(a, b) {
9
+ return (recency(b) ?? -Infinity) - (recency(a) ?? -Infinity);
10
+ }
11
+ const entries = (count) => `${count} ${count === 1 ? 'entry' : 'entries'}`;
12
+ // Decide which cache entries a retention policy evicts. Pure: listing and
13
+ // removing is the caller's job. Applied in order keepPerTask → maxAge →
14
+ // maxSize, each on what the previous step kept. Evicting never causes a false
15
+ // hit, only a rebuild.
16
+ function planRetention(all, policy, now) {
17
+ const evict = [];
18
+ const unavailable = [];
19
+ let keep = [...all];
20
+ if (policy.keepPerTask !== undefined) {
21
+ const byTask = new Map();
22
+ for (const entry of keep) {
23
+ byTask.set(entry.taskId, [...(byTask.get(entry.taskId) ?? []), entry]);
24
+ }
25
+ keep = [];
26
+ for (const versions of byTask.values()) {
27
+ versions.sort(byRecencyDesc);
28
+ keep.push(...versions.slice(0, policy.keepPerTask));
29
+ evict.push(...versions.slice(policy.keepPerTask));
30
+ }
31
+ }
32
+ if (policy.maxAge !== undefined) {
33
+ const maxAge = policy.maxAge;
34
+ const undated = keep.filter((entry) => recency(entry) === null);
35
+ if (undated.length > 0) {
36
+ unavailable.push(`max-age: ${entries(undated.length)} ${undated.length === 1 ? 'has' : 'have'} no timestamp and ${undated.length === 1 ? 'was' : 'were'} kept`);
37
+ }
38
+ evict.push(...keep.filter((entry) => recency(entry) !== null && now - recency(entry) > maxAge));
39
+ keep = keep.filter((entry) => !evict.includes(entry));
40
+ }
41
+ if (policy.maxSize !== undefined) {
42
+ const unsized = keep.filter((entry) => entry.size === null);
43
+ if (unsized.length > 0) {
44
+ unavailable.push(`max-size: the backend cannot report the size of ${entries(unsized.length)}`);
45
+ }
46
+ else {
47
+ let total = keep.reduce((sum, entry) => sum + entry.size, 0);
48
+ for (const entry of [...keep].sort(byRecencyDesc).reverse()) {
49
+ if (total <= policy.maxSize) {
50
+ break;
51
+ }
52
+ evict.push(entry);
53
+ total -= entry.size;
54
+ }
55
+ keep = keep.filter((entry) => !evict.includes(entry));
56
+ }
57
+ }
58
+ return { keep, evict, unavailable };
59
+ }
60
+ exports.planRetention = planRetention;
61
+ //# sourceMappingURL=retention.js.map
package/dist/cli.js CHANGED
@@ -14,6 +14,11 @@ const get_scheduler_execute_result_1 = require("./executer/get-scheduler-execute
14
14
  const reset_work_tree_1 = require("./executer/reset-work-tree");
15
15
  const execute_work_tree_1 = require("./executer/execute-work-tree");
16
16
  const package_1 = require("./docker/package");
17
+ const explain_1 = require("./cache/explain");
18
+ const work_graph_serializer_1 = require("./planner/work-graph-serializer");
19
+ const dry_run_1 = require("./executer/dry-run");
20
+ const cache_sync_1 = require("./cache/cache-sync");
21
+ const cache_inventory_1 = require("./cache/cache-inventory");
17
22
  const isCliTask = (val) => val.type === 'task';
18
23
  exports.isCliTask = isCliTask;
19
24
  const isCliService = (val) => val.type === 'service';
@@ -48,6 +53,10 @@ class Cli {
48
53
  workers: options?.workers ?? 0,
49
54
  processManager,
50
55
  type,
56
+ explain: options?.explain ?? false,
57
+ cacheReadOnly: options?.cacheReadOnly ?? false,
58
+ timeout: options?.timeout ?? null,
59
+ skipDeps: options?.skipDeps ?? true,
51
60
  });
52
61
  }
53
62
  const result = (0, get_scheduler_execute_result_1.getSchedulerExecuteResult)(workTree);
@@ -104,6 +113,40 @@ class Cli {
104
113
  validate() {
105
114
  return (0, validate_1.validate)(this.workTree, this.environment);
106
115
  }
116
+ // Read-only cache prediction for every task in scope: executes no command,
117
+ // starts no container/service, performs no cache push/pull.
118
+ async explain(options) {
119
+ return (0, explain_1.explainWorkTree)(this.workTree, options?.cacheDefault ?? 'checksum', this.environment);
120
+ }
121
+ // Serialize the in-scope work graph for rendering (mermaid/dot). Pure read of
122
+ // the planner's graph — nothing executes.
123
+ graph(format) {
124
+ return (0, work_graph_serializer_1.serializeWorkGraph)(this.workTree, format);
125
+ }
126
+ // Ordered execution plan with predicted cache decisions, reusing the explain
127
+ // engine — executes nothing, starts nothing, performs no cache push/pull.
128
+ async dryRun(options) {
129
+ return (0, dry_run_1.planDryRun)(this.workTree, options?.cacheDefault ?? 'checksum', this.environment);
130
+ }
131
+ // Move cache entries between the tasks' own caches and a named remote without
132
+ // executing anything (`cache pull` / `cache push`).
133
+ async syncCache(options) {
134
+ return (0, cache_sync_1.syncCache)(this.workTree, options, this.environment);
135
+ }
136
+ // Entries of a named cache (default: the local `default` cache).
137
+ async listCache(cacheName) {
138
+ return (0, cache_inventory_1.listCache)(this.workTree, cacheName, this.environment);
139
+ }
140
+ retentionPolicy(cacheName, overrides) {
141
+ return (0, cache_inventory_1.retentionPolicyOf)(this.workTree, cacheName, overrides);
142
+ }
143
+ async pruneCache(cacheName, policy, dryRun = false) {
144
+ return (0, cache_inventory_1.pruneCache)(this.workTree, cacheName, policy, { dryRun }, this.environment);
145
+ }
146
+ // Apply declared retention to the local caches this build file uses.
147
+ async autoPrune() {
148
+ return (0, cache_inventory_1.autoPrune)(this.workTree, this.environment);
149
+ }
107
150
  task(name) {
108
151
  const task = this.workTree.tasks[name];
109
152
  if (!task) {
@@ -4,6 +4,7 @@ exports.usingContainer = void 0;
4
4
  const remove_container_1 = require("./remove-container");
5
5
  const log_1 = require("../log");
6
6
  const execute_docker_1 = require("../executer/execute-docker");
7
+ const work_instance_id_1 = require("../planner/work-instance-id");
7
8
  async function usingContainer(docker, item, createOptions, stateKey, callback) {
8
9
  let container = null;
9
10
  let succeeded = false;
@@ -22,7 +23,10 @@ async function usingContainer(docker, item, createOptions, stateKey, callback) {
22
23
  if (container) {
23
24
  await container.pause();
24
25
  }
25
- const containers = await docker.listContainers({ all: true, filters: { label: [`hammerkit-id=${item.id()}`] } });
26
+ const containers = await docker.listContainers({
27
+ all: true,
28
+ filters: { label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(item)}`] },
29
+ });
26
30
  for (const container of containers) {
27
31
  item.status.write('debug', `found container ${container.Id}`);
28
32
  if (container.Labels['hammerkit-state'] != stateKey) {
@@ -35,6 +39,18 @@ async function usingContainer(docker, item, createOptions, stateKey, callback) {
35
39
  if (container) {
36
40
  await (0, remove_container_1.removeContainer)(container);
37
41
  }
42
+ // A task run that failed has written into the outputs an earlier run's
43
+ // container still vouches for (its state label is what currentStateKey
44
+ // reads), so that record goes too.
45
+ if (stateKey) {
46
+ const containers = await docker.listContainers({
47
+ all: true,
48
+ filters: { label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(item)}`] },
49
+ });
50
+ for (const old of containers) {
51
+ await (0, remove_container_1.removeContainer)(docker.getContainer(old.Id));
52
+ }
53
+ }
38
54
  }
39
55
  }
40
56
  catch (e) {
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.archiveTaskEntry = void 0;
4
+ const get_cache_directory_1 = require("../optimizer/get-cache-directory");
5
+ const work_instance_id_1 = require("../planner/work-instance-id");
6
+ const get_work_cache_stats_1 = require("../optimizer/get-work-cache-stats");
7
+ const cache_metadata_1 = require("./cache-metadata");
8
+ const work_task_cache_description_1 = require("../optimizer/work-task-cache-description");
9
+ // Package a task's current outputs as a cache entry (metadata + output
10
+ // archives) in its staging directory, and return that directory for a backend
11
+ // to push from. Used after a task ran, and by `cache push` for a task whose
12
+ // outputs are up to date in this checkout but were never stored in a backend.
13
+ async function archiveTaskEntry(work, environment) {
14
+ const cacheDir = (0, get_cache_directory_1.getCacheDirectory)((0, work_instance_id_1.getWorkInstanceId)(work));
15
+ const { stats } = await (0, get_work_cache_stats_1.getWorkItemCacheStats)(work, environment);
16
+ await (0, cache_metadata_1.writeCacheMetadata)(environment, work, stats, (0, work_task_cache_description_1.getWorkTaskCacheDescription)(work));
17
+ await work.runtime.archive(environment, cacheDir);
18
+ return cacheDir;
19
+ }
20
+ exports.archiveTaskEntry = archiveTaskEntry;
21
+ //# sourceMappingURL=archive-task-entry.js.map
@@ -1,19 +1,30 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.awaitRunningNeeds = exports.awaitRunningNeed = exports.awaitCompleted = exports.awaitCompletedDependencies = exports.awaitNoRequirements = exports.awaitRequirement = void 0;
3
+ exports.awaitRunningNeeds = exports.awaitRunningNeed = exports.awaitCompleted = exports.awaitCompletedDependencies = exports.awaitNoRequirements = exports.awaitDependentNeed = exports.awaitRequirement = void 0;
4
4
  const work_item_1 = require("../planner/work-item");
5
5
  const state_resolver_1 = require("./state-resolver");
6
- async function awaitRequirement(svc, abort) {
7
- await Promise.race(svc.requiredBy.map((required) => {
8
- if ((0, work_item_1.isWorkTaskItem)(required)) {
9
- return (0, state_resolver_1.awaitState)('await-requirement', required.state, (state) => state.type === 'ready', abort);
10
- }
11
- else {
12
- return (0, state_resolver_1.awaitState)('await-requirement', required.state, (state) => state.type === 'ready', abort);
13
- }
14
- }));
6
+ // Resolve true once any requirer is `ready` (about to run). With
7
+ // `untilAllDone`, resolve false instead when every requirer has finished
8
+ // without needing the service — e.g. all of them were cache hits — so the
9
+ // service is never started.
10
+ async function awaitRequirement(svc, abort, options = { untilAllDone: false }) {
11
+ const ready = Promise.race(svc.requiredBy.map((required) => (0, state_resolver_1.awaitState)('await-requirement', required.state, (state) => state.type === 'ready', abort))).then(() => true);
12
+ if (!options.untilAllDone) {
13
+ return ready;
14
+ }
15
+ const allDone = awaitNoRequirements(svc, abort).then(() => false);
16
+ return Promise.race([ready, allDone]);
15
17
  }
16
18
  exports.awaitRequirement = awaitRequirement;
19
+ // Resolve true once any dependent is `ready` — it missed the cache and is about
20
+ // to run, so it needs this task's outputs — and false once every dependent
21
+ // finished without needing it (all cache hits, or skipped themselves).
22
+ async function awaitDependentNeed(dependents, abort) {
23
+ const ready = Promise.race(dependents.map((dependent) => (0, state_resolver_1.awaitState)('await-dependent', dependent.state, (s) => s.type === 'ready', abort))).then(() => true);
24
+ const allDone = Promise.all(dependents.map((dependent) => (0, state_resolver_1.awaitState)('await-dependent-done', dependent.state, (s) => s.type === 'completed' || s.type === 'error' || s.type === 'crash' || s.type === 'canceled', abort))).then(() => false);
25
+ return Promise.race([ready, allDone]);
26
+ }
27
+ exports.awaitDependentNeed = awaitDependentNeed;
17
28
  async function awaitNoRequirements(svc, abort) {
18
29
  await Promise.all(svc.requiredBy.map((required) => {
19
30
  if ((0, work_item_1.isWorkTaskItem)(required)) {
@@ -36,8 +47,15 @@ function awaitCompleted(work, dep, abort) {
36
47
  return (0, state_resolver_1.isState)('await-completed-' + work.name, dep.state, isCompleted, abort);
37
48
  }
38
49
  exports.awaitCompleted = awaitCompleted;
39
- function awaitRunningNeed(work, dep, abort) {
40
- return (0, state_resolver_1.isState)('await-running-need-' + work.name, dep.state, isRunning, abort);
50
+ // Wait for a needed service to run. A service that reaches a terminal state
51
+ // first (it failed to start or exited) can never become ready, so the waiting
52
+ // item fails instead of waiting forever.
53
+ async function awaitRunningNeed(work, dep, abort) {
54
+ const state = await (0, state_resolver_1.isState)('await-running-need-' + work.name, dep.state, isRunningOrDone, abort);
55
+ if (state.type !== 'running') {
56
+ throw new Error(`needed service ${dep.name} ${state.type === 'end' ? 'stopped' : 'failed'} before it was ready`);
57
+ }
58
+ return state;
41
59
  }
42
60
  exports.awaitRunningNeed = awaitRunningNeed;
43
61
  async function awaitRunningNeeds(work, needs, abort) {
@@ -50,7 +68,7 @@ exports.awaitRunningNeeds = awaitRunningNeeds;
50
68
  function isCompleted(val) {
51
69
  return val.type === 'completed';
52
70
  }
53
- function isRunning(val) {
54
- return val.type === 'running';
71
+ function isRunningOrDone(val) {
72
+ return val.type === 'running' || val.type === 'end' || val.type === 'error' || val.type === 'canceled';
55
73
  }
56
74
  //# sourceMappingURL=await-completed-dependencies.js.map
@@ -2,10 +2,17 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.writeCacheMetadata = void 0;
4
4
  const get_cache_directory_1 = require("../optimizer/get-cache-directory");
5
- async function writeCacheMetadata(environment, taskId, stats, description) {
5
+ const work_task_cache_description_1 = require("../optimizer/work-task-cache-description");
6
+ const last_resolved_1 = require("../cache/last-resolved");
7
+ const work_instance_id_1 = require("../planner/work-instance-id");
8
+ async function writeCacheMetadata(environment, task, stats, description) {
9
+ const taskId = (0, work_instance_id_1.getWorkInstanceId)(task);
6
10
  await environment.file.createDirectory((0, get_cache_directory_1.getCacheDirectory)(taskId));
7
11
  await environment.file.writeFile((0, get_cache_directory_1.getCacheStatsFile)(taskId), JSON.stringify(stats));
8
- await environment.file.writeFile((0, get_cache_directory_1.getCacheDescriptionFile)(taskId), JSON.stringify(description));
12
+ await environment.file.writeFile((0, get_cache_directory_1.getCacheDescriptionFile)(taskId), JSON.stringify((0, work_task_cache_description_1.storedCacheDescription)(description)));
13
+ // Additive per-task-name record for `explain` — never affects the hit/miss
14
+ // decision, written wherever the id-keyed metadata is.
15
+ await (0, last_resolved_1.writeLastResolvedRecord)(environment, task, { description, stats });
9
16
  }
10
17
  exports.writeCacheMetadata = writeCacheMetadata;
11
18
  //# sourceMappingURL=cache-metadata.js.map
@@ -12,6 +12,7 @@ const execution_steps_1 = require("./execution-steps");
12
12
  const docker_task_1 = require("./docker-task");
13
13
  const replace_env_variables_1 = require("../environment/replace-env-variables");
14
14
  const get_service_containers_1 = require("./get-service-containers");
15
+ const work_instance_id_1 = require("../planner/work-instance-id");
15
16
  async function dockerService(docker, item, options, environment) {
16
17
  let container = null;
17
18
  try {
@@ -30,7 +31,7 @@ async function dockerService(docker, item, options, environment) {
30
31
  Env: Object.keys(envs).map((k) => `${k}=${envs[k]}`),
31
32
  Labels: {
32
33
  app: 'hammerkit',
33
- 'hammerkit-id': item.id(),
34
+ 'hammerkit-id': (0, work_instance_id_1.getWorkInstanceId)(item),
34
35
  'hammerkit-pid': process.pid.toString(),
35
36
  'hammerkit-type': 'service',
36
37
  'hammerkit-state': options.stateKey,
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.dockerTask = exports.getNeedsNetwork = void 0;
3
+ exports.dockerTask = exports.buildCreateOptions = exports.getNeedsNetwork = void 0;
4
4
  const service_dns_1 = require("./service-dns");
5
5
  const execute_docker_1 = require("./execute-docker");
6
6
  const abort_1 = require("./abort");
@@ -12,6 +12,8 @@ const tar_1 = require("tar");
12
12
  const replace_env_variables_1 = require("../environment/replace-env-variables");
13
13
  const get_container_binds_1 = require("./get-container-binds");
14
14
  const get_service_containers_1 = require("./get-service-containers");
15
+ const work_instance_id_1 = require("../planner/work-instance-id");
16
+ const get_outputs_to_reset_1 = require("../planner/utils/get-outputs-to-reset");
15
17
  function getNeedsNetwork(serviceContainers, needs) {
16
18
  const links = [];
17
19
  const hosts = [];
@@ -34,17 +36,21 @@ function buildCreateOptions(item, stateKey, serviceContainers, environment) {
34
36
  const network = getNeedsNetwork(serviceContainers, item.needs);
35
37
  const binds = (0, get_container_binds_1.getContainerBinds)(item);
36
38
  const envs = (0, replace_env_variables_1.getEnvironmentVariables)(item.data.envs);
39
+ // Running as the host's uid (Linux), the user has no home in the image and
40
+ // Docker sets HOME=/, which isn't writable: tools then fail to create their
41
+ // caches. Set on the container only, so it's not part of the cache key.
42
+ const home = item.data.user && !('HOME' in envs) ? { HOME: '/tmp' } : {};
37
43
  return {
38
44
  abortSignal: environment.abortCtrl.signal,
39
45
  Image: item.data.image,
40
46
  Tty: true,
41
47
  Entrypoint: [item.data.shell],
42
48
  Cmd: ['-c', 'sleep 3600'],
43
- Env: Object.entries(envs).map(([key, value]) => `${key}=${value}`),
49
+ Env: Object.entries({ ...envs, ...home }).map(([key, value]) => `${key}=${value}`),
44
50
  WorkingDir: (0, execute_docker_1.convertToPosixPath)(item.data.cwd),
45
51
  Labels: {
46
52
  app: 'hammerkit',
47
- 'hammerkit-id': item.id(),
53
+ 'hammerkit-id': (0, work_instance_id_1.getWorkInstanceId)(item),
48
54
  'hammerkit-pid': process.pid.toString(),
49
55
  'hammerkit-type': 'task',
50
56
  'hammerkit-state': stateKey,
@@ -57,9 +63,15 @@ function buildCreateOptions(item, stateKey, serviceContainers, environment) {
57
63
  },
58
64
  };
59
65
  }
66
+ exports.buildCreateOptions = buildCreateOptions;
60
67
  async function dockerTask(docker, item, environment, options) {
61
68
  item.status.write('info', `execute ${item.name} in container`);
62
69
  try {
70
+ const outputsToReset = (0, get_outputs_to_reset_1.getOutputsToReset)(item.data);
71
+ // outputs on the host: file outputs (bind mounts) and exported directories
72
+ for (const generate of outputsToReset.filter((g) => g.isFile || g.export)) {
73
+ await environment.file.remove(generate.path);
74
+ }
63
75
  await (0, execution_steps_1.prepareMounts)(item, environment);
64
76
  (0, abort_1.checkForAbort)(options.abort);
65
77
  await (0, execution_steps_1.pullImage)(item, docker);
@@ -71,6 +83,9 @@ async function dockerTask(docker, item, environment, options) {
71
83
  (0, print_container_options_1.printContainerOptions)(item.status, containerOptions);
72
84
  await (0, using_container_1.usingContainer)(docker, item, containerOptions, options.stateKey, async (container) => {
73
85
  await (0, execution_steps_1.setUserPermissions)(item, container, environment);
86
+ for (const generate of outputsToReset.filter((g) => !g.isFile)) {
87
+ await (0, execute_docker_1.clearContainerDirectory)(item.status, environment, container, generate.path);
88
+ }
74
89
  for (const cmd of item.data.cmds) {
75
90
  (0, abort_1.checkForAbort)(options.abort);
76
91
  item.status.write('info', `execute cmd "${cmd.cmd}" in container`);