hammerkit 1.25.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/executer/container-task-state.js +8 -6
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +1 -1
- package/dist/planner/service-definition.js +3 -2
- package/dist/planner/utils/task-arguments.js +155 -0
- package/dist/planner/work-cache-id.js +15 -1
- package/dist/planner/work-runtime-docker.js +2 -0
- package/dist/planner/work-runtime-kubernetes.js +2 -1
- package/dist/planner/work-runtime-local.js +16 -8
- package/dist/planner/work-service.js +7 -3
- package/dist/planner/work-state-record.js +30 -0
- package/dist/program.js +59 -7
- package/dist/schema/work-scope-parser.js +8 -0
- package/package.json +1 -1
|
@@ -3,12 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.removeContainerTaskState = exports.writeContainerTaskState = exports.readContainerTaskState = exports.getContainerTaskStateFile = void 0;
|
|
4
4
|
const path_1 = require("path");
|
|
5
5
|
const get_cache_directory_1 = require("../optimizer/get-cache-directory");
|
|
6
|
-
const
|
|
6
|
+
const work_state_record_1 = require("../planner/work-state-record");
|
|
7
7
|
// The record says "the outputs of this container task belong to this state
|
|
8
|
-
// key", like the state file of a local task. It lives
|
|
9
|
-
// directory, not the project, so writing it never touches a
|
|
8
|
+
// key", like the state file of a local task (see work-state-record.ts). It lives
|
|
9
|
+
// in the hammerkit directory, not the project, so writing it never touches a
|
|
10
|
+
// task's src; the record name folds in the project root, so two checkouts keep
|
|
11
|
+
// their own.
|
|
10
12
|
function getContainerTaskStateFile(task) {
|
|
11
|
-
return (0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'state', (0,
|
|
13
|
+
return (0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'state', (0, work_state_record_1.getStateRecordName)(task));
|
|
12
14
|
}
|
|
13
15
|
exports.getContainerTaskStateFile = getContainerTaskStateFile;
|
|
14
16
|
async function readContainerTaskState(environment, task) {
|
|
@@ -16,12 +18,12 @@ async function readContainerTaskState(environment, task) {
|
|
|
16
18
|
if (!(await environment.file.exists(stateFile))) {
|
|
17
19
|
return null;
|
|
18
20
|
}
|
|
19
|
-
return (await environment.file.read(stateFile))
|
|
21
|
+
return (0, work_state_record_1.parseStateRecord)(task, await environment.file.read(stateFile));
|
|
20
22
|
}
|
|
21
23
|
exports.readContainerTaskState = readContainerTaskState;
|
|
22
24
|
async function writeContainerTaskState(environment, task, stateKey) {
|
|
23
25
|
await environment.file.createDirectory((0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'state'));
|
|
24
|
-
await environment.file.writeFile(getContainerTaskStateFile(task), stateKey);
|
|
26
|
+
await environment.file.writeFile(getContainerTaskStateFile(task), (0, work_state_record_1.formatStateRecord)(task, stateKey));
|
|
25
27
|
}
|
|
26
28
|
exports.writeContainerTaskState = writeContainerTaskState;
|
|
27
29
|
async function removeContainerTaskState(environment, task) {
|
|
@@ -30,7 +30,7 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
|
|
|
30
30
|
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(service.data.envs);
|
|
31
31
|
const name = (0, resources_1.getResourceName)(service);
|
|
32
32
|
const healthcheckCommand = (0, work_service_1.getHealthcheckCommand)(service.data);
|
|
33
|
-
const serviceCommand = (0, work_service_1.getServiceCommand)(service.data);
|
|
33
|
+
const serviceCommand = (0, work_service_1.getServiceCommand)(service.data, { kubernetes: true });
|
|
34
34
|
const probe = healthcheckCommand
|
|
35
35
|
? {
|
|
36
36
|
exec: {
|
|
@@ -4,6 +4,7 @@ exports.getServiceDefinitionHash = void 0;
|
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const work_secret_1 = require("./work-secret");
|
|
6
6
|
const work_service_1 = require("./work-service");
|
|
7
|
+
const work_cache_id_1 = require("./work-cache-id");
|
|
7
8
|
// Everything a running container of the service was created from, plus the
|
|
8
9
|
// definitions of the services it needs: a dependent links to the container it
|
|
9
10
|
// started with, so a recreated need means a recreated dependent too. Stored as
|
|
@@ -30,8 +31,8 @@ function getServiceDefinitionHash(item) {
|
|
|
30
31
|
? `provider:${(0, work_secret_1.getSecretName)(s)}:${(0, work_secret_1.describeSecretSource)(s)}`
|
|
31
32
|
: `${s.source.type}:${(0, work_secret_1.getSecretName)(s)}`),
|
|
32
33
|
// the init task's id covers its image, commands, envs and mounts: a changed
|
|
33
|
-
// init recreates the service, so it runs again
|
|
34
|
-
init: service.init ? service.init.task
|
|
34
|
+
// init recreates the service, so it runs again (arguments after -- don't)
|
|
35
|
+
init: service.init ? (0, work_cache_id_1.getDeclaredWorkTaskId)(service.init.task) : null,
|
|
35
36
|
needs: item.needs
|
|
36
37
|
.filter((need) => (0, work_service_1.isContainerWorkService)(need.service.data))
|
|
37
38
|
.map((need) => `${need.name}=${getServiceDefinitionHash(need.service)}`),
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isRefusedByWindowsPowerShell = exports.applyArguments = exports.getArgumentsTarget = exports.getKubernetesCommandArgs = exports.getKubernetesCommandText = exports.escapeKubernetesText = exports.appendArguments = exports.quoteArgument = exports.getShellQuoting = void 0;
|
|
4
|
+
const os_1 = require("os");
|
|
5
|
+
const POSIX_SHELLS = ['sh', 'bash', 'zsh', 'dash', 'ash', 'ksh', 'mksh'];
|
|
6
|
+
const POWERSHELLS = ['powershell', 'pwsh'];
|
|
7
|
+
// The quoting of a shell, by the name of its program; null for a shell whose
|
|
8
|
+
// quoting isn't known (fish, cmd.exe, …). Guessing would let shell syntax in an
|
|
9
|
+
// argument run as a second command, so such a shell takes no arguments.
|
|
10
|
+
function getShellQuoting(shell) {
|
|
11
|
+
const program = shell.trim().split(/\s+/)[0] ?? '';
|
|
12
|
+
const name = (program.split(/[\\/]/).pop() ?? '').toLowerCase().replace(/\.exe$/, '');
|
|
13
|
+
if (POSIX_SHELLS.includes(name)) {
|
|
14
|
+
return 'posix';
|
|
15
|
+
}
|
|
16
|
+
if (POWERSHELLS.includes(name)) {
|
|
17
|
+
return 'powershell';
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
exports.getShellQuoting = getShellQuoting;
|
|
22
|
+
// Single quotes take everything literally in both shells. In sh a quote ends the
|
|
23
|
+
// string, so it is closed, escaped and reopened; PowerShell doubles it, and reads
|
|
24
|
+
// the typographic single quotes as quotes too.
|
|
25
|
+
function quoteArgument(arg, quoting) {
|
|
26
|
+
if (quoting === 'powershell') {
|
|
27
|
+
return `'${arg.replace(/['‘’‚‛]/g, (quote) => quote + quote)}'`;
|
|
28
|
+
}
|
|
29
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
30
|
+
}
|
|
31
|
+
exports.quoteArgument = quoteArgument;
|
|
32
|
+
function appendArguments(command, args, quoting) {
|
|
33
|
+
// an exec-form command is shown as sh would read it; it runs from `parsed`
|
|
34
|
+
const display = quoting === 'exec' ? 'posix' : quoting;
|
|
35
|
+
return {
|
|
36
|
+
cwd: command.cwd,
|
|
37
|
+
cmd: [command.cmd, ...args.map((arg) => quoteArgument(arg, display))].join(' '),
|
|
38
|
+
parsed: {
|
|
39
|
+
command: command.parsed.command,
|
|
40
|
+
args: [...command.parsed.args, ...args],
|
|
41
|
+
},
|
|
42
|
+
arguments: { cmd: command.cmd.length, args: command.parsed.args.length },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
exports.appendArguments = appendArguments;
|
|
46
|
+
// Kubernetes expands `$(NAME)` with the container's env and turns `$$` into `$`
|
|
47
|
+
// in a container's command and args, before any shell sees them, so a quoted
|
|
48
|
+
// argument isn't text there unless its `$` is doubled. The part of the command
|
|
49
|
+
// the build file declares keeps its meaning.
|
|
50
|
+
function escapeKubernetesText(text) {
|
|
51
|
+
return text.replace(/\$/g, '$$$$');
|
|
52
|
+
}
|
|
53
|
+
exports.escapeKubernetesText = escapeKubernetesText;
|
|
54
|
+
function getKubernetesCommandText(command) {
|
|
55
|
+
if (!command.arguments) {
|
|
56
|
+
return command.cmd;
|
|
57
|
+
}
|
|
58
|
+
const from = command.arguments.cmd;
|
|
59
|
+
return command.cmd.slice(0, from) + escapeKubernetesText(command.cmd.slice(from));
|
|
60
|
+
}
|
|
61
|
+
exports.getKubernetesCommandText = getKubernetesCommandText;
|
|
62
|
+
function getKubernetesCommandArgs(command) {
|
|
63
|
+
const from = command.arguments?.args ?? command.parsed.args.length;
|
|
64
|
+
return command.parsed.args.map((arg, index) => (index >= from ? escapeKubernetesText(arg) : arg));
|
|
65
|
+
}
|
|
66
|
+
exports.getKubernetesCommandArgs = getKubernetesCommandArgs;
|
|
67
|
+
// The command arguments after `--` land in: the last command of a task, the
|
|
68
|
+
// `cmd` of a container service. Null when there is none.
|
|
69
|
+
function getArgumentsTarget(item) {
|
|
70
|
+
if (item.type === 'local-task' || item.type === 'container-task') {
|
|
71
|
+
return item.cmds[item.cmds.length - 1] ?? null;
|
|
72
|
+
}
|
|
73
|
+
if (item.type === 'container-service') {
|
|
74
|
+
return item.cmd;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
exports.getArgumentsTarget = getArgumentsTarget;
|
|
79
|
+
// Appends the arguments to the one task or service a run selects, before
|
|
80
|
+
// anything asks for its id: the last command is part of the task id, so the
|
|
81
|
+
// arguments are part of its cache key. A run with arguments is a one-off (one
|
|
82
|
+
// test, one flag), so the task skips the cache unless `keepCache`; even then the
|
|
83
|
+
// key keeps runs with other arguments apart. Dependencies and needed services
|
|
84
|
+
// are planned items of their own and keep their commands and caching. No
|
|
85
|
+
// arguments change nothing, so a task run without them keeps its cache entries.
|
|
86
|
+
function applyArguments(selection, args, keepCache = false) {
|
|
87
|
+
if (args.length === 0) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const selected = [...selection.tasks, ...selection.services];
|
|
91
|
+
if (selected.length === 0) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (selected.length > 1) {
|
|
95
|
+
throw new Error(`arguments after -- go to one task or service, but this run selects ${selected.map((s) => s.name).join(', ')}`);
|
|
96
|
+
}
|
|
97
|
+
if (args.some((arg) => arg.includes('\0'))) {
|
|
98
|
+
throw new Error('an argument after -- contains a NUL character');
|
|
99
|
+
}
|
|
100
|
+
const item = selected[0];
|
|
101
|
+
if (item.type === 'kubernetes-service') {
|
|
102
|
+
throw new Error(`service ${item.name} is a kubernetes service and has no cmd for the arguments after --`);
|
|
103
|
+
}
|
|
104
|
+
const target = getArgumentsTarget(item);
|
|
105
|
+
if (!target) {
|
|
106
|
+
throw new Error(item.type === 'container-service'
|
|
107
|
+
? `service ${item.name} has no cmd for the arguments after --`
|
|
108
|
+
: `task ${item.name} has no command for the arguments after --`);
|
|
109
|
+
}
|
|
110
|
+
const quoting = getItemQuoting(item);
|
|
111
|
+
if (item.type === 'local-task' && quoting === 'powershell') {
|
|
112
|
+
const refused = args.find(isRefusedByWindowsPowerShell);
|
|
113
|
+
if (refused !== undefined) {
|
|
114
|
+
throw new Error(`task ${item.name} runs in Windows PowerShell, which would split or run the argument ${JSON.stringify(refused)}, so it is refused: an empty argument, a ", the characters & | < > ^ % !, line breaks, and a space with a trailing \\`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (item.type === 'container-service') {
|
|
118
|
+
item.cmd = appendArguments(target, args, quoting);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
item.cmds = [...item.cmds.slice(0, -1), appendArguments(target, args, quoting)];
|
|
122
|
+
if (!keepCache) {
|
|
123
|
+
// not implicit, so the run's --cache default doesn't bring the method back
|
|
124
|
+
item.caching = { ...item.caching, method: 'none', implicit: false };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
exports.applyArguments = applyArguments;
|
|
129
|
+
// Windows PowerShell 5.1 builds a new command line for a native program, and
|
|
130
|
+
// starts a .cmd or .bat shim (yarn.cmd, most of node_modules/.bin) through
|
|
131
|
+
// cmd.exe. There an empty argument is dropped, a " splits or joins arguments,
|
|
132
|
+
// an argument with a space that ends in \ is quoted as "a b\" and so escapes
|
|
133
|
+
// its own closing quote, and cmd reads & | < > ^ % ! and a line break as
|
|
134
|
+
// syntax, so such an argument can't be passed as text.
|
|
135
|
+
function isRefusedByWindowsPowerShell(arg) {
|
|
136
|
+
return arg === '' || /["&|<>^%!\r\n]/.test(arg) || (/\s/.test(arg) && arg.endsWith('\\'));
|
|
137
|
+
}
|
|
138
|
+
exports.isRefusedByWindowsPowerShell = isRefusedByWindowsPowerShell;
|
|
139
|
+
function getItemQuoting(item) {
|
|
140
|
+
if (item.type === 'local-task') {
|
|
141
|
+
// what executeCommand runs a local command in
|
|
142
|
+
return (0, os_1.platform)() === 'win32' ? 'powershell' : 'posix';
|
|
143
|
+
}
|
|
144
|
+
const shell = item.type === 'kubernetes-service' ? null : item.shell;
|
|
145
|
+
if (shell === null) {
|
|
146
|
+
return 'exec';
|
|
147
|
+
}
|
|
148
|
+
const quoting = getShellQuoting(shell);
|
|
149
|
+
if (!quoting) {
|
|
150
|
+
throw new Error(`${item.type === 'container-task' ? 'task' : 'service'} ${item.name} runs in ${shell}, whose quoting hammerkit ` +
|
|
151
|
+
`doesn't know, so it takes no arguments after --; use sh, bash, zsh, dash, ash, ksh, mksh or PowerShell`);
|
|
152
|
+
}
|
|
153
|
+
return quoting;
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=task-arguments.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getWorkTaskId = void 0;
|
|
3
|
+
exports.getDeclaredWorkTaskId = exports.getWorkTaskId = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const work_task_cache_description_1 = require("../optimizer/work-task-cache-description");
|
|
6
6
|
function getWorkTaskId(item) {
|
|
@@ -9,4 +9,18 @@ function getWorkTaskId(item) {
|
|
|
9
9
|
return (0, crypto_1.createHash)('sha1').update(jsonData).digest('hex');
|
|
10
10
|
}
|
|
11
11
|
exports.getWorkTaskId = getWorkTaskId;
|
|
12
|
+
// The id of the task as the build file declares it: without the arguments
|
|
13
|
+
// after -- a run appended to its last command. A service's definition takes its
|
|
14
|
+
// init task by this id, so `run <init> -- ...` leaves a running service as it is.
|
|
15
|
+
function getDeclaredWorkTaskId(item) {
|
|
16
|
+
const description = (0, work_task_cache_description_1.getWorkTaskCacheDescription)(item);
|
|
17
|
+
const cmds = description.cmds?.map((cmd, index) => {
|
|
18
|
+
const declared = item.data.cmds[index];
|
|
19
|
+
return declared.arguments ? { ...cmd, cmd: declared.cmd.slice(0, declared.arguments.cmd) } : cmd;
|
|
20
|
+
});
|
|
21
|
+
return (0, crypto_1.createHash)('sha1')
|
|
22
|
+
.update(JSON.stringify({ ...description, cmds }))
|
|
23
|
+
.digest('hex');
|
|
24
|
+
}
|
|
25
|
+
exports.getDeclaredWorkTaskId = getDeclaredWorkTaskId;
|
|
12
26
|
//# sourceMappingURL=work-cache-id.js.map
|
|
@@ -39,6 +39,8 @@ function dockerTaskRuntime(task, workEnvironment) {
|
|
|
39
39
|
});
|
|
40
40
|
},
|
|
41
41
|
async restore(environment, path) {
|
|
42
|
+
// the restored outputs aren't the ones the record names any more
|
|
43
|
+
await (0, container_task_state_1.removeContainerTaskState)(environment, task);
|
|
42
44
|
await restoreContainer(docker, environment, task, path);
|
|
43
45
|
},
|
|
44
46
|
async archive(environment, path) {
|
|
@@ -22,6 +22,7 @@ const ensure_http_route_1 = require("../kubernetes/ensure-http-route");
|
|
|
22
22
|
const find_process_1 = __importDefault(require("find-process"));
|
|
23
23
|
const log_1 = require("../log");
|
|
24
24
|
const version_1 = require("../version");
|
|
25
|
+
const task_arguments_1 = require("./utils/task-arguments");
|
|
25
26
|
const restore_kubernetes_data_1 = require("../kubernetes/restore-kubernetes-data");
|
|
26
27
|
const store_kubernetes_data_1 = require("../kubernetes/store-kubernetes-data");
|
|
27
28
|
const remove_persistent_data_1 = require("../kubernetes/remove-persistent-data");
|
|
@@ -110,7 +111,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
110
111
|
image: task.data.image,
|
|
111
112
|
// through the task's shell, as on Docker
|
|
112
113
|
command: [task.data.shell, '-c'],
|
|
113
|
-
args: [
|
|
114
|
+
args: [(0, task_arguments_1.getKubernetesCommandText)(cmd)],
|
|
114
115
|
workingDir: cmd.cwd,
|
|
115
116
|
env: [
|
|
116
117
|
...Object.entries(envs).map(([key, value]) => ({ name: key, value })),
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getLocalWorkRuntime = void 0;
|
|
6
|
+
exports.getLocalWorkRuntime = exports.getStateFilename = void 0;
|
|
7
7
|
const tar_1 = require("tar");
|
|
8
8
|
const path_1 = require("path");
|
|
9
9
|
const local_task_1 = require("../executer/local-task");
|
|
@@ -11,9 +11,11 @@ const event_cache_1 = require("../executer/event-cache");
|
|
|
11
11
|
const get_outputs_to_reset_1 = require("./utils/get-outputs-to-reset");
|
|
12
12
|
const find_process_1 = __importDefault(require("find-process"));
|
|
13
13
|
const stream_1 = require("stream");
|
|
14
|
+
const work_state_record_1 = require("./work-state-record");
|
|
14
15
|
function getStateFilename(task) {
|
|
15
|
-
return (0, path_1.join)(task.data.cwd, '.hammerkit',
|
|
16
|
+
return (0, path_1.join)(task.data.cwd, '.hammerkit', 'state', (0, work_state_record_1.getStateRecordName)(task));
|
|
16
17
|
}
|
|
18
|
+
exports.getStateFilename = getStateFilename;
|
|
17
19
|
function getPidFilename(task) {
|
|
18
20
|
return (0, path_1.join)(task.data.cwd, '.hammerkit', `${task.id()}.pid`);
|
|
19
21
|
}
|
|
@@ -39,6 +41,8 @@ function getLocalWorkRuntime(task) {
|
|
|
39
41
|
// pidfile check happens in execute (initialize has no Environment in scope)
|
|
40
42
|
},
|
|
41
43
|
async restore(environment, path) {
|
|
44
|
+
// the restored outputs aren't the ones the record names any more
|
|
45
|
+
await removeStateFile(environment, task);
|
|
42
46
|
await restoreLocal(environment, task.data, path);
|
|
43
47
|
},
|
|
44
48
|
async archive(environment, path) {
|
|
@@ -54,7 +58,7 @@ function getLocalWorkRuntime(task) {
|
|
|
54
58
|
});
|
|
55
59
|
return;
|
|
56
60
|
}
|
|
57
|
-
await environment.file.createDirectory((0, path_1.join)(task.data.cwd, '.hammerkit'));
|
|
61
|
+
await environment.file.createDirectory((0, path_1.join)(task.data.cwd, '.hammerkit', 'state'));
|
|
58
62
|
const pidFile = getPidFilename(task);
|
|
59
63
|
await environment.file.writeFile(pidFile, `${process.pid}`);
|
|
60
64
|
try {
|
|
@@ -62,16 +66,14 @@ function getLocalWorkRuntime(task) {
|
|
|
62
66
|
// fails overwrites outputs, so the previous record goes first, and only
|
|
63
67
|
// a run that succeeded writes a new one.
|
|
64
68
|
const stateFile = getStateFilename(task);
|
|
65
|
-
|
|
66
|
-
await environment.file.remove(stateFile);
|
|
67
|
-
}
|
|
69
|
+
await removeStateFile(environment, task);
|
|
68
70
|
for (const generate of (0, get_outputs_to_reset_1.getOutputsToReset)(task.data)) {
|
|
69
71
|
await environment.file.remove(generate.path);
|
|
70
72
|
}
|
|
71
73
|
await (0, local_task_1.localTask)(task, environment, options);
|
|
72
74
|
const failed = ['crash', 'error', 'canceled'].includes(options.state.current.type);
|
|
73
75
|
if (!failed) {
|
|
74
|
-
await environment.file.writeFile(stateFile, options.stateKey);
|
|
76
|
+
await environment.file.writeFile(stateFile, (0, work_state_record_1.formatStateRecord)(task, options.stateKey));
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
finally {
|
|
@@ -95,7 +97,7 @@ function getLocalWorkRuntime(task) {
|
|
|
95
97
|
return null;
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
|
-
return await environment.file.read(stateFileName);
|
|
100
|
+
return (0, work_state_record_1.parseStateRecord)(task, await environment.file.read(stateFileName));
|
|
99
101
|
},
|
|
100
102
|
async remove(environment) {
|
|
101
103
|
for (const generate of task.data.generates) {
|
|
@@ -109,6 +111,12 @@ function getLocalWorkRuntime(task) {
|
|
|
109
111
|
};
|
|
110
112
|
}
|
|
111
113
|
exports.getLocalWorkRuntime = getLocalWorkRuntime;
|
|
114
|
+
async function removeStateFile(environment, task) {
|
|
115
|
+
const stateFile = getStateFilename(task);
|
|
116
|
+
if (await environment.file.exists(stateFile)) {
|
|
117
|
+
await environment.file.remove(stateFile);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
112
120
|
async function archiveLocal(environment, task, path) {
|
|
113
121
|
for (const generatedArchive of (0, event_cache_1.getArchivePaths)(task.data, path)) {
|
|
114
122
|
await environment.file.writeStream(generatedArchive.filename,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getHealthcheckCommand = exports.getServiceCommand = exports.getServiceWorkingDir = exports.isContainerWorkService = void 0;
|
|
4
|
+
const task_arguments_1 = require("./utils/task-arguments");
|
|
4
5
|
const isContainerWorkService = (svc) => 'image' in svc && svc.type === 'container-service';
|
|
5
6
|
exports.isContainerWorkService = isContainerWorkService;
|
|
6
7
|
// The container's working directory: the declared `workdir`, else the build
|
|
@@ -10,14 +11,17 @@ exports.getServiceWorkingDir = getServiceWorkingDir;
|
|
|
10
11
|
// The container's entrypoint and command for a service: with a `shell`, its
|
|
11
12
|
// `cmd` runs as `<shell> -c "<cmd>"` in place of the image entrypoint; without
|
|
12
13
|
// one the tokenized `cmd` becomes the command and the image entrypoint stays.
|
|
13
|
-
|
|
14
|
+
// For Kubernetes, the arguments after -- are escaped so they stay text there.
|
|
15
|
+
function getServiceCommand(service, options = {}) {
|
|
14
16
|
if (!service.cmd) {
|
|
15
17
|
return { entrypoint: null, cmd: null };
|
|
16
18
|
}
|
|
17
19
|
if (service.shell) {
|
|
18
|
-
|
|
20
|
+
const cmd = options.kubernetes ? (0, task_arguments_1.getKubernetesCommandText)(service.cmd) : service.cmd.cmd;
|
|
21
|
+
return { entrypoint: [service.shell, '-c'], cmd: [cmd] };
|
|
19
22
|
}
|
|
20
|
-
|
|
23
|
+
const args = options.kubernetes ? (0, task_arguments_1.getKubernetesCommandArgs)(service.cmd) : service.cmd.parsed.args;
|
|
24
|
+
return { entrypoint: null, cmd: [service.cmd.parsed.command, ...args] };
|
|
21
25
|
}
|
|
22
26
|
exports.getServiceCommand = getServiceCommand;
|
|
23
27
|
// The healthcheck as an exec command, through the service's `shell` if it has one.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseStateRecord = exports.formatStateRecord = exports.getStateRecordName = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
// The state record says "the outputs at this task's paths were made by this
|
|
6
|
+
// variant of the task (its id), from this state key". It is kept per task
|
|
7
|
+
// *name*, because every variant of a task (other arguments after --, another
|
|
8
|
+
// --set value) writes the same output paths: a record per id would let a run of
|
|
9
|
+
// one variant overwrite the outputs while the record of another one still
|
|
10
|
+
// claims them. The id stored with the key makes the next run of that other
|
|
11
|
+
// variant a miss.
|
|
12
|
+
function getStateRecordName(task) {
|
|
13
|
+
return (0, crypto_1.createHash)('sha1').update(`${task.name}\0${task.data.projectRoot}`).digest('hex');
|
|
14
|
+
}
|
|
15
|
+
exports.getStateRecordName = getStateRecordName;
|
|
16
|
+
function formatStateRecord(task, stateKey) {
|
|
17
|
+
return `${task.id()}:${stateKey}`;
|
|
18
|
+
}
|
|
19
|
+
exports.formatStateRecord = formatStateRecord;
|
|
20
|
+
// The state key of the record when this variant made the outputs, else null.
|
|
21
|
+
function parseStateRecord(task, content) {
|
|
22
|
+
const record = content.trim();
|
|
23
|
+
const separator = record.indexOf(':');
|
|
24
|
+
if (separator < 0 || record.slice(0, separator) !== task.id()) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return record.slice(separator + 1) || null;
|
|
28
|
+
}
|
|
29
|
+
exports.parseStateRecord = parseStateRecord;
|
|
30
|
+
//# sourceMappingURL=work-state-record.js.map
|
package/dist/program.js
CHANGED
|
@@ -28,6 +28,7 @@ const cache_inventory_1 = require("./cache/cache-inventory");
|
|
|
28
28
|
const remote_commands_1 = require("./remote/remote-commands");
|
|
29
29
|
const server_command_1 = require("./server/server-command");
|
|
30
30
|
const run_worker_command_1 = require("./server/run-worker-command");
|
|
31
|
+
const task_arguments_1 = require("./planner/utils/task-arguments");
|
|
31
32
|
async function createCli(fileName, environment, workScope) {
|
|
32
33
|
const { ctx, scope } = await (0, schema_parser_1.createParseContext)(fileName, environment);
|
|
33
34
|
const referencedScope = await (0, reference_parser_1.parseReferences)(ctx, scope, environment);
|
|
@@ -80,6 +81,31 @@ function printTaskDefinition(environment, definition) {
|
|
|
80
81
|
(0, log_1.printProperty)(environment, `env ${reference.name}`, `from $${reference.from} ${colors_1.default.grey(reference.available ? '(set)' : '(not set)')}`);
|
|
81
82
|
}
|
|
82
83
|
}
|
|
84
|
+
// `ls --json`: what a caller (an agent picking a task) needs to know, with the
|
|
85
|
+
// command that arguments after `--` are appended to (null: it takes none).
|
|
86
|
+
function getListJson(tasks, services) {
|
|
87
|
+
return {
|
|
88
|
+
tasks: tasks.map(({ item }) => ({
|
|
89
|
+
name: item.name,
|
|
90
|
+
description: item.data.description,
|
|
91
|
+
type: item.data.type,
|
|
92
|
+
...(item.data.type === 'container-task' ? { image: item.data.image } : {}),
|
|
93
|
+
deps: item.deps.map((d) => d.name),
|
|
94
|
+
needs: item.needs.map((n) => n.name),
|
|
95
|
+
labels: item.data.labels,
|
|
96
|
+
command: (0, task_arguments_1.getArgumentsTarget)(item.data)?.cmd ?? null,
|
|
97
|
+
})),
|
|
98
|
+
services: services.map(({ item }) => ({
|
|
99
|
+
name: item.name,
|
|
100
|
+
description: item.data.description,
|
|
101
|
+
type: item.data.type,
|
|
102
|
+
...(item.data.type === 'container-service' ? { image: item.data.image } : {}),
|
|
103
|
+
ports: item.data.ports.map((p) => ({ host: p.hostPort, container: p.containerPort })),
|
|
104
|
+
labels: item.data.labels,
|
|
105
|
+
command: (0, task_arguments_1.getArgumentsTarget)(item.data)?.cmd ?? null,
|
|
106
|
+
})),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
83
109
|
function parseWorkLabelScope(options) {
|
|
84
110
|
const scope = {
|
|
85
111
|
environmentName: null,
|
|
@@ -103,11 +129,20 @@ function parseWorkLabelScope(options) {
|
|
|
103
129
|
}
|
|
104
130
|
return scope;
|
|
105
131
|
}
|
|
132
|
+
// A task given arguments after -- skips the cache, unless the caller asks for a
|
|
133
|
+
// cache method on the command line.
|
|
134
|
+
function keepsCache(command) {
|
|
135
|
+
return command.getOptionValueSource('cache') === 'cli';
|
|
136
|
+
}
|
|
106
137
|
async function getProgram(environment, argv) {
|
|
107
138
|
const program = new commander_1.Command();
|
|
108
139
|
(0, server_command_1.registerServerCommand)(program, environment);
|
|
109
140
|
(0, run_worker_command_1.registerRunWorkerCommand)(program, environment);
|
|
110
|
-
|
|
141
|
+
// What follows `--` belongs to the one task or service of a run or up, not to
|
|
142
|
+
// hammerkit: a `--set` or `--file` there is an argument like any other.
|
|
143
|
+
const separator = argv.indexOf('--');
|
|
144
|
+
const passthrough = separator >= 0 ? argv.slice(separator + 1) : [];
|
|
145
|
+
const parsed = (0, parse_set_arguments_1.parseSetArguments)(separator >= 0 ? argv.slice(0, separator) : argv);
|
|
111
146
|
const args = parsed.args;
|
|
112
147
|
environment.processEnvs = { ...environment.processEnvs, ...parsed.values };
|
|
113
148
|
const fileIndex = args.indexOf('--file');
|
|
@@ -116,17 +151,28 @@ async function getProgram(environment, argv) {
|
|
|
116
151
|
args.splice(fileIndex, 2);
|
|
117
152
|
}
|
|
118
153
|
(0, remote_commands_1.registerRemoteCommands)(program, environment, fileName);
|
|
154
|
+
program.hook('preAction', (_, actionCommand) => {
|
|
155
|
+
const takesArguments = actionCommand.parent === program && ['run', 'up'].includes(actionCommand.name());
|
|
156
|
+
if (passthrough.length > 0 && !takesArguments) {
|
|
157
|
+
program.error(`${actionCommand.name()} takes no arguments after --, only run and up do`, { exitCode: 1 });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
119
160
|
if (await environment.file.exists(fileName)) {
|
|
120
161
|
program
|
|
121
162
|
.command('ls')
|
|
122
163
|
.description('list all tasks')
|
|
123
164
|
.addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
|
|
124
165
|
.addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
|
|
166
|
+
.addOption(new commander_1.Option('--json', 'emit the tasks and services as JSON').default(false))
|
|
125
167
|
.action(async (options) => {
|
|
126
168
|
const cli = await createCli(fileName, environment, parseWorkLabelScope(options));
|
|
127
169
|
const items = cli.ls();
|
|
128
170
|
const tasks = items.filter(cli_1.isCliTask);
|
|
129
171
|
const services = items.filter(cli_1.isCliService);
|
|
172
|
+
if (options.json) {
|
|
173
|
+
environment.stdout.write(`${JSON.stringify(getListJson(tasks, services), null, 2)}\n`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
130
176
|
if (services.length > 0) {
|
|
131
177
|
(0, log_1.printTitle)(environment, 'Services');
|
|
132
178
|
for (const service of services) {
|
|
@@ -527,7 +573,8 @@ async function getProgram(environment, argv) {
|
|
|
527
573
|
});
|
|
528
574
|
program
|
|
529
575
|
.command('up')
|
|
530
|
-
.description(
|
|
576
|
+
.description("start services(s), with the arguments after -- appended to the selected service's cmd")
|
|
577
|
+
.usage('[options] [-- args...]')
|
|
531
578
|
.addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
|
|
532
579
|
.addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
|
|
533
580
|
.addOption(new commander_1.Option('-c, --concurrency <number>', 'parallel worker count (default 4, unbounded when every task and service requests cpus and memory)').argParser(parseInt))
|
|
@@ -546,9 +593,12 @@ async function getProgram(environment, argv) {
|
|
|
546
593
|
.choices(['checksum', 'modify-date', 'none']))
|
|
547
594
|
.addOption(new commander_1.Option('--cache-read-only', `restore from cache backends but never push to them (or set ${read_only_1.CACHE_READ_ONLY_ENV}=1)`).default(false))
|
|
548
595
|
.addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
|
|
549
|
-
.action(async (options) => {
|
|
596
|
+
.action(async (options, command) => {
|
|
550
597
|
try {
|
|
551
|
-
const scope =
|
|
598
|
+
const scope = {
|
|
599
|
+
...parseWorkLabelScope(options),
|
|
600
|
+
args: { values: passthrough, services: true, cache: keepsCache(command) },
|
|
601
|
+
};
|
|
552
602
|
const cli = await createCli(fileName, environment, scope);
|
|
553
603
|
const result = await cli.runUp({
|
|
554
604
|
cacheDefault: options.cache,
|
|
@@ -601,7 +651,8 @@ async function getProgram(environment, argv) {
|
|
|
601
651
|
});
|
|
602
652
|
program
|
|
603
653
|
.command('run [task]', { isDefault: true })
|
|
604
|
-
.description('execute task')
|
|
654
|
+
.description('execute task, with the arguments after -- appended to its last command')
|
|
655
|
+
.usage('[options] [task] [-- args...]')
|
|
605
656
|
.addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
|
|
606
657
|
.addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
|
|
607
658
|
.addOption(new commander_1.Option('-c, --concurrency <number>', 'parallel worker count (default 4, unbounded when every task and service requests cpus and memory)').argParser(parseInt))
|
|
@@ -621,14 +672,15 @@ async function getProgram(environment, argv) {
|
|
|
621
672
|
.addOption(new commander_1.Option('--cache-read-only', `restore from cache backends but never push to them (or set ${read_only_1.CACHE_READ_ONLY_ENV}=1)`).default(false))
|
|
622
673
|
.addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
|
|
623
674
|
.addOption(new commander_1.Option('--no-skip-deps', 'run dependencies even when nothing needing them has to run'))
|
|
624
|
-
.action(async (task, options) => {
|
|
675
|
+
.action(async (task, options, command) => {
|
|
625
676
|
try {
|
|
626
677
|
// For machine-readable JSON, suppress the human progress logger (which
|
|
627
678
|
// also writes to stdout) so the only thing on stdout is the JSON.
|
|
628
679
|
const runEnvironment = options.summaryJson
|
|
629
680
|
? { ...environment, stdout: (0, empty_writable_1.emptyWritable)(), console: (0, log_1.consoleContext)((0, empty_writable_1.emptyWritable)()) }
|
|
630
681
|
: environment;
|
|
631
|
-
const
|
|
682
|
+
const args = { values: passthrough, services: false, cache: keepsCache(command) };
|
|
683
|
+
const cli = await createCli(fileName, runEnvironment, task ? { taskName: task, environmentName: options.env, args } : { ...parseWorkLabelScope(options), args });
|
|
632
684
|
if (cli.tasks().length === 0) {
|
|
633
685
|
program.error('No tasks found', { exitCode: 127 });
|
|
634
686
|
return;
|
|
@@ -6,6 +6,7 @@ const label_values_1 = require("../executer/label-values");
|
|
|
6
6
|
const append_work_service_1 = require("../planner/utils/append-work-service");
|
|
7
7
|
const append_work_task_1 = require("../planner/utils/append-work-task");
|
|
8
8
|
const append_work_environment_1 = require("../planner/utils/append-work-environment");
|
|
9
|
+
const task_arguments_1 = require("../planner/utils/task-arguments");
|
|
9
10
|
function defaultEnvironment(context) {
|
|
10
11
|
if ('default' in context.environments) {
|
|
11
12
|
const defaultEnv = context.environments['default'];
|
|
@@ -41,17 +42,24 @@ function getWorkContext(context, scope, environment) {
|
|
|
41
42
|
// its init task's need on it, which narrowing follows
|
|
42
43
|
const tasks = Object.values(context.tasks).map((task) => (0, append_work_task_1.appendWorkTask)(workTree, task.cwd, task, environment, context));
|
|
43
44
|
const services = Object.values(context.services).map((service) => (0, append_work_service_1.appendWorkService)(workTree, service, environment, context));
|
|
45
|
+
const selectedTasks = [];
|
|
46
|
+
const selectedServices = [];
|
|
44
47
|
for (const item of tasks) {
|
|
45
48
|
if (matchesTaskWorkScope(item, scope)) {
|
|
46
49
|
requested.push(item.name);
|
|
50
|
+
selectedTasks.push(item.data);
|
|
47
51
|
applyTask(filteredWorkTree, item);
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
for (const item of services) {
|
|
51
55
|
if (matchesServiceWorkScope(item.data, scope)) {
|
|
56
|
+
selectedServices.push(item.data);
|
|
52
57
|
applyService(filteredWorkTree, item);
|
|
53
58
|
}
|
|
54
59
|
}
|
|
60
|
+
if (scope.args) {
|
|
61
|
+
(0, task_arguments_1.applyArguments)({ tasks: selectedTasks, services: scope.args.services ? selectedServices : [] }, scope.args.values, scope.args.cache);
|
|
62
|
+
}
|
|
55
63
|
keepRequirersInScope(filteredWorkTree);
|
|
56
64
|
return filteredWorkTree;
|
|
57
65
|
}
|
package/package.json
CHANGED