hammerkit 2.0.0-next.5 → 2.0.0-next.7

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 (38) hide show
  1. package/dist/cache/explain.js +7 -5
  2. package/dist/dashboard/assets/graph-canvas-DLioOiRN.css +1 -0
  3. package/dist/dashboard/assets/graph-canvas-HmkW_-XJ.js +8 -0
  4. package/dist/dashboard/assets/index-gIBDC_m8.css +2 -0
  5. package/dist/dashboard/assets/index-vE22S5E_.js +17 -0
  6. package/dist/dashboard/index.html +2 -2
  7. package/dist/executer/docker-task.js +3 -0
  8. package/dist/executer/execute-work-task.js +3 -1
  9. package/dist/executer/local-task.js +3 -0
  10. package/dist/executer/scheduler/enqueue-next.js +6 -1
  11. package/dist/executer/scheduler/state-key.js +71 -34
  12. package/dist/executer/watch-loop.js +18 -5
  13. package/dist/file/get-file-context.js +5 -1
  14. package/dist/kubernetes/claim-layout.js +10 -1
  15. package/dist/kubernetes/ensure-persistent-data.js +84 -10
  16. package/dist/kubernetes/follow-pod-logs.js +124 -27
  17. package/dist/kubernetes/network-policy.js +4 -4
  18. package/dist/kubernetes/restore-kubernetes-data.js +21 -3
  19. package/dist/kubernetes/store-kubernetes-data.js +13 -2
  20. package/dist/kubernetes/volumes.js +34 -7
  21. package/dist/optimizer/calculate-checksum.js +46 -3
  22. package/dist/optimizer/get-work-cache-stats.js +36 -24
  23. package/dist/planner/work-item-executions.js +27 -0
  24. package/dist/planner/work-runtime-kubernetes.js +33 -38
  25. package/dist/server/capacity.js +63 -0
  26. package/dist/server/dashboard.js +2 -2
  27. package/dist/server/executor.js +10 -0
  28. package/dist/server/overview.js +42 -9
  29. package/dist/server/plan.js +16 -3
  30. package/dist/server/policy.js +9 -3
  31. package/dist/server/run-worker.js +37 -4
  32. package/dist/server/runs.js +49 -4
  33. package/dist/server/server-config.js +4 -1
  34. package/dist/server/server.js +10 -7
  35. package/dist/utils/limit.js +30 -0
  36. package/package.json +1 -1
  37. package/dist/dashboard/assets/index-D51NA6-h.js +0 -14
  38. package/dist/dashboard/assets/index-mmvC6u9O.css +0 -2
@@ -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.isRolledOut = exports.kubernetesServiceRuntime = exports.kubernetesTaskRuntime = exports.getTaskJobContainers = exports.describeFailedJob = void 0;
6
+ exports.kubernetesForwardRuntime = exports.isRolledOut = exports.kubernetesServiceRuntime = exports.kubernetesTaskRuntime = exports.getTaskJobContainers = exports.describeFailedJob = exports.recordJobExecution = 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");
@@ -12,6 +12,7 @@ const apply_1 = require("../kubernetes/apply");
12
12
  const ensure_kubernetes_service_exists_1 = require("../kubernetes/ensure-kubernetes-service-exists");
13
13
  const ensure_kubernetes_deployment_exists_1 = require("../kubernetes/ensure-kubernetes-deployment-exists");
14
14
  const follow_pod_logs_1 = require("../kubernetes/follow-pod-logs");
15
+ const work_item_executions_1 = require("./work-item-executions");
15
16
  const service_definition_1 = require("./service-definition");
16
17
  const ensure_namespace_1 = require("../kubernetes/ensure-namespace");
17
18
  const ensure_persistent_data_1 = require("../kubernetes/ensure-persistent-data");
@@ -62,32 +63,17 @@ async function getJobPod(instance, kubernetes, job) {
62
63
  .sort((a, b) => (a.metadata?.creationTimestamp?.toISOString() ?? '').localeCompare(b.metadata?.creationTimestamp?.toISOString() ?? ''))
63
64
  .pop();
64
65
  }
65
- // a task's log is kept to this, the run's own cap is smaller
66
- const MAX_JOB_LOG_BYTES = 4 * 1024 * 1024;
67
- // What the command printed, as the task's output: read from the cluster once the Job
68
- // has ended. Without a pod (it never started) there is nothing to say.
69
- async function writeJobLogs(instance, kubernetes, task, job, container) {
70
- try {
71
- const pod = await getJobPod(instance, kubernetes, job);
72
- if (!pod?.metadata?.name) {
73
- return;
74
- }
75
- const log = await instance.coreApi.readNamespacedPodLog({
76
- name: pod.metadata.name,
77
- namespace: kubernetes.namespace,
78
- container,
79
- limitBytes: MAX_JOB_LOG_BYTES,
80
- });
81
- for (const line of String(log ?? '').split(/\r?\n/)) {
82
- if (line.length > 0) {
83
- task.status.console('stdout', line);
84
- }
85
- }
86
- }
87
- catch (e) {
88
- task.status.write('debug', `the log of ${job} could not be read: ${(0, log_1.getErrorMessage)(e)}`);
66
+ // When the command of a Job ran, as its container says: what the task spent before it (the
67
+ // pod scheduled, the image pulled, the init containers) and after it is not the command's.
68
+ async function recordJobExecution(instance, kubernetes, task, job, container) {
69
+ const pod = await getJobPod(instance, kubernetes, job).catch(() => undefined);
70
+ const status = pod?.status?.containerStatuses?.find((s) => s.name === container);
71
+ const terminated = status?.state?.terminated ?? status?.lastState?.terminated;
72
+ if (terminated?.startedAt && terminated.finishedAt) {
73
+ (0, work_item_executions_1.recordExecution)(task, new Date(terminated.startedAt), new Date(terminated.finishedAt));
89
74
  }
90
75
  }
76
+ exports.recordJobExecution = recordJobExecution;
91
77
  // the command of a Job exited with this code, as a crash on docker or a host
92
78
  class JobCrash extends Error {
93
79
  constructor(exitCode) {
@@ -394,24 +380,33 @@ function kubernetesTaskRuntime(task, kubernetes) {
394
380
  };
395
381
  try {
396
382
  const pod = await (0, apply_1.apply)(instance, spec);
397
- if (!pod.status?.succeeded) {
398
- try {
399
- await (0, await_running_state_1.awaitJobCompletion)(instance, kubernetes, spec.metadata.name, options.abort);
400
- }
401
- catch (e) {
402
- if (!(e instanceof abort_1.AbortError)) {
403
- await writeJobLogs(instance, kubernetes, task, podName, container);
404
- const failure = await describeFailedJob(instance, kubernetes, task.name, podName, container, inJob);
405
- if (failure?.type === 'crash') {
406
- throw new JobCrash(failure.exitCode);
383
+ // what the command prints, while it prints it; the last lines once the pod ended
384
+ const logs = (0, follow_pod_logs_1.followPodLogs)(instance, kubernetes.namespace, `job-name=${podName}`, container, (line) => task.status.console('stdout', line), { onError: (text) => task.status.write('debug', text) });
385
+ try {
386
+ if (!pod.status?.succeeded) {
387
+ try {
388
+ await (0, await_running_state_1.awaitJobCompletion)(instance, kubernetes, spec.metadata.name, options.abort);
389
+ }
390
+ catch (e) {
391
+ if (!(e instanceof abort_1.AbortError)) {
392
+ await logs.settle();
393
+ await recordJobExecution(instance, kubernetes, task.data, podName, container);
394
+ const failure = await describeFailedJob(instance, kubernetes, task.name, podName, container, inJob);
395
+ if (failure?.type === 'crash') {
396
+ throw new JobCrash(failure.exitCode);
397
+ }
398
+ throw new Error(failure?.message ?? (0, log_1.getErrorMessage)(e));
407
399
  }
408
- throw new Error(failure?.message ?? (0, log_1.getErrorMessage)(e));
400
+ throw e;
409
401
  }
410
- throw e;
411
402
  }
403
+ await logs.settle();
404
+ }
405
+ finally {
406
+ logs.stop();
412
407
  }
413
- await writeJobLogs(instance, kubernetes, task, podName, container);
414
408
  task.status.write('debug', 'pod completed');
409
+ await recordJobExecution(instance, kubernetes, task.data, podName, container);
415
410
  // its init containers are done with the credentials
416
411
  await (0, job_secrets_1.removeJobCredentials)(instance, kubernetes, task);
417
412
  if (i < task.data.cmds.length) {
@@ -4,6 +4,9 @@ exports.formatMemory = exports.formatCpu = exports.HostCapacity = exports.Kubern
4
4
  const os_1 = require("os");
5
5
  const log_1 = require("../log");
6
6
  const apply_1 = require("../kubernetes/apply");
7
+ const job_placement_1 = require("../kubernetes/job-placement");
8
+ const volumes_1 = require("../kubernetes/volumes");
9
+ const network_policy_1 = require("../kubernetes/network-policy");
7
10
  const UNITS = {
8
11
  n: 1e-9,
9
12
  u: 1e-6,
@@ -90,6 +93,17 @@ function why(what, e, rule) {
90
93
  }
91
94
  return `${what}: ${(0, log_1.getErrorMessage)(e)}`;
92
95
  }
96
+ // `why`, in a few words
97
+ function briefly(e, what) {
98
+ const code = (0, apply_1.statusCodeOf)(e);
99
+ if (code === 403) {
100
+ return what === 'nodes' ? 'may not list nodes' : 'may not read metrics';
101
+ }
102
+ if (code === 404 && what === 'metrics') {
103
+ return 'no metrics API';
104
+ }
105
+ return what === 'nodes' ? 'nodes not readable' : 'metrics not readable';
106
+ }
93
107
  const ENDED = ['Succeeded', 'Failed'];
94
108
  // On Kubernetes: per namespace of the server, its pods' requests, its quota and its usage.
95
109
  class KubernetesCapacity {
@@ -107,6 +121,37 @@ class KubernetesCapacity {
107
121
  catch (e) {
108
122
  problems.push(why('the namespaces of the server', e));
109
123
  }
124
+ const unknown = {};
125
+ // the runs by the value of their label, and what the pods of each run or sandbox take
126
+ const byLabel = new Map(runs.map((run) => [(0, job_placement_1.getRunLabelValue)(run.id), run]));
127
+ const uses = new Map();
128
+ const useOf = (namespace, labels, owner) => {
129
+ const run = byLabel.get(labels?.[job_placement_1.RUN_LABEL] ?? '');
130
+ const sandbox = labels?.[volumes_1.SANDBOX_LABEL] ?? labels?.[network_policy_1.PROXY_OF_SANDBOX_LABEL];
131
+ const key = run ? `run ${run.id}` : sandbox ? `sandbox ${sandbox}` : `other ${namespace}`;
132
+ let use = uses.get(key);
133
+ if (!use) {
134
+ use = run
135
+ ? {
136
+ kind: 'run',
137
+ run: run.id,
138
+ tasks: run.tasks,
139
+ ...(run.up ? { up: { name: run.up.name, services: run.up.services } } : {}),
140
+ repo: run.source.type === 'upload' ? 'an upload' : run.source.repo,
141
+ owner: { iss: run.owner.iss, sub: run.owner.sub, ...(run.owner.name ? { name: run.owner.name } : {}) },
142
+ pods: 0,
143
+ requested: { cpu: 0, memory: 0 },
144
+ }
145
+ : {
146
+ kind: sandbox ? 'sandbox' : 'other',
147
+ ...(owner ? { owner } : {}),
148
+ pods: 0,
149
+ requested: { cpu: 0, memory: 0 },
150
+ };
151
+ uses.set(key, use);
152
+ }
153
+ return use;
154
+ };
110
155
  const said = new Set();
111
156
  const once = (text) => {
112
157
  if (!said.has(text)) {
@@ -123,6 +168,8 @@ class KubernetesCapacity {
123
168
  // a namespace whose pods could be listed and that has none asks for nothing
124
169
  requested: { cpu: 0, memory: 0 },
125
170
  };
171
+ // the use of each pod that runs, by its name
172
+ const usesOfPods = new Map();
126
173
  try {
127
174
  const pods = await this.instance.coreApi.listNamespacedPod({ namespace });
128
175
  for (const pod of pods.items) {
@@ -130,9 +177,13 @@ class KubernetesCapacity {
130
177
  continue;
131
178
  }
132
179
  group.pods++;
180
+ const use = useOf(namespace, pod.metadata?.labels, group.owner);
181
+ usesOfPods.set(pod.metadata?.name ?? '', use);
182
+ use.pods++;
133
183
  for (const container of pod.spec?.containers ?? []) {
134
184
  const requests = container.resources?.requests ?? {};
135
185
  add(group.requested, parseQuantity(requests.cpu) ?? 0, parseQuantity(requests.memory) ?? 0);
186
+ add(use.requested, parseQuantity(requests.cpu) ?? 0, parseQuantity(requests.memory) ?? 0);
136
187
  }
137
188
  }
138
189
  }
@@ -162,13 +213,19 @@ class KubernetesCapacity {
162
213
  const metrics = await this.instance.metrics.getPodMetrics(namespace);
163
214
  const used = { cpu: 0, memory: 0 };
164
215
  for (const pod of metrics.items) {
216
+ const use = usesOfPods.get(pod.metadata?.name ?? '');
165
217
  for (const container of pod.containers) {
166
218
  add(used, parseQuantity(container.usage.cpu), parseQuantity(container.usage.memory));
219
+ if (use) {
220
+ use.used ?? (use.used = { cpu: 0, memory: 0 });
221
+ add(use.used, parseQuantity(container.usage.cpu), parseQuantity(container.usage.memory));
222
+ }
167
223
  }
168
224
  }
169
225
  group.used = used;
170
226
  }
171
227
  catch (e) {
228
+ unknown.used ?? (unknown.used = briefly(e, 'metrics'));
172
229
  once(why('what the pods use (metrics)', e, 'list on pods.metrics.k8s.io in them (runRole, see the backend docs)'));
173
230
  }
174
231
  }
@@ -204,11 +261,13 @@ class KubernetesCapacity {
204
261
  machines.used = nodesUsed;
205
262
  }
206
263
  catch (e) {
264
+ unknown.used ?? (unknown.used = briefly(e, 'metrics'));
207
265
  once(why('what the nodes use (metrics)', e, 'list on nodes.metrics.k8s.io (see the backend docs)'));
208
266
  }
209
267
  }
210
268
  }
211
269
  catch (e) {
270
+ unknown.total = briefly(e, 'nodes');
212
271
  // listing nodes is optional, and many clusters do not let a workload do it
213
272
  if ((0, apply_1.statusCodeOf)(e) === 403) {
214
273
  once('the nodes: not shown, the server may not list nodes (optional, see the backend docs)');
@@ -223,7 +282,11 @@ class KubernetesCapacity {
223
282
  ...(machines ? { machines } : {}),
224
283
  runs: total,
225
284
  groups,
285
+ uses: [...uses.values()].sort((a, b) => (b.used?.cpu ?? 0) - (a.used?.cpu ?? 0) ||
286
+ (b.requested.cpu ?? 0) - (a.requested.cpu ?? 0) ||
287
+ (b.requested.memory ?? 0) - (a.requested.memory ?? 0)),
226
288
  problems,
289
+ ...(Object.keys(unknown).length > 0 ? { unknown } : {}),
227
290
  };
228
291
  }
229
292
  }
@@ -64,8 +64,8 @@ const TYPES = {
64
64
  };
65
65
  async function handleDashboard(context, principal, req, res, url) {
66
66
  const now = context.now ?? Date.now;
67
- const grant = (0, policy_1.getGrant)(context.config.entitlements, principal);
68
- const viewer = { principal, grant, activity: context.config.runs.activity === 'everyone' };
67
+ const viewer = (0, overview_1.getViewer)(context.config, principal);
68
+ const { grant } = viewer;
69
69
  const pathname = url.pathname;
70
70
  for (const [name, value] of Object.entries(exports.HEADERS)) {
71
71
  res.setHeader(name, value);
@@ -220,6 +220,7 @@ class HostExecutor {
220
220
  let claim;
221
221
  // the files of the sandbox the tasks work on
222
222
  let work;
223
+ const sourceStarted = new Date();
223
224
  if (source.type === 'sandbox' && source.check) {
224
225
  // a check works on the files a push of the sandbox commits, copied out of it: never
225
226
  // on the sandbox, whose ignored files (node_modules, outputs) its owner writes
@@ -303,12 +304,17 @@ class HostExecutor {
303
304
  sandboxNetwork: network,
304
305
  };
305
306
  }
307
+ emitPhase(context, 'source', sourceStarted);
306
308
  const home = (0, path_1.join)(root, 'home');
307
309
  await (0, promises_1.mkdir)(home, { recursive: true });
308
310
  const cache = (0, path_1.join)(this.options.cache, cacheNamespace(run));
309
311
  await (0, promises_1.mkdir)(cache, { recursive: true });
310
312
  const placement = this.options.placement;
313
+ const namespaceStarted = new Date();
311
314
  const placed = placement ? await placement.prepare(run, context, { root: work ?? cwd, claim }) : undefined;
315
+ if (placed) {
316
+ emitPhase(context, 'namespace', namespaceStarted);
317
+ }
312
318
  try {
313
319
  await this.runInProcess(worker, {
314
320
  ...input,
@@ -504,4 +510,8 @@ class HostExecutor {
504
510
  }
505
511
  }
506
512
  exports.HostExecutor = HostExecutor;
513
+ // A step of a run before its tasks, for its timeline (RunPhase)
514
+ function emitPhase(context, phase, startedAt) {
515
+ context.emit({ type: 'phase', phase, startedAt: startedAt.toISOString(), finishedAt: new Date().toISOString() });
516
+ }
507
517
  //# sourceMappingURL=executor.js.map
@@ -13,6 +13,7 @@ function getViewer(config, principal) {
13
13
  principal,
14
14
  grant: (0, policy_1.getGrant)(config.entitlements, principal),
15
15
  activity: config.runs.activity === 'everyone',
16
+ nameOf: (owner) => (0, policy_1.subjectName)(config.entitlements, owner),
16
17
  };
17
18
  }
18
19
  exports.getViewer = getViewer;
@@ -91,7 +92,8 @@ function mayRead(viewer, run) {
91
92
  return owns(viewer, run) || viewer.grant.observe || decides(viewer, run);
92
93
  }
93
94
  exports.mayRead = mayRead;
94
- function toListedRun(viewer, run, queue) {
95
+ // `entryOf` names where in its repository the build file of a version is.
96
+ function toListedRun(viewer, run, queue, entryOf) {
95
97
  const { owner, results, plan, options, up, error, ...rest } = run;
96
98
  const mine = owns(viewer, run);
97
99
  const may = runActions(viewer, run);
@@ -106,11 +108,11 @@ function toListedRun(viewer, run, queue) {
106
108
  ...(result.cache ? { cached: result.cache.hit } : {}),
107
109
  },
108
110
  ]));
111
+ const entry = entryOf?.(run.definition);
112
+ const name = owner.name ?? viewer.nameOf?.(owner);
109
113
  return {
110
114
  ...rest,
111
- ...(mine
112
- ? { plan, options }
113
- : { owner: { iss: owner.iss, sub: owner.sub, ...(owner.name ? { name: owner.name } : {}) } }),
115
+ ...(mine ? { plan, options } : { owner: { iss: owner.iss, sub: owner.sub, ...(name ? { name } : {}) } }),
114
116
  ...(up
115
117
  ? {
116
118
  up: {
@@ -124,6 +126,7 @@ function toListedRun(viewer, run, queue) {
124
126
  ...(queue ? { queue } : {}),
125
127
  ...(may ? { may } : {}),
126
128
  ...(!mine && mayRead(viewer, run) ? { readable: true } : {}),
129
+ ...(entry ? { entry } : {}),
127
130
  };
128
131
  }
129
132
  exports.toListedRun = toListedRun;
@@ -149,7 +152,8 @@ function queuePlace(registry, viewer, run) {
149
152
  return { position: queue.position, waitsFor: visibleReason(queue.waitsFor, visible) };
150
153
  }
151
154
  exports.queuePlace = queuePlace;
152
- function getOverview(snapshot, usage, viewer) {
155
+ function getOverview(snapshot, usage, viewer, entryOf) {
156
+ const list = (run, place) => toListedRun(viewer, run, place, entryOf);
153
157
  const runs = new Map();
154
158
  for (const run of [
155
159
  ...snapshot.running,
@@ -168,7 +172,7 @@ function getOverview(snapshot, usage, viewer) {
168
172
  return {
169
173
  ...place,
170
174
  mine: owns(viewer, entry.run),
171
- ...(maySee(viewer, entry.run) ? { run: toListedRun(viewer, entry.run, place) } : {}),
175
+ ...(maySee(viewer, entry.run) ? { run: list(entry.run, place) } : {}),
172
176
  };
173
177
  });
174
178
  const running = snapshot.running.filter((run) => maySee(viewer, run));
@@ -178,19 +182,19 @@ function getOverview(snapshot, usage, viewer) {
178
182
  places: {
179
183
  max: snapshot.places.max,
180
184
  used: snapshot.places.used,
181
- runs: running.map((run) => toListedRun(viewer, run)),
185
+ runs: running.map((run) => list(run)),
182
186
  others: snapshot.running.length - running.length,
183
187
  },
184
188
  queue,
185
189
  approvals: {
186
190
  waiting: snapshot.awaiting.runs.length,
187
191
  max: snapshot.awaiting.max,
188
- runs: snapshot.awaiting.runs.filter((run) => maySee(viewer, run)).map((run) => toListedRun(viewer, run)),
192
+ runs: snapshot.awaiting.runs.filter((run) => maySee(viewer, run)).map((run) => list(run)),
189
193
  },
190
194
  services: {
191
195
  open: snapshot.services.taken,
192
196
  ...(snapshot.services.max !== undefined ? { max: snapshot.services.max } : {}),
193
- runs: snapshot.services.runs.filter((run) => maySee(viewer, run)).map((run) => toListedRun(viewer, run)),
197
+ runs: snapshot.services.runs.filter((run) => maySee(viewer, run)).map((run) => list(run)),
194
198
  },
195
199
  limits: {
196
200
  waiting: meter(usage.waiting, limits?.waiting),
@@ -291,6 +295,35 @@ function visibleCapacity(capacity, viewer) {
291
295
  ...capacity,
292
296
  // what the cluster answered is for operators; others learn that something is missing
293
297
  problems: viewer.grant.observe || capacity.problems.length === 0 ? capacity.problems : ['some of it could not be read'],
298
+ ...(capacity.unknown
299
+ ? {
300
+ unknown: everyone
301
+ ? capacity.unknown
302
+ : {
303
+ ...(capacity.unknown.total ? { total: 'not readable' } : {}),
304
+ ...(capacity.unknown.used ? { used: 'not readable' } : {}),
305
+ },
306
+ }
307
+ : {}),
308
+ // what others' runs and sandboxes take shows, not what they are
309
+ ...(capacity.uses
310
+ ? {
311
+ uses: capacity.uses.map((use) => {
312
+ const mine = use.owner?.iss === viewer.principal.iss && use.owner?.sub === viewer.principal.sub;
313
+ const name = use.owner && !use.owner.name ? viewer.nameOf?.(use.owner) : undefined;
314
+ return everyone || mine || !use.owner
315
+ ? name && use.owner
316
+ ? { ...use, owner: { ...use.owner, name } }
317
+ : use
318
+ : {
319
+ kind: use.kind,
320
+ pods: use.pods,
321
+ requested: use.requested,
322
+ ...(use.used ? { used: use.used } : {}),
323
+ };
324
+ }),
325
+ }
326
+ : {}),
294
327
  groups: capacity.groups.map((group) => {
295
328
  const mine = group.owner?.iss === viewer.principal.iss && group.owner?.sub === viewer.principal.sub;
296
329
  if (everyone || mine || !group.owner) {
@@ -294,6 +294,13 @@ function describeMountSource(mount) {
294
294
  const home = (0, normalize_path_1.getHomeReference)(mount.mount.split(':')[0]);
295
295
  return home ? `${mount.localPath} (${home} is the home directory)` : mount.localPath;
296
296
  }
297
+ // where the build file of an item is: in the bundle, or in the repository of a git include
298
+ function declaredIn(root, scope) {
299
+ if (scope.remote) {
300
+ return `${scope.remote.git} ${(0, path_1.relative)(scope.remote.root, scope.fileName).split(path_1.sep).join('/')}`;
301
+ }
302
+ return (0, path_1.relative)(root, scope.fileName).split(path_1.sep).join('/');
303
+ }
297
304
  function toPlan(root, item) {
298
305
  const data = item.data;
299
306
  const secrets = secretsOf(item).map(describeSecret);
@@ -322,6 +329,7 @@ function toPlan(root, item) {
322
329
  const isTask = data.type === 'local-task' || data.type === 'container-task';
323
330
  return {
324
331
  kind: isTask ? 'task' : 'service',
332
+ file: declaredIn(root, data.scope),
325
333
  ...(data.description ? { description: data.description } : {}),
326
334
  runtime: (0, work_task_1.isAggregateTask)(data) ? 'none' : data.type === 'local-task' ? 'local' : 'docker',
327
335
  ...((0, work_task_1.isContainerWorkTask)(data) || (0, work_service_1.isContainerWorkService)(data) ? { image: data.image } : {}),
@@ -330,13 +338,18 @@ function toPlan(root, item) {
330
338
  network: (0, work_network_1.toNetworkSchema)('network' in data ? data.network : { type: 'open' }),
331
339
  ...('args' in data && data.args !== null ? { args: data.args } : {}),
332
340
  deps: item.deps.map((dep) => dep.name).sort(),
333
- needs: item.needs.map((need) => need.name).sort(),
341
+ // by their names in the plan, not the names the task reaches them at (`postgres` of an include)
342
+ needs: item.needs.map((need) => need.service.name).sort(),
334
343
  accounts,
335
344
  secrets,
336
345
  serverEnvs: 'envs' in data ? data.envs.processNames ?? [] : [],
337
346
  mounts,
338
347
  ports,
339
- generates: 'generates' in data ? data.generates.map((generate) => generate.path) : [],
348
+ // where in the bundle, as the build file names them: the directory the server read it in is
349
+ // its own
350
+ generates: 'generates' in data
351
+ ? data.generates.map((generate) => insideBundle(root, data.cwd, generate.path)[0] ?? generate.path)
352
+ : [],
340
353
  ...(capAdd.length > 0 ? { capAdd: [...capAdd] } : {}),
341
354
  flags,
342
355
  };
@@ -400,7 +413,7 @@ async function planBundle(bundle, environment, options = {}) {
400
413
  ? item.data.mounts.flatMap((mount) => insideBundle(directory, item.data.cwd, mount.localPath))
401
414
  : [],
402
415
  deps: item.deps.map((dep) => dep.name).sort(),
403
- needs: item.needs.map((need) => need.name).sort(),
416
+ needs: item.needs.map((need) => need.service.name).sort(),
404
417
  };
405
418
  }
406
419
  const plan = {};
@@ -1,8 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.decideRun = exports.sandboxRepos = exports.decideSandbox = exports.sandboxPushBranches = exports.sandboxBranchType = exports.endpointLevel = exports.getGrant = exports.usedAccounts = exports.mayYank = exports.canUse = exports.maySeeDefinitions = exports.mayPublish = exports.mayPropose = exports.mayReadChecks = exports.mayRun = exports.matches = void 0;
3
+ exports.decideRun = exports.sandboxRepos = exports.decideSandbox = exports.sandboxPushBranches = exports.sandboxBranchType = exports.endpointLevel = exports.getGrant = exports.usedAccounts = exports.mayYank = exports.canUse = exports.maySeeDefinitions = exports.mayPublish = exports.mayPropose = exports.mayReadChecks = exports.mayRun = exports.matches = exports.subjectName = void 0;
4
4
  const repo_1 = require("./repo");
5
5
  const BUILDS_ORDER = ['run', 'propose', 'publish'];
6
+ // The name an entitlement gives a subject, for the pages to name its runs by when its tokens
7
+ // name nobody.
8
+ function subjectName(entitlements, who) {
9
+ return entitlements.find((entitlement) => entitlement.subject?.name && entitlement.subject.iss === who.iss && entitlement.subject.sub === who.sub)?.subject?.name;
10
+ }
11
+ exports.subjectName = subjectName;
6
12
  function matches(entitlement, principal) {
7
13
  if (entitlement.subject) {
8
14
  return entitlement.subject.iss === principal.iss && entitlement.subject.sub === principal.sub;
@@ -34,9 +40,9 @@ const mayPublish = (grant) => atLeast(grant, 'publish');
34
40
  exports.mayPublish = mayPublish;
35
41
  // A caller who can neither run nor publish anything sees nothing of the server's
36
42
  // build files either. Someone who uses an account sees them: they have to know what
37
- // they approve.
43
+ // they approve. Who observes sees them too, as they see what every run ran.
38
44
  function maySeeDefinitions(grant) {
39
- return grant.builds !== null || Object.keys(grant.accounts).length > 0;
45
+ return grant.builds !== null || Object.keys(grant.accounts).length > 0 || grant.observe;
40
46
  }
41
47
  exports.maySeeDefinitions = maySeeDefinitions;
42
48
  // `use` includes deciding about the runs others request on the account.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.workerMain = exports.readInput = exports.taskCauses = exports.describeFailures = exports.runTasks = exports.getEndpointTargets = exports.publishEndpoints = exports.getRunNetwork = exports.workerInputSchema = void 0;
3
+ exports.workerMain = exports.readInput = exports.taskCommands = exports.taskCauses = exports.describeFailures = exports.runTasks = exports.getEndpointTargets = exports.publishEndpoints = exports.getRunNetwork = exports.workerInputSchema = void 0;
4
4
  const readline_1 = require("readline");
5
5
  const zod_1 = require("zod");
6
6
  const cli_1 = require("../cli");
@@ -10,6 +10,7 @@ const schema_parser_1 = require("../schema/schema-parser");
10
10
  const reference_parser_1 = require("../schema/reference-parser");
11
11
  const work_scope_parser_1 = require("../schema/work-scope-parser");
12
12
  const work_item_status_1 = require("../planner/work-item-status");
13
+ const work_item_executions_1 = require("../planner/work-item-executions");
13
14
  const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
14
15
  const assert_item_paths_1 = require("../planner/utils/assert-item-paths");
15
16
  const hosted_volumes_1 = require("../planner/utils/hosted-volumes");
@@ -218,6 +219,7 @@ up) {
218
219
  }
219
220
  }
220
221
  }
222
+ let planned = false;
221
223
  for (const { name, scope } of scopes) {
222
224
  const { ctx, scope: parsed } = await (0, schema_parser_1.createParseContext)(input.entry, environment, {
223
225
  directory: input.directory,
@@ -234,6 +236,16 @@ up) {
234
236
  if (cli.tasks().length === 0 && (!input.up || cli.services().length === 0)) {
235
237
  throw new Error(`no tasks found for ${name}`);
236
238
  }
239
+ if (!planned) {
240
+ // from the start of this process, which the executor started for the run
241
+ planned = true;
242
+ emit({
243
+ type: 'phase',
244
+ phase: 'plan',
245
+ startedAt: new Date(performance.timeOrigin).toISOString(),
246
+ finishedAt: new Date().toISOString(),
247
+ });
248
+ }
237
249
  const execution = input.up
238
250
  ? cli.up({ ...execOptions, workers: cli.defaultWorkers(), daemon: false, wait: 'ready' })
239
251
  : cli.exec({ ...execOptions, workers: cli.defaultWorkers() });
@@ -392,6 +404,19 @@ function taskCauses(causes, redact) {
392
404
  : {};
393
405
  }
394
406
  exports.taskCauses = taskCauses;
407
+ // When the commands of a task that is over ran, as its runtime saw them.
408
+ function taskCommands(task) {
409
+ const executions = (0, work_item_executions_1.readExecutions)(task);
410
+ return executions.length
411
+ ? {
412
+ commands: executions.slice(0, runs_1.MAX_COMMANDS).map((e) => ({
413
+ startedAt: e.startedAt.toISOString(),
414
+ finishedAt: e.finishedAt.toISOString(),
415
+ })),
416
+ }
417
+ : {};
418
+ }
419
+ exports.taskCommands = taskCommands;
395
420
  // A task event when a task has moved on since the last look, with what its result needs
396
421
  // once it is over.
397
422
  function reportTasks(emit, workTree, seen,
@@ -414,17 +439,25 @@ redact) {
414
439
  state: status,
415
440
  ...(status === 'skipped' ? {} : { durationMs: state.duration }),
416
441
  ...taskCauses(state.missCauses, redact),
442
+ ...(status === 'succeeded' ? taskCommands(task.data) : {}),
417
443
  };
418
444
  }
419
445
  else if (state.type === 'crash') {
420
- event = { type: 'task', task: task.name, state: 'failed', exitCode: state.exitCode, ...since() };
446
+ event = {
447
+ type: 'task',
448
+ task: task.name,
449
+ state: 'failed',
450
+ exitCode: state.exitCode,
451
+ ...since(),
452
+ ...taskCommands(task.data),
453
+ };
421
454
  }
422
455
  else if (state.type === 'error') {
423
456
  const error = redact(state.errorMessage).slice(0, runs_1.MAX_TASK_ERROR);
424
- event = { type: 'task', task: task.name, state: 'failed', error, ...since() };
457
+ event = { type: 'task', task: task.name, state: 'failed', error, ...since(), ...taskCommands(task.data) };
425
458
  }
426
459
  else if (state.type === 'canceled') {
427
- event = { type: 'task', task: task.name, state: 'cancelled' };
460
+ event = { type: 'task', task: task.name, state: 'cancelled', ...taskCommands(task.data) };
428
461
  }
429
462
  if (event && before?.status !== event.state) {
430
463
  seen.set(task.name, { status: event.state, started });