hammerkit 1.27.0-next.18 → 1.27.0-next.19

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.
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.explainWorkTree = exports.explainTask = exports.describeCause = void 0;
4
+ const work_task_1 = require("../planner/work-task");
4
5
  const state_key_1 = require("../executer/scheduler/state-key");
5
6
  const work_task_cache_description_1 = require("../optimizer/work-task-cache-description");
6
7
  const get_cache_directory_1 = require("../optimizer/get-cache-directory");
@@ -157,6 +158,22 @@ async function explainTask(item, defaultCacheMethod, environment, memo = new Map
157
158
  visiting.delete(item.id());
158
159
  return explanation;
159
160
  }
161
+ // Mirrors executeWorkTask: an aggregate is never looked up itself, and is reported
162
+ // cached when all its dependencies were. What changed is in its dependencies.
163
+ if ((0, work_task_1.isAggregateTask)(item.data)) {
164
+ const causes = [];
165
+ for (const dep of item.deps) {
166
+ const depExplanation = await explainTask(dep, defaultCacheMethod, environment, memo, visiting);
167
+ if (depExplanation.status !== 'hit') {
168
+ causes.push({ kind: 'dependency-changed', identifier: dep.name });
169
+ }
170
+ }
171
+ const status = causes.length === 0 ? 'hit' : 'miss';
172
+ const explanation = { taskId: item.id(), taskName: item.name, status, causes };
173
+ memo.set(item.id(), explanation);
174
+ visiting.delete(item.id());
175
+ return explanation;
176
+ }
160
177
  const runtimeStateKey = await safeCurrentStateKey(item, environment);
161
178
  const hasEntry = await safeHas(item, stateKey, environment);
162
179
  const predictedHit = runtimeStateKey === stateKey || hasEntry;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.executeWorkTask = void 0;
4
+ const work_task_1 = require("../planner/work-task");
4
5
  const states_1 = require("./states");
5
6
  const abort_1 = require("./abort");
6
7
  const log_1 = require("../log");
@@ -12,7 +13,8 @@ const archive_task_entry_1 = require("./archive-task-entry");
12
13
  const units_1 = require("../utils/units");
13
14
  async function pushToBackend(work, environment, cacheState, options) {
14
15
  const { resolved, stateKey, provable } = cacheState;
15
- if (resolved.method === 'none' || !provable) {
16
+ // an aggregate has nothing to store, see checkCacheState
17
+ if (resolved.method === 'none' || !provable || (0, work_task_1.isAggregateTask)(work.data)) {
16
18
  return;
17
19
  }
18
20
  if (options.cacheReadOnly) {
@@ -29,7 +31,7 @@ async function pushToBackend(work, environment, cacheState, options) {
29
31
  }
30
32
  }
31
33
  // A task without cmds only groups its dependencies (`ci: { deps: [test, build] }`).
32
- // Having no src of its own it is never a cache hit, so its dependencies are
34
+ // It is never a cache hit itself (see checkCacheState), so its dependencies are
33
35
  // always checked and their outputs restored. When every one of them was a hit,
34
36
  // nothing ran: report the group as cached too. Reporting only, the cache
35
37
  // decision is untouched.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkCacheState = exports.UNCACHED_STATE_KEY = void 0;
4
4
  const work_item_1 = require("../../planner/work-item");
5
+ const work_task_1 = require("../../planner/work-task");
5
6
  const get_cache_directory_1 = require("../../optimizer/get-cache-directory");
6
7
  const cache_metadata_1 = require("../cache-metadata");
7
8
  const work_task_cache_description_1 = require("../../optimizer/work-task-cache-description");
@@ -35,6 +36,12 @@ options = {}) {
35
36
  item.status.write('debug', `${item.name} always runs, it or one of its dependencies has no src files`);
36
37
  return { cached: false, stateKey, resolved, provable };
37
38
  }
39
+ // An aggregate runs nothing, so running it costs nothing, and it is never a hit itself:
40
+ // asked for, it still has its dependencies checked and their outputs restored. Its key
41
+ // counts for what depends on it, and it is reported cached when they all were.
42
+ if ((0, work_item_1.isWorkTaskItem)(item) && (0, work_task_1.isAggregateTask)(item.data)) {
43
+ return { cached: false, stateKey, resolved, provable };
44
+ }
38
45
  if ((0, work_item_1.isWorkTaskItem)(item)) {
39
46
  const runtimeStateKey = await item.runtime.currentStateKey(environment);
40
47
  if (runtimeStateKey === stateKey) {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.computeStateKey = exports.combineStateKeys = exports.isOwnProvable = exports.resolveEffective = void 0;
4
4
  const get_work_cache_stats_1 = require("../../optimizer/get-work-cache-stats");
5
+ const work_task_1 = require("../../planner/work-task");
5
6
  // The state-key engine is shared by the scheduler's hit/miss decision
6
7
  // (`checkCacheState`) and the read-only cache-explain engine, so a prediction
7
8
  // can never diverge from the decision it predicts — there is one implementation.
@@ -20,6 +21,10 @@ exports.resolveEffective = resolveEffective;
20
21
  // whose `src` entries all match no file (a typo, a moved path). Services are not
21
22
  // affected.
22
23
  function isOwnProvable(item, unmatched) {
24
+ if ((0, work_task_1.isAggregateTask)(item.data)) {
25
+ // it runs nothing and reads nothing: what its dependencies prove is all there is
26
+ return true;
27
+ }
23
28
  // unmatched ⊆ declared, so this is also false when nothing is declared
24
29
  return unmatched.length < item.data.src.filter((src) => !src.inherited).length;
25
30
  }
@@ -17,8 +17,10 @@ function appendWorkDependencies(workTree, referenced, item, environment, context
17
17
  }
18
18
  }
19
19
  }
20
- if (((0, work_task_1.isContainerWorkTask)(depNode.data) || (0, work_service_1.isContainerWorkService)(depNode.data)) &&
21
- ((0, work_task_1.isContainerWorkTask)(item.data) || (0, work_service_1.isContainerWorkService)(item.data))) {
20
+ // an aggregate hands on what its dependencies generate, as if what depends on it
21
+ // depended on them
22
+ if (((0, work_task_1.isContainerWorkTask)(depNode.data) || (0, work_service_1.isContainerWorkService)(depNode.data) || (0, work_task_1.isAggregateTask)(depNode.data)) &&
23
+ ((0, work_task_1.isContainerWorkTask)(item.data) || (0, work_service_1.isContainerWorkService)(item.data) || (0, work_task_1.isAggregateTask)(item.data))) {
22
24
  // the outputs of the dep's own deps too, which stay those of the task that made them
23
25
  // (on Kubernetes, they are in its claim)
24
26
  for (const generate of depNode.data.generates) {
@@ -39,7 +41,7 @@ function appendWorkDependencies(workTree, referenced, item, environment, context
39
41
  });
40
42
  }
41
43
  }
42
- else {
44
+ else if ((0, work_task_1.isWorkTask)(item.data)) {
43
45
  if (!item.data.generates.some((v) => v.volumeName === generate.volumeName)) {
44
46
  item.data.generates.push({
45
47
  volumeName: generate.volumeName,
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.appendWorkTask = void 0;
4
4
  const work_secret_1 = require("../work-secret");
5
+ const work_task_1 = require("../work-task");
5
6
  const parse_work_generate_1 = require("./parse-work-generate");
6
7
  const template_value_1 = require("./template-value");
7
8
  const parse_work_mounts_1 = require("./parse-work-mounts");
@@ -25,7 +26,8 @@ function appendWorkTask(workTree, cwd, referenceTask, environment, context) {
25
26
  const parsed = parseTask(cwd, referenceTask, environment, context);
26
27
  // on a server's cluster a local task runs in a tool image like a container task
27
28
  const server = workTree.environment.type === 'kubernetes' ? workTree.environment.server : undefined;
28
- const task = !server
29
+ // an aggregate runs nothing, so it needs no pod either
30
+ const task = !server || (0, work_task_1.isAggregateTask)(parsed)
29
31
  ? parsed
30
32
  : parsed.type === 'local-task'
31
33
  ? (0, job_placement_1.runInToolImage)(parsed, server, referenceTask.schema.platform)
@@ -107,9 +109,17 @@ function parseTask(cwd, task, environment, context) {
107
109
  throw new Error(`task ${task.relativeName}: network ${(0, work_network_1.formatNetwork)(baseWorkNode.network)} needs a container, ` +
108
110
  `hammerkit can't limit the network of a local task`);
109
111
  }
112
+ const aggregate = task.deps.length > 0 &&
113
+ task.needs.length === 0 &&
114
+ baseWorkNode.src.length === 0 &&
115
+ !baseWorkNode.continuous &&
116
+ baseWorkNode.cmds.length === 0 &&
117
+ baseWorkNode.generates.length === 0 &&
118
+ baseWorkNode.secrets.length === 0;
110
119
  return {
111
120
  ...baseWorkNode,
112
121
  type: 'local-task',
122
+ ...(aggregate ? { aggregate } : {}),
113
123
  };
114
124
  }
115
125
  }
@@ -4,6 +4,8 @@ exports.getWorkTaskRuntime = exports.getWorkServiceRuntime = void 0;
4
4
  const work_runtime_docker_1 = require("../work-runtime-docker");
5
5
  const work_runtime_kubernetes_1 = require("../work-runtime-kubernetes");
6
6
  const work_runtime_local_1 = require("../work-runtime-local");
7
+ const work_runtime_aggregate_1 = require("../work-runtime-aggregate");
8
+ const work_task_1 = require("../work-task");
7
9
  const work_item_1 = require("../work-item");
8
10
  function getWorkServiceRuntime(workTree, service) {
9
11
  if ((0, work_item_1.isContainerWorkServiceItem)(service)) {
@@ -24,7 +26,10 @@ function getWorkServiceRuntime(workTree, service) {
24
26
  }
25
27
  exports.getWorkServiceRuntime = getWorkServiceRuntime;
26
28
  function getWorkTaskRuntime(workTree, task) {
27
- if ((0, work_item_1.isLocalWorkTaskItem)(task)) {
29
+ if ((0, work_task_1.isAggregateTask)(task.data)) {
30
+ return (0, work_runtime_aggregate_1.aggregateWorkRuntime)();
31
+ }
32
+ else if ((0, work_item_1.isLocalWorkTaskItem)(task)) {
28
33
  return (0, work_runtime_local_1.getLocalWorkRuntime)(task);
29
34
  }
30
35
  else if ((0, work_item_1.isContainerWorkTaskItem)(task)) {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.applyNetworkCeiling = exports.formatNetwork = exports.getTcpForwards = exports.matchesHost = exports.isLimitedNetwork = exports.toNetworkSchema = exports.exceedsNetwork = exports.getEffectiveNetwork = exports.limitNetwork = exports.parseNetworkOption = exports.parseWorkNetwork = exports.OPEN_NETWORK = void 0;
4
4
  const network_schema_1 = require("../schema/network-schema");
5
+ const work_task_1 = require("./work-task");
5
6
  exports.OPEN_NETWORK = { type: 'open' };
6
7
  function parseWorkNetwork(schema) {
7
8
  if (!schema || schema === 'open') {
@@ -153,7 +154,8 @@ exports.formatNetwork = formatNetwork;
153
154
  function applyNetworkCeiling(workTree, ceiling) {
154
155
  const limit = ceiling && isLimitedNetwork(ceiling) ? ceiling : null;
155
156
  if (limit) {
156
- const local = Object.values(workTree.tasks).find((task) => task.data.type === 'local-task');
157
+ // an aggregate runs nothing, so there is nothing of it to limit
158
+ const local = Object.values(workTree.tasks).find((task) => task.data.type === 'local-task' && !(0, work_task_1.isAggregateTask)(task.data));
157
159
  if (local) {
158
160
  throw new Error(`--network ${formatNetwork(limit)} needs every task in a container, ` +
159
161
  `hammerkit can't limit the network of the local task ${local.name}`);
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.aggregateWorkRuntime = void 0;
4
+ // An aggregate runs nothing, so it touches nothing: no pid or state file in its
5
+ // directory (on a server that is a checkout it doesn't trust), no outputs.
6
+ function aggregateWorkRuntime() {
7
+ return {
8
+ async initialize() {
9
+ // nothing to prepare
10
+ },
11
+ async remove() {
12
+ // it has no outputs of its own
13
+ },
14
+ async execute() {
15
+ // its state stays running, and executeWorkTask completes it
16
+ },
17
+ async stop() {
18
+ // nothing is running
19
+ },
20
+ async archive() {
21
+ // never pushed, see pushToBackend
22
+ },
23
+ async restore() {
24
+ // never a hit, see checkCacheState
25
+ },
26
+ async currentStateKey() {
27
+ return null;
28
+ },
29
+ };
30
+ }
31
+ exports.aggregateWorkRuntime = aggregateWorkRuntime;
32
+ //# sourceMappingURL=work-runtime-aggregate.js.map
@@ -1,8 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isWorkTask = exports.isContainerWorkTask = void 0;
3
+ exports.isAggregateTask = exports.isWorkTask = exports.isContainerWorkTask = void 0;
4
4
  const isContainerWorkTask = (val) => val.type === 'container-task';
5
5
  exports.isContainerWorkTask = isContainerWorkTask;
6
6
  const isWorkTask = (val) => val.type === 'container-task' || val.type === 'local-task';
7
7
  exports.isWorkTask = isWorkTask;
8
+ // A task that only groups its dependencies: `deps`, and no `image`, `cmds`, `src`,
9
+ // `generates`, `secrets`, `needs` or `continuous`. It needs no runtime, hands on what its dependencies generate to what
10
+ // depends on it, and is up to date (or verified) when all its dependencies are.
11
+ const isAggregateTask = (val) => val.type === 'local-task' && val.aggregate === true;
12
+ exports.isAggregateTask = isAggregateTask;
8
13
  //# sourceMappingURL=work-task.js.map
@@ -188,7 +188,8 @@ function findPolicyProblems(items, policy, root) {
188
188
  function findItemPolicyProblems(item, policy, root) {
189
189
  const problems = [];
190
190
  const runtime = item.data.type === 'local-task' ? 'local' : 'docker';
191
- if (policy.runtimes && !policy.runtimes.includes(runtime)) {
191
+ // an aggregate runs nothing, on no runtime
192
+ if (policy.runtimes && !(0, work_task_1.isAggregateTask)(item.data) && !policy.runtimes.includes(runtime)) {
192
193
  problems.push(`${item.name}: runs on the ${runtime} runtime, which this server does not allow (runtimes: ${policy.runtimes.join(', ')})`);
193
194
  }
194
195
  const data = item.data;
@@ -221,7 +222,8 @@ function findItemPolicyProblems(item, policy, root) {
221
222
  reason: ` to what reads ${reads.join(', ')}`,
222
223
  });
223
224
  }
224
- for (const { ceiling, setting, reason } of ceilings) {
225
+ // an aggregate reaches nothing: it runs no command
226
+ for (const { ceiling, setting, reason } of (0, work_task_1.isAggregateTask)(data) ? [] : ceilings) {
225
227
  // on a cluster a local task is a pod of a pool's tool image, limited like any other
226
228
  if (data.type === 'local-task' && policy.backend !== 'kubernetes') {
227
229
  problems.push(`${item.name}: is a local task, which can't be limited to the network this server allows${reason} ` +
@@ -297,7 +299,7 @@ function toPlan(root, item) {
297
299
  : [];
298
300
  const ports = 'ports' in data ? data.ports.map((port) => `${port.hostPort ?? ''}:${port.containerPort}`) : [];
299
301
  const flags = [];
300
- if (data.type === 'local-task') {
302
+ if (data.type === 'local-task' && !(0, work_task_1.isAggregateTask)(data)) {
301
303
  flags.push('local-task');
302
304
  }
303
305
  if ('ports' in data && data.ports.some((port) => port.hostPort !== null)) {
@@ -310,7 +312,7 @@ function toPlan(root, item) {
310
312
  return {
311
313
  kind: isTask ? 'task' : 'service',
312
314
  ...(data.description ? { description: data.description } : {}),
313
- runtime: data.type === 'local-task' ? 'local' : 'docker',
315
+ runtime: (0, work_task_1.isAggregateTask)(data) ? 'none' : data.type === 'local-task' ? 'local' : 'docker',
314
316
  ...((0, work_task_1.isContainerWorkTask)(data) || (0, work_service_1.isContainerWorkService)(data) ? { image: data.image } : {}),
315
317
  cmds: 'cmds' in data ? data.cmds.map((cmd) => cmd.cmd) : 'cmd' in data && data.cmd ? [data.cmd.cmd] : [],
316
318
  command: (0, task_arguments_1.getArgumentsTarget)(data)?.cmd ?? null,
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.18",
4
+ "version": "1.27.0-next.19",
5
5
  "keywords": [
6
6
  "build",
7
7
  "build-tool",