hammerkit 1.27.0-next.20 → 1.27.0-next.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.schema.json +18 -0
- package/dist/executer/execution-steps.js +20 -2
- package/dist/executer/reset-work-tree.js +4 -0
- package/dist/program.js +21 -9
- package/dist/schema/build-file-include-schema.js +10 -4
- package/dist/schema/schema-parser.js +36 -4
- package/dist/schema/work-scope-parser.js +27 -10
- package/package.json +1 -1
package/build.schema.json
CHANGED
|
@@ -595,6 +595,24 @@
|
|
|
595
595
|
{
|
|
596
596
|
"type": "string"
|
|
597
597
|
},
|
|
598
|
+
{
|
|
599
|
+
"type": "object",
|
|
600
|
+
"properties": {
|
|
601
|
+
"path": {
|
|
602
|
+
"type": "string",
|
|
603
|
+
"minLength": 1,
|
|
604
|
+
"description": "Build file or directory, relative to the including file"
|
|
605
|
+
},
|
|
606
|
+
"with": {
|
|
607
|
+
"$ref": "#/definitions/hammerkit/properties/envs",
|
|
608
|
+
"description": "Environment values for the current build task\nhttps://hammerkit.dev/docs/build-file/environment-variables"
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
"required": [
|
|
612
|
+
"path"
|
|
613
|
+
],
|
|
614
|
+
"additionalProperties": false
|
|
615
|
+
},
|
|
598
616
|
{
|
|
599
617
|
"type": "object",
|
|
600
618
|
"properties": {
|
|
@@ -9,6 +9,7 @@ const work_task_1 = require("../planner/work-task");
|
|
|
9
9
|
const work_service_1 = require("../planner/work-service");
|
|
10
10
|
const get_container_binds_1 = require("./get-container-binds");
|
|
11
11
|
const assert_item_paths_1 = require("../planner/utils/assert-item-paths");
|
|
12
|
+
const log_1 = require("../log");
|
|
12
13
|
// The files a server run works on: its checkout, or a sandbox. Somebody else wrote them.
|
|
13
14
|
function hostedRoot(environment) {
|
|
14
15
|
return environment.hosted ? environment.cwd : undefined;
|
|
@@ -66,10 +67,27 @@ async function prepareMounts(item, environment) {
|
|
|
66
67
|
continue;
|
|
67
68
|
}
|
|
68
69
|
if ((0, path_1.extname)(mount.localPath)) {
|
|
69
|
-
|
|
70
|
+
try {
|
|
71
|
+
await environment.file.writeFile(mount.localPath, '');
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
// Docker would make a directory there
|
|
75
|
+
throw new Error(`mount ${mount.localPath} does not exist and can't be created (${(0, log_1.getErrorMessage)(e)})`);
|
|
76
|
+
}
|
|
70
77
|
}
|
|
71
78
|
else {
|
|
72
|
-
|
|
79
|
+
try {
|
|
80
|
+
await environment.file.createDirectory(mount.localPath);
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
// on a server a root-owned directory in the run's files could break its cleanup
|
|
84
|
+
if (environment.hosted) {
|
|
85
|
+
throw e;
|
|
86
|
+
}
|
|
87
|
+
// hammerkit may run in a container whose file system is not the one of the Docker
|
|
88
|
+
// host: Docker creates the missing directory of a bind on its own
|
|
89
|
+
item.status.write('warn', `can't create ${mount.localPath} (${(0, log_1.getErrorMessage)(e)}), Docker creates it on its host as root`);
|
|
90
|
+
}
|
|
73
91
|
}
|
|
74
92
|
}
|
|
75
93
|
if ((0, work_task_1.isContainerWorkTask)(item.data)) {
|
|
@@ -5,6 +5,10 @@ const state_1 = require("./state");
|
|
|
5
5
|
function resetWorkTree(workTree, type) {
|
|
6
6
|
return {
|
|
7
7
|
tasks: Object.entries(workTree.tasks).reduce((tasks, [key, task]) => {
|
|
8
|
+
// down only stops services: a task it selects names the services it needs
|
|
9
|
+
if (type === 'down') {
|
|
10
|
+
return tasks;
|
|
11
|
+
}
|
|
8
12
|
task.state = new state_1.State({
|
|
9
13
|
type: 'pending',
|
|
10
14
|
stateKey: null,
|
package/dist/program.js
CHANGED
|
@@ -111,6 +111,18 @@ function getListJson(tasks, services) {
|
|
|
111
111
|
})),
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
+
// `up` and `down` select by name or by label, not both: a name outside the labels would
|
|
115
|
+
// silently select nothing
|
|
116
|
+
function parseWorkNameScope(names, options) {
|
|
117
|
+
const scope = parseWorkLabelScope(options);
|
|
118
|
+
if (names.length === 0) {
|
|
119
|
+
return scope;
|
|
120
|
+
}
|
|
121
|
+
if (scope.filterLabels || scope.excludeLabels) {
|
|
122
|
+
throw new Error('name tasks and services or select them with --filter/--exclude, not both');
|
|
123
|
+
}
|
|
124
|
+
return { ...scope, names };
|
|
125
|
+
}
|
|
114
126
|
function parseWorkLabelScope(options) {
|
|
115
127
|
const scope = {
|
|
116
128
|
environmentName: null,
|
|
@@ -587,9 +599,9 @@ async function getProgram(environment, argv) {
|
|
|
587
599
|
}
|
|
588
600
|
});
|
|
589
601
|
program
|
|
590
|
-
.command('up')
|
|
591
|
-
.description("start services(s), with the arguments after -- appended to the selected service's cmd")
|
|
592
|
-
.usage('[options] [-- args...]')
|
|
602
|
+
.command('up [names...]')
|
|
603
|
+
.description("start services(s), all or the named tasks and services, with the arguments after -- appended to the selected service's cmd")
|
|
604
|
+
.usage('[options] [names...] [-- args...]')
|
|
593
605
|
.addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
|
|
594
606
|
.addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
|
|
595
607
|
.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))
|
|
@@ -610,10 +622,10 @@ async function getProgram(environment, argv) {
|
|
|
610
622
|
.addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
|
|
611
623
|
.addOption(new commander_1.Option('--max-timeout <duration>', 'fail any task after this long, also one with a longer timeout of its own (e.g. 2m)').argParser(units_1.parseDuration))
|
|
612
624
|
.addOption(new commander_1.Option('--network <level>', 'the most any task or service reaches besides its needs: none, open or allow:<host>,<host>').argParser(parseNetworkArgument))
|
|
613
|
-
.action(async (options, command) => {
|
|
625
|
+
.action(async (names, options, command) => {
|
|
614
626
|
try {
|
|
615
627
|
const scope = {
|
|
616
|
-
...
|
|
628
|
+
...parseWorkNameScope(names, options),
|
|
617
629
|
args: { values: passthrough, services: true, cache: keepsCache(command) },
|
|
618
630
|
};
|
|
619
631
|
const cli = await createCli(fileName, environment, scope);
|
|
@@ -646,14 +658,14 @@ async function getProgram(environment, argv) {
|
|
|
646
658
|
}
|
|
647
659
|
});
|
|
648
660
|
program
|
|
649
|
-
.command('down')
|
|
650
|
-
.description('stop services(s)')
|
|
661
|
+
.command('down [names...]')
|
|
662
|
+
.description('stop services(s), all or the named services and the services the named tasks need')
|
|
651
663
|
.addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
|
|
652
664
|
.addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
|
|
653
665
|
.addOption(new commander_1.Option('--env <name>', 'environment'))
|
|
654
|
-
.action(async (options) => {
|
|
666
|
+
.action(async (names, options) => {
|
|
655
667
|
try {
|
|
656
|
-
const scope =
|
|
668
|
+
const scope = { ...parseWorkNameScope(names, options), stop: true };
|
|
657
669
|
const cli = await createCli(fileName, environment, scope);
|
|
658
670
|
const result = await cli.runDown();
|
|
659
671
|
if (!result.success) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isGitSource = exports.buildFileIncludeSchema = exports.buildFileGitSourceSchema = void 0;
|
|
3
|
+
exports.isGitSource = exports.buildFileIncludeSchema = exports.buildFileLocalSourceSchema = exports.buildFileGitSourceSchema = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const envs_schema_1 = require("./envs-schema");
|
|
6
6
|
// A repo URL or ref starting with `-` would be read by git as an option
|
|
@@ -22,10 +22,16 @@ exports.buildFileGitSourceSchema = (0, zod_1.object)({
|
|
|
22
22
|
// overrides for the top-level `envs` the included file declares as its inputs
|
|
23
23
|
with: envs_schema_1.envsSchema.optional(),
|
|
24
24
|
}).strict();
|
|
25
|
-
// A
|
|
26
|
-
|
|
25
|
+
// A local path with values for the inputs of the file it names, like `with` of a git
|
|
26
|
+
// source. A value may pass on an `envs` value of the including file as `${NAME}`.
|
|
27
|
+
exports.buildFileLocalSourceSchema = (0, zod_1.object)({
|
|
28
|
+
path: (0, zod_1.string)().min(1).describe('Build file or directory, relative to the including file'),
|
|
29
|
+
with: envs_schema_1.envsSchema.optional(),
|
|
30
|
+
}).strict();
|
|
31
|
+
// A string is a local path; an object is a local path with inputs, or a git source (ADR-0004).
|
|
32
|
+
exports.buildFileIncludeSchema = (0, zod_1.union)([(0, zod_1.string)(), exports.buildFileLocalSourceSchema, exports.buildFileGitSourceSchema]);
|
|
27
33
|
function isGitSource(include) {
|
|
28
|
-
return typeof include !== 'string';
|
|
34
|
+
return typeof include !== 'string' && 'git' in include;
|
|
29
35
|
}
|
|
30
36
|
exports.isGitSource = isGitSource;
|
|
31
37
|
//# sourceMappingURL=build-file-include-schema.js.map
|
|
@@ -89,13 +89,45 @@ async function locateInclude(ctx, scope, include, base, environment) {
|
|
|
89
89
|
if (!(0, resolve_git_source_1.isWithin)(remote.root, path)) {
|
|
90
90
|
throw new Error(`${include.path} in ${include.git} points outside the repository`);
|
|
91
91
|
}
|
|
92
|
-
return { fileName: await (0, default_build_file_1.getBuildFilename)(path, environment), remote, inputs: include.with };
|
|
92
|
+
return { fileName: await (0, default_build_file_1.getBuildFilename)(path, environment), remote, inputs: passEnvs(include.with, scope) };
|
|
93
93
|
}
|
|
94
|
-
const
|
|
94
|
+
const localPath = typeof include === 'string' ? include : include.path;
|
|
95
|
+
const path = (0, path_1.join)(base, localPath);
|
|
95
96
|
if (scope.remote && !(0, resolve_git_source_1.isWithin)(scope.remote.root, path)) {
|
|
96
|
-
throw new Error(`${
|
|
97
|
+
throw new Error(`${localPath} in ${scope.fileName} points outside ${scope.remote.git}`);
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
+
const inputs = typeof include === 'string' ? undefined : passEnvs(include.with, scope);
|
|
100
|
+
return { fileName: await (0, default_build_file_1.getBuildFilename)(path, environment), remote: scope.remote, inputs };
|
|
101
|
+
}
|
|
102
|
+
// `${NAME}` (or `${NAME:-default}`) in a `with` value reads as it does in the including
|
|
103
|
+
// file: the environment or a `.env` file first, then that file's own `envs` value. So one
|
|
104
|
+
// file holds a value such as NODE_VERSION for all it includes, and CI can still set it. A
|
|
105
|
+
// name the file does not declare is left to the included file, which reads it the same way.
|
|
106
|
+
function passEnvs(inputs, scope) {
|
|
107
|
+
if (!inputs) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const envs = scope.schema.envs ?? {};
|
|
111
|
+
const result = {};
|
|
112
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
113
|
+
result[key] =
|
|
114
|
+
typeof value === 'string'
|
|
115
|
+
? value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (match, name, fallback) => name in envs ? passEnv(name, `${envs[name]}`, fallback) : match)
|
|
116
|
+
: value;
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
// The including file's value as an interpolation the included file resolves in the same
|
|
121
|
+
// order. A value built from other variables is passed on as it is written.
|
|
122
|
+
function passEnv(name, value, fallback) {
|
|
123
|
+
const reference = /^\$([A-Za-z_][A-Za-z0-9_]*)$/.exec(value);
|
|
124
|
+
if (reference) {
|
|
125
|
+
return fallback === undefined ? `\${${reference[1]}}` : `\${${reference[1]}:-${fallback}}`;
|
|
126
|
+
}
|
|
127
|
+
if (value.includes('$') || value.includes('}')) {
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
return `\${${name}:-${value}}`;
|
|
99
131
|
}
|
|
100
132
|
// `includes pull` fetches each source once, however often the build files name it.
|
|
101
133
|
async function resolveRemote(ctx, source, environment) {
|
|
@@ -47,19 +47,27 @@ function getWorkContext(context, scope, environment) {
|
|
|
47
47
|
// its init task's need on it, which narrowing follows
|
|
48
48
|
const tasks = Object.values(context.tasks).map((task) => (0, append_work_task_1.appendWorkTask)(workTree, task.cwd, task, environment, context));
|
|
49
49
|
const services = Object.values(context.services).map((service) => (0, append_work_service_1.appendWorkService)(workTree, service, environment, context));
|
|
50
|
+
const apply = { servicesAlone: !(0, work_scope_1.isContextTaskFilter)(scope) && !!scope.names && !!scope.stop };
|
|
50
51
|
const selectedTasks = [];
|
|
51
52
|
const selectedServices = [];
|
|
52
53
|
for (const item of tasks) {
|
|
53
54
|
if (matchesTaskWorkScope(item, scope)) {
|
|
54
55
|
requested.push(item.name);
|
|
55
56
|
selectedTasks.push(item.data);
|
|
56
|
-
applyTask(filteredWorkTree, item);
|
|
57
|
+
applyTask(filteredWorkTree, item, apply);
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
for (const item of services) {
|
|
60
61
|
if (matchesServiceWorkScope(item.data, scope)) {
|
|
61
62
|
selectedServices.push(item.data);
|
|
62
|
-
applyService(filteredWorkTree, item);
|
|
63
|
+
applyService(filteredWorkTree, item, apply);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!(0, work_scope_1.isContextTaskFilter)(scope) && scope.names) {
|
|
67
|
+
const known = new Set([...tasks.map((task) => task.name), ...services.map((service) => service.data.name)]);
|
|
68
|
+
const unknown = scope.names.filter((name) => !known.has(name));
|
|
69
|
+
if (unknown.length > 0) {
|
|
70
|
+
throw new Error(`no task or service named ${unknown.join(', ')}, see hammerkit ls`);
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
73
|
if (scope.args) {
|
|
@@ -86,31 +94,34 @@ function keepRequirersInScope(workTree) {
|
|
|
86
94
|
}
|
|
87
95
|
}
|
|
88
96
|
}
|
|
89
|
-
function applyTask(workTree, item) {
|
|
97
|
+
function applyTask(workTree, item, options) {
|
|
90
98
|
if (workTree.tasks[item.name]) {
|
|
91
99
|
return;
|
|
92
100
|
}
|
|
93
101
|
workTree.tasks[item.name] = item;
|
|
94
|
-
applyNeedsAndDeps(workTree, item);
|
|
102
|
+
applyNeedsAndDeps(workTree, item, options);
|
|
95
103
|
}
|
|
96
|
-
function applyService(workTree, need) {
|
|
104
|
+
function applyService(workTree, need, options) {
|
|
97
105
|
const item = (0, append_work_service_1.getPlannedService)(need);
|
|
98
106
|
if (workTree.services[item.name]) {
|
|
99
107
|
return;
|
|
100
108
|
}
|
|
101
109
|
workTree.services[item.name] = item;
|
|
102
|
-
|
|
110
|
+
if (options.servicesAlone) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
applyNeedsAndDeps(workTree, item, options);
|
|
103
114
|
// a service in the run brings its init task, which runs once it is healthy
|
|
104
115
|
if (item.data.type === 'container-service' && item.data.init) {
|
|
105
|
-
applyTask(workTree, item.data.init.task);
|
|
116
|
+
applyTask(workTree, item.data.init.task, options);
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
|
-
function applyNeedsAndDeps(workTree, item) {
|
|
119
|
+
function applyNeedsAndDeps(workTree, item, options) {
|
|
109
120
|
for (const dep of item.deps) {
|
|
110
|
-
applyTask(workTree, dep);
|
|
121
|
+
applyTask(workTree, dep, options);
|
|
111
122
|
}
|
|
112
123
|
for (const need of item.needs) {
|
|
113
|
-
applyService(workTree, need.service);
|
|
124
|
+
applyService(workTree, need.service, options);
|
|
114
125
|
}
|
|
115
126
|
}
|
|
116
127
|
function matchesTaskWorkScope(task, scope) {
|
|
@@ -120,6 +131,9 @@ function matchesTaskWorkScope(task, scope) {
|
|
|
120
131
|
if (scope.serviceNames) {
|
|
121
132
|
return false;
|
|
122
133
|
}
|
|
134
|
+
if (scope.names) {
|
|
135
|
+
return scope.names.includes(task.name);
|
|
136
|
+
}
|
|
123
137
|
return (0, label_values_1.appliesToLabels)(task.data.labels, scope);
|
|
124
138
|
}
|
|
125
139
|
function matchesServiceWorkScope(service, scope) {
|
|
@@ -129,6 +143,9 @@ function matchesServiceWorkScope(service, scope) {
|
|
|
129
143
|
if (scope.serviceNames) {
|
|
130
144
|
return scope.serviceNames.includes(service.name);
|
|
131
145
|
}
|
|
146
|
+
if (scope.names) {
|
|
147
|
+
return scope.names.includes(service.name);
|
|
148
|
+
}
|
|
132
149
|
return (0, label_values_1.appliesToLabels)(service.labels, scope);
|
|
133
150
|
}
|
|
134
151
|
//# sourceMappingURL=work-scope-parser.js.map
|
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.21",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"build",
|
|
7
7
|
"build-tool",
|