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 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
- await environment.file.writeFile(mount.localPath, '');
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
- await environment.file.createDirectory(mount.localPath);
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
- ...parseWorkLabelScope(options),
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 = parseWorkLabelScope(options);
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 string is a local path; an object is a git source (ADR-0004).
26
- exports.buildFileIncludeSchema = (0, zod_1.union)([(0, zod_1.string)(), exports.buildFileGitSourceSchema]);
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 path = (0, path_1.join)(base, include);
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(`${include} in ${scope.fileName} points outside ${scope.remote.git}`);
97
+ throw new Error(`${localPath} in ${scope.fileName} points outside ${scope.remote.git}`);
97
98
  }
98
- return { fileName: await (0, default_build_file_1.getBuildFilename)(path, environment), remote: scope.remote };
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
- applyNeedsAndDeps(workTree, item);
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.20",
4
+ "version": "1.27.0-next.21",
5
5
  "keywords": [
6
6
  "build",
7
7
  "build-tool",