hammerkit 1.6.0 → 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 +12 -1
  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 -32
  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 -18
  189. package/dist/testing/integration/env.spec.js +0 -48
  190. package/dist/testing/integration/environment.spec.js +0 -97
  191. package/dist/testing/integration/error.spec.js +0 -19
  192. package/dist/testing/integration/export.spec.js +0 -22
  193. package/dist/testing/integration/extend.spec.js +0 -29
  194. package/dist/testing/integration/glob.spec.js +0 -40
  195. package/dist/testing/integration/include.spec.js +0 -31
  196. package/dist/testing/integration/invalid.spec.js +0 -17
  197. package/dist/testing/integration/invalid_loop.spec.js +0 -56
  198. package/dist/testing/integration/k8s-state-cache.spec.js +0 -41
  199. package/dist/testing/integration/kubernetes-service.spec.js +0 -148
  200. package/dist/testing/integration/kubernetes.spec.js +0 -99
  201. package/dist/testing/integration/labels.spec.js +0 -42
  202. package/dist/testing/integration/local.spec.js +0 -14
  203. package/dist/testing/integration/monorepo.spec.js +0 -15
  204. package/dist/testing/integration/multi-line.spec.js +0 -17
  205. package/dist/testing/integration/reference.spec.js +0 -21
  206. package/dist/testing/integration/services.spec.js +0 -38
  207. package/dist/testing/integration/store.spec.js +0 -52
  208. package/dist/testing/integration/unknown.spec.js +0 -28
  209. package/dist/testing/integration/validate.spec.js +0 -160
  210. package/dist/testing/integration/watch-dependency.spec.js +0 -33
  211. package/dist/testing/integration/watch.spec.js +0 -45
  212. package/dist/testing/requires-kubernetes.js +0 -18
  213. package/dist/testing/requires-linux-containers.js +0 -18
  214. package/dist/testing/requires-windows-containers.js +0 -18
  215. package/dist/testing/test-case.js +0 -75
  216. package/dist/testing/test-streams.js +0 -28
  217. package/dist/testing/test-suite-setup.js +0 -3
  218. package/dist/testing/test-suite.js +0 -3
  219. package/dist/testing/testing-timeout.js +0 -32
  220. package/dist/utils/abort-event.spec.js +0 -46
  221. package/dist/utils/buffer-context.spec.js +0 -38
  222. package/dist/utils/ci.spec.js +0 -53
  223. package/dist/utils/debouncer.spec.js +0 -58
  224. package/dist/utils/emitter.spec.js +0 -34
  225. package/dist/utils/empty-writable.spec.js +0 -18
  226. package/dist/utils/environment-config.spec.js +0 -26
  227. package/dist/utils/fail-never.spec.js +0 -9
  228. package/dist/utils/sleep.spec.js +0 -22
  229. package/dist/version.spec.js +0 -12
  230. package/specs/build-file/caches/spec.md +0 -114
  231. package/specs/build-file/environment-variables/spec.md +0 -82
  232. package/specs/build-file/includes/spec.md +0 -60
  233. package/specs/build-file/references/spec.md +0 -56
  234. package/specs/build-file/spec.md +0 -62
  235. package/specs/cli/clean/spec.md +0 -85
  236. package/specs/cli/down/spec.md +0 -85
  237. package/specs/cli/execute/spec.md +0 -101
  238. package/specs/cli/init/spec.md +0 -64
  239. package/specs/cli/ls/spec.md +0 -66
  240. package/specs/cli/package/spec.md +0 -110
  241. package/specs/cli/spec.md +0 -73
  242. package/specs/cli/store-restore/spec.md +0 -74
  243. package/specs/cli/up/spec.md +0 -80
  244. package/specs/cli/validate/spec.md +0 -70
  245. package/specs/labels/spec.md +0 -91
  246. package/specs/service/container/spec.md +0 -120
  247. package/specs/service/kubernetes/spec.md +0 -97
  248. package/specs/service/spec.md +0 -105
  249. package/specs/task/caching/spec.md +0 -107
  250. package/specs/task/container/spec.md +0 -118
  251. package/specs/task/dependencies/spec.md +0 -70
  252. package/specs/task/extending/spec.md +0 -74
  253. package/specs/task/kubernetes/spec.md +0 -125
  254. package/specs/task/needs/spec.md +0 -59
  255. package/specs/task/spec.md +0 -87
  256. package/specs/task/watching/spec.md +0 -69
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeStateKey = exports.combineStateKeys = exports.resolveEffective = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const get_work_cache_stats_1 = require("../../optimizer/get-work-cache-stats");
6
+ // The state-key engine is shared by the scheduler's hit/miss decision
7
+ // (`checkCacheState`) and the read-only cache-explain engine, so a prediction
8
+ // can never diverge from the decision it predicts — there is one implementation.
9
+ function resolveEffective(item, fallbackMethod) {
10
+ const declared = item.data.caching;
11
+ if (declared.implicit) {
12
+ return { ...declared, method: fallbackMethod };
13
+ }
14
+ return declared;
15
+ }
16
+ exports.resolveEffective = resolveEffective;
17
+ // A task whose inputs cannot be observed cannot be proven up to date, so it
18
+ // always runs, and so does everything depending on it (specs/task SC-001). That
19
+ // is a task that declares no `src` of its own — the sources it inherits from its
20
+ // dependencies say nothing about the files its own commands read — and a task
21
+ // whose `src` entries all match no file (a typo, a moved path). Services are not
22
+ // affected.
23
+ function isOwnProvable(item, unmatched) {
24
+ // unmatched ⊆ declared, so this is also false when nothing is declared
25
+ return unmatched.length < item.data.src.filter((src) => !src.inherited).length;
26
+ }
27
+ // Fold a task's own state key together with the state keys of its dependencies.
28
+ // A change anywhere in the dependency subtree must change the resulting key, so
29
+ // that a downstream task is invalidated when an upstream source changes — even
30
+ // if the downstream task's own sources are untouched.
31
+ function combineStateKeys(ownKey, depKeys) {
32
+ if (depKeys.length === 0) {
33
+ return ownKey;
34
+ }
35
+ // Sort so the key is independent of dependency declaration order.
36
+ const sorted = [...depKeys].sort();
37
+ return (0, crypto_1.createHash)('md5')
38
+ .update([ownKey, ...sorted].join(','))
39
+ .digest('hex');
40
+ }
41
+ exports.combineStateKeys = combineStateKeys;
42
+ // `none` only disables the cache *check* for a task; its sources still influence
43
+ // what depends on it, so a concrete method is always used for the hash itself.
44
+ function concreteMethod(resolved, defaultCacheMethod) {
45
+ return resolved.method === 'none' ? defaultCacheMethod : resolved.method;
46
+ }
47
+ // Compute an item's effective state key from its source files plus the effective
48
+ // state keys of its dependencies, recursively. This is derived purely from the
49
+ // work tree and the filesystem, so it does not depend on scheduler/run state —
50
+ // important because it runs before dependencies are awaited.
51
+ async function resolveEffectiveStateKey(item, defaultCacheMethod, environment, memo) {
52
+ const existing = memo.get(item.id());
53
+ if (existing !== undefined) {
54
+ return existing;
55
+ }
56
+ const { stateKey, provable } = await computeStateKey(item, defaultCacheMethod, environment, memo);
57
+ const effective = { stateKey, provable };
58
+ memo.set(item.id(), effective);
59
+ return effective;
60
+ }
61
+ // Compute the combined state key for a single item together with the source
62
+ // stats it was derived from, sharing one filesystem walk with callers that also
63
+ // need the stats (the scheduler, the explain engine).
64
+ async function computeStateKey(item, defaultCacheMethod, environment, memo = new Map()) {
65
+ const resolved = resolveEffective(item, defaultCacheMethod);
66
+ const { stats, unmatched } = await (0, get_work_cache_stats_1.getWorkItemCacheStats)(item, environment);
67
+ const ownKey = (0, get_work_cache_stats_1.getStateKey)(stats, concreteMethod(resolved, defaultCacheMethod));
68
+ const depKeys = [];
69
+ let provable = isOwnProvable(item, unmatched);
70
+ for (const dep of item.deps) {
71
+ const effective = await resolveEffectiveStateKey(dep, defaultCacheMethod, environment, memo);
72
+ depKeys.push(effective.stateKey);
73
+ provable = provable && effective.provable;
74
+ }
75
+ return { stateKey: combineStateKeys(ownKey, depKeys), stats, resolved, provable, unmatched };
76
+ }
77
+ exports.computeStateKey = computeStateKey;
78
+ //# sourceMappingURL=state-key.js.map
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.awaitDeployRunningState = exports.awaitJobState = exports.awaitRunningState = void 0;
3
+ exports.deleteJobAndWait = exports.awaitDeployRunningState = exports.deleteJob = exports.awaitJobCompletion = exports.awaitRunningState = void 0;
4
+ const abort_1 = require("../executer/abort");
5
+ const abort_event_1 = require("../utils/abort-event");
6
+ const apply_1 = require("./apply");
4
7
  function awaitRunningState(instance, env, name, phase) {
5
8
  return new Promise((resolve, reject) => {
6
9
  const req = instance.watch.watch(`/api/v1/namespaces/${env.namespace}/pods`, {}, (type, obj) => {
@@ -22,27 +25,56 @@ function awaitRunningState(instance, env, name, phase) {
22
25
  });
23
26
  }
24
27
  exports.awaitRunningState = awaitRunningState;
25
- function awaitJobState(instance, env, name) {
26
- return new Promise((resolve, reject) => {
27
- const req = instance.watch.watch(`/apis/batch/v1/namespaces/${env.namespace}/jobs/${name}`, {}, (type, obj) => {
28
- if (obj && obj.metadata?.name === name && obj.metadata?.namespace === env.namespace && obj.status?.succeeded) {
29
- req.then((r) => r.abort());
30
- resolve();
31
- }
32
- }, (err) => {
33
- if (err && err.message === 'aborted') {
34
- return;
35
- }
36
- if (err) {
37
- reject(err);
38
- }
39
- else {
40
- resolve();
41
- }
28
+ // Wait for a job to finish by polling its status: resolves once it succeeded,
29
+ // rejects when it failed (its `Failed` condition, i.e. retries are exhausted).
30
+ // On abort the job is deleted — with its pods — and an AbortError is thrown, so
31
+ // a cancelled or timed-out task leaves nothing running on the cluster.
32
+ async function awaitJobCompletion(instance, env, name, abort, pollInterval = 1000) {
33
+ for (;;) {
34
+ if (abort.aborted) {
35
+ await deleteJob(instance, env, name);
36
+ throw new abort_1.AbortError();
37
+ }
38
+ const job = await instance.batchApi.readNamespacedJobStatus(name, env.namespace);
39
+ const status = job.body.status;
40
+ if (status?.succeeded) {
41
+ return;
42
+ }
43
+ const failed = status?.conditions?.find((c) => c.type === 'Failed' && c.status === 'True');
44
+ if (failed) {
45
+ throw new Error(`job ${name} failed: ${failed.message ?? failed.reason ?? 'unknown reason'}`);
46
+ }
47
+ await sleep(pollInterval, abort);
48
+ }
49
+ }
50
+ exports.awaitJobCompletion = awaitJobCompletion;
51
+ function sleep(ms, abort) {
52
+ if (abort.aborted) {
53
+ return Promise.resolve();
54
+ }
55
+ return new Promise((resolve) => {
56
+ const timer = setTimeout(() => {
57
+ listener.close();
58
+ resolve();
59
+ }, ms);
60
+ // not aborted yet, so this callback can only fire later, after assignment
61
+ const listener = (0, abort_event_1.listenOnAbort)(abort, () => {
62
+ clearTimeout(timer);
63
+ resolve();
42
64
  });
43
65
  });
44
66
  }
45
- exports.awaitJobState = awaitJobState;
67
+ async function deleteJob(instance, env, name) {
68
+ try {
69
+ await instance.batchApi.deleteNamespacedJob(name, env.namespace, undefined, undefined, 0, undefined, 'Background');
70
+ }
71
+ catch (e) {
72
+ if ((0, apply_1.statusCodeOf)(e) !== 404) {
73
+ throw e;
74
+ }
75
+ }
76
+ }
77
+ exports.deleteJob = deleteJob;
46
78
  function awaitDeployRunningState(instance, env, name) {
47
79
  return new Promise((resolve, reject) => {
48
80
  const req = instance.watch.watch(`/apis/apps/v1/namespaces/${env.namespace}/deployments`, {}, (type, obj) => {
@@ -68,4 +100,26 @@ function awaitDeployRunningState(instance, env, name) {
68
100
  });
69
101
  }
70
102
  exports.awaitDeployRunningState = awaitDeployRunningState;
103
+ // Delete a job and wait until it is gone: its name is reused for the task's next
104
+ // command (and the next run with the same state), and creating it while the old
105
+ // one is still being deleted would pick up the old job.
106
+ async function deleteJobAndWait(instance, env, name, abort, pollInterval = 500) {
107
+ await deleteJob(instance, env, name);
108
+ for (;;) {
109
+ try {
110
+ await instance.batchApi.readNamespacedJob(name, env.namespace);
111
+ }
112
+ catch (e) {
113
+ if ((0, apply_1.statusCodeOf)(e) === 404) {
114
+ return;
115
+ }
116
+ throw e;
117
+ }
118
+ if (abort.aborted) {
119
+ throw new abort_1.AbortError();
120
+ }
121
+ await sleep(pollInterval, abort);
122
+ }
123
+ }
124
+ exports.deleteJobAndWait = deleteJobAndWait;
71
125
  //# sourceMappingURL=await-running-state.js.map
package/dist/log.js CHANGED
@@ -109,9 +109,14 @@ async function printWorkTreeResult(schedulerState, env) {
109
109
  for (const task of (0, plan_work_tasks_1.iterateWorkTasks)(schedulerState)) {
110
110
  let message = `${formatDate(new Date())} ${getType('task')} ${getNodeName(task.name, maxNodeNameLength)} - ${getStateText(task.state.current)}`;
111
111
  if (task.state.current.type === 'completed') {
112
- message += ` in ${task.state.current.duration}ms`;
113
- if (task.state.current.cached) {
114
- message += ' [CACHED]';
112
+ if (task.state.current.skipped) {
113
+ message += ' [SKIPPED]';
114
+ }
115
+ else {
116
+ message += ` in ${task.state.current.duration}ms`;
117
+ if (task.state.current.cached) {
118
+ message += ' [CACHED]';
119
+ }
115
120
  }
116
121
  }
117
122
  if (task.state.current.type === 'crash') {
@@ -263,9 +268,14 @@ function writeWorkTreeStatus(schedulerState, env, ticker) {
263
268
  }
264
269
  }
265
270
  else if (task.state.current.type === 'completed') {
266
- message += ` in ${task.state.current.duration}ms`;
267
- if (task.state.current.cached) {
268
- message += ' [CACHED]';
271
+ if (task.state.current.skipped) {
272
+ message += ' [SKIPPED]';
273
+ }
274
+ else {
275
+ message += ` in ${task.state.current.duration}ms`;
276
+ if (task.state.current.cached) {
277
+ message += ' [CACHED]';
278
+ }
269
279
  }
270
280
  }
271
281
  else if (task.state.current.type === 'error') {
@@ -4,11 +4,12 @@ exports.liveLogger = void 0;
4
4
  const log_1 = require("../log");
5
5
  function liveLogger(state, env) {
6
6
  const maxNodeNameLength = (0, log_1.getWorkItemMaxLength)(state.current);
7
- env.status.on((log) => {
7
+ const listener = env.status.on((log) => {
8
8
  (0, log_1.writeWorkItemLogToConsole)(env, log, maxNodeNameLength);
9
9
  });
10
10
  return {
11
11
  async complete(evt, env) {
12
+ listener.close();
12
13
  await (0, log_1.printWorkTreeResult)(evt.state, env);
13
14
  },
14
15
  };
@@ -2,9 +2,15 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.calculateChecksum = void 0;
4
4
  const crypto_1 = require("crypto");
5
+ // Hashes the raw bytes: decoding to text first would map every invalid UTF-8
6
+ // sequence to U+FFFD, so two different binary files could share a checksum.
7
+ // For UTF-8 text the result is unchanged.
5
8
  async function calculateChecksum(environment, path) {
6
- const input = await environment.file.read(path);
7
- return (0, crypto_1.createHash)('sha1').update(input).digest('hex');
9
+ const hash = (0, crypto_1.createHash)('sha1');
10
+ for await (const chunk of environment.file.readStream(path)) {
11
+ hash.update(chunk);
12
+ }
13
+ return hash.digest('hex');
8
14
  }
9
15
  exports.calculateChecksum = calculateChecksum;
10
16
  //# sourceMappingURL=calculate-checksum.js.map
@@ -1,45 +1,95 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getStateKey = exports.getWorkCacheStats = void 0;
3
+ exports.getStateKey = exports.getWorkItemCacheStats = exports.getWorkCacheStats = void 0;
4
4
  const calculate_checksum_1 = require("./calculate-checksum");
5
5
  const path_1 = require("path");
6
6
  const crypto_1 = require("crypto");
7
- async function addWorkCacheStats(result, cwd, path, matcher, context) {
7
+ function isExcluded(path, excluded) {
8
+ return excluded.some((excludedPath) => path === excludedPath || path.startsWith(excludedPath + path_1.sep));
9
+ }
10
+ async function addWorkCacheStats(result, cwd, path, matcher, context, root, excluded) {
11
+ if (isExcluded(path, excluded)) {
12
+ return 0;
13
+ }
8
14
  const exists = await context.file.exists(path);
9
15
  if (!exists) {
10
- return;
16
+ return 0;
11
17
  }
12
18
  const stats = await context.file.stats(path);
13
19
  if (stats.type === 'file') {
14
- if (matcher(path)) {
15
- const checksum = await (0, calculate_checksum_1.calculateChecksum)(context, path);
16
- result.files[(0, path_1.relative)(cwd, path)] = { lastModified: stats.lastModified, checksum };
20
+ if (!matcher(path)) {
21
+ return 0;
17
22
  }
23
+ const checksum = await (0, calculate_checksum_1.calculateChecksum)(context, path);
24
+ // '/'-separated on every OS, so a Windows host keys a container task like Linux
25
+ result.files[(0, path_1.relative)(cwd, path).split(path_1.sep).join(path_1.posix.sep)] = { lastModified: stats.lastModified, checksum };
26
+ return 1;
18
27
  }
19
28
  else if (stats.type === 'directory') {
20
- const files = await context.file.listFiles(path);
21
- for (const file of files) {
22
- const subPath = (0, path_1.join)(path, file);
23
- if (matcher(subPath)) {
24
- await addWorkCacheStats(result, cwd, (0, path_1.join)(path, file), matcher, context);
25
- }
29
+ // A glob such as src/**/*.ts never matches a directory itself, so a
30
+ // directory is walked when files inside it could match.
31
+ if (!root && !matcher(path, true)) {
32
+ return 0;
33
+ }
34
+ let matched = 0;
35
+ for (const file of await context.file.listFiles(path)) {
36
+ matched += await addWorkCacheStats(result, cwd, (0, path_1.join)(path, file), matcher, context, false, excluded);
26
37
  }
38
+ return matched;
27
39
  }
40
+ return 0;
28
41
  }
29
42
  async function getWorkCacheStats(work, environment) {
43
+ return (await collectSourceStats(work, environment, [])).stats;
44
+ }
45
+ exports.getWorkCacheStats = getWorkCacheStats;
46
+ // `excluded` paths are left out of the walk, see getWorkItemCacheStats. A src
47
+ // entry inside an excluded path is represented there, so it is never unmatched.
48
+ async function collectSourceStats(work, environment, excluded) {
30
49
  const result = {
31
50
  created: new Date(),
32
51
  files: {},
33
52
  };
53
+ const unmatched = [];
34
54
  for (const src of work.src) {
35
- await addWorkCacheStats(result, work.cwd, src.absolutePath, (file) => src.matcher(file, work.cwd), environment);
55
+ const matcher = (file, partial) => src.matcher(file, work.cwd, partial);
56
+ const matched = await addWorkCacheStats(result, work.cwd, src.absolutePath, matcher, environment, true, excluded);
57
+ if (matched === 0 && !src.inherited && !isExcluded(src.absolutePath, excluded)) {
58
+ unmatched.push(src);
59
+ }
36
60
  }
37
- return result;
61
+ return { stats: result, unmatched };
62
+ }
63
+ // The source stats of a task or service, leaving out everything its (transitive)
64
+ // dependencies generate. Those outputs are already represented by the
65
+ // dependencies' task ids and state keys — and hashing them would key the item
66
+ // differently before and after its dependencies ran, since the key is computed
67
+ // before they do.
68
+ function getWorkItemCacheStats(item, environment) {
69
+ return collectSourceStats(item.data, environment, dependencyOutputs(item));
70
+ }
71
+ exports.getWorkItemCacheStats = getWorkItemCacheStats;
72
+ function dependencyOutputs(item) {
73
+ const outputs = new Set();
74
+ const visited = new Set();
75
+ const visit = (dep) => {
76
+ if (visited.has(dep)) {
77
+ return;
78
+ }
79
+ visited.add(dep);
80
+ for (const generate of dep.data.generates) {
81
+ outputs.add(generate.path);
82
+ }
83
+ dep.deps.forEach(visit);
84
+ };
85
+ item.deps.forEach(visit);
86
+ return [...outputs];
38
87
  }
39
- exports.getWorkCacheStats = getWorkCacheStats;
40
88
  function getStateKey(stats, cacheMethod) {
41
89
  const contents = [];
42
- for (const key of Object.keys(stats.files)) {
90
+ // Sorted: the walk follows the filesystem's listing order, which differs
91
+ // between machines (ext4 and overlayfs list in hash order).
92
+ for (const key of Object.keys(stats.files).sort()) {
43
93
  if (cacheMethod === 'checksum') {
44
94
  contents.push(`${key}:${stats.files[key].checksum}`);
45
95
  }
@@ -1,14 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getWorkTaskCacheDescription = void 0;
3
+ exports.storedCacheDescription = exports.getWorkTaskCacheDescription = void 0;
4
4
  const os_1 = require("os");
5
+ const crypto_1 = require("crypto");
5
6
  const replace_env_variables_1 = require("../environment/replace-env-variables");
6
- function getWorkTaskCacheDescription(task) {
7
+ const portable_path_1 = require("../planner/utils/portable-path");
8
+ // Every path is made project-relative (portablePath) so the description, and the
9
+ // task id hashed from it, is identical wherever the project is checked out.
10
+ //
11
+ // `deps` holds the dependencies' task ids, so a change to a dependency's
12
+ // definition (command, env, image, …) gives every task depending on it a new id.
13
+ // The state key only folds in the dependencies' *sources*.
14
+ function getWorkTaskCacheDescription(item) {
15
+ const task = item.data;
16
+ const portable = (path) => (0, portable_path_1.portablePath)(task.projectRoot, path);
7
17
  const envs = (0, replace_env_variables_1.getEnvironmentVariables)(task.envs);
8
18
  return {
9
19
  shell: task.shell ?? undefined,
10
20
  platform: task.type === 'container-task' ? task.image : (0, os_1.platform)(),
11
- generates: task.generates.map((g) => g.path).sort() ?? undefined,
21
+ // outputs hold native binaries, and docker pulls images for the host
22
+ // architecture, so container tasks are keyed by it too
23
+ arch: process.arch,
24
+ generates: task.generates.map((g) => portable(g.path)).sort() ?? undefined,
12
25
  src: task.src.map((s) => s.source).sort() ?? undefined,
13
26
  envs: Object.keys(envs)
14
27
  .sort()
@@ -16,10 +29,26 @@ function getWorkTaskCacheDescription(task) {
16
29
  map[key] = envs[key];
17
30
  return map;
18
31
  }, {}) ?? undefined,
19
- cmds: task.cmds.map((c) => ({ cmd: c.cmd, cwd: c.cwd })),
32
+ cmds: task.cmds.map((c) => ({ cmd: c.cmd, cwd: portable(c.cwd) })),
20
33
  mounts: task.type === 'container-task' ? task.mounts.map((m) => m.mount).sort() : undefined,
21
- cwd: task.cwd,
34
+ cwd: portable(task.cwd),
35
+ deps: item.deps.length > 0 ? item.deps.map((dep) => dep.id()).sort() : undefined,
22
36
  };
23
37
  }
24
38
  exports.getWorkTaskCacheDescription = getWorkTaskCacheDescription;
39
+ // The form written to disk: description.json in a cache entry (pushed to remote
40
+ // backends, readable by everyone who can pull) and the local explain record.
41
+ // Env is where tokens get passed, so each value is stored as a digest; the task
42
+ // id is still hashed from the values themselves.
43
+ function storedCacheDescription(description) {
44
+ if (!description.envs) {
45
+ return description;
46
+ }
47
+ const envs = {};
48
+ for (const [key, value] of Object.entries(description.envs)) {
49
+ envs[key] = `sha256:${(0, crypto_1.createHash)('sha256').update(value).digest('hex')}`;
50
+ }
51
+ return { ...description, envs };
52
+ }
53
+ exports.storedCacheDescription = storedCacheDescription;
25
54
  //# sourceMappingURL=work-task-cache-description.js.map
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findProjectRoot = void 0;
4
+ const path_1 = require("path");
5
+ // The project root anchors cache identity: every path that participates in a
6
+ // task or service id is expressed relative to it, so the same checkout yields the
7
+ // same ids wherever it lives (an agent sandbox, a CI runner, a laptop). The git
8
+ // root is used when there is one, so the root does not move with the build file
9
+ // the CLI was invoked with (`-f apps/web/.hammerkit.yaml` vs the repo root);
10
+ // otherwise the directory of the main build file.
11
+ async function findProjectRoot(buildFileDirectory, environment) {
12
+ let current = buildFileDirectory;
13
+ for (;;) {
14
+ // `.git` is a directory in a normal clone and a file in a worktree/submodule
15
+ if (await environment.file.exists((0, path_1.join)(current, '.git'))) {
16
+ return current;
17
+ }
18
+ const parent = (0, path_1.dirname)(current);
19
+ if (parent === current) {
20
+ return buildFileDirectory;
21
+ }
22
+ current = parent;
23
+ }
24
+ }
25
+ exports.findProjectRoot = findProjectRoot;
26
+ //# sourceMappingURL=project-root.js.map
@@ -51,6 +51,7 @@ function parseService(workTree, service, environment, context) {
51
51
  const envs = (0, replace_env_variables_1.buildEnvironmentVariables)(service.envs, environment, context);
52
52
  const workService = {
53
53
  cwd: service.cwd,
54
+ projectRoot: context.projectRoot,
54
55
  name: service.relativeName,
55
56
  ports: (0, parse_work_ports_1.parseWorkPorts)(service.schema, envs),
56
57
  scope: service.scope,
@@ -94,7 +95,7 @@ function parseService(workTree, service, environment, context) {
94
95
  }
95
96
  : null,
96
97
  envs: (0, replace_env_variables_1.buildEnvironmentVariables)(service.envs, environment, context),
97
- image: service.schema.image,
98
+ image: (0, template_value_1.templateValue)(service.schema.image, envs),
98
99
  cwd: service.cwd,
99
100
  cmd: service.schema.cmd ? (0, parse_work_command_1.parseWorkCommand)(service.cwd, service.schema.cmd, envs) : null,
100
101
  volumes: (0, parse_work_volume_1.parseWorkVolumes)(service.cwd, service.schema.volumes, envs),
@@ -16,11 +16,15 @@ const replace_env_variables_1 = require("../../environment/replace-env-variables
16
16
  const lazy_resolver_1 = require("../../executer/lazy-resolver");
17
17
  const get_work_runtime_1 = require("./get-work-runtime");
18
18
  const resolve_cache_1 = require("../../cache/resolve-cache");
19
+ const units_1 = require("../../utils/units");
19
20
  function appendWorkTask(workTree, cwd, referenceTask, environment, context) {
20
21
  const task = parseTask(cwd, referenceTask, environment, context);
21
22
  if (!workTree.tasks[task.name]) {
22
23
  const workItem = {
23
- id: (0, lazy_resolver_1.lazyResolver)(() => (0, work_cache_id_1.getWorkTaskId)(task)),
24
+ // A dependency cycle is rejected before anything runs (checkForLoop), but
25
+ // the id may be asked for earlier; re-entry yields a placeholder instead of
26
+ // recursing forever.
27
+ id: (0, lazy_resolver_1.lazyResolver)(() => (0, work_cache_id_1.getWorkTaskId)(workItem), () => `cycle:${task.name}`),
24
28
  name: task.name,
25
29
  data: task,
26
30
  status: environment.status.from(task),
@@ -53,6 +57,7 @@ function parseTask(cwd, task, environment, context) {
53
57
  description: (0, template_value_1.templateValue)(task.schema.description || '', envs).trim(),
54
58
  name: task.relativeName,
55
59
  cwd,
60
+ projectRoot: context.projectRoot,
56
61
  cmds: (0, parse_work_command_1.parseWorkCommands)(cwd, task.schema.cmds || [], envs),
57
62
  src: (0, parse_work_source_1.parseWorkSource)(cwd, task.schema.src, envs),
58
63
  generates: (0, parse_work_generate_1.parseWorkGenerate)(cwd, task.schema, envs),
@@ -61,6 +66,7 @@ function parseTask(cwd, task, environment, context) {
61
66
  caching: (0, resolve_cache_1.resolveCache)(task.schema.cache ?? null, context.caches, task.relativeName),
62
67
  shell: task.schema.shell ? (0, template_value_1.templateValue)(task.schema.shell, envs) : '/bin/sh',
63
68
  continuous: task.schema.continuous ?? false,
69
+ timeout: task.schema.timeout ? (0, units_1.parseDuration)(task.schema.timeout) : null,
64
70
  };
65
71
  if ((0, build_file_task_schema_1.isBuildFileContainerTaskSchema)(task.schema)) {
66
72
  return {
@@ -15,38 +15,25 @@ function parseWorkSource(cwd, sources, envs) {
15
15
  return result;
16
16
  }
17
17
  for (const source of sources) {
18
- const wildcardIndex = source.indexOf('*');
19
- if (wildcardIndex >= 0) {
20
- if (wildcardIndex === 0) {
21
- const absolutePath = cwd;
22
- result.push({
23
- matcher: (file, cwd) => {
24
- const matcher = new minimatch_1.Minimatch(source, { dot: true });
25
- return matcher.match(toPosix((0, path_1.relative)(cwd, file)));
26
- },
27
- inherited: null,
28
- source,
29
- absolutePath,
30
- isFile: false,
31
- });
32
- }
33
- else {
34
- const prefixSource = source.substring(0, wildcardIndex);
35
- const absolutePath = (0, path_1.join)(cwd, (0, template_value_1.templateValue)(prefixSource, envs));
36
- result.push({
37
- matcher: (file, cwd) => {
38
- const matcher = new minimatch_1.Minimatch(toPosix((0, path_1.join)(cwd, source)), { dot: true });
39
- return matcher.match(toPosix(file));
40
- },
41
- inherited: null,
42
- source,
43
- absolutePath,
44
- isFile: false,
45
- });
46
- }
18
+ // env variables are substituted before matching, so `$DIR/*.ts` globs the
19
+ // directory the task actually reads; `source` keeps the declared form.
20
+ const pattern = path_1.posix.normalize(toPosix((0, template_value_1.templateValue)(source, envs)));
21
+ const matcher = new minimatch_1.Minimatch(pattern, { dot: true });
22
+ if (isGlob(matcher)) {
23
+ // Walk from the literal directory prefix and match paths relative to the
24
+ // declaring task's cwd — which stays correct when the source is inherited
25
+ // by a dependant in another directory.
26
+ const prefix = literalPrefix(pattern);
27
+ result.push({
28
+ matcher: (file, _cwd, partial) => matcher.match(toPosix((0, path_1.relative)(cwd, file)), partial),
29
+ inherited: null,
30
+ source,
31
+ absolutePath: prefix ? (0, path_1.join)(cwd, prefix) : cwd,
32
+ isFile: false,
33
+ });
47
34
  }
48
35
  else {
49
- const absolutePath = (0, path_1.join)(cwd, (0, template_value_1.templateValue)(source, envs));
36
+ const absolutePath = (0, path_1.join)(cwd, pattern);
50
37
  result.push({
51
38
  matcher: (file) => file.startsWith(absolutePath),
52
39
  absolutePath,
@@ -59,6 +46,24 @@ function parseWorkSource(cwd, sources, envs) {
59
46
  return result;
60
47
  }
61
48
  exports.parseWorkSource = parseWorkSource;
49
+ // minimatch parses a literal path into a single set of plain strings; anything
50
+ // else (wildcards, ?, [...], {a,b}, extglobs) is a glob.
51
+ function isGlob(matcher) {
52
+ return matcher.set.length !== 1 || matcher.set[0].some((part) => typeof part !== 'string');
53
+ }
54
+ // The leading directories of a glob that contain no glob syntax — where the
55
+ // walk for matching files starts.
56
+ function literalPrefix(pattern) {
57
+ const segments = pattern.split('/');
58
+ const literal = [];
59
+ for (const segment of segments.slice(0, -1)) {
60
+ if (/[*?[\]{}()!+@]/.test(segment)) {
61
+ break;
62
+ }
63
+ literal.push(segment);
64
+ }
65
+ return literal.join('/');
66
+ }
62
67
  function createSource(absolutePath) {
63
68
  return {
64
69
  matcher: (file) => file.startsWith(absolutePath),
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.portablePath = void 0;
4
+ const os_1 = require("os");
5
+ const path_1 = require("path");
6
+ const toPosix = (value) => (path_1.sep === path_1.posix.sep ? value : value.split(path_1.sep).join(path_1.posix.sep));
7
+ function within(base, path) {
8
+ const rel = (0, path_1.relative)(base, path);
9
+ if (rel.startsWith('..') || (0, path_1.isAbsolute)(rel)) {
10
+ return null;
11
+ }
12
+ return toPosix(rel);
13
+ }
14
+ // Express an absolute path in a machine-independent form for cache identity:
15
+ // relative to the project root when inside it, relative to the home directory
16
+ // (`~/…`) when inside that, and unchanged otherwise (a fixed system path such as
17
+ // `/var/run/docker.sock` is already the same on every machine).
18
+ function portablePath(projectRoot, path) {
19
+ const inRoot = within(projectRoot, path);
20
+ if (inRoot !== null) {
21
+ return inRoot.length > 0 ? inRoot : '.';
22
+ }
23
+ const inHome = within((0, os_1.homedir)(), path);
24
+ if (inHome !== null) {
25
+ return inHome.length > 0 ? `~/${inHome}` : '~';
26
+ }
27
+ return toPosix(path);
28
+ }
29
+ exports.portablePath = portablePath;
30
+ //# sourceMappingURL=portable-path.js.map
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getWorkTaskId = void 0;
4
4
  const crypto_1 = require("crypto");
5
5
  const work_task_cache_description_1 = require("../optimizer/work-task-cache-description");
6
- function getWorkTaskId(task) {
7
- const description = (0, work_task_cache_description_1.getWorkTaskCacheDescription)(task);
6
+ function getWorkTaskId(item) {
7
+ const description = (0, work_task_cache_description_1.getWorkTaskCacheDescription)(item);
8
8
  const jsonData = JSON.stringify(description);
9
9
  return (0, crypto_1.createHash)('sha1').update(jsonData).digest('hex');
10
10
  }