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,73 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.printDryRun = exports.planDryRun = void 0;
7
+ const colors_1 = __importDefault(require("colors"));
8
+ const validate_1 = require("../planner/validate");
9
+ const explain_1 = require("../cache/explain");
10
+ const log_1 = require("../log");
11
+ // Deterministic topological order (dependencies before dependents). Visited-set
12
+ // guards against runaway recursion; a real cycle is reported separately.
13
+ function topoOrder(workTree) {
14
+ const visited = new Set();
15
+ const order = [];
16
+ const visit = (task) => {
17
+ if (visited.has(task.id())) {
18
+ return;
19
+ }
20
+ visited.add(task.id());
21
+ for (const dep of [...task.deps].sort((a, b) => a.name.localeCompare(b.name))) {
22
+ visit(dep);
23
+ }
24
+ order.push(task);
25
+ };
26
+ for (const task of Object.values(workTree.tasks).sort((a, b) => a.name.localeCompare(b.name))) {
27
+ visit(task);
28
+ }
29
+ return order;
30
+ }
31
+ // Compute the ordered execution plan with a predicted cache decision per task,
32
+ // reusing the cache-explain engine — no command runs, no container/service
33
+ // starts, no cache push/pull occurs.
34
+ async function planDryRun(workTree, defaultCacheMethod, environment) {
35
+ for (const task of Object.values(workTree.tasks)) {
36
+ const path = (0, validate_1.hasDependencyCycle)(task, []);
37
+ if (path && path.length > 0) {
38
+ return { entries: [], cycle: path.map((n) => n.name) };
39
+ }
40
+ }
41
+ const explanations = await (0, explain_1.explainWorkTree)(workTree, defaultCacheMethod, environment);
42
+ const byName = new Map(explanations.map((e) => [e.taskName, e]));
43
+ const entries = [];
44
+ for (const task of topoOrder(workTree)) {
45
+ const explanation = byName.get(task.name);
46
+ entries.push({
47
+ taskId: task.id(),
48
+ taskName: task.name,
49
+ status: explanation?.status ?? 'miss',
50
+ causes: explanation?.causes ?? [],
51
+ });
52
+ }
53
+ return { entries, cycle: null };
54
+ }
55
+ exports.planDryRun = planDryRun;
56
+ function printDryRun(environment, plan) {
57
+ (0, log_1.printTitle)(environment, 'Dry run (no commands executed)');
58
+ if (plan.entries.length === 0) {
59
+ environment.stdout.write(' no tasks in scope\n');
60
+ return;
61
+ }
62
+ plan.entries.forEach((entry, index) => {
63
+ const label = entry.status === 'hit'
64
+ ? colors_1.default.green('cache hit')
65
+ : entry.status === 'uncacheable'
66
+ ? colors_1.default.grey('uncacheable')
67
+ : colors_1.default.yellow('cache miss');
68
+ const causes = entry.causes.length > 0 ? colors_1.default.grey(` (${entry.causes.map(explain_1.describeCause).join(', ')})`) : '';
69
+ environment.stdout.write(` ${index + 1}. ${entry.taskName}: ${label}${causes}\n`);
70
+ });
71
+ }
72
+ exports.printDryRun = printDryRun;
73
+ //# sourceMappingURL=dry-run.js.map
@@ -5,9 +5,10 @@ const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
5
5
  const get_cache_directory_1 = require("../optimizer/get-cache-directory");
6
6
  const path_1 = require("path");
7
7
  const move_files_1 = require("../file/move-files");
8
+ const work_instance_id_1 = require("../planner/work-instance-id");
8
9
  async function restoreCache(environment, path, workTree) {
9
10
  for (const task of (0, plan_work_tasks_1.iterateWorkTasks)(workTree)) {
10
- const cachePath = (0, get_cache_directory_1.getCacheDirectory)(task.id());
11
+ const cachePath = (0, get_cache_directory_1.getCacheDirectory)((0, work_instance_id_1.getWorkInstanceId)(task));
11
12
  const sourceCacheDir = (0, path_1.join)(path, task.id());
12
13
  // TODO move into local runtime
13
14
  await (0, move_files_1.moveFiles)(task, environment, function* () {
@@ -17,7 +18,7 @@ async function restoreCache(environment, path, workTree) {
17
18
  await task.runtime.restore(environment, sourceCacheDir);
18
19
  }
19
20
  for (const service of (0, plan_work_tasks_1.iterateWorkServices)(workTree)) {
20
- const cachePath = (0, get_cache_directory_1.getCacheDirectory)(service.id());
21
+ const cachePath = (0, get_cache_directory_1.getCacheDirectory)((0, work_instance_id_1.getWorkInstanceId)(service));
21
22
  const sourceCacheDir = (0, path_1.join)(path, service.id());
22
23
  // TODO move into local runtime
23
24
  await (0, move_files_1.moveFiles)(service, environment, function* () {
@@ -50,7 +51,7 @@ function* getArchivePaths(task, path) {
50
51
  exports.getArchivePaths = getArchivePaths;
51
52
  async function storeCache(environment, path, workTree) {
52
53
  for (const task of (0, plan_work_tasks_1.iterateWorkTasks)(workTree)) {
53
- const cachePath = (0, get_cache_directory_1.getCacheDirectory)(task.id());
54
+ const cachePath = (0, get_cache_directory_1.getCacheDirectory)((0, work_instance_id_1.getWorkInstanceId)(task));
54
55
  const sourceCacheDir = (0, path_1.join)(path, task.id());
55
56
  await (0, move_files_1.moveFiles)(task, environment, function* () {
56
57
  yield { from: (0, path_1.join)(cachePath, 'stats.json'), to: (0, path_1.join)(sourceCacheDir, 'stats.json') };
@@ -60,7 +61,7 @@ async function storeCache(environment, path, workTree) {
60
61
  await task.runtime.archive(environment, sourceCacheDir);
61
62
  }
62
63
  for (const service of (0, plan_work_tasks_1.iterateWorkServices)(workTree)) {
63
- const cachePath = (0, get_cache_directory_1.getCacheDirectory)(service.id());
64
+ const cachePath = (0, get_cache_directory_1.getCacheDirectory)((0, work_instance_id_1.getWorkInstanceId)(service));
64
65
  const sourceCacheDir = (0, path_1.join)(path, service.id());
65
66
  await (0, move_files_1.moveFiles)(service, environment, function* () {
66
67
  yield { from: (0, path_1.join)(cachePath, 'stats.json'), to: (0, path_1.join)(sourceCacheDir, 'stats.json') };
@@ -77,7 +78,7 @@ async function cleanCache(workTree, environment, options) {
77
78
  if (options?.cache) {
78
79
  await task.data.caching.backend.clear(task.id(), environment);
79
80
  }
80
- const cachePath = (0, get_cache_directory_1.getCacheDirectory)(task.id());
81
+ const cachePath = (0, get_cache_directory_1.getCacheDirectory)((0, work_instance_id_1.getWorkInstanceId)(task));
81
82
  if (await environment.file.exists(cachePath)) {
82
83
  task.status.write('info', `remove cache ${cachePath}`);
83
84
  await environment.file.remove(cachePath);
@@ -7,12 +7,21 @@ const log_1 = require("../log");
7
7
  const abort_1 = require("./abort");
8
8
  const abort_event_1 = require("../utils/abort-event");
9
9
  async function executeCommand(status, abortSignal, cwd, command, envs, environment) {
10
+ const windows = (0, os_1.platform)() === 'win32';
10
11
  return new Promise((resolve, reject) => {
11
- const ps = (0, child_process_1.exec)(command, {
12
+ const ps = (0, child_process_1.spawn)(command, {
12
13
  env: { ...envs, PATH: environment.processEnvs['PATH'] },
13
14
  cwd,
14
- shell: (0, os_1.platform)() === 'win32' ? 'powershell.exe' : undefined,
15
+ shell: windows ? 'powershell.exe' : true,
16
+ // its own process group, so an abort reaches every process the command
17
+ // started — the shell may run them as children (dash always does), and
18
+ // they would keep running and holding the output open
19
+ detached: !windows,
15
20
  });
21
+ // decode across chunk boundaries, so a multi-byte character split between
22
+ // two chunks stays intact
23
+ ps.stdout?.setEncoding('utf8');
24
+ ps.stderr?.setEncoding('utf8');
16
25
  ps.stdout?.on('data', async (data) => {
17
26
  for (const log of (0, log_1.getLogs)(data)) {
18
27
  status.console('stdout', log);
@@ -35,7 +44,16 @@ async function executeCommand(status, abortSignal, cwd, command, envs, environme
35
44
  resolve(code ?? 0);
36
45
  });
37
46
  const abortListener = (0, abort_event_1.listenOnAbort)(abortSignal, () => {
38
- ps.kill();
47
+ if (windows || !ps.pid) {
48
+ ps.kill();
49
+ return;
50
+ }
51
+ try {
52
+ process.kill(-ps.pid, 'SIGTERM');
53
+ }
54
+ catch {
55
+ // the group is already gone
56
+ }
39
57
  });
40
58
  });
41
59
  }
@@ -3,16 +3,22 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.execCommand = exports.convertToPosixPath = exports.startContainer = exports.getContainerCli = void 0;
6
+ exports.clearContainerDirectory = exports.execCommand = exports.convertToPosixPath = exports.startContainer = exports.getContainerCli = exports.createDockerClient = void 0;
7
7
  const stream_1 = require("../docker/stream");
8
8
  const dockerode_1 = __importDefault(require("dockerode"));
9
9
  const path_1 = require("path");
10
10
  const os_1 = require("os");
11
11
  const abort_event_1 = require("../utils/abort-event");
12
12
  let dockerInstance = null;
13
+ // Without options dockerode reads DOCKER_HOST and the TLS variables; passing
14
+ // `{ host: undefined }` would overwrite the host it parsed from DOCKER_HOST.
15
+ function createDockerClient(host) {
16
+ return host ? new dockerode_1.default({ host }) : new dockerode_1.default();
17
+ }
18
+ exports.createDockerClient = createDockerClient;
13
19
  function getContainerCli(workEnvironment) {
14
20
  if (!dockerInstance) {
15
- dockerInstance = new dockerode_1.default({ host: workEnvironment.host });
21
+ dockerInstance = createDockerClient(workEnvironment.host);
16
22
  }
17
23
  return dockerInstance;
18
24
  }
@@ -127,4 +133,14 @@ async function execCommand(status, environment, container, cwd, cmd, user, timeo
127
133
  });
128
134
  }
129
135
  exports.execCommand = execCommand;
136
+ // Empty a directory inside a running container, as root. Volumes are emptied
137
+ // this way rather than recreated: Docker refuses to remove a volume while any
138
+ // container (a dependent's paused one, say) still mounts it.
139
+ async function clearContainerDirectory(status, environment, container, path) {
140
+ const result = await execCommand(status, environment, container, '/', ['sh', '-c', 'rm -rf "$1"/* "$1"/.[!.]* "$1"/..?*', 'sh', convertToPosixPath(path)], null, undefined, environment.abortCtrl.signal);
141
+ if (result.type !== 'result' || result.result.ExitCode !== 0) {
142
+ throw new Error(`unable to empty ${path}`);
143
+ }
144
+ }
145
+ exports.clearContainerDirectory = clearContainerDirectory;
130
146
  //# sourceMappingURL=execute-docker.js.map
@@ -14,7 +14,11 @@ exports.stopService = stopService;
14
14
  async function executeWorkService(work, environment, options) {
15
15
  try {
16
16
  if (options.type === 'execute') {
17
- await (0, await_completed_dependencies_1.awaitRequirement)(work, environment.abortCtrl.signal);
17
+ const required = await (0, await_completed_dependencies_1.awaitRequirement)(work, environment.abortCtrl.signal, { untilAllDone: !options.watch });
18
+ if (!required) {
19
+ work.status.write('debug', `${work.name} not started, no task needing it had to run`);
20
+ return;
21
+ }
18
22
  }
19
23
  work.state.set({
20
24
  type: 'starting',
@@ -54,6 +58,11 @@ async function executeWorkService(work, environment, options) {
54
58
  errorMessage: (0, log_1.getErrorMessage)(e),
55
59
  stateKey: null,
56
60
  });
61
+ // fail fast like a failing task: whatever needs this service can never
62
+ // run, and would otherwise wait for it forever
63
+ if (!options.watch) {
64
+ environment.abortCtrl.abort();
65
+ }
57
66
  }
58
67
  }
59
68
  }
@@ -6,20 +6,21 @@ const abort_1 = require("./abort");
6
6
  const log_1 = require("../log");
7
7
  const await_completed_dependencies_1 = require("./await-completed-dependencies");
8
8
  const watch_loop_1 = require("./watch-loop");
9
- const get_cache_directory_1 = require("../optimizer/get-cache-directory");
10
- const cache_metadata_1 = require("./cache-metadata");
11
- const get_work_cache_stats_1 = require("../optimizer/get-work-cache-stats");
12
- const work_task_cache_description_1 = require("../optimizer/work-task-cache-description");
13
- async function pushToBackend(work, environment, cacheState) {
14
- const { resolved, stateKey } = cacheState;
15
- if (resolved.method === 'none') {
9
+ const explain_1 = require("../cache/explain");
10
+ const archive_task_entry_1 = require("./archive-task-entry");
11
+ const units_1 = require("../utils/units");
12
+ const abort_event_1 = require("../utils/abort-event");
13
+ async function pushToBackend(work, environment, cacheState, options) {
14
+ const { resolved, stateKey, provable } = cacheState;
15
+ if (resolved.method === 'none' || !provable) {
16
+ return;
17
+ }
18
+ if (options.cacheReadOnly) {
19
+ work.status.write('debug', `${work.name} not pushed to cache "${resolved.name}" (read-only)`);
16
20
  return;
17
21
  }
18
22
  try {
19
- const cacheDir = (0, get_cache_directory_1.getCacheDirectory)(work.id());
20
- const stats = await (0, get_work_cache_stats_1.getWorkCacheStats)(work.data, environment);
21
- await (0, cache_metadata_1.writeCacheMetadata)(environment, work.id(), stats, (0, work_task_cache_description_1.getWorkTaskCacheDescription)(work.data));
22
- await work.runtime.archive(environment, cacheDir);
23
+ const cacheDir = await (0, archive_task_entry_1.archiveTaskEntry)(work, environment);
23
24
  await resolved.backend.push(work.id(), stateKey, cacheDir, environment);
24
25
  work.status.write('info', `${work.name} pushed to cache "${resolved.name}" (${resolved.backend.type})`);
25
26
  }
@@ -27,8 +28,51 @@ async function pushToBackend(work, environment, cacheState) {
27
28
  work.status.write('warn', `${work.name} failed to push to cache "${resolved.name}": ${(0, log_1.getErrorMessage)(e)}`);
28
29
  }
29
30
  }
30
- async function executeWorkTask(work, environment, options) {
31
+ // An abort signal that follows `parent` and additionally fires after `timeout`
32
+ // ms, so a task deadline reuses the runtimes' existing abort/cleanup path.
33
+ function withDeadline(parent, timeout) {
34
+ const controller = new AbortController();
35
+ let expired = false;
36
+ const parentListener = (0, abort_event_1.listenOnAbort)(parent, () => controller.abort());
37
+ const timer = timeout === null
38
+ ? null
39
+ : setTimeout(() => {
40
+ expired = true;
41
+ controller.abort();
42
+ }, timeout);
43
+ return {
44
+ signal: controller.signal,
45
+ expired: () => expired,
46
+ clear() {
47
+ if (timer) {
48
+ clearTimeout(timer);
49
+ }
50
+ parentListener.close();
51
+ },
52
+ };
53
+ }
54
+ // A task without cmds only groups its dependencies (`ci: { deps: [test, build] }`).
55
+ // Having no src of its own it is never a cache hit, so its dependencies are
56
+ // always checked and their outputs restored. When every one of them was a hit,
57
+ // nothing ran: report the group as cached too. Reporting only, the cache
58
+ // decision is untouched.
59
+ function onlyGroupsCacheHits(work) {
60
+ return (work.data.cmds.length === 0 &&
61
+ work.deps.every((dep) => dep.state.current.type === 'completed' && dep.state.current.cached));
62
+ }
63
+ async function executeWorkTask(work, environment, options,
64
+ // when set, the task was not requested and only runs if one of these tasks
65
+ // depending on it misses the cache (see executeWorkTree)
66
+ dependents = null) {
31
67
  try {
68
+ if (dependents) {
69
+ const needed = await (0, await_completed_dependencies_1.awaitDependentNeed)(dependents, environment.abortCtrl.signal);
70
+ if (!needed) {
71
+ work.status.write('debug', `${work.name} skipped, every task depending on it was a cache hit`);
72
+ work.state.set({ type: 'completed', cached: false, skipped: true, duration: 0, stateKey: '' });
73
+ return;
74
+ }
75
+ }
32
76
  work.state.set({
33
77
  type: 'starting',
34
78
  started: new Date(),
@@ -36,11 +80,6 @@ async function executeWorkTask(work, environment, options) {
36
80
  });
37
81
  await (0, watch_loop_1.watchLoop)(work, environment, options, async (cacheState, abort) => {
38
82
  const started = new Date();
39
- work.state.set({
40
- type: 'ready',
41
- stateKey: cacheState.stateKey,
42
- started: new Date(),
43
- });
44
83
  if (cacheState.cached) {
45
84
  work.status.write('debug', 'completed for cached state key ' + cacheState.stateKey);
46
85
  work.state.set({
@@ -51,6 +90,25 @@ async function executeWorkTask(work, environment, options) {
51
90
  });
52
91
  return;
53
92
  }
93
+ // Only a cache miss is `ready`: needed services start on `ready`, so a
94
+ // cached task never starts the services it would have needed.
95
+ work.state.set({
96
+ type: 'ready',
97
+ stateKey: cacheState.stateKey,
98
+ started: new Date(),
99
+ });
100
+ // Cache miss: under the explain flag, report why this task is rebuilding,
101
+ // reusing the cache-explain engine. Captured BEFORE execution overwrites
102
+ // the last-resolved record, then carried onto the completed state so the
103
+ // build summary can show the cause column. Reporting only.
104
+ let missCauses;
105
+ if (options.explain) {
106
+ const explanation = await (0, explain_1.explainTask)(work, options.cacheDefault, environment);
107
+ missCauses = explanation.causes.map(explain_1.describeCause);
108
+ for (const cause of missCauses) {
109
+ work.status.write('info', `cache miss: ${cause}`);
110
+ }
111
+ }
54
112
  await (0, await_completed_dependencies_1.awaitCompletedDependencies)(work, work.deps, abort);
55
113
  await (0, await_completed_dependencies_1.awaitRunningNeeds)(work, work.needs.map((n) => n.service), abort);
56
114
  await options.processManager.task(work, async () => {
@@ -59,22 +117,44 @@ async function executeWorkTask(work, environment, options) {
59
117
  stateKey: cacheState.stateKey,
60
118
  started,
61
119
  });
62
- await work.runtime.execute(environment, {
63
- cache: cacheState,
64
- abort,
65
- stateKey: cacheState.stateKey,
66
- state: work.state,
67
- daemon: options.daemon,
68
- });
120
+ const timeout = work.data.timeout ?? options.timeout;
121
+ const deadline = withDeadline(abort, timeout);
122
+ try {
123
+ await work.runtime.execute(environment, {
124
+ cache: cacheState,
125
+ abort: deadline.signal,
126
+ stateKey: cacheState.stateKey,
127
+ state: work.state,
128
+ daemon: options.daemon,
129
+ });
130
+ }
131
+ finally {
132
+ deadline.clear();
133
+ }
134
+ if (deadline.expired() && timeout !== null) {
135
+ // the runtime saw an abort and cleaned up; report it as a failure
136
+ // (never a cancellation) and stop the run like any failing task
137
+ work.state.set({
138
+ type: 'error',
139
+ stateKey: cacheState.stateKey,
140
+ errorMessage: `timed out after ${(0, units_1.formatDuration)(timeout)}`,
141
+ });
142
+ if (!options.watch) {
143
+ environment.abortCtrl.abort();
144
+ }
145
+ return;
146
+ }
69
147
  (0, abort_1.checkForAbort)(abort);
70
148
  if (work.state.current.type === 'running') {
71
149
  work.status.write('debug', 'completed for state key ' + cacheState.stateKey);
72
- await pushToBackend(work, environment, cacheState);
150
+ await pushToBackend(work, environment, cacheState, options);
151
+ const cached = onlyGroupsCacheHits(work);
73
152
  work.state.set({
74
153
  stateKey: cacheState.stateKey,
75
154
  type: 'completed',
76
- cached: false,
155
+ cached,
77
156
  duration: (0, states_1.getDuration)(started),
157
+ missCauses: cached ? undefined : missCauses,
78
158
  });
79
159
  }
80
160
  else {
@@ -100,6 +180,11 @@ async function executeWorkTask(work, environment, options) {
100
180
  errorMessage: (0, log_1.getErrorMessage)(e),
101
181
  stateKey: null,
102
182
  });
183
+ // Stop the run like a failing command does: dependents wait for this task
184
+ // to complete, which it now never will.
185
+ if (!options.watch) {
186
+ environment.abortCtrl.abort();
187
+ }
103
188
  }
104
189
  }
105
190
  }
@@ -6,9 +6,10 @@ const execute_work_service_1 = require("./execute-work-service");
6
6
  const execute_work_task_1 = require("./execute-work-task");
7
7
  async function executeWorkTree(work, environment, options) {
8
8
  const itemPromises = [];
9
+ const dependents = skippableDependents(work, options);
9
10
  for (const task of (0, plan_work_tasks_1.iterateWorkTasks)(work)) {
10
11
  if (task.state.current.type === 'pending') {
11
- itemPromises.push((0, execute_work_task_1.executeWorkTask)(task, environment, options));
12
+ itemPromises.push((0, execute_work_task_1.executeWorkTask)(task, environment, options, dependents.get(task.name) ?? null));
12
13
  }
13
14
  }
14
15
  for (const service of (0, plan_work_tasks_1.iterateWorkServices)(work)) {
@@ -24,4 +25,28 @@ async function executeWorkTree(work, environment, options) {
24
25
  await Promise.all(itemPromises);
25
26
  }
26
27
  exports.executeWorkTree = executeWorkTree;
28
+ // For every task that may be skipped when nothing needs it, the tasks in this
29
+ // run that depend on it. Requested tasks always run; so does anything a service
30
+ // depends on, and everything in watch mode or with skipping turned off.
31
+ function skippableDependents(work, options) {
32
+ const result = new Map();
33
+ if (options.type !== 'execute' || !options.skipDeps || options.watch || !work.requested) {
34
+ return result;
35
+ }
36
+ const neededByService = new Set();
37
+ for (const service of (0, plan_work_tasks_1.iterateWorkServices)(work)) {
38
+ for (const dep of service.deps) {
39
+ neededByService.add(dep.name);
40
+ }
41
+ }
42
+ for (const task of (0, plan_work_tasks_1.iterateWorkTasks)(work)) {
43
+ for (const dep of task.deps) {
44
+ if (work.requested.includes(dep.name) || neededByService.has(dep.name)) {
45
+ continue;
46
+ }
47
+ result.set(dep.name, [...(result.get(dep.name) ?? []), task]);
48
+ }
49
+ }
50
+ return result;
51
+ }
27
52
  //# sourceMappingURL=execute-work-tree.js.map
@@ -26,18 +26,13 @@ async function prepareVolume(item, docker) {
26
26
  }
27
27
  }
28
28
  if ((0, work_task_1.isContainerWorkTask)(item.data)) {
29
+ // a task's outputs are emptied inside its container (see dockerTask)
29
30
  for (const generate of item.data.generates) {
30
31
  if (generate.isFile) {
31
32
  continue;
32
33
  }
33
- if (generate.resetOnChange && !generate.inherited) {
34
- item.status.write('debug', 'recreate volume');
35
- await (0, get_docker_executor_1.recreateVolume)(docker, item.status, generate.volumeName);
36
- }
37
- else {
38
- item.status.write('debug', 'ensure volume exists');
39
- await (0, get_docker_executor_1.ensureVolumeExists)(docker, item.status, generate.volumeName);
40
- }
34
+ item.status.write('debug', 'ensure volume exists');
35
+ await (0, get_docker_executor_1.ensureVolumeExists)(docker, item.status, generate.volumeName);
41
36
  }
42
37
  }
43
38
  }
@@ -1,10 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getContainerBinds = void 0;
4
+ const path_1 = require("path");
5
+ function isInside(path, parent) {
6
+ const rel = (0, path_1.relative)(parent, path);
7
+ return rel !== '' && !rel.startsWith('..') && !(0, path_1.isAbsolute)(rel);
8
+ }
4
9
  function getContainerBinds(item) {
10
+ const sources = item.data.src.map((s) => s.absolutePath);
5
11
  const items = [
6
12
  ...item.data.mounts,
7
- ...item.data.src.map((s) => ({ localPath: s.absolutePath, containerPath: s.absolutePath })),
13
+ // A source inside another source (a dependency's `app/app.csproj` under the
14
+ // task's own `app`) is already visible through the outer bind. Binding it
15
+ // again would nest bind mounts, which Docker Desktop then refuses to let the
16
+ // host delete while the paused container exists.
17
+ ...sources
18
+ .filter((path) => !sources.some((other) => isInside(path, other)))
19
+ .map((path) => ({ localPath: path, containerPath: path })),
8
20
  ...item.data.generates.filter((g) => !g.isFile).map((v) => ({ localPath: v.volumeName, containerPath: v.path })),
9
21
  ...item.data.generates.filter((g) => g.isFile).map((v) => ({ localPath: v.path, containerPath: v.path })),
10
22
  ];
@@ -12,7 +12,10 @@ function getSchedulerExecuteResult(state) {
12
12
  }
13
13
  }
14
14
  for (const service of (0, plan_work_tasks_1.iterateWorkServices)(state)) {
15
- if (service.state.current.type === 'canceled') {
15
+ const current = service.state.current;
16
+ if (current.type === 'canceled' ||
17
+ current.type === 'error' ||
18
+ (current.type === 'end' && current.reason === 'crash')) {
16
19
  return {
17
20
  state,
18
21
  success: false,
@@ -1,13 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.lazyResolver = void 0;
4
- function lazyResolver(factory) {
4
+ // Memoizes factory(). While the factory is still running, a re-entrant call
5
+ // (a cycle) returns onReentry() when given, instead of recursing forever.
6
+ function lazyResolver(factory, onReentry) {
5
7
  let value = null;
6
8
  let resolved = false;
9
+ let resolving = false;
7
10
  return () => {
8
11
  if (!resolved) {
9
- value = factory();
10
- resolved = true;
12
+ if (resolving && onReentry) {
13
+ return onReentry();
14
+ }
15
+ resolving = true;
16
+ try {
17
+ value = factory();
18
+ resolved = true;
19
+ }
20
+ finally {
21
+ resolving = false;
22
+ }
11
23
  }
12
24
  return value;
13
25
  };
@@ -24,6 +24,8 @@ function resetWorkTree(workTree, type) {
24
24
  return services;
25
25
  }, {}),
26
26
  environment: workTree.environment,
27
+ caches: workTree.caches,
28
+ requested: workTree.requested,
27
29
  };
28
30
  }
29
31
  exports.resetWorkTree = resetWorkTree;