hammerkit 1.6.0-alpha.3 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/README.md +4 -4
  2. package/build.schema.json +49 -2
  3. package/dist/cache/backends/local-cache-backend.js +58 -1
  4. package/dist/cache/backends/registry/registry-client.js +264 -0
  5. package/dist/cache/backends/registry/registry-credentials.js +81 -0
  6. package/dist/cache/backends/registry/registry-reference.js +36 -0
  7. package/dist/cache/backends/registry-cache-backend.js +145 -0
  8. package/dist/cache/backends/s3-cache-backend.js +99 -46
  9. package/dist/cache/cache-backend-registry.js +2 -0
  10. package/dist/cache/cache-inventory.js +84 -0
  11. package/dist/cache/cache-sync.js +77 -0
  12. package/dist/cache/explain.js +214 -0
  13. package/dist/cache/last-resolved.js +48 -0
  14. package/dist/cache/read-only.js +15 -0
  15. package/dist/cache/retention.js +61 -0
  16. package/dist/cli.js +43 -0
  17. package/dist/docker/using-container.js +5 -1
  18. package/dist/executer/archive-task-entry.js +21 -0
  19. package/dist/executer/await-completed-dependencies.js +32 -14
  20. package/dist/executer/cache-metadata.js +9 -2
  21. package/dist/executer/docker-service.js +2 -1
  22. package/dist/executer/docker-task.js +2 -1
  23. package/dist/executer/dry-run.js +73 -0
  24. package/dist/executer/event-cache.js +6 -5
  25. package/dist/executer/execute-command.js +21 -3
  26. package/dist/executer/execute-work-service.js +10 -1
  27. package/dist/executer/execute-work-task.js +95 -25
  28. package/dist/executer/execute-work-tree.js +26 -1
  29. package/dist/executer/get-scheduler-execute-result.js +4 -1
  30. package/dist/executer/lazy-resolver.js +15 -3
  31. package/dist/executer/reset-work-tree.js +2 -0
  32. package/dist/executer/run-summary.js +102 -0
  33. package/dist/executer/scheduler/enqueue-next.js +15 -60
  34. package/dist/executer/scheduler/state-key.js +78 -0
  35. package/dist/kubernetes/await-running-state.js +73 -19
  36. package/dist/log.js +16 -6
  37. package/dist/logging/live-logger.js +2 -1
  38. package/dist/optimizer/calculate-checksum.js +8 -2
  39. package/dist/optimizer/get-work-cache-stats.js +66 -16
  40. package/dist/optimizer/work-task-cache-description.js +34 -5
  41. package/dist/planner/project-root.js +26 -0
  42. package/dist/planner/utils/append-work-service.js +2 -1
  43. package/dist/planner/utils/append-work-task.js +7 -1
  44. package/dist/planner/utils/parse-work-source.js +35 -30
  45. package/dist/planner/utils/portable-path.js +30 -0
  46. package/dist/planner/work-cache-id.js +2 -2
  47. package/dist/planner/work-graph-serializer.js +103 -0
  48. package/dist/planner/work-instance-id.js +15 -0
  49. package/dist/planner/work-runtime-docker.js +53 -15
  50. package/dist/planner/work-runtime-kubernetes.js +35 -3
  51. package/dist/planner/work-runtime-local.js +9 -3
  52. package/dist/planner/work-service-id.js +16 -4
  53. package/dist/program.js +235 -1
  54. package/dist/schema/build-file-base-task-schema.js +4 -0
  55. package/dist/schema/cache-schema.js +21 -2
  56. package/dist/schema/duration-schema.js +18 -0
  57. package/dist/schema/reference-parser.js +3 -0
  58. package/dist/schema/size-schema.js +18 -0
  59. package/dist/schema/work-scope-parser.js +15 -2
  60. package/dist/utils/units.js +87 -0
  61. package/package.json +15 -4
  62. package/.agents/FEEDBACK.md +0 -74
  63. package/.agents/skills/before-commit/SKILL.md +0 -101
  64. package/.agents/skills/do-the-work/SKILL.md +0 -64
  65. package/.agents/skills/fix-ci/SKILL.md +0 -98
  66. package/.agents/skills/speckit-git-commit/SKILL.md +0 -53
  67. package/.agents/skills/speckit-git-feature/SKILL.md +0 -72
  68. package/.agents/skills/speckit-git-initialize/SKILL.md +0 -54
  69. package/.agents/skills/speckit-git-remote/SKILL.md +0 -50
  70. package/.agents/skills/speckit-git-validate/SKILL.md +0 -54
  71. package/.devcontainer/Dockerfile +0 -5
  72. package/.devcontainer/devcontainer.json +0 -40
  73. package/.gitbook.yaml +0 -5
  74. package/.specify/extensions/.registry +0 -37
  75. package/.specify/extensions/git/README.md +0 -100
  76. package/.specify/extensions/git/commands/speckit.git.commit.md +0 -48
  77. package/.specify/extensions/git/commands/speckit.git.feature.md +0 -67
  78. package/.specify/extensions/git/commands/speckit.git.initialize.md +0 -49
  79. package/.specify/extensions/git/commands/speckit.git.remote.md +0 -45
  80. package/.specify/extensions/git/commands/speckit.git.validate.md +0 -49
  81. package/.specify/extensions/git/config-template.yml +0 -62
  82. package/.specify/extensions/git/extension.yml +0 -140
  83. package/.specify/extensions/git/git-config.yml +0 -62
  84. package/.specify/extensions.yml +0 -149
  85. package/.specify/feature.json +0 -3
  86. package/.specify/init-options.json +0 -10
  87. package/.specify/integration.json +0 -15
  88. package/.specify/integrations/claude.manifest.json +0 -16
  89. package/.specify/integrations/speckit.manifest.json +0 -17
  90. package/.specify/memory/constitution.md +0 -162
  91. package/.specify/templates/checklist-template.md +0 -40
  92. package/.specify/templates/constitution-template.md +0 -50
  93. package/.specify/templates/plan-template.md +0 -113
  94. package/.specify/templates/spec-template.md +0 -131
  95. package/.specify/templates/tasks-template.md +0 -252
  96. package/.specify/workflows/speckit/workflow.yml +0 -77
  97. package/.specify/workflows/workflow-registry.json +0 -13
  98. package/AGENTS.md +0 -94
  99. package/Dockerfile +0 -25
  100. package/FOLLOWUPS.md +0 -34
  101. package/SUMMARY.md +0 -68
  102. package/VALUES.md +0 -53
  103. package/dist/cache/auto-pull.spec.js +0 -123
  104. package/dist/cache/backends/local-cache-backend.spec.js +0 -72
  105. package/dist/cache/backends/s3-cache-backend.spec.js +0 -118
  106. package/dist/cache/cache-backend-registry.spec.js +0 -22
  107. package/dist/cache/resolve-cache.spec.js +0 -52
  108. package/dist/cli.spec.js +0 -23
  109. package/dist/console/get-logger.spec.js +0 -23
  110. package/dist/docker/get-default-platform.spec.js +0 -29
  111. package/dist/docker/longest-common-prefix.spec.js +0 -26
  112. package/dist/docker/package.spec.js +0 -145
  113. package/dist/docker/remove-container.spec.js +0 -32
  114. package/dist/docker/using-container.spec.js +0 -90
  115. package/dist/environment/merge-environment-variables.spec.js +0 -24
  116. package/dist/environment/replace-env-variables.spec.js +0 -47
  117. package/dist/executer/abort.spec.js +0 -18
  118. package/dist/executer/environment-mock.spec.js +0 -22
  119. package/dist/executer/execute.spec.js +0 -51
  120. package/dist/executer/get-service-containers.spec.js +0 -59
  121. package/dist/executer/get-service-env-hints.spec.js +0 -65
  122. package/dist/executer/label-values.spec.js +0 -61
  123. package/dist/executer/lazy-resolver.spec.js +0 -20
  124. package/dist/executer/process-manager.spec.js +0 -54
  125. package/dist/executer/scheduler/enqueue-next.spec.js +0 -136
  126. package/dist/executer/state-resolver.spec.js +0 -37
  127. package/dist/executer/state.spec.js +0 -66
  128. package/dist/executer/states.spec.js +0 -14
  129. package/dist/file/get-file-context.spec.js +0 -178
  130. package/dist/file/move-files.spec.js +0 -48
  131. package/dist/kubernetes/ensure-http-route.spec.js +0 -70
  132. package/dist/kubernetes/ensure-namespace.spec.js +0 -39
  133. package/dist/kubernetes/resolve-pod-name.spec.js +0 -81
  134. package/dist/log.spec.js +0 -131
  135. package/dist/logging/grouped-logger.spec.js +0 -84
  136. package/dist/logging/interactive-logger.spec.js +0 -57
  137. package/dist/logging/live-logger.spec.js +0 -37
  138. package/dist/logging/loggers.spec.js +0 -154
  139. package/dist/optimizer/calculate-checksum.spec.js +0 -34
  140. package/dist/optimizer/get-cache-directory.spec.js +0 -59
  141. package/dist/optimizer/get-work-cache-stats.spec.js +0 -79
  142. package/dist/optimizer/work-task-cache-description.spec.js +0 -45
  143. package/dist/parser/default-build-file.spec.js +0 -48
  144. package/dist/parser/parse-label-arguments.spec.js +0 -27
  145. package/dist/parser/read-build-file.spec.js +0 -89
  146. package/dist/parser/read-env-file.spec.js +0 -42
  147. package/dist/planner/utils/append-work-service.spec.js +0 -71
  148. package/dist/planner/utils/get-container-user.spec.js +0 -30
  149. package/dist/planner/utils/get-default-kube-config.spec.js +0 -11
  150. package/dist/planner/utils/normalize-path.spec.js +0 -24
  151. package/dist/planner/utils/parse-work-command.spec.js +0 -21
  152. package/dist/planner/utils/parse-work-generate.spec.js +0 -45
  153. package/dist/planner/utils/parse-work-mount.spec.js +0 -62
  154. package/dist/planner/utils/parse-work-mounts.spec.js +0 -19
  155. package/dist/planner/utils/parse-work-port.spec.js +0 -24
  156. package/dist/planner/utils/parse-work-ports.spec.js +0 -24
  157. package/dist/planner/utils/parse-work-source.spec.js +0 -58
  158. package/dist/planner/utils/parse-work-volume.spec.js +0 -18
  159. package/dist/planner/utils/plan-work-tasks.spec.js +0 -30
  160. package/dist/planner/utils/plan-work-volume.spec.js +0 -33
  161. package/dist/planner/utils/split-name.spec.js +0 -16
  162. package/dist/planner/utils/template-value.spec.js +0 -14
  163. package/dist/planner/validate.spec.js +0 -251
  164. package/dist/planner/work-cache-id.spec.js +0 -173
  165. package/dist/planner/work-runtime-local.spec.js +0 -193
  166. package/dist/program-commands.spec.js +0 -84
  167. package/dist/program.spec.js +0 -43
  168. package/dist/schema/build-file-schema.spec.js +0 -61
  169. package/dist/schema/doc-build-files.spec.js +0 -223
  170. package/dist/schema/parse-error.spec.js +0 -18
  171. package/dist/schema/reference-parser.spec.js +0 -83
  172. package/dist/schema/schema-parser.spec.js +0 -80
  173. package/dist/schema/type-guards.spec.js +0 -23
  174. package/dist/schema/work-scope-parser.spec.js +0 -66
  175. package/dist/service/update-service-status.spec.js +0 -27
  176. package/dist/testing/ensure-local-registry.js +0 -134
  177. package/dist/testing/example-test-suite.js +0 -63
  178. package/dist/testing/expect.js +0 -82
  179. package/dist/testing/get-test-suite.js +0 -9
  180. package/dist/testing/http-get.js +0 -21
  181. package/dist/testing/integration/cache.spec.js +0 -49
  182. package/dist/testing/integration/cancellation.spec.js +0 -45
  183. package/dist/testing/integration/clean.spec.js +0 -43
  184. package/dist/testing/integration/cmd.spec.js +0 -15
  185. package/dist/testing/integration/concurrency.spec.js +0 -19
  186. package/dist/testing/integration/docker-package-registry.spec.js +0 -44
  187. package/dist/testing/integration/docker-service-crash.spec.js +0 -36
  188. package/dist/testing/integration/docker.spec.js +0 -17
  189. package/dist/testing/integration/env.spec.js +0 -48
  190. package/dist/testing/integration/environment.spec.js +0 -97
  191. package/dist/testing/integration/error.spec.js +0 -19
  192. package/dist/testing/integration/export.spec.js +0 -22
  193. package/dist/testing/integration/extend.spec.js +0 -29
  194. package/dist/testing/integration/glob.spec.js +0 -40
  195. package/dist/testing/integration/include.spec.js +0 -31
  196. package/dist/testing/integration/invalid.spec.js +0 -17
  197. package/dist/testing/integration/invalid_loop.spec.js +0 -56
  198. package/dist/testing/integration/k8s-state-cache.spec.js +0 -41
  199. package/dist/testing/integration/kubernetes-service.spec.js +0 -148
  200. package/dist/testing/integration/kubernetes.spec.js +0 -99
  201. package/dist/testing/integration/labels.spec.js +0 -42
  202. package/dist/testing/integration/local.spec.js +0 -14
  203. package/dist/testing/integration/monorepo.spec.js +0 -15
  204. package/dist/testing/integration/multi-line.spec.js +0 -17
  205. package/dist/testing/integration/reference.spec.js +0 -21
  206. package/dist/testing/integration/services.spec.js +0 -38
  207. package/dist/testing/integration/store.spec.js +0 -52
  208. package/dist/testing/integration/unknown.spec.js +0 -28
  209. package/dist/testing/integration/validate.spec.js +0 -160
  210. package/dist/testing/integration/watch-dependency.spec.js +0 -33
  211. package/dist/testing/integration/watch.spec.js +0 -45
  212. package/dist/testing/requires-kubernetes.js +0 -18
  213. package/dist/testing/requires-linux-containers.js +0 -18
  214. package/dist/testing/requires-windows-containers.js +0 -18
  215. package/dist/testing/test-case.js +0 -75
  216. package/dist/testing/test-streams.js +0 -28
  217. package/dist/testing/test-suite-setup.js +0 -3
  218. package/dist/testing/test-suite.js +0 -3
  219. package/dist/testing/testing-timeout.js +0 -32
  220. package/dist/utils/abort-event.spec.js +0 -46
  221. package/dist/utils/buffer-context.spec.js +0 -38
  222. package/dist/utils/ci.spec.js +0 -53
  223. package/dist/utils/debouncer.spec.js +0 -58
  224. package/dist/utils/emitter.spec.js +0 -34
  225. package/dist/utils/empty-writable.spec.js +0 -18
  226. package/dist/utils/environment-config.spec.js +0 -26
  227. package/dist/utils/fail-never.spec.js +0 -9
  228. package/dist/utils/sleep.spec.js +0 -22
  229. package/dist/version.spec.js +0 -12
  230. package/specs/build-file/caches/spec.md +0 -114
  231. package/specs/build-file/environment-variables/spec.md +0 -82
  232. package/specs/build-file/includes/spec.md +0 -60
  233. package/specs/build-file/references/spec.md +0 -56
  234. package/specs/build-file/spec.md +0 -62
  235. package/specs/cli/clean/spec.md +0 -85
  236. package/specs/cli/down/spec.md +0 -85
  237. package/specs/cli/execute/spec.md +0 -101
  238. package/specs/cli/init/spec.md +0 -64
  239. package/specs/cli/ls/spec.md +0 -66
  240. package/specs/cli/package/spec.md +0 -110
  241. package/specs/cli/spec.md +0 -73
  242. package/specs/cli/store-restore/spec.md +0 -74
  243. package/specs/cli/up/spec.md +0 -80
  244. package/specs/cli/validate/spec.md +0 -70
  245. package/specs/labels/spec.md +0 -91
  246. package/specs/service/container/spec.md +0 -120
  247. package/specs/service/kubernetes/spec.md +0 -97
  248. package/specs/service/spec.md +0 -105
  249. package/specs/task/caching/spec.md +0 -107
  250. package/specs/task/container/spec.md +0 -118
  251. package/specs/task/dependencies/spec.md +0 -70
  252. package/specs/task/extending/spec.md +0 -74
  253. package/specs/task/kubernetes/spec.md +0 -125
  254. package/specs/task/needs/spec.md +0 -59
  255. package/specs/task/spec.md +0 -87
  256. package/specs/task/watching/spec.md +0 -69
@@ -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
  }
@@ -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,42 @@ 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
+ async function executeWorkTask(work, environment, options,
55
+ // when set, the task was not requested and only runs if one of these tasks
56
+ // depending on it misses the cache (see executeWorkTree)
57
+ dependents = null) {
31
58
  try {
59
+ if (dependents) {
60
+ const needed = await (0, await_completed_dependencies_1.awaitDependentNeed)(dependents, environment.abortCtrl.signal);
61
+ if (!needed) {
62
+ work.status.write('debug', `${work.name} skipped, every task depending on it was a cache hit`);
63
+ work.state.set({ type: 'completed', cached: false, skipped: true, duration: 0, stateKey: '' });
64
+ return;
65
+ }
66
+ }
32
67
  work.state.set({
33
68
  type: 'starting',
34
69
  started: new Date(),
@@ -36,11 +71,6 @@ async function executeWorkTask(work, environment, options) {
36
71
  });
37
72
  await (0, watch_loop_1.watchLoop)(work, environment, options, async (cacheState, abort) => {
38
73
  const started = new Date();
39
- work.state.set({
40
- type: 'ready',
41
- stateKey: cacheState.stateKey,
42
- started: new Date(),
43
- });
44
74
  if (cacheState.cached) {
45
75
  work.status.write('debug', 'completed for cached state key ' + cacheState.stateKey);
46
76
  work.state.set({
@@ -51,6 +81,25 @@ async function executeWorkTask(work, environment, options) {
51
81
  });
52
82
  return;
53
83
  }
84
+ // Only a cache miss is `ready`: needed services start on `ready`, so a
85
+ // cached task never starts the services it would have needed.
86
+ work.state.set({
87
+ type: 'ready',
88
+ stateKey: cacheState.stateKey,
89
+ started: new Date(),
90
+ });
91
+ // Cache miss: under the explain flag, report why this task is rebuilding,
92
+ // reusing the cache-explain engine. Captured BEFORE execution overwrites
93
+ // the last-resolved record, then carried onto the completed state so the
94
+ // build summary can show the cause column. Reporting only.
95
+ let missCauses;
96
+ if (options.explain) {
97
+ const explanation = await (0, explain_1.explainTask)(work, options.cacheDefault, environment);
98
+ missCauses = explanation.causes.map(explain_1.describeCause);
99
+ for (const cause of missCauses) {
100
+ work.status.write('info', `cache miss: ${cause}`);
101
+ }
102
+ }
54
103
  await (0, await_completed_dependencies_1.awaitCompletedDependencies)(work, work.deps, abort);
55
104
  await (0, await_completed_dependencies_1.awaitRunningNeeds)(work, work.needs.map((n) => n.service), abort);
56
105
  await options.processManager.task(work, async () => {
@@ -59,22 +108,43 @@ async function executeWorkTask(work, environment, options) {
59
108
  stateKey: cacheState.stateKey,
60
109
  started,
61
110
  });
62
- await work.runtime.execute(environment, {
63
- cache: cacheState,
64
- abort,
65
- stateKey: cacheState.stateKey,
66
- state: work.state,
67
- daemon: options.daemon,
68
- });
111
+ const timeout = work.data.timeout ?? options.timeout;
112
+ const deadline = withDeadline(abort, timeout);
113
+ try {
114
+ await work.runtime.execute(environment, {
115
+ cache: cacheState,
116
+ abort: deadline.signal,
117
+ stateKey: cacheState.stateKey,
118
+ state: work.state,
119
+ daemon: options.daemon,
120
+ });
121
+ }
122
+ finally {
123
+ deadline.clear();
124
+ }
125
+ if (deadline.expired() && timeout !== null) {
126
+ // the runtime saw an abort and cleaned up; report it as a failure
127
+ // (never a cancellation) and stop the run like any failing task
128
+ work.state.set({
129
+ type: 'error',
130
+ stateKey: cacheState.stateKey,
131
+ errorMessage: `timed out after ${(0, units_1.formatDuration)(timeout)}`,
132
+ });
133
+ if (!options.watch) {
134
+ environment.abortCtrl.abort();
135
+ }
136
+ return;
137
+ }
69
138
  (0, abort_1.checkForAbort)(abort);
70
139
  if (work.state.current.type === 'running') {
71
140
  work.status.write('debug', 'completed for state key ' + cacheState.stateKey);
72
- await pushToBackend(work, environment, cacheState);
141
+ await pushToBackend(work, environment, cacheState, options);
73
142
  work.state.set({
74
143
  stateKey: cacheState.stateKey,
75
144
  type: 'completed',
76
145
  cached: false,
77
146
  duration: (0, states_1.getDuration)(started),
147
+ missCauses,
78
148
  });
79
149
  }
80
150
  else {
@@ -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
@@ -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;
@@ -0,0 +1,102 @@
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.printRunSummary = exports.formatDuration = exports.summarizeRun = void 0;
7
+ const colors_1 = __importDefault(require("colors"));
8
+ const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
9
+ const log_1 = require("../log");
10
+ function statusOf(state) {
11
+ switch (state.type) {
12
+ case 'completed':
13
+ return state.skipped ? 'skipped' : state.cached ? 'cached' : 'executed';
14
+ case 'error':
15
+ case 'crash':
16
+ return 'failed';
17
+ default:
18
+ // canceled, or never started (pending/starting/ready/running left over from
19
+ // an aborted run) — all surface as cancelled/skipped.
20
+ return 'cancelled';
21
+ }
22
+ }
23
+ // Derive the run summary purely from the final task states already on the work
24
+ // tree — nothing is re-run. Deterministically ordered by task name.
25
+ function summarizeRun(workTree, totalDuration) {
26
+ const tasks = [];
27
+ for (const task of (0, plan_work_tasks_1.iterateWorkTasks)(workTree)) {
28
+ const state = task.state.current;
29
+ const cause = state.type === 'completed' && state.missCauses?.length ? state.missCauses.join(', ') : undefined;
30
+ tasks.push({
31
+ taskId: task.id(),
32
+ taskName: task.name,
33
+ status: statusOf(state),
34
+ duration: state.type === 'completed' ? state.duration : 0,
35
+ cause,
36
+ });
37
+ }
38
+ tasks.sort((a, b) => a.taskName.localeCompare(b.taskName));
39
+ const executed = tasks.filter((t) => t.status === 'executed').length;
40
+ const cached = tasks.filter((t) => t.status === 'cached').length;
41
+ const skipped = tasks.filter((t) => t.status === 'skipped').length;
42
+ const failed = tasks.filter((t) => t.status === 'failed').length;
43
+ const cancelled = tasks.filter((t) => t.status === 'cancelled').length;
44
+ const ran = executed + cached;
45
+ return {
46
+ tasks,
47
+ executed,
48
+ cached,
49
+ skipped,
50
+ failed,
51
+ cancelled,
52
+ cacheHitRatio: ran === 0 ? 0 : cached / ran,
53
+ totalDuration,
54
+ };
55
+ }
56
+ exports.summarizeRun = summarizeRun;
57
+ function formatDuration(ms) {
58
+ if (ms < 1000) {
59
+ return `${ms}ms`;
60
+ }
61
+ return `${(ms / 1000).toFixed(1)}s`;
62
+ }
63
+ exports.formatDuration = formatDuration;
64
+ function colorStatus(status) {
65
+ const label = status.padEnd(9);
66
+ switch (status) {
67
+ case 'executed':
68
+ return colors_1.default.cyan(label);
69
+ case 'cached':
70
+ return colors_1.default.green(label);
71
+ case 'skipped':
72
+ return colors_1.default.grey(label);
73
+ case 'failed':
74
+ return colors_1.default.red(label);
75
+ case 'cancelled':
76
+ return colors_1.default.grey(label);
77
+ }
78
+ }
79
+ // Bound the name column so a single very long task name cannot blow out the
80
+ // width on large graphs.
81
+ const MAX_NAME_WIDTH = 60;
82
+ function printRunSummary(environment, summary) {
83
+ (0, log_1.printTitle)(environment, 'Summary');
84
+ if (summary.tasks.length === 0) {
85
+ environment.stdout.write(' nothing ran\n');
86
+ return;
87
+ }
88
+ const nameWidth = Math.min(MAX_NAME_WIDTH, summary.tasks.reduce((max, t) => Math.max(max, t.taskName.length), 0));
89
+ for (const task of summary.tasks) {
90
+ const name = task.taskName.length > nameWidth ? `${task.taskName.slice(0, nameWidth - 1)}…` : task.taskName.padEnd(nameWidth);
91
+ const cause = task.cause ? ` ${colors_1.default.grey(task.cause)}` : '';
92
+ environment.stdout.write(` ${name} ${colorStatus(task.status)} ${colors_1.default.grey(formatDuration(task.duration))}${cause}\n`);
93
+ }
94
+ const ratio = Math.round(summary.cacheHitRatio * 100);
95
+ const skippedSuffix = summary.skipped > 0 ? `, ${summary.skipped} skipped` : '';
96
+ const failedSuffix = summary.failed > 0 ? `, ${summary.failed} failed` : '';
97
+ const cancelledSuffix = summary.cancelled > 0 ? `, ${summary.cancelled} cancelled` : '';
98
+ environment.stdout.write(` ${summary.executed} executed, ${summary.cached} cached${skippedSuffix}${failedSuffix}${cancelledSuffix} ` +
99
+ `(${ratio}% cache hit), ${formatDuration(summary.totalDuration)} total\n`);
100
+ }
101
+ exports.printRunSummary = printRunSummary;
102
+ //# sourceMappingURL=run-summary.js.map
@@ -1,91 +1,46 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkCacheState = void 0;
4
- const get_work_cache_stats_1 = require("../../optimizer/get-work-cache-stats");
5
4
  const work_item_1 = require("../../planner/work-item");
6
5
  const get_cache_directory_1 = require("../../optimizer/get-cache-directory");
7
6
  const cache_metadata_1 = require("../cache-metadata");
8
7
  const work_task_cache_description_1 = require("../../optimizer/work-task-cache-description");
9
8
  const log_1 = require("../../log");
10
- const crypto_1 = require("crypto");
11
- function resolveEffective(item, fallbackMethod) {
12
- const declared = item.data.caching;
13
- if (declared.implicit) {
14
- return { ...declared, method: fallbackMethod };
15
- }
16
- return declared;
17
- }
18
- // Fold a task's own state key together with the state keys of its dependencies.
19
- // A change anywhere in the dependency subtree must change the resulting key, so
20
- // that a downstream task is invalidated when an upstream source changes — even
21
- // if the downstream task's own sources are untouched.
22
- function combineStateKeys(ownKey, depKeys) {
23
- if (depKeys.length === 0) {
24
- return ownKey;
25
- }
26
- // Sort so the key is independent of dependency declaration order.
27
- const sorted = [...depKeys].sort();
28
- return (0, crypto_1.createHash)('md5')
29
- .update([ownKey, ...sorted].join(','))
30
- .digest('hex');
31
- }
32
- // Compute an item's effective state key from its source files plus the effective
33
- // state keys of its dependencies, recursively. This is derived purely from the
34
- // work tree and the filesystem, so it does not depend on scheduler/run state —
35
- // important because checkCacheState runs before dependencies are awaited.
36
- async function resolveEffectiveStateKey(item, defaultCacheMethod, environment, memo) {
37
- const existing = memo.get(item.id());
38
- if (existing !== undefined) {
39
- return existing;
40
- }
41
- const resolved = resolveEffective(item, defaultCacheMethod);
42
- // Always hash with a concrete method; `none` only disables the cache *check*
43
- // for that task, its sources still influence what depends on it.
44
- const method = resolved.method === 'none' ? defaultCacheMethod : resolved.method;
45
- const stats = await (0, get_work_cache_stats_1.getWorkCacheStats)(item.data, environment);
46
- const ownKey = (0, get_work_cache_stats_1.getStateKey)(stats, method);
47
- const depKeys = [];
48
- for (const dep of item.deps) {
49
- depKeys.push(await resolveEffectiveStateKey(dep, defaultCacheMethod, environment, memo));
50
- }
51
- const combined = combineStateKeys(ownKey, depKeys);
52
- memo.set(item.id(), combined);
53
- return combined;
54
- }
9
+ const state_key_1 = require("./state-key");
10
+ const work_instance_id_1 = require("../../planner/work-instance-id");
55
11
  async function checkCacheState(item, defaultCacheMethod, environment) {
56
- const resolved = resolveEffective(item, defaultCacheMethod);
57
- const currentStats = await (0, get_work_cache_stats_1.getWorkCacheStats)(item.data, environment);
58
- const ownKey = (0, get_work_cache_stats_1.getStateKey)(currentStats, resolved.method === 'none' ? defaultCacheMethod : resolved.method);
59
- const memo = new Map();
60
- const depKeys = [];
61
- for (const dep of item.deps) {
62
- depKeys.push(await resolveEffectiveStateKey(dep, defaultCacheMethod, environment, memo));
12
+ const { stateKey, stats: currentStats, resolved, provable, unmatched, } = await (0, state_key_1.computeStateKey)(item, defaultCacheMethod, environment);
13
+ for (const src of unmatched) {
14
+ item.status.write('warn', `src "${src.source}" matches no files`);
63
15
  }
64
- const stateKey = combineStateKeys(ownKey, depKeys);
65
16
  if (resolved.method === 'none') {
66
17
  item.status.write('debug', `${item.name} is skipping cache check, because caching is disabled`);
67
- return { cached: false, stateKey, resolved };
18
+ return { cached: false, stateKey, resolved, provable };
19
+ }
20
+ if ((0, work_item_1.isWorkTaskItem)(item) && !provable) {
21
+ item.status.write('debug', `${item.name} always runs, it or one of its dependencies has no src files`);
22
+ return { cached: false, stateKey, resolved, provable };
68
23
  }
69
24
  if ((0, work_item_1.isWorkTaskItem)(item)) {
70
25
  const runtimeStateKey = await item.runtime.currentStateKey(environment);
71
26
  if (runtimeStateKey === stateKey) {
72
- return { cached: true, stateKey, resolved };
27
+ return { cached: true, stateKey, resolved, provable };
73
28
  }
74
29
  try {
75
- const cacheDir = (0, get_cache_directory_1.getCacheDirectory)(item.id());
30
+ const cacheDir = (0, get_cache_directory_1.getCacheDirectory)((0, work_instance_id_1.getWorkInstanceId)(item));
76
31
  const pulled = await resolved.backend.pull(item.id(), stateKey, cacheDir, environment);
77
32
  if (pulled) {
78
33
  item.status.write('info', `${item.name} pulled from cache "${resolved.name}" (${resolved.backend.type})`);
79
34
  await item.runtime.restore(environment, cacheDir);
80
- await (0, cache_metadata_1.writeCacheMetadata)(environment, item.id(), currentStats, (0, work_task_cache_description_1.getWorkTaskCacheDescription)(item.data));
81
- return { cached: true, stateKey, resolved };
35
+ await (0, cache_metadata_1.writeCacheMetadata)(environment, item, currentStats, (0, work_task_cache_description_1.getWorkTaskCacheDescription)(item));
36
+ return { cached: true, stateKey, resolved, provable };
82
37
  }
83
38
  }
84
39
  catch (e) {
85
40
  item.status.write('warn', `${item.name} failed to pull from cache "${resolved.name}": ${(0, log_1.getErrorMessage)(e)} — continuing with local execution`);
86
41
  }
87
42
  }
88
- return { cached: false, stateKey, resolved };
43
+ return { cached: false, stateKey, resolved, provable };
89
44
  }
90
45
  exports.checkCacheState = checkCacheState;
91
46
  //# sourceMappingURL=enqueue-next.js.map