hammerkit 1.27.0-next.23 → 1.27.0-next.25

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.
package/build.schema.json CHANGED
@@ -459,6 +459,91 @@
459
459
  },
460
460
  "additionalProperties": false,
461
461
  "description": "local task"
462
+ },
463
+ {
464
+ "type": "object",
465
+ "properties": {
466
+ "deps": {
467
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/deps"
468
+ },
469
+ "needs": {
470
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/needs"
471
+ },
472
+ "description": {
473
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/description"
474
+ },
475
+ "extend": {
476
+ "type": "string"
477
+ },
478
+ "reset": {
479
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/reset"
480
+ },
481
+ "envs": {
482
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/envs"
483
+ },
484
+ "labels": {
485
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/labels"
486
+ },
487
+ "cache": {
488
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/cache"
489
+ },
490
+ "cmds": {
491
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/cmds"
492
+ },
493
+ "src": {
494
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/src"
495
+ },
496
+ "generates": {
497
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/generates"
498
+ },
499
+ "shell": {
500
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/shell"
501
+ },
502
+ "continuous": {
503
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/continuous"
504
+ },
505
+ "timeout": {
506
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/timeout"
507
+ },
508
+ "secrets": {
509
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/secrets"
510
+ },
511
+ "account": {
512
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/account"
513
+ },
514
+ "resources": {
515
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/resources"
516
+ },
517
+ "network": {
518
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/network"
519
+ },
520
+ "args": {
521
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/args"
522
+ },
523
+ "runner": {
524
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/runner"
525
+ },
526
+ "publish": {
527
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/publish"
528
+ },
529
+ "image": {
530
+ "type": "string"
531
+ },
532
+ "mounts": {
533
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/mounts"
534
+ },
535
+ "volumeSize": {
536
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/volumeSize"
537
+ },
538
+ "capAdd": {
539
+ "$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/capAdd"
540
+ }
541
+ },
542
+ "required": [
543
+ "extend"
544
+ ],
545
+ "additionalProperties": false,
546
+ "description": "container task taking its image from the task it extends"
462
547
  }
463
548
  ]
464
549
  }
@@ -80,6 +80,12 @@ function createLocalCacheBackend(spec) {
80
80
  return false;
81
81
  }
82
82
  },
83
+ async touch(taskId, stateKey, environment) {
84
+ const remoteDir = (0, path_1.join)(root, taskId, stateKey);
85
+ if (await environment.file.exists((0, path_1.join)(remoteDir, 'stats.json'))) {
86
+ await (0, promises_1.writeFile)((0, path_1.join)(remoteDir, LAST_ACCESS_FILE), `${Date.now()}`);
87
+ }
88
+ },
83
89
  async push(taskId, stateKey, from, environment) {
84
90
  const remoteDir = (0, path_1.join)(root, taskId, stateKey);
85
91
  await environment.file.createDirectory(remoteDir);
package/dist/cli.js CHANGED
@@ -81,6 +81,7 @@ class Cli {
81
81
  timeout: options?.timeout ?? null,
82
82
  maxTimeout: options?.maxTimeout ?? null,
83
83
  skipDeps: options?.skipDeps ?? true,
84
+ restore: options?.restore ?? true,
84
85
  wait: options?.wait ?? 'ready',
85
86
  resourceCheck: options?.resourceCheck ?? true,
86
87
  network: options?.network ?? null,
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.restoreDeferred = exports.clearDeferredRestore = exports.deferRestore = void 0;
4
+ const abort_1 = require("./abort");
5
+ // Keyed by the item's state, which every run replaces (resetWorkTree): a hit
6
+ // deferred in one run of a Cli is never restored in a later one, over outputs
7
+ // that run built.
8
+ const deferred = new WeakMap();
9
+ function deferRestore(item, run) {
10
+ deferred.set(item.state, { run, promise: null });
11
+ }
12
+ exports.deferRestore = deferRestore;
13
+ // Every cache check starts from scratch: a key that changed since a hit was
14
+ // deferred must not restore the old entry.
15
+ function clearDeferredRestore(item) {
16
+ deferred.delete(item.state);
17
+ }
18
+ exports.clearDeferredRestore = clearDeferredRestore;
19
+ // Restore every given item that was a hit without its outputs, in parallel,
20
+ // each at most once however many items wait on it.
21
+ async function restoreDeferred(work, items, abort) {
22
+ await Promise.all(items.map((item) => {
23
+ const entry = deferred.get(item.state);
24
+ if (!entry) {
25
+ return Promise.resolve();
26
+ }
27
+ if (!entry.promise) {
28
+ work.status.write('debug', `restore ${item.name} from cache, ${work.name} needs its outputs`);
29
+ entry.promise = entry.run();
30
+ }
31
+ return entry.promise;
32
+ }));
33
+ (0, abort_1.checkForAbort)(abort);
34
+ }
35
+ exports.restoreDeferred = restoreDeferred;
36
+ //# sourceMappingURL=deferred-restore.js.map
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.executeWorkService = exports.stopService = void 0;
4
4
  const work_item_1 = require("../planner/work-item");
5
5
  const await_completed_dependencies_1 = require("./await-completed-dependencies");
6
+ const deferred_restore_1 = require("./deferred-restore");
6
7
  const abort_1 = require("./abort");
7
8
  const log_1 = require("../log");
8
9
  const watch_loop_1 = require("./watch-loop");
@@ -35,6 +36,7 @@ async function executeWorkService(work, environment, options) {
35
36
  });
36
37
  await (0, watch_loop_1.watchLoop)(work, environment, options, async (cacheState, abort, stop) => {
37
38
  await (0, await_completed_dependencies_1.awaitCompletedDependencies)(work, work.deps, abort);
39
+ await (0, deferred_restore_1.restoreDeferred)(work, (0, await_completed_dependencies_1.transitiveDeps)(work.deps), abort);
38
40
  work.state.set({
39
41
  type: 'ready',
40
42
  stateKey: cacheState.stateKey,
@@ -11,6 +11,7 @@ const deadline_1 = require("./deadline");
11
11
  const explain_1 = require("../cache/explain");
12
12
  const archive_task_entry_1 = require("./archive-task-entry");
13
13
  const units_1 = require("../utils/units");
14
+ const deferred_restore_1 = require("./deferred-restore");
14
15
  async function pushToBackend(work, environment, cacheState, options) {
15
16
  const { resolved, stateKey, provable } = cacheState;
16
17
  // an aggregate has nothing to store, see checkCacheState
@@ -89,6 +90,10 @@ dependents = null) {
89
90
  }
90
91
  }
91
92
  await (0, await_completed_dependencies_1.awaitCompletedDependencies)(work, work.deps, abort);
93
+ // an aggregate runs nothing, so it needs no outputs on disk
94
+ if (!(0, work_task_1.isAggregateTask)(work.data)) {
95
+ await (0, deferred_restore_1.restoreDeferred)(work, (0, await_completed_dependencies_1.transitiveDeps)(work.deps), abort);
96
+ }
92
97
  await (0, await_completed_dependencies_1.awaitRunningNeeds)(work, work.needs.map((n) => n.service), abort);
93
98
  await options.processManager.task(work, async () => {
94
99
  work.state.set({
@@ -9,13 +9,17 @@ const work_task_cache_description_1 = require("../../optimizer/work-task-cache-d
9
9
  const log_1 = require("../../log");
10
10
  const state_key_1 = require("./state-key");
11
11
  const work_instance_id_1 = require("../../planner/work-instance-id");
12
+ const deferred_restore_1 = require("../deferred-restore");
12
13
  // The key a task whose cache is off runs and records its outputs under. It is
13
14
  // no hash, so a later run with its cache on never takes those outputs as its own.
14
15
  exports.UNCACHED_STATE_KEY = 'uncached';
15
16
  async function checkCacheState(item, defaultCacheMethod, environment,
16
17
  // watch mode compares keys to rerun on a change, so it needs one even when
17
18
  // the cache is off
19
+ // restore: false only looks a backend hit up, its outputs are downloaded once
20
+ // something that runs depends on them (`run --no-restore`, see deferred-restore)
18
21
  options = {}) {
22
+ (0, deferred_restore_1.clearDeferredRestore)(item);
19
23
  // A task whose cache is off is never looked up, so hashing its sources buys
20
24
  // nothing, and a `**` over a node_modules takes most of a minute (every run
21
25
  // with arguments). Its dependents still hash its sources for their own key.
@@ -47,13 +51,30 @@ options = {}) {
47
51
  if (runtimeStateKey === stateKey) {
48
52
  return { cached: true, stateKey, resolved, provable };
49
53
  }
54
+ if (options.restore === false && options.watch) {
55
+ item.status.write('debug', `${item.name} restores cache hits, --no-restore has no effect in watch mode`);
56
+ }
57
+ if (options.restore === false && !options.watch) {
58
+ try {
59
+ if (await resolved.backend.has(item.id(), stateKey, environment)) {
60
+ item.status.write('info', `${item.name} found in cache "${resolved.name}" (${resolved.backend.type}), not restored`);
61
+ await touchEntry(item, resolved, stateKey, environment);
62
+ (0, deferred_restore_1.deferRestore)(item, async () => {
63
+ if (!(await restoreFromBackend(item, resolved, stateKey, currentStats, environment))) {
64
+ throw new Error(`${item.name} is no longer in cache "${resolved.name}", its outputs can not be restored. ` +
65
+ 'Run again without --no-restore.');
66
+ }
67
+ });
68
+ return { cached: true, stateKey, resolved, provable };
69
+ }
70
+ }
71
+ catch (e) {
72
+ item.status.write('warn', `${item.name} failed to look up cache "${resolved.name}": ${(0, log_1.getErrorMessage)(e)} — continuing with local execution`);
73
+ }
74
+ return { cached: false, stateKey, resolved, provable };
75
+ }
50
76
  try {
51
- const cacheDir = (0, get_cache_directory_1.getCacheDirectory)((0, work_instance_id_1.getWorkInstanceId)(item));
52
- const pulled = await resolved.backend.pull(item.id(), stateKey, cacheDir, environment);
53
- if (pulled) {
54
- item.status.write('info', `${item.name} pulled from cache "${resolved.name}" (${resolved.backend.type})`);
55
- await item.runtime.restore(environment, cacheDir, stateKey);
56
- await (0, cache_metadata_1.writeCacheMetadata)(environment, item, currentStats, (0, work_task_cache_description_1.getWorkTaskCacheDescription)(item));
77
+ if (await restoreFromBackend(item, resolved, stateKey, currentStats, environment)) {
57
78
  return { cached: true, stateKey, resolved, provable };
58
79
  }
59
80
  }
@@ -64,4 +85,27 @@ options = {}) {
64
85
  return { cached: false, stateKey, resolved, provable };
65
86
  }
66
87
  exports.checkCacheState = checkCacheState;
88
+ // Pull a task's entry and restore its outputs into the checkout. False when the
89
+ // backend has no such entry.
90
+ async function restoreFromBackend(item, resolved, stateKey, currentStats, environment) {
91
+ const cacheDir = (0, get_cache_directory_1.getCacheDirectory)((0, work_instance_id_1.getWorkInstanceId)(item));
92
+ const pulled = await resolved.backend.pull(item.id(), stateKey, cacheDir, environment);
93
+ if (!pulled) {
94
+ return false;
95
+ }
96
+ item.status.write('info', `${item.name} pulled from cache "${resolved.name}" (${resolved.backend.type})`);
97
+ await item.runtime.restore(environment, cacheDir, stateKey);
98
+ await (0, cache_metadata_1.writeCacheMetadata)(environment, item, currentStats, (0, work_task_cache_description_1.getWorkTaskCacheDescription)(item));
99
+ return true;
100
+ }
101
+ // A hit that is only looked up still counts as used, so retention keeps it.
102
+ // Best effort: the hit stands whether or not the marker could be written.
103
+ async function touchEntry(item, resolved, stateKey, environment) {
104
+ try {
105
+ await resolved.backend.touch?.(item.id(), stateKey, environment);
106
+ }
107
+ catch (e) {
108
+ item.status.write('debug', `${item.name} could not mark its cache entry as used: ${(0, log_1.getErrorMessage)(e)}`);
109
+ }
110
+ }
67
111
  //# sourceMappingURL=enqueue-next.js.map
@@ -12,7 +12,10 @@ async function watchLoop(work, environment, options, factory) {
12
12
  // kubernetes-service has no such field. Read it wherever it exists.
13
13
  const continuous = 'continuous' in work.data ? work.data.continuous : false;
14
14
  const watchMode = options.watch && !options.daemon && !continuous;
15
- const currentStats = await (0, enqueue_next_1.checkCacheState)(work, options.cacheDefault, environment, { watch: watchMode });
15
+ const currentStats = await (0, enqueue_next_1.checkCacheState)(work, options.cacheDefault, environment, {
16
+ watch: watchMode,
17
+ restore: options.restore,
18
+ });
16
19
  (0, abort_1.checkForAbort)(environment.abortCtrl.signal);
17
20
  const watchState = watchMode
18
21
  ? (0, watch_state_key_1.watchStateKey)(work, currentStats, environment, options)
@@ -143,7 +143,11 @@ function appendServiceInit(workTree, service, item, environment, context) {
143
143
  task.needs[selfNeed] = { name: task.needs[selfNeed].name, service: view };
144
144
  }
145
145
  else {
146
- task.needs.push({ name: item.name, service: view });
146
+ const name = getInitNeedName(service);
147
+ if (task.needs.some((need) => need.name === name)) {
148
+ throw new Error(`${task.name} needs two services as ${name}, give one another name with name:`);
149
+ }
150
+ task.needs.push({ name, service: view });
147
151
  }
148
152
  removeRequirer(item, task.name);
149
153
  // explicit, so neither the default method nor `--cache` turns it back on
@@ -153,6 +157,18 @@ function appendServiceInit(workTree, service, item, environment, context) {
153
157
  }
154
158
  item.data.init = { task, state };
155
159
  }
160
+ // The host name the init reaches its service by: the name its task declares the
161
+ // need by, else the service's name in its own build file. Planning the task
162
+ // skips that need (it is the init's), and the full name of a service of an
163
+ // include, `services:db`, is no host name: Docker and DNS take no colon.
164
+ function getInitNeedName(service) {
165
+ const declared = service.init?.task.needs.find((need) => need.service.relativeName === service.relativeName);
166
+ if (declared) {
167
+ return declared.relativeName;
168
+ }
169
+ const prefix = service.scope.namePrefix;
170
+ return prefix.length > 0 ? service.relativeName.slice(prefix.length + 1) : service.relativeName;
171
+ }
156
172
  const initViews = new WeakMap();
157
173
  // The service itself for the view its init task needs, else the item.
158
174
  function getPlannedService(item) {
package/dist/program.js CHANGED
@@ -705,6 +705,7 @@ async function getProgram(environment, argv) {
705
705
  .addOption(new commander_1.Option('--max-timeout <duration>', 'fail any task after this long, also one with a longer timeout of its own (e.g. 2m)').argParser(units_1.parseDuration))
706
706
  .addOption(new commander_1.Option('--network <level>', 'the most any task or service reaches besides its needs: none, open or allow:<host>,<host>').argParser(parseNetworkArgument))
707
707
  .addOption(new commander_1.Option('--no-skip-deps', 'run dependencies even when nothing needing them has to run'))
708
+ .addOption(new commander_1.Option('--no-restore', 'only look cache hits up, download their outputs only when a task that runs needs them'))
708
709
  .action(async (task, options, command) => {
709
710
  try {
710
711
  // For machine-readable JSON, suppress the human progress logger (which
@@ -740,6 +741,7 @@ async function getProgram(environment, argv) {
740
741
  maxTimeout: options.maxTimeout ?? null,
741
742
  network: options.network ?? null,
742
743
  skipDeps: options.skipDeps !== false,
744
+ restore: options.restore !== false,
743
745
  });
744
746
  // Reporting only: the summary never changes the exit code or behavior.
745
747
  if (!options.watch) {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildFileContainerTaskSchema = void 0;
3
+ exports.CONTAINER_TASK_PROPERTIES = exports.buildFileExtendingContainerTaskSchema = exports.buildFileContainerTaskSchema = void 0;
4
4
  const build_file_base_task_schema_1 = require("./build-file-base-task-schema");
5
5
  const zod_1 = require("zod");
6
6
  const size_schema_1 = require("./size-schema");
@@ -17,4 +17,15 @@ exports.buildFileContainerTaskSchema = build_file_base_task_schema_1.buildFileBa
17
17
  })
18
18
  .strict()
19
19
  .describe('container task');
20
+ // A task that extends another takes the image of its base, so it may set the
21
+ // container properties without an image of its own; once extended, a task whose
22
+ // base has no image either is refused them (applySchemaExtension).
23
+ exports.buildFileExtendingContainerTaskSchema = exports.buildFileContainerTaskSchema
24
+ .extend({
25
+ image: (0, zod_1.string)().optional(),
26
+ extend: (0, zod_1.string)(),
27
+ })
28
+ .strict()
29
+ .describe('container task taking its image from the task it extends');
30
+ exports.CONTAINER_TASK_PROPERTIES = ['mounts', 'volumeSize', 'capAdd'];
20
31
  //# sourceMappingURL=build-file-container-task-schema.js.map
@@ -4,7 +4,11 @@ exports.isBuildFileLocalTaskSchema = exports.isBuildFileContainerTaskSchema = ex
4
4
  const zod_1 = require("zod");
5
5
  const build_file_container_task_schema_1 = require("./build-file-container-task-schema");
6
6
  const build_file_local_task_schema_1 = require("./build-file-local-task-schema");
7
- exports.buildFileTaskSchema = (0, zod_1.union)([build_file_container_task_schema_1.buildFileContainerTaskSchema, build_file_local_task_schema_1.buildFileLocalTaskSchema]);
7
+ exports.buildFileTaskSchema = (0, zod_1.union)([
8
+ build_file_container_task_schema_1.buildFileContainerTaskSchema,
9
+ build_file_local_task_schema_1.buildFileLocalTaskSchema,
10
+ build_file_container_task_schema_1.buildFileExtendingContainerTaskSchema,
11
+ ]);
8
12
  const isBuildFileContainerTaskSchema = (val) => 'image' in val;
9
13
  exports.isBuildFileContainerTaskSchema = isBuildFileContainerTaskSchema;
10
14
  const isBuildFileLocalTaskSchema = (val) => !('image' in val);
@@ -7,6 +7,7 @@ const split_name_1 = require("../planner/utils/split-name");
7
7
  const read_env_file_1 = require("../parser/read-env-file");
8
8
  const label_values_1 = require("../executer/label-values");
9
9
  const merge_environment_variables_1 = require("../environment/merge-environment-variables");
10
+ const build_file_container_task_schema_1 = require("./build-file-container-task-schema");
10
11
  const resolve_cache_1 = require("../cache/resolve-cache");
11
12
  const project_root_1 = require("../planner/project-root");
12
13
  const secret_catalog_1 = require("../secrets/secret-catalog");
@@ -222,6 +223,7 @@ function applySchemaExtension(reference, task) {
222
223
  if (!reset.has('mounts'))
223
224
  taskSchema.mounts = extendArray(taskSchema.mounts, extend.schema.mounts);
224
225
  }
226
+ rejectContainerPropertiesWithoutImage(task);
225
227
  // deps and needs resolve in the file that declares them, so every base up the
226
228
  // chain contributes its own, not only the nearest; what a task resets is not
227
229
  // passed on to the tasks extending it either
@@ -236,6 +238,16 @@ function applySchemaExtension(reference, task) {
236
238
  }
237
239
  task.extension = 'applied';
238
240
  }
241
+ function rejectContainerPropertiesWithoutImage(task) {
242
+ if ((0, build_file_task_schema_1.isBuildFileContainerTaskSchema)(task.schema)) {
243
+ return;
244
+ }
245
+ const schema = task.schema;
246
+ const property = build_file_container_task_schema_1.CONTAINER_TASK_PROPERTIES.find((key) => schema[key] !== undefined);
247
+ if (property) {
248
+ throw new Error(`task ${task.relativeName} sets ${property}, but neither it nor its base has an image`);
249
+ }
250
+ }
239
251
  function extendArray(base, extend) {
240
252
  if (!extend) {
241
253
  return base;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hammerkit",
3
3
  "description": "Containerized build tool with incremental caching that runs the same build file locally, in Docker and on Kubernetes.",
4
- "version": "1.27.0-next.23",
4
+ "version": "1.27.0-next.25",
5
5
  "keywords": [
6
6
  "build",
7
7
  "build-tool",