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.
- package/dist/cache/explain.js +17 -0
- package/dist/executer/execute-work-task.js +4 -2
- package/dist/executer/scheduler/enqueue-next.js +7 -0
- package/dist/executer/scheduler/state-key.js +5 -0
- package/dist/planner/utils/append-work-dependencies.js +5 -3
- package/dist/planner/utils/append-work-task.js +11 -1
- package/dist/planner/utils/get-work-runtime.js +6 -1
- package/dist/planner/work-network.js +3 -1
- package/dist/planner/work-runtime-aggregate.js +32 -0
- package/dist/planner/work-task.js +6 -1
- package/dist/server/plan.js +6 -4
- package/package.json +1 -1
package/dist/cache/explain.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
package/dist/server/plan.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
+
"version": "1.27.0-next.19",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"build",
|
|
7
7
|
"build-tool",
|