hammerkit 1.27.0-next.20 → 1.27.0-next.22
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 +38 -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/remote/host-config.js +5 -0
- package/dist/remote/remote-work-commands.js +32 -0
- package/dist/schema/build-file-base-task-schema.js +3 -0
- package/dist/schema/build-file-container-service-schema.js +2 -0
- package/dist/schema/build-file-include-schema.js +18 -4
- package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
- package/dist/schema/reference-parser.js +99 -10
- package/dist/schema/schema-parser.js +92 -41
- package/dist/schema/work-scope-parser.js +27 -10
- package/dist/secrets/command-secret-provider.js +3 -1
- package/dist/secrets/secret-catalog.js +43 -19
- package/dist/server/definitions-api.js +62 -1
- package/dist/server/plan.js +45 -41
- package/package.json +1 -1
package/build.schema.json
CHANGED
|
@@ -296,6 +296,9 @@
|
|
|
296
296
|
"type": "string",
|
|
297
297
|
"minLength": 1
|
|
298
298
|
},
|
|
299
|
+
"publish": {
|
|
300
|
+
"type": "boolean"
|
|
301
|
+
},
|
|
299
302
|
"image": {
|
|
300
303
|
"type": "string"
|
|
301
304
|
},
|
|
@@ -379,6 +382,9 @@
|
|
|
379
382
|
"runner": {
|
|
380
383
|
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/runner"
|
|
381
384
|
},
|
|
385
|
+
"publish": {
|
|
386
|
+
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/publish"
|
|
387
|
+
},
|
|
382
388
|
"platform": {
|
|
383
389
|
"type": "object",
|
|
384
390
|
"properties": {
|
|
@@ -520,6 +526,9 @@
|
|
|
520
526
|
},
|
|
521
527
|
"args": {
|
|
522
528
|
"type": "boolean"
|
|
529
|
+
},
|
|
530
|
+
"publish": {
|
|
531
|
+
"type": "boolean"
|
|
523
532
|
}
|
|
524
533
|
},
|
|
525
534
|
"required": [
|
|
@@ -576,6 +585,9 @@
|
|
|
576
585
|
"number"
|
|
577
586
|
]
|
|
578
587
|
}
|
|
588
|
+
},
|
|
589
|
+
"publish": {
|
|
590
|
+
"type": "boolean"
|
|
579
591
|
}
|
|
580
592
|
},
|
|
581
593
|
"required": [
|
|
@@ -595,6 +607,28 @@
|
|
|
595
607
|
{
|
|
596
608
|
"type": "string"
|
|
597
609
|
},
|
|
610
|
+
{
|
|
611
|
+
"type": "object",
|
|
612
|
+
"properties": {
|
|
613
|
+
"path": {
|
|
614
|
+
"type": "string",
|
|
615
|
+
"minLength": 1,
|
|
616
|
+
"description": "Build file or directory, relative to the including file"
|
|
617
|
+
},
|
|
618
|
+
"with": {
|
|
619
|
+
"$ref": "#/definitions/hammerkit/properties/envs",
|
|
620
|
+
"description": "Environment values for the current build task\nhttps://hammerkit.dev/docs/build-file/environment-variables"
|
|
621
|
+
},
|
|
622
|
+
"publish": {
|
|
623
|
+
"type": "boolean",
|
|
624
|
+
"description": "false: a server leaves out what this file declares"
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
"required": [
|
|
628
|
+
"path"
|
|
629
|
+
],
|
|
630
|
+
"additionalProperties": false
|
|
631
|
+
},
|
|
598
632
|
{
|
|
599
633
|
"type": "object",
|
|
600
634
|
"properties": {
|
|
@@ -615,6 +649,10 @@
|
|
|
615
649
|
"with": {
|
|
616
650
|
"$ref": "#/definitions/hammerkit/properties/envs",
|
|
617
651
|
"description": "Environment values for the current build task\nhttps://hammerkit.dev/docs/build-file/environment-variables"
|
|
652
|
+
},
|
|
653
|
+
"publish": {
|
|
654
|
+
"type": "boolean",
|
|
655
|
+
"description": "false: a server leaves out what this file declares"
|
|
618
656
|
}
|
|
619
657
|
},
|
|
620
658
|
"required": [
|
|
@@ -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) {
|
|
@@ -5,6 +5,7 @@ const path_1 = require("path");
|
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const yaml_1 = require("yaml");
|
|
7
7
|
const get_cache_directory_1 = require("../optimizer/get-cache-directory");
|
|
8
|
+
const build_file_secret_provider_schema_1 = require("../schema/build-file-secret-provider-schema");
|
|
8
9
|
// Names with a meaning of their own in `run.on` / `--on`
|
|
9
10
|
exports.RESERVED_SERVER_NAMES = ['local', 'auto'];
|
|
10
11
|
const serverNamePattern = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
@@ -43,6 +44,10 @@ exports.hostConfigSchema = zod_1.z
|
|
|
43
44
|
servers: zod_1.z.record(exports.hostServerSchema).default({}),
|
|
44
45
|
tokenCommands: zod_1.z.record(tokenCommandSchema).optional(),
|
|
45
46
|
run: zod_1.z.object({ on: zod_1.z.string().optional() }).passthrough().default({}),
|
|
47
|
+
// how this machine reads secrets, beside what the build files declare (see
|
|
48
|
+
// collectSecretCatalog); a server reads only its own config
|
|
49
|
+
secretProviders: build_file_secret_provider_schema_1.buildFileSecretProvidersSchema.optional(),
|
|
50
|
+
secretAccounts: build_file_secret_provider_schema_1.buildFileSecretAccountsSchema.optional(),
|
|
46
51
|
})
|
|
47
52
|
.passthrough();
|
|
48
53
|
// Names like "constructor" must not find inherited members.
|
|
@@ -17,6 +17,22 @@ const units_1 = require("../utils/units");
|
|
|
17
17
|
const service_tools_1 = require("../harness/service-tools");
|
|
18
18
|
const path_1 = require("path");
|
|
19
19
|
const check_keys_1 = require("../optimizer/check-keys");
|
|
20
|
+
function describeOutcome(planned) {
|
|
21
|
+
switch (planned.outcome) {
|
|
22
|
+
case 'published':
|
|
23
|
+
return 'publishing it would publish it';
|
|
24
|
+
case 'already published':
|
|
25
|
+
return planned.yanked ? 'it is published already, and yanked' : 'it is published already';
|
|
26
|
+
case 'proposed':
|
|
27
|
+
return planned.missing?.length
|
|
28
|
+
? `publishing it would propose it, it names accounts you do not use: ${planned.missing
|
|
29
|
+
.map(server_client_1.printable)
|
|
30
|
+
.join(', ')}`
|
|
31
|
+
: 'publishing it would propose it, somebody who may publish approves it';
|
|
32
|
+
default:
|
|
33
|
+
return 'the server accepts it, publishing it needs builds: propose or publish';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
20
36
|
const RUN_ID = /^run_[A-Za-z0-9_-]{16}$/;
|
|
21
37
|
// how a check passed, one line
|
|
22
38
|
function describePassed(passed) {
|
|
@@ -136,8 +152,24 @@ function registerWorkCommands(remote, context) {
|
|
|
136
152
|
.command('publish [server]')
|
|
137
153
|
.description('publish the build file and the files it includes to a server, runs refer to it by its hash')
|
|
138
154
|
.addOption(new commander_1.Option('--annotation <key=value>', 'say more about where it comes from, or leave one out with key=; path, repo, commit, machine and user are filled in').argParser(collectOption))
|
|
155
|
+
.addOption(new commander_1.Option('--dry-run', "check it against the server's policy and say what publishing would do, without publishing; builds: run is enough"))
|
|
139
156
|
.action(guarded('Remote publish was not successful', async (name, options) => {
|
|
140
157
|
const bundle = await (0, collect_bundle_1.collectBundle)(buildFile, environment);
|
|
158
|
+
if (options.dryRun) {
|
|
159
|
+
const { name: serverName, server, token } = await connect(name);
|
|
160
|
+
const answer = await (0, server_client_1.callServer)(server, token, 'POST', '/v1/definitions/plan', bundle).catch((e) => {
|
|
161
|
+
throw e instanceof server_client_1.ServerError && e.status === 404
|
|
162
|
+
? new Error(`${serverName} can't check a build file without publishing it, it needs hammerkit 1.27`)
|
|
163
|
+
: e;
|
|
164
|
+
});
|
|
165
|
+
const planned = answer.body;
|
|
166
|
+
const files = Object.keys(bundle.files).length;
|
|
167
|
+
environment.stdout.write(`checked with the policy of ${serverName}: ${(0, server_client_1.printable)(planned.hash)} (${files} ${files === 1 ? 'file' : 'files'}), nothing was published\n`);
|
|
168
|
+
const names = [...(planned.tasks ?? []), ...(planned.services ?? [])];
|
|
169
|
+
environment.stdout.write(`tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
|
|
170
|
+
environment.stdout.write(`${describeOutcome(planned)}\n`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
141
173
|
const annotations = (0, annotations_1.applyAnnotations)(await (0, annotations_1.collectAnnotations)(buildFile, Object.keys(bundle.files)), options.annotation ?? []);
|
|
142
174
|
const { name: serverName, server, token } = await connect(name);
|
|
143
175
|
const answer = await (0, server_client_1.callServer)(server, token, 'POST', '/v1/definitions', {
|
|
@@ -49,5 +49,8 @@ exports.buildFileBaseTaskSchema = (0, zod_1.object)({
|
|
|
49
49
|
// Name of the server (see `hammerkit remote`) the task has to run on; defaults
|
|
50
50
|
// to the build file's `runner`. Never a local fallback when it is unknown.
|
|
51
51
|
runner: runner_schema_1.runnerSchema.optional(),
|
|
52
|
+
// false: a server leaves it out of a published version, for a task only this machine
|
|
53
|
+
// runs (a dev watcher, a task that mounts the docker socket)
|
|
54
|
+
publish: (0, zod_1.boolean)().optional(),
|
|
52
55
|
});
|
|
53
56
|
//# sourceMappingURL=build-file-base-task-schema.js.map
|
|
@@ -47,6 +47,8 @@ exports.buildFileContainerServiceSchema = (0, zod_1.object)({
|
|
|
47
47
|
network: network_schema_1.networkSchema.optional(),
|
|
48
48
|
// whether its `cmd` takes the arguments after `--` of `up`: false refuses them
|
|
49
49
|
args: (0, zod_1.boolean)().optional(),
|
|
50
|
+
// false: a server leaves it out of a published version
|
|
51
|
+
publish: (0, zod_1.boolean)().optional(),
|
|
50
52
|
})
|
|
51
53
|
.strict()
|
|
52
54
|
.describe('container service');
|
|
@@ -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.isPublished = 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
|
|
@@ -21,11 +21,25 @@ exports.buildFileGitSourceSchema = (0, zod_1.object)({
|
|
|
21
21
|
path: (0, zod_1.string)().optional().describe('Build file or directory within the repository'),
|
|
22
22
|
// overrides for the top-level `envs` the included file declares as its inputs
|
|
23
23
|
with: envs_schema_1.envsSchema.optional(),
|
|
24
|
+
publish: (0, zod_1.boolean)().optional().describe('false: a server leaves out what this file declares'),
|
|
24
25
|
}).strict();
|
|
25
|
-
// A
|
|
26
|
-
|
|
26
|
+
// A local path, with values for the inputs of the file it names like `with` of a git
|
|
27
|
+
// source (a value may pass on an `envs` value of the including file as `${NAME}`), and
|
|
28
|
+
// whether a server publishes what it declares.
|
|
29
|
+
exports.buildFileLocalSourceSchema = (0, zod_1.object)({
|
|
30
|
+
path: (0, zod_1.string)().min(1).describe('Build file or directory, relative to the including file'),
|
|
31
|
+
with: envs_schema_1.envsSchema.optional(),
|
|
32
|
+
publish: (0, zod_1.boolean)().optional().describe('false: a server leaves out what this file declares'),
|
|
33
|
+
}).strict();
|
|
34
|
+
// A string is a local path; an object is a local path with options, or a git source (ADR-0004).
|
|
35
|
+
exports.buildFileIncludeSchema = (0, zod_1.union)([(0, zod_1.string)(), exports.buildFileLocalSourceSchema, exports.buildFileGitSourceSchema]);
|
|
27
36
|
function isGitSource(include) {
|
|
28
|
-
return typeof include !== 'string';
|
|
37
|
+
return typeof include !== 'string' && 'git' in include;
|
|
29
38
|
}
|
|
30
39
|
exports.isGitSource = isGitSource;
|
|
40
|
+
// `publish: false`: what the file declares is left out of a version a server publishes
|
|
41
|
+
function isPublished(include) {
|
|
42
|
+
return typeof include === 'string' || include.publish !== false;
|
|
43
|
+
}
|
|
44
|
+
exports.isPublished = isPublished;
|
|
31
45
|
//# sourceMappingURL=build-file-include-schema.js.map
|
|
@@ -13,5 +13,7 @@ exports.buildFileKubernetesServiceSchema = (0, zod_1.object)({
|
|
|
13
13
|
selector: build_file_kubernetes_service_selector_schema_1.buildFileKubernetesServiceSelectorSchema,
|
|
14
14
|
labels: labels_schema_1.labelsSchema.optional(),
|
|
15
15
|
ports: (0, zod_1.array)((0, zod_1.union)([(0, zod_1.string)(), (0, zod_1.number)()])),
|
|
16
|
+
// false: a server leaves it out of a published version
|
|
17
|
+
publish: (0, zod_1.boolean)().optional(),
|
|
16
18
|
}).strict();
|
|
17
19
|
//# sourceMappingURL=build-file-kubernetes-service-schema.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.findService = exports.findTask = exports.parseReferences = void 0;
|
|
3
|
+
exports.findService = exports.findTask = exports.parseReferences = exports.getPublishedFiles = void 0;
|
|
4
4
|
const build_file_task_schema_1 = require("./build-file-task-schema");
|
|
5
5
|
const build_file_service_schema_1 = require("./build-file-service-schema");
|
|
6
6
|
const split_name_1 = require("../planner/utils/split-name");
|
|
@@ -10,11 +10,59 @@ const merge_environment_variables_1 = require("../environment/merge-environment-
|
|
|
10
10
|
const resolve_cache_1 = require("../cache/resolve-cache");
|
|
11
11
|
const project_root_1 = require("../planner/project-root");
|
|
12
12
|
const secret_catalog_1 = require("../secrets/secret-catalog");
|
|
13
|
+
const host_config_1 = require("../remote/host-config");
|
|
14
|
+
// The providers and accounts of the host config, see HostSecretDeclarations
|
|
15
|
+
async function readHostSecrets(environment) {
|
|
16
|
+
const config = await (0, host_config_1.readHostConfig)(environment);
|
|
17
|
+
return {
|
|
18
|
+
fileName: (0, host_config_1.getHostConfigPath)(environment),
|
|
19
|
+
secretProviders: config.secretProviders,
|
|
20
|
+
secretAccounts: config.secretAccounts,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// What a task or service names that a server left out (`publish: false`)
|
|
24
|
+
class UnpublishedError extends Error {
|
|
25
|
+
constructor(name, reason) {
|
|
26
|
+
super(`${name} is not published (${reason})`);
|
|
27
|
+
this.name = name;
|
|
28
|
+
this.reason = reason;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// The task or service `user` names another one by, which a server left out: the error says
|
|
32
|
+
// which published one still needs it, as an error elsewhere is left as it is
|
|
33
|
+
function findPublished(user, verb, find) {
|
|
34
|
+
try {
|
|
35
|
+
return find();
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
if (e instanceof UnpublishedError) {
|
|
39
|
+
throw new Error(`${user} ${verb} ${e.name}, which is not published (${e.reason})`);
|
|
40
|
+
}
|
|
41
|
+
throw e;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// The build files a server publishes: the entry file and what it includes or references,
|
|
45
|
+
// except through an include with `publish: false`. The key of each is in ctx.files.
|
|
46
|
+
function getPublishedFiles(mainScope) {
|
|
47
|
+
const published = new Set([mainScope]);
|
|
48
|
+
const queue = [mainScope];
|
|
49
|
+
for (let scope = queue.shift(); scope; scope = queue.shift()) {
|
|
50
|
+
for (const reference of Object.values(scope.references)) {
|
|
51
|
+
if (reference.publish && !published.has(reference.scope)) {
|
|
52
|
+
published.add(reference.scope);
|
|
53
|
+
queue.push(reference.scope);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return published;
|
|
58
|
+
}
|
|
59
|
+
exports.getPublishedFiles = getPublishedFiles;
|
|
13
60
|
function combinePath(namePrefix, name) {
|
|
14
61
|
return namePrefix.length > 0 ? `${namePrefix}:${name}` : name;
|
|
15
62
|
}
|
|
16
63
|
async function parseReferences(ctx, mainScope, environment) {
|
|
17
64
|
const declaredCaches = {};
|
|
65
|
+
const unpublished = {};
|
|
18
66
|
const reference = {
|
|
19
67
|
files: {},
|
|
20
68
|
tasks: {},
|
|
@@ -24,9 +72,28 @@ async function parseReferences(ctx, mainScope, environment) {
|
|
|
24
72
|
caches: {},
|
|
25
73
|
secrets: (0, secret_catalog_1.emptySecretCatalog)(),
|
|
26
74
|
projectRoot: await (0, project_root_1.findProjectRoot)(mainScope.cwd, environment),
|
|
75
|
+
unpublished,
|
|
76
|
+
};
|
|
77
|
+
// a server leaves out what is not published: it doesn't run, and isn't checked against
|
|
78
|
+
// its policy. Locally everything is there.
|
|
79
|
+
const publishedFiles = environment.hosted ? getPublishedFiles(mainScope) : null;
|
|
80
|
+
const isLeftOut = (file, name, schema) => {
|
|
81
|
+
if (!publishedFiles) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (!publishedFiles.has(file)) {
|
|
85
|
+
unpublished[name] = 'its build file is included with publish: false';
|
|
86
|
+
}
|
|
87
|
+
else if (schema.publish === false) {
|
|
88
|
+
unpublished[name] = 'publish: false';
|
|
89
|
+
}
|
|
90
|
+
return name in unpublished;
|
|
27
91
|
};
|
|
28
92
|
for (const file of Object.values(ctx.files)) {
|
|
29
|
-
|
|
93
|
+
// what a server leaves out of a file it doesn't publish: its tasks and services (see
|
|
94
|
+
// isLeftOut), and where it would run or store things, which only a local run reads
|
|
95
|
+
const published = !publishedFiles || publishedFiles.has(file);
|
|
96
|
+
if (published && environment.hosted && (file.schema.secretProviders || file.schema.secretAccounts)) {
|
|
30
97
|
throw new Error(`${file.fileName} declares secret providers or accounts, which a server defines in its own config`);
|
|
31
98
|
}
|
|
32
99
|
if (!reference.envFiles[file.cwd]) {
|
|
@@ -38,6 +105,9 @@ async function parseReferences(ctx, mainScope, environment) {
|
|
|
38
105
|
for (const [taskName, task] of Object.entries(file.schema.tasks)) {
|
|
39
106
|
const taskEnvs = (0, merge_environment_variables_1.mergeEnvironmentVariables)(task.envs, buildFileEnvs);
|
|
40
107
|
const relativeName = combinePath(file.namePrefix, taskName);
|
|
108
|
+
if (isLeftOut(file, relativeName, task)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
41
111
|
reference.tasks[relativeName] = {
|
|
42
112
|
type: 'task',
|
|
43
113
|
schema: task,
|
|
@@ -51,7 +121,7 @@ async function parseReferences(ctx, mainScope, environment) {
|
|
|
51
121
|
};
|
|
52
122
|
}
|
|
53
123
|
}
|
|
54
|
-
if (file.schema.environments) {
|
|
124
|
+
if (file.schema.environments && published) {
|
|
55
125
|
for (const [envName, env] of Object.entries(file.schema.environments)) {
|
|
56
126
|
reference.environments[envName] = {
|
|
57
127
|
type: 'environment',
|
|
@@ -61,7 +131,7 @@ async function parseReferences(ctx, mainScope, environment) {
|
|
|
61
131
|
};
|
|
62
132
|
}
|
|
63
133
|
}
|
|
64
|
-
if (file.schema.caches) {
|
|
134
|
+
if (file.schema.caches && published) {
|
|
65
135
|
for (const [cacheName, cache] of Object.entries(file.schema.caches)) {
|
|
66
136
|
declaredCaches[cacheName] = cache;
|
|
67
137
|
}
|
|
@@ -69,6 +139,9 @@ async function parseReferences(ctx, mainScope, environment) {
|
|
|
69
139
|
if (file.schema.services) {
|
|
70
140
|
for (const [serviceName, service] of Object.entries(file.schema.services)) {
|
|
71
141
|
const relativeName = combinePath(file.namePrefix, serviceName);
|
|
142
|
+
if (isLeftOut(file, relativeName, service)) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
72
145
|
const serviceEnvs = (0, merge_environment_variables_1.mergeEnvironmentVariables)((0, build_file_service_schema_1.isBuildFileKubernetesServiceSchema)(service) ? {} : service.envs, buildFileEnvs);
|
|
73
146
|
reference.services[relativeName] = {
|
|
74
147
|
type: 'service',
|
|
@@ -98,7 +171,7 @@ async function parseReferences(ctx, mainScope, environment) {
|
|
|
98
171
|
reference.caches = (0, resolve_cache_1.withBuiltinCaches)(declaredCaches);
|
|
99
172
|
reference.secrets = environment.hosted
|
|
100
173
|
? { ...environment.hosted.catalog, explicitAccounts: true }
|
|
101
|
-
: (0, secret_catalog_1.collectSecretCatalog)(Object.values(ctx.files), environment, reference);
|
|
174
|
+
: (0, secret_catalog_1.collectSecretCatalog)(Object.values(ctx.files), environment, reference, await readHostSecrets(environment));
|
|
102
175
|
return reference;
|
|
103
176
|
}
|
|
104
177
|
exports.parseReferences = parseReferences;
|
|
@@ -122,7 +195,8 @@ function applySchemaExtension(reference, task) {
|
|
|
122
195
|
task.extension = 'applied';
|
|
123
196
|
return;
|
|
124
197
|
}
|
|
125
|
-
const
|
|
198
|
+
const name = task.schema.extend;
|
|
199
|
+
const extend = findPublished(task.relativeName, 'extends', () => findTask(reference, task.scope, name, null)).task;
|
|
126
200
|
applySchemaExtension(reference, extend);
|
|
127
201
|
task.schema.shell = task.schema.shell ?? extend.schema.shell;
|
|
128
202
|
task.schema.cache = task.schema.cache ?? extend.schema.cache;
|
|
@@ -182,7 +256,7 @@ function extendObject(base, extend) {
|
|
|
182
256
|
}
|
|
183
257
|
function resolveDeps(reference, taskOrService, scope, schema) {
|
|
184
258
|
for (const depName of schema.deps || []) {
|
|
185
|
-
const depTask = findTask(reference, scope, depName, null);
|
|
259
|
+
const depTask = findPublished(taskOrService.relativeName, 'depends on', () => findTask(reference, scope, depName, null));
|
|
186
260
|
taskOrService.deps.push({
|
|
187
261
|
task: depTask.task,
|
|
188
262
|
type: depTask.type,
|
|
@@ -195,7 +269,8 @@ function resolveInit(reference, service) {
|
|
|
195
269
|
if ((0, build_file_service_schema_1.isBuildFileKubernetesServiceSchema)(service.schema) || !service.schema.init) {
|
|
196
270
|
return;
|
|
197
271
|
}
|
|
198
|
-
const
|
|
272
|
+
const init = service.schema.init;
|
|
273
|
+
const initTask = findPublished(service.relativeName, 'runs as init', () => findTask(reference, service.scope, init, null));
|
|
199
274
|
service.init = {
|
|
200
275
|
task: initTask.task,
|
|
201
276
|
type: initTask.type,
|
|
@@ -213,13 +288,13 @@ function resolveNeeds(reference, taskOrService, scope, schema) {
|
|
|
213
288
|
// the host name it is reached by: a service of an include, `services:postgres`, as
|
|
214
289
|
// `services-postgres`, since Docker and DNS take no colon
|
|
215
290
|
relativeName: needName.replace(/:/g, '-'),
|
|
216
|
-
service: findService(reference, scope, needName),
|
|
291
|
+
service: findPublished(taskOrService.relativeName, 'needs', () => findService(reference, scope, needName)),
|
|
217
292
|
});
|
|
218
293
|
}
|
|
219
294
|
else {
|
|
220
295
|
taskOrService.needs.push({
|
|
221
296
|
relativeName: needName.name,
|
|
222
|
-
service: findService(reference, scope, needName.service),
|
|
297
|
+
service: findPublished(taskOrService.relativeName, 'needs', () => findService(reference, scope, needName.service)),
|
|
223
298
|
});
|
|
224
299
|
}
|
|
225
300
|
}
|
|
@@ -230,6 +305,7 @@ function resolveNeeds(reference, taskOrService, scope, schema) {
|
|
|
230
305
|
}
|
|
231
306
|
}
|
|
232
307
|
function findTask(ctx, scope, name, type) {
|
|
308
|
+
refuseNotFetched(scope, name);
|
|
233
309
|
const ref = (0, split_name_1.splitName)(name);
|
|
234
310
|
if (ref.prefix) {
|
|
235
311
|
if (scope.references[ref.prefix]) {
|
|
@@ -245,15 +321,25 @@ function findTask(ctx, scope, name, type) {
|
|
|
245
321
|
throw new Error('unable to find ' + name); // TODO improve
|
|
246
322
|
}
|
|
247
323
|
exports.findTask = findTask;
|
|
324
|
+
// A name in a git source a server did not fetch, since it is left out
|
|
325
|
+
function refuseNotFetched(scope, name) {
|
|
326
|
+
if (scope.notFetched) {
|
|
327
|
+
throw new UnpublishedError(combinePath(scope.namePrefix, name), 'its build file is included with publish: false');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
248
330
|
function resolveTask(ctx, scope, name, type) {
|
|
249
331
|
const fullName = combinePath(scope.namePrefix, name);
|
|
250
332
|
const buildFileTask = ctx.tasks[fullName];
|
|
333
|
+
if (!buildFileTask && ctx.unpublished?.[fullName]) {
|
|
334
|
+
throw new UnpublishedError(fullName, ctx.unpublished[fullName]);
|
|
335
|
+
}
|
|
251
336
|
if (!buildFileTask) {
|
|
252
337
|
throw new Error('unable to find ' + fullName);
|
|
253
338
|
}
|
|
254
339
|
return { task: buildFileTask, type };
|
|
255
340
|
}
|
|
256
341
|
function findService(ctx, scope, name) {
|
|
342
|
+
refuseNotFetched(scope, name);
|
|
257
343
|
const ref = (0, split_name_1.splitName)(name);
|
|
258
344
|
if (ref.prefix) {
|
|
259
345
|
if (scope.references[ref.prefix]) {
|
|
@@ -272,6 +358,9 @@ exports.findService = findService;
|
|
|
272
358
|
function resolveService(ctx, scope, name) {
|
|
273
359
|
const fullName = combinePath(scope.namePrefix, name);
|
|
274
360
|
const service = ctx.services[fullName];
|
|
361
|
+
if (!service && ctx.unpublished?.[fullName]) {
|
|
362
|
+
throw new UnpublishedError(fullName, ctx.unpublished[fullName]);
|
|
363
|
+
}
|
|
275
364
|
if (!service) {
|
|
276
365
|
throw new Error('unable to find ' + fullName);
|
|
277
366
|
}
|
|
@@ -13,60 +13,79 @@ async function createParseContext(fileName, environment, options) {
|
|
|
13
13
|
files: {},
|
|
14
14
|
refreshed: options?.refresh ? new Map() : undefined,
|
|
15
15
|
directory: options?.directory,
|
|
16
|
+
checkGitSource: options?.checkGitSource,
|
|
16
17
|
};
|
|
17
18
|
const scope = await appendBuildFile(workDirectory(ctx, (0, path_1.dirname)(fileName)), ctx, environment, fileName, []);
|
|
18
19
|
return { ctx, scope };
|
|
19
20
|
}
|
|
20
21
|
exports.createParseContext = createParseContext;
|
|
21
|
-
async function appendBuildFile(cwd, ctx, environment, fileName, namePrefix, remote, inputs
|
|
22
|
+
async function appendBuildFile(cwd, ctx, environment, fileName, namePrefix, remote, inputs,
|
|
23
|
+
// on a server: the file is included with `publish: false`, or below such an include
|
|
24
|
+
leftOut = false) {
|
|
22
25
|
const relativeName = namePrefix.join(':');
|
|
23
|
-
// the same file included with other inputs is another set of tasks
|
|
24
|
-
|
|
26
|
+
// the same file included with other inputs is another set of tasks, and on a server a
|
|
27
|
+
// file left out is another one than the same file published (its git sources differ)
|
|
28
|
+
const key = `${cwd}:${fileName}${inputs ? `:${JSON.stringify(inputs)}` : ''}${leftOut ? ':left-out' : ''}`;
|
|
25
29
|
if (ctx.files[key]) {
|
|
26
30
|
return ctx.files[key];
|
|
27
31
|
}
|
|
28
32
|
const input = await (0, read_build_file_1.read)(fileName, environment);
|
|
29
33
|
const result = await build_file_schema_1.buildFileSchema.safeParseAsync(input);
|
|
30
|
-
if (result.success) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (scope.references[referenceName]) {
|
|
48
|
-
throw new Error(referenceName + ' already exists');
|
|
49
|
-
}
|
|
50
|
-
scope.references[referenceName] = { type: 'reference', scope: referenceScope };
|
|
34
|
+
if (!result.success) {
|
|
35
|
+
throw new parse_error_1.ParseError(result.error, fileName);
|
|
36
|
+
}
|
|
37
|
+
const scope = {
|
|
38
|
+
fileName,
|
|
39
|
+
cwd,
|
|
40
|
+
schema: inputs ? applyInputs(result.data, inputs, namePrefix, fileName) : result.data,
|
|
41
|
+
namePrefix: relativeName,
|
|
42
|
+
remote,
|
|
43
|
+
inputs,
|
|
44
|
+
references: {},
|
|
45
|
+
};
|
|
46
|
+
ctx.files[key] = scope;
|
|
47
|
+
for (const block of ['references', 'includes']) {
|
|
48
|
+
for (const [name, include] of Object.entries(scope.schema[block] ?? {})) {
|
|
49
|
+
if (scope.references[name]) {
|
|
50
|
+
throw new Error(name + ' already exists');
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
scope.references[includeName] = { type: 'include', scope: includeScope };
|
|
52
|
+
const publish = (0, build_file_include_schema_1.isPublished)(include);
|
|
53
|
+
const type = block === 'references' ? 'reference' : 'include';
|
|
54
|
+
// a server does not fetch what it leaves out: it doesn't run, and isn't checked
|
|
55
|
+
const leaveOut = !!environment.hosted && (leftOut || !publish);
|
|
56
|
+
if ((0, build_file_include_schema_1.isGitSource)(include) && leaveOut) {
|
|
57
|
+
scope.references[name] = { type, publish, scope: notFetched(cwd, [...namePrefix, name], include) };
|
|
58
|
+
continue;
|
|
61
59
|
}
|
|
60
|
+
if ((0, build_file_include_schema_1.isGitSource)(include)) {
|
|
61
|
+
ctx.checkGitSource?.(include, scope, block, name);
|
|
62
|
+
}
|
|
63
|
+
const target = await locateInclude(ctx, scope, include, block === 'references'
|
|
64
|
+
? // a remote file's paths resolve within its repository
|
|
65
|
+
remote
|
|
66
|
+
? (0, path_1.dirname)(scope.fileName)
|
|
67
|
+
: readDirectory(ctx, scope.cwd)
|
|
68
|
+
: (0, path_1.dirname)(scope.fileName), environment);
|
|
69
|
+
const targetScope = await appendBuildFile(
|
|
70
|
+
// a reference works in its own directory, an include in the one of the including file
|
|
71
|
+
block === 'references' ? workDirectory(ctx, (0, path_1.dirname)(target.fileName)) : cwd, ctx, environment, target.fileName, [...namePrefix, name], target.remote, target.inputs, leaveOut);
|
|
72
|
+
scope.references[name] = { type, scope: targetScope, publish };
|
|
62
73
|
}
|
|
63
|
-
return scope;
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
throw new parse_error_1.ParseError(result.error, fileName);
|
|
67
74
|
}
|
|
75
|
+
return scope;
|
|
68
76
|
}
|
|
69
77
|
exports.appendBuildFile = appendBuildFile;
|
|
78
|
+
// The scope of a git source a server does not fetch, see ParseScope.notFetched
|
|
79
|
+
function notFetched(cwd, namePrefix, include) {
|
|
80
|
+
return {
|
|
81
|
+
fileName: `${include.git}${include.path ? ` ${include.path}` : ''}`,
|
|
82
|
+
cwd,
|
|
83
|
+
schema: {},
|
|
84
|
+
namePrefix: namePrefix.join(':'),
|
|
85
|
+
references: {},
|
|
86
|
+
notFetched: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
70
89
|
// The directory a build file works in, for the directory it was read from: the same
|
|
71
90
|
// one, unless the files were read from elsewhere (see BuildFileDirectory).
|
|
72
91
|
function workDirectory(ctx, directory) {
|
|
@@ -89,13 +108,45 @@ async function locateInclude(ctx, scope, include, base, environment) {
|
|
|
89
108
|
if (!(0, resolve_git_source_1.isWithin)(remote.root, path)) {
|
|
90
109
|
throw new Error(`${include.path} in ${include.git} points outside the repository`);
|
|
91
110
|
}
|
|
92
|
-
return { fileName: await (0, default_build_file_1.getBuildFilename)(path, environment), remote, inputs: include.with };
|
|
111
|
+
return { fileName: await (0, default_build_file_1.getBuildFilename)(path, environment), remote, inputs: passEnvs(include.with, scope) };
|
|
93
112
|
}
|
|
94
|
-
const
|
|
113
|
+
const localPath = typeof include === 'string' ? include : include.path;
|
|
114
|
+
const path = (0, path_1.join)(base, localPath);
|
|
95
115
|
if (scope.remote && !(0, resolve_git_source_1.isWithin)(scope.remote.root, path)) {
|
|
96
|
-
throw new Error(`${
|
|
116
|
+
throw new Error(`${localPath} in ${scope.fileName} points outside ${scope.remote.git}`);
|
|
117
|
+
}
|
|
118
|
+
const inputs = typeof include === 'string' ? undefined : passEnvs(include.with, scope);
|
|
119
|
+
return { fileName: await (0, default_build_file_1.getBuildFilename)(path, environment), remote: scope.remote, inputs };
|
|
120
|
+
}
|
|
121
|
+
// `${NAME}` (or `${NAME:-default}`) in a `with` value reads as it does in the including
|
|
122
|
+
// file: the environment or a `.env` file first, then that file's own `envs` value. So one
|
|
123
|
+
// file holds a value such as NODE_VERSION for all it includes, and CI can still set it. A
|
|
124
|
+
// name the file does not declare is left to the included file, which reads it the same way.
|
|
125
|
+
function passEnvs(inputs, scope) {
|
|
126
|
+
if (!inputs) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const envs = scope.schema.envs ?? {};
|
|
130
|
+
const result = {};
|
|
131
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
132
|
+
result[key] =
|
|
133
|
+
typeof value === 'string'
|
|
134
|
+
? value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (match, name, fallback) => name in envs ? passEnv(name, `${envs[name]}`, fallback) : match)
|
|
135
|
+
: value;
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
// The including file's value as an interpolation the included file resolves in the same
|
|
140
|
+
// order. A value built from other variables is passed on as it is written.
|
|
141
|
+
function passEnv(name, value, fallback) {
|
|
142
|
+
const reference = /^\$([A-Za-z_][A-Za-z0-9_]*)$/.exec(value);
|
|
143
|
+
if (reference) {
|
|
144
|
+
return fallback === undefined ? `\${${reference[1]}}` : `\${${reference[1]}:-${fallback}}`;
|
|
145
|
+
}
|
|
146
|
+
if (value.includes('$') || value.includes('}')) {
|
|
147
|
+
return value;
|
|
97
148
|
}
|
|
98
|
-
return {
|
|
149
|
+
return `\${${name}:-${value}}`;
|
|
99
150
|
}
|
|
100
151
|
// `includes pull` fetches each source once, however often the build files name it.
|
|
101
152
|
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
|
|
@@ -150,9 +150,11 @@ function candidates(command, path) {
|
|
|
150
150
|
.map((dir) => (0, path_1.resolve)(dir, command));
|
|
151
151
|
return bases.flatMap((base) => extensions.map((extension) => base + extension));
|
|
152
152
|
}
|
|
153
|
+
// bigint: a Windows file id has more digits than a number keeps, and two files whose ids
|
|
154
|
+
// round to the same number would be the same file
|
|
153
155
|
function statOf(path) {
|
|
154
156
|
try {
|
|
155
|
-
const stat = (0, fs_1.statSync)(path);
|
|
157
|
+
const stat = (0, fs_1.statSync)(path, { bigint: true });
|
|
156
158
|
return stat.isFile() ? stat : null;
|
|
157
159
|
}
|
|
158
160
|
catch {
|
|
@@ -11,10 +11,12 @@ function emptySecretCatalog() {
|
|
|
11
11
|
exports.emptySecretCatalog = emptySecretCatalog;
|
|
12
12
|
const FULL_COMMIT = /^[0-9a-f]{40}$/i;
|
|
13
13
|
// Providers and accounts are global names, like caches, collected from every
|
|
14
|
-
// loaded build file. A name declared twice is an error rather than a silent
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
14
|
+
// loaded build file. A name declared twice is an error rather than a silent override:
|
|
15
|
+
// which definition runs with a service account's credential must not depend on include
|
|
16
|
+
// order. The host config then replaces what the build files declare, name by name (and
|
|
17
|
+
// its default account replaces theirs): the machine that runs a task says how it reads
|
|
18
|
+
// secrets, and a build file that still declares them loads as it is.
|
|
19
|
+
function collectSecretCatalog(files, environment, context, host) {
|
|
18
20
|
const catalog = emptySecretCatalog();
|
|
19
21
|
for (const file of files) {
|
|
20
22
|
const providers = Object.entries(file.schema.secretProviders ?? {});
|
|
@@ -28,25 +30,13 @@ function collectSecretCatalog(files, environment, context) {
|
|
|
28
30
|
if (catalog.providers[name]) {
|
|
29
31
|
throw new Error(`secret provider ${name} is declared in ${catalog.providers[name].declaredIn} and in ${file.fileName}`);
|
|
30
32
|
}
|
|
31
|
-
catalog.providers[name] =
|
|
32
|
-
name,
|
|
33
|
-
type: 'command',
|
|
34
|
-
command: provider.command.map((part) => applyEnvs(part, envs)),
|
|
35
|
-
env: provider.env ?? {},
|
|
36
|
-
timeoutMs: (0, units_1.parseDuration)(provider.timeout ?? DEFAULT_TIMEOUT),
|
|
37
|
-
declaredIn: file.fileName,
|
|
38
|
-
};
|
|
33
|
+
catalog.providers[name] = toProvider(name, provider, envs, file.fileName);
|
|
39
34
|
}
|
|
40
35
|
for (const [name, account] of accounts) {
|
|
41
36
|
if (catalog.accounts[name]) {
|
|
42
37
|
throw new Error(`secret account ${name} is declared in ${catalog.accounts[name].declaredIn} and in ${file.fileName}`);
|
|
43
38
|
}
|
|
44
|
-
catalog.accounts[name] =
|
|
45
|
-
name,
|
|
46
|
-
default: account.default ?? false,
|
|
47
|
-
providers: account.providers,
|
|
48
|
-
declaredIn: file.fileName,
|
|
49
|
-
};
|
|
39
|
+
catalog.accounts[name] = toAccount(name, account, file.fileName);
|
|
50
40
|
}
|
|
51
41
|
}
|
|
52
42
|
const defaults = Object.values(catalog.accounts).filter((account) => account.default);
|
|
@@ -55,14 +45,48 @@ function collectSecretCatalog(files, environment, context) {
|
|
|
55
45
|
}
|
|
56
46
|
for (const account of Object.values(catalog.accounts)) {
|
|
57
47
|
for (const provider of Object.keys(account.providers)) {
|
|
58
|
-
if (!catalog.providers[provider]) {
|
|
48
|
+
if (!catalog.providers[provider] && !host?.secretProviders?.[provider]) {
|
|
59
49
|
throw new Error(`secret account ${account.name} (${account.declaredIn}) names provider ${provider}, which is not declared`);
|
|
60
50
|
}
|
|
61
51
|
}
|
|
62
52
|
}
|
|
53
|
+
if (host) {
|
|
54
|
+
// the host config has no envs of its own: `${NAME}` is read from the process
|
|
55
|
+
for (const [name, provider] of Object.entries(host.secretProviders ?? {})) {
|
|
56
|
+
catalog.providers[name] = toProvider(name, provider, {}, host.fileName);
|
|
57
|
+
}
|
|
58
|
+
const accounts = Object.entries(host.secretAccounts ?? {});
|
|
59
|
+
const hostDefaults = accounts.filter(([, account]) => account.default);
|
|
60
|
+
if (hostDefaults.length > 1) {
|
|
61
|
+
throw new Error(`more than one default secret account: ${hostDefaults.map(([name]) => name).join(', ')} (${host.fileName})`);
|
|
62
|
+
}
|
|
63
|
+
if (hostDefaults.length > 0) {
|
|
64
|
+
for (const account of Object.values(catalog.accounts)) {
|
|
65
|
+
account.default = false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// a provider an account of the host names is only checked when a secret uses it:
|
|
69
|
+
// a project that doesn't declare it, and reads no secret, loads anyway
|
|
70
|
+
for (const [name, account] of accounts) {
|
|
71
|
+
catalog.accounts[name] = toAccount(name, account, host.fileName);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
63
74
|
return catalog;
|
|
64
75
|
}
|
|
65
76
|
exports.collectSecretCatalog = collectSecretCatalog;
|
|
77
|
+
function toProvider(name, provider, envs, declaredIn) {
|
|
78
|
+
return {
|
|
79
|
+
name,
|
|
80
|
+
type: 'command',
|
|
81
|
+
command: provider.command.map((part) => applyEnvs(part, envs)),
|
|
82
|
+
env: provider.env ?? {},
|
|
83
|
+
timeoutMs: (0, units_1.parseDuration)(provider.timeout ?? DEFAULT_TIMEOUT),
|
|
84
|
+
declaredIn,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function toAccount(name, account, declaredIn) {
|
|
88
|
+
return { name, default: account.default ?? false, providers: account.providers, declaredIn };
|
|
89
|
+
}
|
|
66
90
|
// A tag or branch can be moved, and a moved file runs with the credentials of a
|
|
67
91
|
// service account: providers and accounts from a git include are pinned.
|
|
68
92
|
function requirePinnedSource(file) {
|
|
@@ -23,10 +23,22 @@ const MAX_PUBLISH_BYTES = bundle_1.MAX_BUNDLE_BYTES * 2;
|
|
|
23
23
|
* GET /v1/definitions/<hash> one version, with `arguments`: per task null when
|
|
24
24
|
* a run of it may pass arguments, else why not
|
|
25
25
|
* GET /v1/definitions/<hash>/files its files
|
|
26
|
+
* POST /v1/definitions/plan what publishing a bundle would do: plans it with this
|
|
27
|
+
* server's policy and stores nothing. For anybody with
|
|
28
|
+
* a `builds` level, `run` too
|
|
26
29
|
* POST /v1/definitions/<hash>/yank pull it back (body: optional {reason})
|
|
27
30
|
* POST /v1/definitions/<hash>/unyank let it go on
|
|
28
31
|
*/
|
|
29
32
|
async function handleDefinitions(config, store, proposals, log, principal, req, res, url) {
|
|
33
|
+
if (url.pathname === '/v1/definitions/plan') {
|
|
34
|
+
if (req.method !== 'POST') {
|
|
35
|
+
(0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'POST' });
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
await plan(config, store, log, principal, req, res);
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
30
42
|
const match = /^\/v1\/definitions(?:\/([^/]+)(\/files|\/yank|\/unyank)?)?$/.exec(url.pathname);
|
|
31
43
|
if (!match) {
|
|
32
44
|
return false;
|
|
@@ -124,6 +136,55 @@ async function yank(config, store, log, principal, hash, undo, req, res) {
|
|
|
124
136
|
log(`${undo ? 'unyank' : 'yank'} ${hash} iss=${principal.iss} sub=${principal.sub}${body.data.reason ? ` reason=${(0, oauth_1.quote)(body.data.reason)}` : ''}`);
|
|
125
137
|
(0, http_1.send)(res, 200, changed);
|
|
126
138
|
}
|
|
139
|
+
// Answers what POST /v1/definitions would: the version with its plan, and whether the
|
|
140
|
+
// caller would publish or propose it. A developer checks a build file against the policy
|
|
141
|
+
// before the account that publishes does. Nothing is stored, nothing is logged but the call.
|
|
142
|
+
async function plan(config, store, log, principal, req, res) {
|
|
143
|
+
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
144
|
+
if (grant.builds === null) {
|
|
145
|
+
(0, http_1.send)(res, 403, { error: 'you may not check build files, it needs builds: run, propose or publish' });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const parsed = bundle_1.bundleSchema.safeParse(await (0, http_1.readJson)(req, MAX_PUBLISH_BYTES));
|
|
149
|
+
if (!parsed.success) {
|
|
150
|
+
const issue = parsed.error.issues[0];
|
|
151
|
+
(0, http_1.send)(res, 400, { error: `${issue.path.join('.') || 'request'}: ${issue.message}` });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
// like a proposal: a caller who may not publish does not make the server fetch
|
|
156
|
+
const { version, existing } = await store.prepare(parsed.data, principal, {
|
|
157
|
+
noRemote: (0, policy_1.mayPublish)(grant) ? undefined : 'check',
|
|
158
|
+
});
|
|
159
|
+
const missing = (version.accounts ?? []).filter((account) => !(0, policy_1.canUse)(grant, account));
|
|
160
|
+
const outcome = existing
|
|
161
|
+
? 'already published'
|
|
162
|
+
: (0, policy_1.mayPublish)(grant) && missing.length === 0
|
|
163
|
+
? 'published'
|
|
164
|
+
: (0, policy_1.mayPublish)(grant) || (0, policy_1.mayPropose)(grant)
|
|
165
|
+
? 'proposed'
|
|
166
|
+
: null;
|
|
167
|
+
log(`plan ${version.hash} iss=${principal.iss} sub=${principal.sub} tasks=${version.tasks.length}`);
|
|
168
|
+
(0, http_1.send)(res, 200, {
|
|
169
|
+
hash: version.hash,
|
|
170
|
+
tasks: version.tasks,
|
|
171
|
+
services: version.services,
|
|
172
|
+
plan: version.plan,
|
|
173
|
+
accounts: version.accounts,
|
|
174
|
+
outcome,
|
|
175
|
+
// a yanked version is not checked again: it is the one that was published
|
|
176
|
+
...(existing?.yanked ? { yanked: true } : {}),
|
|
177
|
+
...(missing.length > 0 ? { missing } : {}),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
if (e instanceof definitions_1.DefinitionError) {
|
|
182
|
+
(0, http_1.send)(res, 400, { error: `the build file does not load: ${e.message}` });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
throw e;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
127
188
|
async function publish(config, store, proposals, log, principal, req, res) {
|
|
128
189
|
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
129
190
|
if (!(0, policy_1.mayPublish)(grant) && !(0, policy_1.mayPropose)(grant)) {
|
|
@@ -151,7 +212,7 @@ async function publish(config, store, proposals, log, principal, req, res) {
|
|
|
151
212
|
// a version can't run before it is approved, but it has to load, as it would
|
|
152
213
|
// have when published, so the approver is not asked about a broken one
|
|
153
214
|
const { version, existing } = await store.prepare(parsed.data, principal, {
|
|
154
|
-
noRemote:
|
|
215
|
+
noRemote: (0, policy_1.mayPublish)(grant) ? undefined : 'proposal',
|
|
155
216
|
annotations,
|
|
156
217
|
});
|
|
157
218
|
if (existing) {
|
package/dist/server/plan.js
CHANGED
|
@@ -40,50 +40,33 @@ const COMMIT_PATTERN = /^[0-9a-f]{40}([0-9a-f]{24})?$/i;
|
|
|
40
40
|
// A hash alone can only name what is inside the bundle, so everything a build
|
|
41
41
|
// file pulls in from elsewhere has to be pinned: a git include needs a commit
|
|
42
42
|
// and a remote that is not a path on the server.
|
|
43
|
-
function
|
|
43
|
+
function checkGitSource(path, where, source, noRemote) {
|
|
44
|
+
if (noRemote === 'proposal') {
|
|
45
|
+
throw new DefinitionError(`${path}: ${where}: a proposal may not include from git, the server would fetch it before anybody approved it`);
|
|
46
|
+
}
|
|
47
|
+
if (noRemote === 'check') {
|
|
48
|
+
throw new DefinitionError(`${path}: ${where}: checking a build file that includes from git needs builds: publish, the server would fetch it`);
|
|
49
|
+
}
|
|
44
50
|
const problems = [];
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
catch {
|
|
54
|
-
continue; // a file that isn't valid yaml is reported when the bundle is parsed
|
|
55
|
-
}
|
|
56
|
-
for (const block of ['includes', 'references']) {
|
|
57
|
-
for (const [name, include] of Object.entries(document?.[block] ?? {})) {
|
|
58
|
-
const source = include;
|
|
59
|
-
if (!source || typeof source !== 'object' || source.git === undefined) {
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (noRemote) {
|
|
63
|
-
problems.push(`${path}: ${block}.${name}: a proposal may not include from git, the server would fetch it before anybody approved it`);
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
if (typeof source.git !== 'string' || !/^(https:\/\/|ssh:\/\/|git@[\w.-]+:)/.test(source.git)) {
|
|
67
|
-
problems.push(`${path}: ${block}.${name}: git has to be an https:// or ssh:// address`);
|
|
68
|
-
}
|
|
69
|
-
if (typeof source.ref !== 'string' || !COMMIT_PATTERN.test(source.ref)) {
|
|
70
|
-
problems.push(`${path}: ${block}.${name}: pin ref to a commit, a branch or tag can move`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
51
|
+
if (!/^(https:\/\/|ssh:\/\/|git@[\w.-]+:)/.test(source.git)) {
|
|
52
|
+
problems.push(`${path}: ${where}: git has to be an https:// or ssh:// address`);
|
|
53
|
+
}
|
|
54
|
+
if (typeof source.ref !== 'string' || !COMMIT_PATTERN.test(source.ref)) {
|
|
55
|
+
problems.push(`${path}: ${where}: pin ref to a commit, a branch or tag can move`);
|
|
56
|
+
}
|
|
57
|
+
if (problems.length > 0) {
|
|
58
|
+
throw new DefinitionError(problems.join('\n'));
|
|
74
59
|
}
|
|
75
|
-
return problems;
|
|
76
60
|
}
|
|
77
61
|
// What a build file may not name on a server: the places it would run or store things
|
|
78
62
|
// are the operator's. The docker host, a Kubernetes cluster and a remote cache are
|
|
79
63
|
// credentials of the machine, not of the build file, and a project file that could carry
|
|
80
64
|
// them could send runs, or the cache, anywhere.
|
|
81
|
-
|
|
65
|
+
// A file a server leaves out (`publish: false`) is not checked, and neither is a service
|
|
66
|
+
// with `publish: false`: what is left out is never run there.
|
|
67
|
+
function findHostProblems(files) {
|
|
82
68
|
const problems = [];
|
|
83
|
-
for (const [path, content] of Object.entries(
|
|
84
|
-
if (!/\.ya?ml$/i.test(path)) {
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
69
|
+
for (const [path, content] of Object.entries(files)) {
|
|
87
70
|
let document;
|
|
88
71
|
try {
|
|
89
72
|
document = (0, yaml_1.parse)(content);
|
|
@@ -105,6 +88,7 @@ function findHostProblems(bundle) {
|
|
|
105
88
|
for (const [name, service] of Object.entries(document.services ?? {})) {
|
|
106
89
|
if (service &&
|
|
107
90
|
typeof service === 'object' &&
|
|
91
|
+
service.publish !== false &&
|
|
108
92
|
('selector' in service || 'kubeconfig' in service || 'context' in service)) {
|
|
109
93
|
problems.push(`${path}: services.${name}: Kubernetes services are not supported on a server`);
|
|
110
94
|
}
|
|
@@ -112,6 +96,17 @@ function findHostProblems(bundle) {
|
|
|
112
96
|
}
|
|
113
97
|
return problems;
|
|
114
98
|
}
|
|
99
|
+
// The files of the bundle a server publishes, see getPublishedFiles
|
|
100
|
+
function publishedBundleFiles(bundle, directory, entry) {
|
|
101
|
+
const files = {};
|
|
102
|
+
for (const scope of (0, reference_parser_1.getPublishedFiles)(entry)) {
|
|
103
|
+
const path = (0, path_1.relative)(directory, scope.fileName).split(path_1.sep).join('/');
|
|
104
|
+
if (!scope.remote && path in bundle.files) {
|
|
105
|
+
files[path] = bundle.files[path];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return files;
|
|
109
|
+
}
|
|
115
110
|
// What a build file may read while it is inspected: its own files. The rest of
|
|
116
111
|
// the server is not its business (a missing file is "not there", not an error).
|
|
117
112
|
function confined(files, root) {
|
|
@@ -342,10 +337,6 @@ function describeFailure(e) {
|
|
|
342
337
|
// when somebody wants to run it. The same function answers what a run will do,
|
|
343
338
|
// with the scope of the run: which tasks, services and accounts it involves.
|
|
344
339
|
async function planBundle(bundle, environment, options = {}) {
|
|
345
|
-
const problems = [...findLooseIncludes(bundle, options.noRemote), ...(options.policy ? findHostProblems(bundle) : [])];
|
|
346
|
-
if (problems.length > 0) {
|
|
347
|
-
throw new DefinitionError(problems.join('\n'));
|
|
348
|
-
}
|
|
349
340
|
const directory = await (0, promises_1.mkdtemp)((0, path_1.join)((0, os_1.tmpdir)(), 'hammerkit-definition-'));
|
|
350
341
|
try {
|
|
351
342
|
for (const [path, content] of Object.entries(bundle.files)) {
|
|
@@ -358,7 +349,20 @@ async function planBundle(bundle, environment, options = {}) {
|
|
|
358
349
|
file: confined((0, get_file_context_1.getFileContext)(directory), directory),
|
|
359
350
|
};
|
|
360
351
|
try {
|
|
361
|
-
const { ctx, scope } = await (0, schema_parser_1.createParseContext)((0, path_1.join)(directory, bundle.entry), inspected
|
|
352
|
+
const { ctx, scope } = await (0, schema_parser_1.createParseContext)((0, path_1.join)(directory, bundle.entry), inspected, {
|
|
353
|
+
// a hash alone can only name what is inside the bundle, see checkGitSource
|
|
354
|
+
checkGitSource: (source, from, block, name) => checkGitSource(
|
|
355
|
+
// a file of a git include is named by its repository
|
|
356
|
+
from.remote
|
|
357
|
+
? `${from.remote.git} ${(0, path_1.relative)(from.remote.root, from.fileName).split(path_1.sep).join('/')}`
|
|
358
|
+
: (0, path_1.relative)(directory, from.fileName), `${block}.${name}`, source, options.noRemote),
|
|
359
|
+
});
|
|
360
|
+
if (options.policy) {
|
|
361
|
+
const problems = findHostProblems(publishedBundleFiles(bundle, directory, scope));
|
|
362
|
+
if (problems.length > 0) {
|
|
363
|
+
throw new DefinitionError(problems.join('\n'));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
362
366
|
const context = await (0, reference_parser_1.parseReferences)(ctx, scope, inspected);
|
|
363
367
|
const workTree = (0, work_scope_parser_1.getWorkContext)(context, options.scope ?? { environmentName: null }, inspected);
|
|
364
368
|
const items = itemsOf(workTree);
|
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.22",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"build",
|
|
7
7
|
"build-tool",
|