hammerkit 1.27.0-next.2 → 1.27.0-next.20

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.
Files changed (100) hide show
  1. package/build.schema.json +9 -0
  2. package/dist/cache/backends/registry-cache-backend.js +2 -2
  3. package/dist/cache/explain.js +17 -0
  4. package/dist/cli.js +1 -0
  5. package/dist/docker/item-network.js +13 -5
  6. package/dist/executer/deadline.js +17 -3
  7. package/dist/executer/docker-service.js +30 -8
  8. package/dist/executer/event-cache.js +20 -2
  9. package/dist/executer/execute-work-task.js +5 -3
  10. package/dist/executer/execution-steps.js +5 -2
  11. package/dist/executer/scheduler/enqueue-next.js +7 -0
  12. package/dist/executer/scheduler/state-key.js +42 -5
  13. package/dist/harness/connect.js +20 -6
  14. package/dist/harness/mcp-command.js +22 -6
  15. package/dist/harness/mcp.js +63 -114
  16. package/dist/harness/opencode.js +158 -50
  17. package/dist/harness/pi.js +29 -0
  18. package/dist/harness/sandbox-client.js +207 -65
  19. package/dist/harness/service-tools.js +141 -0
  20. package/dist/harness/task-tools.js +221 -0
  21. package/dist/kubernetes/apply.js +27 -16
  22. package/dist/kubernetes/claim-layout.js +71 -0
  23. package/dist/kubernetes/ensure-http-route.js +1 -1
  24. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +5 -9
  25. package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
  26. package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
  27. package/dist/kubernetes/ensure-persistent-data.js +30 -20
  28. package/dist/kubernetes/get-service-ip.js +2 -2
  29. package/dist/kubernetes/job-secrets.js +33 -19
  30. package/dist/kubernetes/kept-claims.js +22 -10
  31. package/dist/kubernetes/network-policy.js +85 -27
  32. package/dist/kubernetes/remove-persistent-data.js +3 -1
  33. package/dist/kubernetes/resources.js +25 -3
  34. package/dist/kubernetes/restore-kubernetes-data.js +9 -3
  35. package/dist/kubernetes/sandbox-runner.js +626 -0
  36. package/dist/kubernetes/secrets.js +7 -11
  37. package/dist/kubernetes/volumes.js +6 -3
  38. package/dist/network/egress-proxy.js +45 -6
  39. package/dist/optimizer/calculate-checksum.js +3 -2
  40. package/dist/optimizer/check-key.js +94 -0
  41. package/dist/optimizer/check-keys.js +64 -0
  42. package/dist/optimizer/get-work-cache-stats.js +17 -3
  43. package/dist/optimizer/work-task-cache-description.js +51 -1
  44. package/dist/planner/utils/append-work-dependencies.js +10 -5
  45. package/dist/planner/utils/append-work-service.js +1 -0
  46. package/dist/planner/utils/append-work-task.js +15 -2
  47. package/dist/planner/utils/get-work-runtime.js +6 -1
  48. package/dist/planner/utils/normalize-path.js +19 -1
  49. package/dist/planner/utils/parse-work-mount.js +7 -1
  50. package/dist/planner/utils/parse-work-port.js +4 -1
  51. package/dist/planner/utils/task-arguments.js +3 -0
  52. package/dist/planner/validate.js +22 -0
  53. package/dist/planner/work-network.js +3 -1
  54. package/dist/planner/work-runtime-aggregate.js +32 -0
  55. package/dist/planner/work-runtime-docker.js +2 -1
  56. package/dist/planner/work-runtime-kubernetes.js +97 -48
  57. package/dist/planner/work-runtime-local.js +2 -1
  58. package/dist/planner/work-task.js +6 -1
  59. package/dist/program.js +14 -4
  60. package/dist/remote/annotations.js +13 -1
  61. package/dist/remote/device-login.js +7 -2
  62. package/dist/remote/host-config.js +64 -3
  63. package/dist/remote/remote-commands.js +41 -9
  64. package/dist/remote/remote-work-commands.js +246 -1
  65. package/dist/remote/server-client.js +15 -2
  66. package/dist/remote/token-source.js +64 -26
  67. package/dist/run-program.js +9 -0
  68. package/dist/schema/build-file-base-task-schema.js +5 -1
  69. package/dist/schema/build-file-container-service-schema.js +2 -0
  70. package/dist/schema/reference-parser.js +9 -1
  71. package/dist/schema/work-scope-parser.js +6 -0
  72. package/dist/secrets/github-app-token-command.js +3 -1
  73. package/dist/server/accounts-api.js +3 -1
  74. package/dist/server/approvals-api.js +1 -1
  75. package/dist/server/checks-api.js +83 -0
  76. package/dist/server/checks.js +116 -0
  77. package/dist/server/definitions-api.js +4 -2
  78. package/dist/server/endpoints.js +612 -0
  79. package/dist/server/executor.js +56 -5
  80. package/dist/server/kubernetes-backend.js +303 -64
  81. package/dist/server/kubernetes-sandbox-storage.js +48 -3
  82. package/dist/server/oauth.js +12 -1
  83. package/dist/server/plan.js +126 -62
  84. package/dist/server/policy.js +32 -2
  85. package/dist/server/repo-access.js +5 -0
  86. package/dist/server/run-plan.js +50 -19
  87. package/dist/server/run-request.js +64 -3
  88. package/dist/server/run-worker.js +146 -10
  89. package/dist/server/runs.js +347 -27
  90. package/dist/server/sandbox-pod.js +28 -0
  91. package/dist/server/sandbox-storage.js +23 -0
  92. package/dist/server/sandboxes-api.js +10 -4
  93. package/dist/server/sandboxes.js +8 -0
  94. package/dist/server/server-command.js +39 -20
  95. package/dist/server/server-config.js +213 -14
  96. package/dist/server/server.js +268 -15
  97. package/dist/server/service-runs.js +82 -0
  98. package/dist/utils/abort-on-signals.js +46 -6
  99. package/dist/utils/tar.js +36 -0
  100. package/package.json +1 -1
@@ -4,6 +4,7 @@ exports.checkIfContextExists = exports.hasDependencyCycle = exports.hasNeedCycle
4
4
  const read_build_file_1 = require("../parser/read-build-file");
5
5
  const plan_work_tasks_1 = require("./utils/plan-work-tasks");
6
6
  const unreferenced_build_file_envs_1 = require("./unreferenced-build-file-envs");
7
+ const normalize_path_1 = require("./utils/normalize-path");
7
8
  async function* validate(workTree, context) {
8
9
  const cycleItems = [];
9
10
  for (const item of (0, plan_work_tasks_1.iterateWorkServices)(workTree)) {
@@ -16,6 +17,10 @@ async function* validate(workTree, context) {
16
17
  yield { type: 'warn', message: 'missing healthcheck', item: service };
17
18
  }
18
19
  for (const mount of service.mounts) {
20
+ const pwd = getPwdMountWarning(mount);
21
+ if (pwd) {
22
+ yield { type: 'warn', message: pwd, item: service };
23
+ }
19
24
  if (!(await context.file.exists(mount.localPath))) {
20
25
  yield {
21
26
  type: 'warn',
@@ -62,6 +67,12 @@ async function* validate(workTree, context) {
62
67
  item: task,
63
68
  };
64
69
  }
70
+ for (const mount of task.type === 'container-task' ? task.mounts : []) {
71
+ const pwd = getPwdMountWarning(mount);
72
+ if (pwd) {
73
+ yield { type: 'warn', message: pwd, item: task };
74
+ }
75
+ }
65
76
  for (const src of task.src) {
66
77
  if (!(await context.file.exists(src.absolutePath))) {
67
78
  yield {
@@ -146,4 +157,15 @@ async function checkIfContextExists(service, context) {
146
157
  return false;
147
158
  }
148
159
  exports.checkIfContextExists = checkIfContextExists;
160
+ // `$PWD` in a mount is the home directory, which reads like a path of the project
161
+ function getPwdMountWarning(mount) {
162
+ const source = mount.mount.split(':')[0];
163
+ if ((0, normalize_path_1.getHomeReference)(source) !== '$PWD') {
164
+ return null;
165
+ }
166
+ const rest = source.substring('$PWD'.length);
167
+ const home = rest === '' || rest.startsWith('/') || rest.startsWith('\\') ? `~${rest}` : '~/…';
168
+ return (`mount ${source} is ${mount.localPath}: $PWD in a mount is the home directory, not the project. ` +
169
+ `Write ${home} for the home directory, or a relative path for the project`);
170
+ }
149
171
  //# sourceMappingURL=validate.js.map
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.applyNetworkCeiling = exports.formatNetwork = exports.getTcpForwards = exports.matchesHost = exports.isLimitedNetwork = exports.toNetworkSchema = exports.exceedsNetwork = exports.getEffectiveNetwork = exports.limitNetwork = exports.parseNetworkOption = exports.parseWorkNetwork = exports.OPEN_NETWORK = void 0;
4
4
  const network_schema_1 = require("../schema/network-schema");
5
+ const work_task_1 = require("./work-task");
5
6
  exports.OPEN_NETWORK = { type: 'open' };
6
7
  function parseWorkNetwork(schema) {
7
8
  if (!schema || schema === 'open') {
@@ -153,7 +154,8 @@ exports.formatNetwork = formatNetwork;
153
154
  function applyNetworkCeiling(workTree, ceiling) {
154
155
  const limit = ceiling && isLimitedNetwork(ceiling) ? ceiling : null;
155
156
  if (limit) {
156
- const local = Object.values(workTree.tasks).find((task) => task.data.type === 'local-task');
157
+ // an aggregate runs nothing, so there is nothing of it to limit
158
+ const local = Object.values(workTree.tasks).find((task) => task.data.type === 'local-task' && !(0, work_task_1.isAggregateTask)(task.data));
157
159
  if (local) {
158
160
  throw new Error(`--network ${formatNetwork(limit)} needs every task in a container, ` +
159
161
  `hammerkit can't limit the network of the local task ${local.name}`);
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.aggregateWorkRuntime = void 0;
4
+ // An aggregate runs nothing, so it touches nothing: no pid or state file in its
5
+ // directory (on a server that is a checkout it doesn't trust), no outputs.
6
+ function aggregateWorkRuntime() {
7
+ return {
8
+ async initialize() {
9
+ // nothing to prepare
10
+ },
11
+ async remove() {
12
+ // it has no outputs of its own
13
+ },
14
+ async execute() {
15
+ // its state stays running, and executeWorkTask completes it
16
+ },
17
+ async stop() {
18
+ // nothing is running
19
+ },
20
+ async archive() {
21
+ // never pushed, see pushToBackend
22
+ },
23
+ async restore() {
24
+ // never a hit, see checkCacheState
25
+ },
26
+ async currentStateKey() {
27
+ return null;
28
+ },
29
+ };
30
+ }
31
+ exports.aggregateWorkRuntime = aggregateWorkRuntime;
32
+ //# sourceMappingURL=work-runtime-aggregate.js.map
@@ -12,6 +12,7 @@ const event_cache_1 = require("../executer/event-cache");
12
12
  const get_docker_executor_1 = require("../executer/get-docker-executor");
13
13
  const path_1 = require("path");
14
14
  const tar_1 = require("tar");
15
+ const tar_2 = require("../utils/tar");
15
16
  const plan_work_volume_1 = require("./utils/plan-work-volume");
16
17
  const work_instance_id_1 = require("./work-instance-id");
17
18
  const stream_1 = require("stream");
@@ -302,7 +303,7 @@ async function archiveContainer(docker, environment, item, path) {
302
303
  for (const generatedArchive of (0, event_cache_1.getArchivePaths)(item.data, path)) {
303
304
  // a file output is a bind mount of a host file, so archive it from the host
304
305
  const readable = hostFiles.has(generatedArchive.path)
305
- ? stream_1.Readable.from((0, tar_1.create)({ cwd: (0, path_1.dirname)(generatedArchive.path) }, [(0, path_1.basename)(generatedArchive.path)]))
306
+ ? stream_1.Readable.from((0, tar_2.packArchive)({ cwd: (0, path_1.dirname)(generatedArchive.path) }, [(0, path_1.basename)(generatedArchive.path)]))
306
307
  : await container.getArchive({
307
308
  path: generatedArchive.path,
308
309
  });
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.kubernetesForwardRuntime = exports.kubernetesServiceRuntime = exports.kubernetesTaskRuntime = exports.describeFailedJob = void 0;
6
+ exports.kubernetesForwardRuntime = exports.kubernetesServiceRuntime = exports.kubernetesTaskRuntime = exports.getTaskJobContainers = exports.describeFailedJob = void 0;
7
7
  const abort_1 = require("../executer/abort");
8
8
  const kubernetes_service_1 = require("../executer/kubernetes-service");
9
9
  const kubernetes_instance_1 = require("../kubernetes/kubernetes-instance");
@@ -34,11 +34,12 @@ const job_placement_1 = require("../kubernetes/job-placement");
34
34
  const network_policy_1 = require("../kubernetes/network-policy");
35
35
  const run_id_1 = require("../utils/run-id");
36
36
  const work_network_1 = require("./work-network");
37
+ const sandbox_runner_1 = require("../kubernetes/sandbox-runner");
37
38
  const work_secret_1 = require("./work-secret");
38
39
  async function listJobNames(instance, kubernetes, task) {
39
40
  const jobs = await instance.batchApi.listNamespacedJob({
40
41
  namespace: kubernetes.namespace,
41
- labelSelector: `hammerkit.dev/id=${task.id()}`,
42
+ labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
42
43
  });
43
44
  return jobs.items.flatMap((job) => (job.metadata?.name ? [job.metadata.name] : []));
44
45
  }
@@ -125,6 +126,41 @@ async function describeFailedJob(instance, kubernetes, task, job, container, sec
125
126
  return undefined;
126
127
  }
127
128
  exports.describeFailedJob = describeFailedJob;
129
+ /**
130
+ * The containers of the Job of a command: the network gate first, nothing runs before the
131
+ * item's policy holds, then the init containers that read its secrets, then the command. The
132
+ * proxy's token (`network.secretEnv`) reaches the init containers through `secretSpec` only:
133
+ * the command's container gets the proxy variables of a limited network without it.
134
+ */
135
+ function getTaskJobContainers(options) {
136
+ const { task, cmd, envs, secrets, network, secretSpec, kubernetes } = options;
137
+ // the arguments after `--` keep their `$` as text, Kubernetes expands `$(NAME)`
138
+ const command = secretSpec.command(task.data.shell, (0, task_arguments_1.getKubernetesCommandText)(cmd));
139
+ const initContainers = [...network.initContainers, ...secretSpec.initContainers];
140
+ return {
141
+ ...(initContainers.length > 0 ? { initContainers } : {}),
142
+ containers: [
143
+ {
144
+ image: task.data.image,
145
+ // through the task's shell, as on Docker
146
+ command: command.command,
147
+ args: command.args,
148
+ workingDir: cmd.cwd,
149
+ env: [
150
+ ...Object.entries(envs).map(([key, value]) => ({ name: key, value })),
151
+ ...secrets.env,
152
+ ...network.env,
153
+ ...command.env,
154
+ ],
155
+ name: options.container,
156
+ volumeMounts: [...options.mounts, ...secrets.mounts, ...secretSpec.mounts],
157
+ ...(0, container_resources_1.getKubernetesResources)(task.data.resources),
158
+ ...(0, job_placement_1.getContainerSecurity)(kubernetes),
159
+ },
160
+ ],
161
+ };
162
+ }
163
+ exports.getTaskJobContainers = getTaskJobContainers;
128
164
  function kubernetesTaskRuntime(task, kubernetes) {
129
165
  const instance = (0, kubernetes_instance_1.createKubernetesInstances)(kubernetes);
130
166
  // on a server that keeps them, the task's claim stays after the run with the state of its
@@ -153,6 +189,28 @@ function kubernetesTaskRuntime(task, kubernetes) {
153
189
  const envs = (0, replace_env_variables_1.getEnvironmentVariables)(task.data.envs);
154
190
  const persistence = await (0, volumes_1.getKubernetesPersistence)(task, kubernetes);
155
191
  const workspace = (0, volumes_1.getWorkspacePodSpec)(kubernetes, persistence, { name: task.name, pool: task.data.pool });
192
+ // a shell of a sandbox: its commands go into the sandbox's runner, when it has one now
193
+ if ((0, sandbox_runner_1.canUseRunner)(task, kubernetes, persistence)) {
194
+ try {
195
+ const result = await (0, sandbox_runner_1.executeInRunner)(instance, task, kubernetes, persistence, options.abort);
196
+ if (result.type === 'crash') {
197
+ options.state.set({ stateKey: options.stateKey, type: 'crash', exitCode: result.exitCode });
198
+ return;
199
+ }
200
+ // stopped while the runner started: no Job for it either
201
+ if (result.type === 'canceled' || options.abort.aborted) {
202
+ options.state.set({ stateKey: options.stateKey, type: 'canceled' });
203
+ return;
204
+ }
205
+ if (result.type === 'done') {
206
+ return;
207
+ }
208
+ }
209
+ catch (e) {
210
+ options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
211
+ return;
212
+ }
213
+ }
156
214
  await (0, ensure_namespace_1.ensureNamespace)(instance, kubernetes.namespace);
157
215
  try {
158
216
  // the task writes to it now: what it holds stops being the outputs of a state, and
@@ -166,10 +224,11 @@ function kubernetesTaskRuntime(task, kubernetes) {
166
224
  }
167
225
  const server = kubernetes.server;
168
226
  const { delivered, inJob } = (0, job_secrets_1.splitJobSecrets)(task.data.secrets, server);
169
- const secrets = (0, secrets_1.getKubernetesSecretRefs)(task, delivered);
227
+ const secrets = (0, secrets_1.getKubernetesSecretRefs)(task, kubernetes, delivered);
170
228
  const admitted = environment.hosted?.accounts;
229
+ let secretHosts;
171
230
  try {
172
- (0, job_secrets_1.assertSecretsReachable)(task, inJob);
231
+ secretHosts = (0, job_secrets_1.getSecretHosts)(task, inJob, server);
173
232
  if (server) {
174
233
  (0, job_secrets_1.assertJobAccounts)(inJob, server, admitted);
175
234
  }
@@ -194,7 +253,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
194
253
  : {}),
195
254
  };
196
255
  const labels = {
197
- 'hammerkit.dev/id': task.id(),
256
+ ...(0, resources_1.getItemLabels)(task, kubernetes),
198
257
  'hammerkit.dev/state': options.stateKey,
199
258
  ...workspace.labels,
200
259
  [network_policy_1.RUN_LABEL]: scope.run,
@@ -210,7 +269,11 @@ function kubernetesTaskRuntime(task, kubernetes) {
210
269
  // NetworkPolicy and proxy of a limited network, both of this run
211
270
  let network;
212
271
  try {
213
- network = await (0, network_policy_1.ensureKubernetesItemNetwork)(instance, kubernetes, task, { followLog: true, ...scope });
272
+ network = await (0, network_policy_1.ensureKubernetesItemNetwork)(instance, kubernetes, task, {
273
+ followLog: true,
274
+ secretHosts,
275
+ ...scope,
276
+ });
214
277
  }
215
278
  catch (e) {
216
279
  await removeTaskSecrets(instance, kubernetes, task).catch(() => undefined);
@@ -218,7 +281,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
218
281
  return;
219
282
  }
220
283
  try {
221
- const podName = (0, resources_1.getResourceName)(task, '-job');
284
+ const podName = (0, resources_1.getResourceName)(task, kubernetes, '-job');
222
285
  let i = 0;
223
286
  for (const cmd of task.data.cmds) {
224
287
  const container = `cmd-${++i}`;
@@ -226,15 +289,13 @@ function kubernetesTaskRuntime(task, kubernetes) {
226
289
  try {
227
290
  // made for each Job right before it, so a later command's token is as fresh
228
291
  const credentials = await (0, job_secrets_1.ensureJobCredentials)(instance, kubernetes, task, inJob, admitted);
229
- secretSpec = (0, job_secrets_1.getJobSecretSpec)(task, inJob, server, credentials, environment);
292
+ secretSpec = (0, job_secrets_1.getJobSecretSpec)(task, inJob, server, credentials, environment, network.secretEnv);
230
293
  }
231
294
  catch (e) {
232
295
  await removeTaskSecrets(instance, kubernetes, task).catch(() => undefined);
233
296
  options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
234
297
  return;
235
298
  }
236
- // the arguments after `--` keep their `$` as text, Kubernetes expands `$(NAME)`
237
- const command = secretSpec.command(task.data.shell, (0, task_arguments_1.getKubernetesCommandText)(cmd));
238
299
  const spec = {
239
300
  kind: 'Job',
240
301
  apiVersion: 'batch/v1',
@@ -255,33 +316,17 @@ function kubernetesTaskRuntime(task, kubernetes) {
255
316
  labels,
256
317
  },
257
318
  spec: {
258
- // the network gate first: nothing runs before the item's policy holds
259
- ...(network.initContainers.length + secretSpec.initContainers.length > 0
260
- ? { initContainers: [...network.initContainers, ...secretSpec.initContainers] }
261
- : {}),
262
- containers: [
263
- {
264
- image: task.data.image,
265
- // through the task's shell, as on Docker
266
- command: command.command,
267
- args: command.args,
268
- workingDir: cmd.cwd,
269
- env: [
270
- ...Object.entries(envs).map(([key, value]) => ({ name: key, value })),
271
- ...secrets.env,
272
- ...network.env,
273
- ...command.env,
274
- ],
275
- name: container,
276
- volumeMounts: [
277
- ...persistence.mounts.map((m) => m.mount),
278
- ...secrets.mounts,
279
- ...secretSpec.mounts,
280
- ],
281
- ...(0, container_resources_1.getKubernetesResources)(task.data.resources),
282
- ...(0, job_placement_1.getContainerSecurity)(kubernetes),
283
- },
284
- ],
319
+ ...getTaskJobContainers({
320
+ task,
321
+ cmd,
322
+ container,
323
+ envs,
324
+ secrets,
325
+ network,
326
+ secretSpec,
327
+ mounts: persistence.mounts.map((m) => m.mount),
328
+ kubernetes,
329
+ }),
285
330
  volumes: [
286
331
  ...persistence.volumes,
287
332
  ...persistence.workspace,
@@ -365,7 +410,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
365
410
  async stop() {
366
411
  const jobs = await instance.batchApi.listNamespacedJob({
367
412
  namespace: kubernetes.namespace,
368
- labelSelector: `hammerkit.dev/id=${task.id()}`,
413
+ labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
369
414
  });
370
415
  for (const job of jobs.items) {
371
416
  if (job.metadata?.name) {
@@ -374,7 +419,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
374
419
  }
375
420
  const deployments = await instance.appsApi.listNamespacedDeployment({
376
421
  namespace: kubernetes.namespace,
377
- labelSelector: `hammerkit.dev/id=${task.id()}`,
422
+ labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
378
423
  });
379
424
  for (const deploy of deployments.items) {
380
425
  if (deploy.metadata?.name) {
@@ -386,7 +431,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
386
431
  }
387
432
  const pods = await instance.coreApi.listNamespacedPod({
388
433
  namespace: kubernetes.namespace,
389
- labelSelector: `hammerkit.dev/id=${task.id()}`,
434
+ labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
390
435
  });
391
436
  for (const pod of pods.items) {
392
437
  if (pod.metadata?.name) {
@@ -408,7 +453,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
408
453
  }
409
454
  const jobs = await instance.batchApi.listNamespacedJob({
410
455
  namespace: kubernetes.namespace,
411
- labelSelector: `hammerkit.dev/id=${task.id()}`,
456
+ labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
412
457
  });
413
458
  const completedStates = jobs.items
414
459
  .filter((j) => j.status?.succeeded && j.metadata?.labels?.['hammerkit.dev/state'])
@@ -445,7 +490,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
445
490
  async initialize(state) {
446
491
  const deployments = await instance.appsApi.listNamespacedDeployment({
447
492
  namespace: kubernetes.namespace,
448
- labelSelector: `hammerkit.dev/id=${service.id()}`,
493
+ labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
449
494
  });
450
495
  const deployment = deployments.items[0];
451
496
  if (!deployment?.metadata?.name) {
@@ -472,7 +517,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
472
517
  async stop() {
473
518
  const deployments = await instance.appsApi.listNamespacedDeployment({
474
519
  namespace: kubernetes.namespace,
475
- labelSelector: `hammerkit.dev/id=${service.id()}`,
520
+ labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
476
521
  });
477
522
  for (const deployment of deployments.items) {
478
523
  if (deployment.metadata?.name) {
@@ -492,9 +537,13 @@ function kubernetesServiceRuntime(service, kubernetes) {
492
537
  await (0, ensure_kubernetes_service_exists_1.ensureKubernetesServiceExists)(instance, kubernetes, service);
493
538
  await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, service, persistence);
494
539
  await (0, secrets_1.ensureKubernetesSecret)(instance, kubernetes, service, service.data.secrets, environment);
495
- const network = await (0, network_policy_1.ensureKubernetesItemNetwork)(instance, kubernetes, service, { followLog: false, run: null });
496
- await (0, ensure_kubernetes_deployment_exists_1.ensureKubernetesDeploymentExists)(instance, kubernetes, service, persistence, (0, secrets_1.getKubernetesSecretRefs)(service, service.data.secrets), options.stateKey, network);
497
- const name = (0, resources_1.getResourceName)(service);
540
+ // on a server, the run's: its runs share the namespace, and the service goes with the run
541
+ const network = await (0, network_policy_1.ensureKubernetesItemNetwork)(instance, kubernetes, service, {
542
+ followLog: false,
543
+ run: kubernetes.server ? (0, job_placement_1.getRunLabelValue)(kubernetes.server.run) : null,
544
+ });
545
+ await (0, ensure_kubernetes_deployment_exists_1.ensureKubernetesDeploymentExists)(instance, kubernetes, service, persistence, (0, secrets_1.getKubernetesSecretRefs)(service, kubernetes, service.data.secrets), options.stateKey, network);
546
+ const name = (0, resources_1.getResourceName)(service, kubernetes);
498
547
  options.state.set({
499
548
  type: 'running',
500
549
  dns: { containerId: name },
@@ -526,7 +575,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
526
575
  }
527
576
  const services = await instance.coreApi.listNamespacedService({
528
577
  namespace: kubernetes.namespace,
529
- labelSelector: `hammerkit.dev/id=${service.id()}`,
578
+ labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
530
579
  });
531
580
  for (const service of services.items) {
532
581
  if (service.metadata?.name) {
@@ -542,7 +591,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
542
591
  async currentStateKey() {
543
592
  const deployments = await instance.appsApi.listNamespacedDeployment({
544
593
  namespace: kubernetes.namespace,
545
- labelSelector: `hammerkit.dev/id=${service.id()}`,
594
+ labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
546
595
  });
547
596
  const deployment = deployments.items[0];
548
597
  return deployment?.metadata?.labels?.['hammerkit.dev/state'] ?? null;
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getLocalWorkRuntime = exports.getStateFilename = void 0;
7
7
  const tar_1 = require("tar");
8
+ const tar_2 = require("../utils/tar");
8
9
  const path_1 = require("path");
9
10
  const local_task_1 = require("../executer/local-task");
10
11
  const event_cache_1 = require("../executer/event-cache");
@@ -121,7 +122,7 @@ async function archiveLocal(environment, task, path) {
121
122
  for (const generatedArchive of (0, event_cache_1.getArchivePaths)(task.data, path)) {
122
123
  await environment.file.writeStream(generatedArchive.filename,
123
124
  // tar's stream is a Minipass stream; Readable.from adapts it to a Node one
124
- stream_1.Readable.from((0, tar_1.create)({ cwd: task.data.cwd, gzip: true }, [(0, path_1.relative)(task.data.cwd, generatedArchive.path)])));
125
+ stream_1.Readable.from((0, tar_2.packArchive)({ cwd: task.data.cwd, gzip: true }, [(0, path_1.relative)(task.data.cwd, generatedArchive.path)])));
125
126
  }
126
127
  }
127
128
  async function restoreLocal(environment, task, path) {
@@ -1,8 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isWorkTask = exports.isContainerWorkTask = void 0;
3
+ exports.isAggregateTask = exports.isWorkTask = exports.isContainerWorkTask = void 0;
4
4
  const isContainerWorkTask = (val) => val.type === 'container-task';
5
5
  exports.isContainerWorkTask = isContainerWorkTask;
6
6
  const isWorkTask = (val) => val.type === 'container-task' || val.type === 'local-task';
7
7
  exports.isWorkTask = isWorkTask;
8
+ // A task that only groups its dependencies: `deps`, and no `image`, `cmds`, `src`,
9
+ // `generates`, `secrets`, `needs` or `continuous`. It needs no runtime, hands on what its dependencies generate to what
10
+ // depends on it, and is up to date (or verified) when all its dependencies are.
11
+ const isAggregateTask = (val) => val.type === 'local-task' && val.aggregate === true;
12
+ exports.isAggregateTask = isAggregateTask;
8
13
  //# sourceMappingURL=work-task.js.map
package/dist/program.js CHANGED
@@ -84,7 +84,8 @@ function printTaskDefinition(environment, definition) {
84
84
  }
85
85
  }
86
86
  // `ls --json`: what a caller (an agent picking a task) needs to know, with the
87
- // command that arguments after `--` are appended to (null: it takes none).
87
+ // command that arguments after `--` are appended to (null: it takes none) and
88
+ // `args` as the build file says it (false: it refuses them).
88
89
  function getListJson(tasks, services) {
89
90
  return {
90
91
  tasks: tasks.map(({ item }) => ({
@@ -96,6 +97,7 @@ function getListJson(tasks, services) {
96
97
  needs: item.needs.map((n) => n.name),
97
98
  labels: item.data.labels,
98
99
  command: (0, task_arguments_1.getArgumentsTarget)(item.data)?.cmd ?? null,
100
+ args: item.data.args,
99
101
  })),
100
102
  services: services.map(({ item }) => ({
101
103
  name: item.name,
@@ -105,6 +107,7 @@ function getListJson(tasks, services) {
105
107
  ports: item.data.ports.map((p) => ({ host: p.hostPort, container: p.containerPort })),
106
108
  labels: item.data.labels,
107
109
  command: (0, task_arguments_1.getArgumentsTarget)(item.data)?.cmd ?? null,
110
+ args: item.data.type === 'container-service' ? item.data.args : null,
108
111
  })),
109
112
  };
110
113
  }
@@ -159,11 +162,14 @@ async function getProgram(environment, argv) {
159
162
  if (fileIndex >= 0) {
160
163
  args.splice(fileIndex, 2);
161
164
  }
162
- (0, remote_commands_1.registerRemoteCommands)(program, environment, fileName);
165
+ (0, remote_commands_1.registerRemoteCommands)(program, environment, fileName, passthrough);
163
166
  program.hook('preAction', (_, actionCommand) => {
164
- const takesArguments = actionCommand.parent === program && ['run', 'up'].includes(actionCommand.name());
167
+ const takesArguments = (actionCommand.parent === program && ['run', 'up'].includes(actionCommand.name())) ||
168
+ (actionCommand.parent?.name() === 'remote' && actionCommand.name() === 'run');
165
169
  if (passthrough.length > 0 && !takesArguments) {
166
- program.error(`${actionCommand.name()} takes no arguments after --, only run and up do`, { exitCode: 1 });
170
+ program.error(`${actionCommand.name()} takes no arguments after --, only run, up and remote run do`, {
171
+ exitCode: 1,
172
+ });
167
173
  }
168
174
  });
169
175
  if (await environment.file.exists(fileName)) {
@@ -602,6 +608,7 @@ async function getProgram(environment, argv) {
602
608
  .choices(['checksum', 'modify-date', 'none']))
603
609
  .addOption(new commander_1.Option('--cache-read-only', `restore from cache backends but never push to them (or set ${read_only_1.CACHE_READ_ONLY_ENV}=1)`).default(false))
604
610
  .addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
611
+ .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))
605
612
  .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))
606
613
  .action(async (options, command) => {
607
614
  try {
@@ -620,6 +627,7 @@ async function getProgram(environment, argv) {
620
627
  wait: options.wait,
621
628
  cacheReadOnly: (0, read_only_1.isCacheReadOnly)(options.cacheReadOnly, environment.processEnvs),
622
629
  timeout: options.timeout ?? null,
630
+ maxTimeout: options.maxTimeout ?? null,
623
631
  network: options.network ?? null,
624
632
  });
625
633
  if (!result.success) {
@@ -682,6 +690,7 @@ async function getProgram(environment, argv) {
682
690
  .addOption(new commander_1.Option('--dry-run', 'print the execution plan with predicted cache hits/misses without running').default(false))
683
691
  .addOption(new commander_1.Option('--cache-read-only', `restore from cache backends but never push to them (or set ${read_only_1.CACHE_READ_ONLY_ENV}=1)`).default(false))
684
692
  .addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
693
+ .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))
685
694
  .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))
686
695
  .addOption(new commander_1.Option('--no-skip-deps', 'run dependencies even when nothing needing them has to run'))
687
696
  .action(async (task, options, command) => {
@@ -716,6 +725,7 @@ async function getProgram(environment, argv) {
716
725
  explain: options.explain,
717
726
  cacheReadOnly: (0, read_only_1.isCacheReadOnly)(options.cacheReadOnly, environment.processEnvs),
718
727
  timeout: options.timeout ?? null,
728
+ maxTimeout: options.maxTimeout ?? null,
719
729
  network: options.network ?? null,
720
730
  skipDeps: options.skipDeps !== false,
721
731
  });
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.applyAnnotations = exports.collectAnnotations = exports.remoteRepo = void 0;
3
+ exports.applyAnnotations = exports.collectAnnotations = exports.readCheckout = exports.remoteRepo = void 0;
4
4
  const child_process_1 = require("child_process");
5
5
  const os_1 = require("os");
6
6
  const path_1 = require("path");
@@ -36,6 +36,18 @@ function remoteRepo(url) {
36
36
  }
37
37
  }
38
38
  exports.remoteRepo = remoteRepo;
39
+ // The checkout a directory is in: the repository of its `origin`, the branch it is on and
40
+ // its commit, what can be found of them.
41
+ async function readCheckout(cwd) {
42
+ if ((await git(cwd, 'rev-parse', '--is-inside-work-tree')) !== 'true') {
43
+ return {};
44
+ }
45
+ const remote = await git(cwd, 'remote', 'get-url', 'origin');
46
+ // a detached HEAD is on no branch
47
+ const branch = await git(cwd, 'symbolic-ref', '--quiet', '--short', 'HEAD');
48
+ return { repo: remote ? remoteRepo(remote) : undefined, branch, commit: await git(cwd, 'rev-parse', 'HEAD') };
49
+ }
50
+ exports.readCheckout = readCheckout;
39
51
  // Where a build file is published from: its path in its repository (or on this
40
52
  // machine, outside of one), the repository and commit, this machine and its user.
41
53
  // `files` are the paths of the bundle, relative to the build file's directory: when
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.deviceLogin = void 0;
3
+ exports.deviceLogin = exports.NoSignInError = void 0;
4
4
  const server_client_1 = require("./server-client");
5
5
  const GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
6
6
  const DEFAULT_INTERVAL_SECONDS = 5;
@@ -8,6 +8,11 @@ const SLOW_DOWN_SECONDS = 5;
8
8
  const FETCH_TIMEOUT_MS = 10000;
9
9
  const MIN_INTERVAL_SECONDS = 1;
10
10
  const MAX_LIFETIME_SECONDS = 30 * 60;
11
+ // The server takes tokens of this issuer but offers no device sign-in for it: the token has
12
+ // to come from elsewhere, such as a token command.
13
+ class NoSignInError extends server_client_1.ServerError {
14
+ }
15
+ exports.NoSignInError = NoSignInError;
11
16
  const isLoopbackHost = (hostname) => ['localhost', '127.0.0.1', '[::1]'].includes(hostname);
12
17
  // An endpoint must use the scheme of the issuer: loopback http is for a test
13
18
  // identity provider on this machine only, never beside an https issuer.
@@ -89,7 +94,7 @@ async function deviceLogin(server, options) {
89
94
  throw new server_client_1.ServerError(`${server.url} does not accept tokens from ${server.issuer} (it accepts ${accepted})`);
90
95
  }
91
96
  if (!offered.clientId) {
92
- throw new server_client_1.ServerError(`${server.url} offers no sign-in for ${server.issuer}, it needs a token obtained elsewhere`);
97
+ throw new NoSignInError(`${server.url} offers no sign-in for ${server.issuer}, it needs a token obtained elsewhere`);
93
98
  }
94
99
  // What was pinned (by `remote add` or an earlier login) wins; the server can
95
100
  // only offer the same, never something else to sign in with.