hammerkit 1.27.0-next.17 → 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/execution-steps.js +5 -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/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/validate.js +22 -0
- 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/remote/remote-work-commands.js +6 -4
- package/dist/remote/server-client.js +15 -2
- package/dist/run-program.js +9 -0
- package/dist/schema/reference-parser.js +8 -1
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/server/plan.js +107 -80
- 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.
|
|
@@ -89,8 +89,11 @@ exports.prepareMounts = prepareMounts;
|
|
|
89
89
|
async function setUserPermissions(item, container, environment) {
|
|
90
90
|
if (item.data.user) {
|
|
91
91
|
// On a server only the volumes: a chown as the container's root on a path of the host
|
|
92
|
-
// would follow a link a task put there after the binds were checked
|
|
93
|
-
|
|
92
|
+
// would follow a link a task put there after the binds were checked. Never a mount: it
|
|
93
|
+
// is a path of the host the build file names, such as /var/run/docker.sock, whose owner
|
|
94
|
+
// is not hammerkit's to change
|
|
95
|
+
const mounted = new Set(item.data.mounts.map((mount) => mount.containerPath));
|
|
96
|
+
const binds = (0, get_container_binds_1.getContainerBinds)(item).filter((bind) => !mounted.has(bind.containerPath) && (!environment.hosted || !(0, path_1.isAbsolute)(bind.localPath)));
|
|
94
97
|
if (!environment.hosted) {
|
|
95
98
|
await (0, set_user_permission_1.setUserPermission)(item.data.cwd, item.status, environment, container, item.data.user);
|
|
96
99
|
}
|
|
@@ -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)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizePath = void 0;
|
|
3
|
+
exports.normalizeHostPath = exports.getHomeReference = exports.normalizePath = void 0;
|
|
4
4
|
const path_1 = require("path");
|
|
5
5
|
function normalizePath(cwd, pwd, path) {
|
|
6
6
|
if (path.startsWith('/')) {
|
|
@@ -14,4 +14,22 @@ function normalizePath(cwd, pwd, path) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
exports.normalizePath = normalizePath;
|
|
17
|
+
// What a path of the host starts with to name the home directory: `~` and `$HOME`, and
|
|
18
|
+
// `$PWD`, which in a mount has always meant the home directory, not the project it reads like
|
|
19
|
+
function getHomeReference(path) {
|
|
20
|
+
for (const reference of ['~', '$HOME']) {
|
|
21
|
+
if (path === reference || path.startsWith(`${reference}/`) || path.startsWith(`${reference}\\`)) {
|
|
22
|
+
return reference;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return path.startsWith('$PWD') ? '$PWD' : null;
|
|
26
|
+
}
|
|
27
|
+
exports.getHomeReference = getHomeReference;
|
|
28
|
+
// A path of the host in a mount: in the home directory when it starts with one of the
|
|
29
|
+
// references above, else in the directory of the build file unless it is absolute
|
|
30
|
+
function normalizeHostPath(cwd, home, path) {
|
|
31
|
+
const reference = getHomeReference(path);
|
|
32
|
+
return reference ? (0, path_1.join)(home, path.substring(reference.length)) : normalizePath(cwd, home, path);
|
|
33
|
+
}
|
|
34
|
+
exports.normalizeHostPath = normalizeHostPath;
|
|
17
35
|
//# sourceMappingURL=normalize-path.js.map
|
|
@@ -7,6 +7,12 @@ const path_1 = require("path");
|
|
|
7
7
|
function parseWorkMount(cwd, dir) {
|
|
8
8
|
const parts = dir.split(':');
|
|
9
9
|
if (parts.length === 1) {
|
|
10
|
+
const home = (0, normalize_path_1.getHomeReference)(dir);
|
|
11
|
+
if (home === '~' || home === '$HOME') {
|
|
12
|
+
// the container has no such directory: the same path on both sides would be one of the
|
|
13
|
+
// project there (`$PWD` has always been, so it stays)
|
|
14
|
+
throw new Error(`mount ${dir} is in the home directory, name where it goes in the container (${dir}:/path)`);
|
|
15
|
+
}
|
|
10
16
|
return parseLocalMount(cwd, dir, dir, dir, false);
|
|
11
17
|
}
|
|
12
18
|
else if (parts.length === 2) {
|
|
@@ -22,7 +28,7 @@ function parseWorkMount(cwd, dir) {
|
|
|
22
28
|
exports.parseWorkMount = parseWorkMount;
|
|
23
29
|
function parseLocalMount(cwd, localPath, containerPath, mount, readOnly) {
|
|
24
30
|
return {
|
|
25
|
-
localPath: (0, normalize_path_1.
|
|
31
|
+
localPath: (0, normalize_path_1.normalizeHostPath)(cwd, (0, os_1.homedir)(), localPath),
|
|
26
32
|
containerPath: (0, normalize_path_1.normalizePath)(cwd, cwd, containerPath),
|
|
27
33
|
isFile: (0, path_1.extname)(localPath).length > 1,
|
|
28
34
|
mount,
|
package/dist/planner/validate.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.checkIfContextExists = exports.hasDependencyCycle = exports.hasNeedCycle
|
|
|
4
4
|
const read_build_file_1 = require("../parser/read-build-file");
|
|
5
5
|
const plan_work_tasks_1 = require("./utils/plan-work-tasks");
|
|
6
6
|
const unreferenced_build_file_envs_1 = require("./unreferenced-build-file-envs");
|
|
7
|
+
const normalize_path_1 = require("./utils/normalize-path");
|
|
7
8
|
async function* validate(workTree, context) {
|
|
8
9
|
const cycleItems = [];
|
|
9
10
|
for (const item of (0, plan_work_tasks_1.iterateWorkServices)(workTree)) {
|
|
@@ -16,6 +17,10 @@ async function* validate(workTree, context) {
|
|
|
16
17
|
yield { type: 'warn', message: 'missing healthcheck', item: service };
|
|
17
18
|
}
|
|
18
19
|
for (const mount of service.mounts) {
|
|
20
|
+
const pwd = getPwdMountWarning(mount);
|
|
21
|
+
if (pwd) {
|
|
22
|
+
yield { type: 'warn', message: pwd, item: service };
|
|
23
|
+
}
|
|
19
24
|
if (!(await context.file.exists(mount.localPath))) {
|
|
20
25
|
yield {
|
|
21
26
|
type: 'warn',
|
|
@@ -62,6 +67,12 @@ async function* validate(workTree, context) {
|
|
|
62
67
|
item: task,
|
|
63
68
|
};
|
|
64
69
|
}
|
|
70
|
+
for (const mount of task.type === 'container-task' ? task.mounts : []) {
|
|
71
|
+
const pwd = getPwdMountWarning(mount);
|
|
72
|
+
if (pwd) {
|
|
73
|
+
yield { type: 'warn', message: pwd, item: task };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
65
76
|
for (const src of task.src) {
|
|
66
77
|
if (!(await context.file.exists(src.absolutePath))) {
|
|
67
78
|
yield {
|
|
@@ -146,4 +157,15 @@ async function checkIfContextExists(service, context) {
|
|
|
146
157
|
return false;
|
|
147
158
|
}
|
|
148
159
|
exports.checkIfContextExists = checkIfContextExists;
|
|
160
|
+
// `$PWD` in a mount is the home directory, which reads like a path of the project
|
|
161
|
+
function getPwdMountWarning(mount) {
|
|
162
|
+
const source = mount.mount.split(':')[0];
|
|
163
|
+
if ((0, normalize_path_1.getHomeReference)(source) !== '$PWD') {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const rest = source.substring('$PWD'.length);
|
|
167
|
+
const home = rest === '' || rest.startsWith('/') || rest.startsWith('\\') ? `~${rest}` : '~/…';
|
|
168
|
+
return (`mount ${source} is ${mount.localPath}: $PWD in a mount is the home directory, not the project. ` +
|
|
169
|
+
`Write ${home} for the home directory, or a relative path for the project`);
|
|
170
|
+
}
|
|
149
171
|
//# sourceMappingURL=validate.js.map
|
|
@@ -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
|
|
@@ -334,7 +334,7 @@ function registerWorkCommands(remote, context) {
|
|
|
334
334
|
throw new Error(`the build file has no task ${(0, server_client_1.printable)(task)}`);
|
|
335
335
|
}
|
|
336
336
|
if (found.key === null) {
|
|
337
|
-
unkeyed.push(
|
|
337
|
+
unkeyed.push({ task, reason: found.reason });
|
|
338
338
|
}
|
|
339
339
|
else {
|
|
340
340
|
keys.push({ task, key: found.key });
|
|
@@ -357,12 +357,14 @@ function registerWorkCommands(remote, context) {
|
|
|
357
357
|
environment.stdout.write(`${(0, server_client_1.printable)(task)}: no run on ${serverName} passed it for these files${check?.reason ? `, ${(0, server_client_1.printable)(check.reason)}` : ''}\n`);
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
|
-
for (const
|
|
361
|
-
environment.stdout.write(`${
|
|
360
|
+
for (const { task, reason } of unkeyed) {
|
|
361
|
+
environment.stdout.write(`${(0, server_client_1.printable)(task)}: can not be verified, ${(0, server_client_1.printable)(reason)}\n`);
|
|
362
362
|
}
|
|
363
363
|
if (missing.length > 0 || unkeyed.length > 0) {
|
|
364
364
|
const run = missing.length > 0 ? `; run on the server: hammerkit remote run ${missing.map(server_client_1.printable).join(' ')}` : '';
|
|
365
|
-
|
|
365
|
+
// whole names: a task of an include is named like build:all
|
|
366
|
+
const names = [...missing, ...unkeyed.map(({ task }) => task)].map(server_client_1.printable);
|
|
367
|
+
fail(`not verified: ${names.join(', ')}${run}`, 1);
|
|
366
368
|
}
|
|
367
369
|
}));
|
|
368
370
|
remote
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.callServer = exports.getMe = exports.getDiscovery = exports.printableWhole = exports.printable = exports.ServerError = void 0;
|
|
3
|
+
exports.callServer = exports.getMe = exports.getDiscovery = exports.printableWhole = exports.printableLines = exports.printable = exports.ServerError = void 0;
|
|
4
4
|
const discovery_1 = require("../server/discovery");
|
|
5
5
|
const TIMEOUT_MS = 10000;
|
|
6
6
|
const WRITE_TIMEOUT_MS = 30000;
|
|
@@ -21,6 +21,17 @@ function printable(value) {
|
|
|
21
21
|
return text.length > 300 ? `${text.slice(0, 300)}… (+${text.length - 300} chars)` : text;
|
|
22
22
|
}
|
|
23
23
|
exports.printable = printable;
|
|
24
|
+
// Text of several lines, such as every problem of a refused publish: each line made
|
|
25
|
+
// printable and cut like `printable`, one per line, and at most `MAX_LINES` of them.
|
|
26
|
+
const MAX_LINES = 100;
|
|
27
|
+
function printableLines(value) {
|
|
28
|
+
const lines = String(value)
|
|
29
|
+
.split(/\r?\n/)
|
|
30
|
+
.filter((line) => line.trim().length > 0);
|
|
31
|
+
const shown = lines.slice(0, MAX_LINES).map(printable);
|
|
32
|
+
return lines.length > MAX_LINES ? [...shown, `… (+${lines.length - MAX_LINES} lines)`] : shown;
|
|
33
|
+
}
|
|
34
|
+
exports.printableLines = printableLines;
|
|
24
35
|
// The same, never cut: for what somebody decides on, such as a command.
|
|
25
36
|
function printableWhole(value) {
|
|
26
37
|
return String(value)
|
|
@@ -92,7 +103,9 @@ async function callServer(server, token, method, path, body) {
|
|
|
92
103
|
throw new ServerError(`${server.url} did not accept the token`);
|
|
93
104
|
}
|
|
94
105
|
if (!response.ok) {
|
|
95
|
-
const
|
|
106
|
+
const lines = answer && typeof answer.error === 'string' ? printableLines(answer.error) : [];
|
|
107
|
+
// a refused publish says every problem, each on a line of its own
|
|
108
|
+
const said = lines.length > 1 ? `:\n${lines.map((line) => ` ${line}`).join('\n')}` : lines.length === 1 ? `: ${lines[0]}` : '';
|
|
96
109
|
throw new ServerError(`${server.url} answered ${response.status}${said}`, response.status);
|
|
97
110
|
}
|
|
98
111
|
return { status: response.status, body: answer };
|
package/dist/run-program.js
CHANGED
|
@@ -7,6 +7,8 @@ exports.runProgram = void 0;
|
|
|
7
7
|
const program_1 = require("./program");
|
|
8
8
|
const parse_error_1 = require("./schema/parse-error");
|
|
9
9
|
const colors_1 = __importDefault(require("colors"));
|
|
10
|
+
const commander_1 = require("commander");
|
|
11
|
+
const log_1 = require("./log");
|
|
10
12
|
function runProgram(env, argv, exitOverride) {
|
|
11
13
|
return (0, program_1.getProgram)(env, argv)
|
|
12
14
|
.then(({ program, args }) => {
|
|
@@ -18,8 +20,15 @@ function runProgram(env, argv, exitOverride) {
|
|
|
18
20
|
for (const error of err.zod.errors) {
|
|
19
21
|
env.stderr.write(` ${colors_1.default.underline(colors_1.default.blue(`error`))} at ${colors_1.default.gray(error.path.join('.'))} ${error.message} ${colors_1.default.gray(error.code)}\n`);
|
|
20
22
|
}
|
|
23
|
+
// printed, but still a failure: CI must not pass on a build file that doesn't parse
|
|
24
|
+
throw err;
|
|
21
25
|
}
|
|
22
26
|
else {
|
|
27
|
+
// what commander reports (program.error, a wrong option) is printed already; anything
|
|
28
|
+
// else, such as a build file that is no YAML, would otherwise end the process silently
|
|
29
|
+
if (!(err instanceof commander_1.CommanderError)) {
|
|
30
|
+
env.stderr.write(`${colors_1.default.red('error')}: ${(0, log_1.getErrorMessage)(err)}\n`);
|
|
31
|
+
}
|
|
23
32
|
throw err;
|
|
24
33
|
}
|
|
25
34
|
});
|
|
@@ -210,7 +210,9 @@ function resolveNeeds(reference, taskOrService, scope, schema) {
|
|
|
210
210
|
for (const needName of schema.needs || []) {
|
|
211
211
|
if (typeof needName === 'string') {
|
|
212
212
|
taskOrService.needs.push({
|
|
213
|
-
|
|
213
|
+
// the host name it is reached by: a service of an include, `services:postgres`, as
|
|
214
|
+
// `services-postgres`, since Docker and DNS take no colon
|
|
215
|
+
relativeName: needName.replace(/:/g, '-'),
|
|
214
216
|
service: findService(reference, scope, needName),
|
|
215
217
|
});
|
|
216
218
|
}
|
|
@@ -221,6 +223,11 @@ function resolveNeeds(reference, taskOrService, scope, schema) {
|
|
|
221
223
|
});
|
|
222
224
|
}
|
|
223
225
|
}
|
|
226
|
+
const names = taskOrService.needs.map((need) => need.relativeName);
|
|
227
|
+
const twice = names.find((name, index) => names.indexOf(name) !== index);
|
|
228
|
+
if (twice) {
|
|
229
|
+
throw new Error(`${taskOrService.relativeName} needs two services as ${twice}, give one another name with name:`);
|
|
230
|
+
}
|
|
224
231
|
}
|
|
225
232
|
function findTask(ctx, scope, name, type) {
|
|
226
233
|
const ref = (0, split_name_1.splitName)(name);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerGithubAppTokenCommand = void 0;
|
|
4
|
+
const commander_1 = require("commander");
|
|
4
5
|
const log_1 = require("../log");
|
|
5
6
|
const github_app_token_1 = require("./github-app-token");
|
|
6
7
|
// `hammerkit github-app-token <owner/repo>`: a secret provider command for the token a
|
|
@@ -18,7 +19,8 @@ function registerGithubAppTokenCommand(program, environment) {
|
|
|
18
19
|
}
|
|
19
20
|
catch (e) {
|
|
20
21
|
environment.stderr.write(`github-app-token: ${(0, log_1.getErrorMessage)(e)}\n`);
|
|
21
|
-
|
|
22
|
+
// said already: the CLI does not print it again
|
|
23
|
+
throw new commander_1.CommanderError(1, 'hammerkit.githubAppToken', (0, log_1.getErrorMessage)(e));
|
|
22
24
|
}
|
|
23
25
|
});
|
|
24
26
|
}
|
package/dist/server/plan.js
CHANGED
|
@@ -13,6 +13,7 @@ const work_scope_parser_1 = require("../schema/work-scope-parser");
|
|
|
13
13
|
const work_task_1 = require("../planner/work-task");
|
|
14
14
|
const work_service_1 = require("../planner/work-service");
|
|
15
15
|
const hosted_volumes_1 = require("../planner/utils/hosted-volumes");
|
|
16
|
+
const normalize_path_1 = require("../planner/utils/normalize-path");
|
|
16
17
|
const log_1 = require("../log");
|
|
17
18
|
const parse_error_1 = require("../schema/parse-error");
|
|
18
19
|
const task_arguments_1 = require("../planner/utils/task-arguments");
|
|
@@ -165,101 +166,127 @@ function describeSecret(secret) {
|
|
|
165
166
|
// server has no account, so nothing would stand between the build file and the
|
|
166
167
|
// operator's own credentials.
|
|
167
168
|
function findPolicyProblems(items, policy, root) {
|
|
168
|
-
|
|
169
|
+
// the problems of each item, said once for the items of a directory that share one, such
|
|
170
|
+
// as the tasks of an included file that all mount the docker socket
|
|
171
|
+
const grouped = new Map();
|
|
169
172
|
for (const item of items) {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
for (const source of 'src' in data ? data.src : []) {
|
|
178
|
-
if (!source.inherited && insideBundle(root, data.cwd, source.absolutePath).length === 0) {
|
|
179
|
-
problems.push(`${item.name}: src ${source.source} is outside of the bundle`);
|
|
173
|
+
const prefix = `${item.name}: `;
|
|
174
|
+
for (const problem of findItemPolicyProblems(item, policy, root)) {
|
|
175
|
+
const said = problem.startsWith(prefix) ? problem.slice(prefix.length) : problem;
|
|
176
|
+
const key = `${item.data.cwd}\0${said}`;
|
|
177
|
+
const group = grouped.get(key);
|
|
178
|
+
if (group) {
|
|
179
|
+
group.names.push(item.name);
|
|
180
180
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (!generate.inherited && insideBundle(root, data.cwd, generate.path).length === 0) {
|
|
184
|
-
problems.push(`${item.name}: generates ${generate.path}, which is outside of the bundle`);
|
|
181
|
+
else {
|
|
182
|
+
grouped.set(key, { names: [item.name], problem: said });
|
|
185
183
|
}
|
|
186
184
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
185
|
+
}
|
|
186
|
+
return [...grouped.values()].map(({ names: [name, ...others], problem }) => others.length > 0 ? `${name} (also ${others.join(', ')}): ${problem}` : `${name}: ${problem}`);
|
|
187
|
+
}
|
|
188
|
+
function findItemPolicyProblems(item, policy, root) {
|
|
189
|
+
const problems = [];
|
|
190
|
+
const runtime = item.data.type === 'local-task' ? 'local' : 'docker';
|
|
191
|
+
// an aggregate runs nothing, on no runtime
|
|
192
|
+
if (policy.runtimes && !(0, work_task_1.isAggregateTask)(item.data) && !policy.runtimes.includes(runtime)) {
|
|
193
|
+
problems.push(`${item.name}: runs on the ${runtime} runtime, which this server does not allow (runtimes: ${policy.runtimes.join(', ')})`);
|
|
194
|
+
}
|
|
195
|
+
const data = item.data;
|
|
196
|
+
// what a task reads and writes is the bundle's: `src: ../..` would have the server
|
|
197
|
+
// copy its own files (its credentials, other callers' caches) into the task
|
|
198
|
+
for (const source of 'src' in data ? data.src : []) {
|
|
199
|
+
if (!source.inherited && insideBundle(root, data.cwd, source.absolutePath).length === 0) {
|
|
200
|
+
problems.push(`${item.name}: src ${source.source} is outside of the bundle`);
|
|
196
201
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
reason: ` to what reads ${reads.join(', ')}`,
|
|
202
|
-
});
|
|
202
|
+
}
|
|
203
|
+
for (const generate of (0, work_task_1.isWorkTask)(data) ? data.generates : []) {
|
|
204
|
+
if (!generate.inherited && insideBundle(root, data.cwd, generate.path).length === 0) {
|
|
205
|
+
problems.push(`${item.name}: generates ${generate.path}, which is outside of the bundle`);
|
|
203
206
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
207
|
+
}
|
|
208
|
+
// what holds a credential gets at most `runs.secretNetwork`, so a tool it runs can't
|
|
209
|
+
// send the credential anywhere else
|
|
210
|
+
const reads = getCredentialReads({
|
|
211
|
+
secrets: secretsOf(item).map(describeSecret),
|
|
212
|
+
serverEnvs: 'envs' in data ? data.envs.processNames ?? [] : [],
|
|
213
|
+
});
|
|
214
|
+
const ceilings = [];
|
|
215
|
+
if (policy.network && (0, work_network_1.isLimitedNetwork)(policy.network)) {
|
|
216
|
+
ceilings.push({ ceiling: policy.network, setting: 'runs.network', reason: '' });
|
|
217
|
+
}
|
|
218
|
+
if (reads.length > 0 && policy.secretNetwork && (0, work_network_1.isLimitedNetwork)(policy.secretNetwork)) {
|
|
219
|
+
ceilings.push({
|
|
220
|
+
ceiling: policy.secretNetwork,
|
|
221
|
+
setting: 'runs.secretNetwork',
|
|
222
|
+
reason: ` to what reads ${reads.join(', ')}`,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// an aggregate reaches nothing: it runs no command
|
|
226
|
+
for (const { ceiling, setting, reason } of (0, work_task_1.isAggregateTask)(data) ? [] : ceilings) {
|
|
227
|
+
// on a cluster a local task is a pod of a pool's tool image, limited like any other
|
|
228
|
+
if (data.type === 'local-task' && policy.backend !== 'kubernetes') {
|
|
229
|
+
problems.push(`${item.name}: is a local task, which can't be limited to the network this server allows${reason} ` +
|
|
230
|
+
`(${setting}: ${(0, work_network_1.formatNetwork)(ceiling)})`);
|
|
214
231
|
}
|
|
215
|
-
if (
|
|
216
|
-
for (
|
|
217
|
-
|
|
218
|
-
if (insideBundle(root, data.cwd, mount.localPath).length === 0) {
|
|
219
|
-
problems.push(`${item.name}: mounts ${mount.localPath}, which is outside of the bundle`);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
232
|
+
else if ('network' in data && (0, work_network_1.exceedsNetwork)(data.network, ceiling)) {
|
|
233
|
+
problems.push(`${item.name}: asks for network ${(0, work_network_1.formatNetwork)(data.network)}, more than this server allows${reason} ` +
|
|
234
|
+
`(${setting}: ${(0, work_network_1.formatNetwork)(ceiling)})`);
|
|
222
235
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
236
|
+
}
|
|
237
|
+
if ((0, work_task_1.isContainerWorkTask)(data) || (0, work_service_1.isContainerWorkService)(data)) {
|
|
238
|
+
for (const mount of data.mounts) {
|
|
239
|
+
// a path of the server is not the build file's to mount: the docker socket, /etc, a home directory
|
|
240
|
+
if (insideBundle(root, data.cwd, mount.localPath).length === 0) {
|
|
241
|
+
problems.push(`${item.name}: mounts ${describeMountSource(mount)}, which is outside of the bundle`);
|
|
227
242
|
}
|
|
228
243
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
// provider only through the egress proxy
|
|
244
|
-
problems.push(`${item.name}: the secret ${describeSecret(secret).target} is read from ${source.provider} in the ` +
|
|
245
|
-
`Job of the task, which network ${(0, work_network_1.formatNetwork)(item.data.network)} would cut off from it: ` +
|
|
246
|
-
`this server names no endpoints of ${source.provider} in backend.providers.${source.provider}.allow ` +
|
|
247
|
-
'for the egress proxy to let through, so the task keeps an open network');
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
if (source.type === 'provider' && policy.reserved?.includes(source.account)) {
|
|
251
|
-
problems.push(`${item.name}: the secret ${describeSecret(secret).target} is read as ${source.account}, which the server uses to read repositories`);
|
|
244
|
+
}
|
|
245
|
+
for (const volume of (0, work_service_1.isContainerWorkService)(data) ? data.volumes : []) {
|
|
246
|
+
// docker takes anything else for a path of the host, such as the docker socket
|
|
247
|
+
if (!(0, hosted_volumes_1.isVolumeName)(volume.name)) {
|
|
248
|
+
problems.push(`${item.name}: volume ${volume.name} is not a volume name, a path of the server can't be mounted`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const secret of secretsOf(item)) {
|
|
252
|
+
const source = secret.source;
|
|
253
|
+
if (policy.backend === 'kubernetes' && source.type === 'provider') {
|
|
254
|
+
// the server reads no secret of a provider on a cluster: the Job of a task does
|
|
255
|
+
if (!(0, work_task_1.isWorkTask)(item.data)) {
|
|
256
|
+
problems.push(`${item.name}: the secret ${describeSecret(secret).target} is read from ${source.provider}, which ` +
|
|
257
|
+
'this server does for tasks only, a service on its cluster gets no provider secret yet');
|
|
252
258
|
}
|
|
253
|
-
else if (
|
|
254
|
-
problems.push(`${item.name}:
|
|
259
|
+
else if (secret.cache) {
|
|
260
|
+
problems.push(`${item.name}: the secret ${describeSecret(secret).target} has cache: true, its digest would need ` +
|
|
261
|
+
'the value before the Job of the task reads it, which this server does not support yet');
|
|
255
262
|
}
|
|
256
|
-
else if (
|
|
257
|
-
|
|
263
|
+
else if ((0, work_network_1.isLimitedNetwork)(item.data.network) && !policy.proxiedProviders?.includes(source.provider)) {
|
|
264
|
+
// the init containers that read it share the pod's NetworkPolicy, and reach the
|
|
265
|
+
// provider only through the egress proxy
|
|
266
|
+
problems.push(`${item.name}: the secret ${describeSecret(secret).target} is read from ${source.provider} in the ` +
|
|
267
|
+
`Job of the task, which network ${(0, work_network_1.formatNetwork)(item.data.network)} would cut off from it: ` +
|
|
268
|
+
`this server names no endpoints of ${source.provider} in backend.providers.${source.provider}.allow ` +
|
|
269
|
+
'for the egress proxy to let through, so the task keeps an open network');
|
|
258
270
|
}
|
|
259
271
|
}
|
|
272
|
+
if (source.type === 'provider' && policy.reserved?.includes(source.account)) {
|
|
273
|
+
problems.push(`${item.name}: the secret ${describeSecret(secret).target} is read as ${source.account}, which the server uses to read repositories`);
|
|
274
|
+
}
|
|
275
|
+
else if (source.type === 'file') {
|
|
276
|
+
problems.push(`${item.name}: a secret can not read a file of the server (file:), use a secret provider`);
|
|
277
|
+
}
|
|
278
|
+
else if (source.type === 'env' && !policy.envs.includes(source.name)) {
|
|
279
|
+
problems.push(`${item.name}: the secret ${describeSecret(secret).target} reads env:${source.name}, which the server does not list in ${policy.envsFrom ?? 'runs.env.allow'}`);
|
|
280
|
+
}
|
|
260
281
|
}
|
|
261
282
|
return problems;
|
|
262
283
|
}
|
|
284
|
+
// A mount of the home directory says so: `$PWD` in a mount is the home directory, not the
|
|
285
|
+
// project, which reads like a path of the checkout
|
|
286
|
+
function describeMountSource(mount) {
|
|
287
|
+
const home = (0, normalize_path_1.getHomeReference)(mount.mount.split(':')[0]);
|
|
288
|
+
return home ? `${mount.localPath} (${home} is the home directory)` : mount.localPath;
|
|
289
|
+
}
|
|
263
290
|
function toPlan(root, item) {
|
|
264
291
|
const data = item.data;
|
|
265
292
|
const secrets = secretsOf(item).map(describeSecret);
|
|
@@ -272,7 +299,7 @@ function toPlan(root, item) {
|
|
|
272
299
|
: [];
|
|
273
300
|
const ports = 'ports' in data ? data.ports.map((port) => `${port.hostPort ?? ''}:${port.containerPort}`) : [];
|
|
274
301
|
const flags = [];
|
|
275
|
-
if (data.type === 'local-task') {
|
|
302
|
+
if (data.type === 'local-task' && !(0, work_task_1.isAggregateTask)(data)) {
|
|
276
303
|
flags.push('local-task');
|
|
277
304
|
}
|
|
278
305
|
if ('ports' in data && data.ports.some((port) => port.hostPort !== null)) {
|
|
@@ -285,7 +312,7 @@ function toPlan(root, item) {
|
|
|
285
312
|
return {
|
|
286
313
|
kind: isTask ? 'task' : 'service',
|
|
287
314
|
...(data.description ? { description: data.description } : {}),
|
|
288
|
-
runtime: data.type === 'local-task' ? 'local' : 'docker',
|
|
315
|
+
runtime: (0, work_task_1.isAggregateTask)(data) ? 'none' : data.type === 'local-task' ? 'local' : 'docker',
|
|
289
316
|
...((0, work_task_1.isContainerWorkTask)(data) || (0, work_service_1.isContainerWorkService)(data) ? { image: data.image } : {}),
|
|
290
317
|
cmds: 'cmds' in data ? data.cmds.map((cmd) => cmd.cmd) : 'cmd' in data && data.cmd ? [data.cmd.cmd] : [],
|
|
291
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",
|