hammerkit 1.27.0-next.9 → 2.0.0-next.2

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 (122) hide show
  1. package/build.schema.json +176 -6
  2. package/dist/cache/backends/local-cache-backend.js +6 -0
  3. package/dist/cache/backends/registry/registry-client.js +1 -0
  4. package/dist/cache/cache-sync.js +1 -1
  5. package/dist/cache/explain.js +17 -0
  6. package/dist/cli.js +2 -0
  7. package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
  8. package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
  9. package/dist/dashboard/favicon.svg +1 -0
  10. package/dist/dashboard/index.html +15 -0
  11. package/dist/docker/item-network.js +13 -5
  12. package/dist/executer/archive-task-entry.js +4 -2
  13. package/dist/executer/deadline.js +17 -3
  14. package/dist/executer/deferred-restore.js +36 -0
  15. package/dist/executer/docker-service.js +30 -8
  16. package/dist/executer/docker-task.js +21 -1
  17. package/dist/executer/execute-work-service.js +2 -0
  18. package/dist/executer/execute-work-task.js +11 -4
  19. package/dist/executer/execution-steps.js +25 -4
  20. package/dist/executer/reset-work-tree.js +4 -0
  21. package/dist/executer/scheduler/enqueue-next.js +57 -6
  22. package/dist/executer/scheduler/state-key.js +42 -5
  23. package/dist/executer/watch-loop.js +6 -3
  24. package/dist/harness/connect.js +20 -6
  25. package/dist/harness/mcp-command.js +22 -6
  26. package/dist/harness/mcp.js +63 -114
  27. package/dist/harness/opencode.js +158 -50
  28. package/dist/harness/pi.js +29 -0
  29. package/dist/harness/sandbox-client.js +194 -23
  30. package/dist/harness/service-tools.js +141 -0
  31. package/dist/harness/task-tools.js +221 -0
  32. package/dist/kubernetes/apply.js +49 -1
  33. package/dist/kubernetes/await-running-state.js +172 -13
  34. package/dist/kubernetes/cluster-state.js +245 -0
  35. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +133 -9
  36. package/dist/kubernetes/ensure-kubernetes-service-exists.js +2 -0
  37. package/dist/kubernetes/ensure-persistent-data.js +211 -6
  38. package/dist/kubernetes/follow-pod-logs.js +182 -0
  39. package/dist/kubernetes/job-placement.js +36 -12
  40. package/dist/kubernetes/kubernetes-instance.js +1 -0
  41. package/dist/kubernetes/network-policy.js +2 -1
  42. package/dist/kubernetes/remove-stale-items.js +46 -0
  43. package/dist/kubernetes/restore-kubernetes-data.js +9 -5
  44. package/dist/kubernetes/sandbox-runner.js +628 -0
  45. package/dist/kubernetes/service-rollouts.js +74 -0
  46. package/dist/kubernetes/store-kubernetes-data.js +5 -2
  47. package/dist/kubernetes/volumes.js +94 -9
  48. package/dist/main.js +2 -0
  49. package/dist/optimizer/calculate-checksum.js +3 -2
  50. package/dist/optimizer/check-key.js +94 -0
  51. package/dist/optimizer/check-keys.js +64 -0
  52. package/dist/optimizer/get-work-cache-stats.js +17 -3
  53. package/dist/optimizer/work-task-cache-description.js +49 -1
  54. package/dist/planner/service-definition.js +9 -4
  55. package/dist/planner/utils/append-work-dependencies.js +5 -3
  56. package/dist/planner/utils/append-work-service.js +18 -1
  57. package/dist/planner/utils/append-work-task.js +13 -3
  58. package/dist/planner/utils/get-work-runtime.js +6 -1
  59. package/dist/planner/utils/normalize-path.js +19 -1
  60. package/dist/planner/utils/parse-work-mount.js +7 -1
  61. package/dist/planner/utils/parse-work-port.js +4 -1
  62. package/dist/planner/validate.js +22 -0
  63. package/dist/planner/work-network.js +3 -1
  64. package/dist/planner/work-runtime-aggregate.js +32 -0
  65. package/dist/planner/work-runtime-kubernetes.js +136 -13
  66. package/dist/planner/work-task.js +6 -1
  67. package/dist/program.js +27 -9
  68. package/dist/remote/device-login.js +7 -2
  69. package/dist/remote/host-config.js +69 -3
  70. package/dist/remote/remote-commands.js +33 -7
  71. package/dist/remote/remote-status.js +112 -0
  72. package/dist/remote/remote-work-commands.js +562 -45
  73. package/dist/remote/server-client.js +15 -2
  74. package/dist/remote/token-source.js +64 -26
  75. package/dist/run-program.js +9 -0
  76. package/dist/schema/build-file-base-task-schema.js +3 -3
  77. package/dist/schema/build-file-container-service-schema.js +10 -0
  78. package/dist/schema/build-file-container-task-schema.js +16 -1
  79. package/dist/schema/build-file-include-schema.js +18 -4
  80. package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
  81. package/dist/schema/build-file-task-schema.js +5 -1
  82. package/dist/schema/capabilities-schema.js +56 -0
  83. package/dist/schema/reference-parser.js +119 -11
  84. package/dist/schema/removed-properties.js +20 -0
  85. package/dist/schema/schema-parser.js +94 -41
  86. package/dist/schema/work-scope-parser.js +33 -10
  87. package/dist/secrets/command-secret-provider.js +3 -1
  88. package/dist/secrets/github-app-token-command.js +3 -1
  89. package/dist/secrets/secret-catalog.js +43 -19
  90. package/dist/server/accounts-api.js +4 -1
  91. package/dist/server/approvals-api.js +1 -1
  92. package/dist/server/capacity-format.js +36 -0
  93. package/dist/server/capacity.js +300 -0
  94. package/dist/server/checks-api.js +83 -0
  95. package/dist/server/checks.js +116 -0
  96. package/dist/server/dashboard.js +196 -0
  97. package/dist/server/definitions-api.js +66 -3
  98. package/dist/server/endpoints.js +794 -0
  99. package/dist/server/executor.js +164 -9
  100. package/dist/server/kubernetes-backend.js +415 -48
  101. package/dist/server/kubernetes-sandbox-storage.js +46 -2
  102. package/dist/server/overview.js +271 -0
  103. package/dist/server/plan.js +170 -121
  104. package/dist/server/policy.js +34 -2
  105. package/dist/server/proposals-api.js +17 -8
  106. package/dist/server/repo-access.js +5 -0
  107. package/dist/server/run-logs.js +4 -0
  108. package/dist/server/run-plan.js +38 -4
  109. package/dist/server/run-request.js +58 -3
  110. package/dist/server/run-worker.js +262 -13
  111. package/dist/server/runs.js +962 -36
  112. package/dist/server/sandbox-pod.js +28 -0
  113. package/dist/server/sandbox-storage.js +23 -0
  114. package/dist/server/sandboxes.js +8 -0
  115. package/dist/server/server-config.js +246 -10
  116. package/dist/server/server.js +429 -32
  117. package/dist/server/service-runs.js +82 -0
  118. package/dist/server/timings.js +132 -0
  119. package/dist/server/wait-reason.js +21 -0
  120. package/dist/utils/tar.js +28 -1
  121. package/dist/utils/terminal-prompt.js +53 -0
  122. package/package.json +8 -2
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskToolArgs = exports.taskToolSchema = exports.createTaskTools = exports.describeTasks = exports.describeRun = exports.readLogHint = exports.readTaskListing = exports.LIST_TASKS_MS = void 0;
4
+ const sandbox_client_1 = require("./sandbox-client");
5
+ // the most tasks a description lists, and of a task's description or command it shows
6
+ const MAX_LISTED = 40;
7
+ const MAX_DESCRIPTION_CHARS = 200;
8
+ const MAX_COMMAND_CHARS = 100;
9
+ // how long a client waits for the tasks of the build file when it starts, it goes on without them
10
+ exports.LIST_TASKS_MS = 15000;
11
+ // The tasks of the published version, or why they could not be read within `ms`.
12
+ async function readTaskListing(list, ms = exports.LIST_TASKS_MS) {
13
+ let timer;
14
+ const timeout = new Promise((_, reject) => {
15
+ timer = setTimeout(() => reject(new Error(`no answer within ${ms / 1000} s`)), ms);
16
+ timer.unref?.();
17
+ });
18
+ try {
19
+ return await Promise.race([list(), timeout]);
20
+ }
21
+ catch (e) {
22
+ return { error: e instanceof Error ? e.message : String(e) };
23
+ }
24
+ finally {
25
+ clearTimeout(timer);
26
+ }
27
+ }
28
+ exports.readTaskListing = readTaskListing;
29
+ // the hint at the end of a cut output or a result, for the model
30
+ function readLogHint(run, task) {
31
+ return `read more of the log with run_logs { "run": "${run}", "task": "${task}" }`;
32
+ }
33
+ exports.readLogHint = readLogHint;
34
+ // A run of a task in a few lines: how it went, why it ran, its last lines.
35
+ function describeRun(task, ran) {
36
+ const result = ran.result;
37
+ const lines = [`run ${ran.run.id}: ${ran.run.state}`];
38
+ if (result) {
39
+ lines.push(`task ${task}: ${result.status}` +
40
+ (result.exitCode !== undefined ? `, exit code ${result.exitCode}` : '') +
41
+ (result.durationMs !== undefined ? `, ${result.durationMs} ms` : '') +
42
+ (result.cache?.hit ? ', from the cache' : ''));
43
+ if (result.error) {
44
+ lines.push(`error: ${result.error}`);
45
+ }
46
+ if (result.cache?.causes?.length) {
47
+ lines.push(`ran because: ${result.cache.causes.join('; ')}`);
48
+ }
49
+ }
50
+ else if (ran.run.error) {
51
+ lines.push(`error: ${ran.run.error}`);
52
+ }
53
+ if (result && result.lastLog.length > 0) {
54
+ lines.push('last lines:', ...result.lastLog);
55
+ }
56
+ lines.push(readLogHint(ran.run.id, task));
57
+ return lines.join('\n');
58
+ }
59
+ exports.describeRun = describeRun;
60
+ /**
61
+ * The tasks for the model, one line each: what it is for (or its command, when the build
62
+ * file says nothing) and whether it takes arguments. `shell` is left out, the shell tool runs it.
63
+ */
64
+ function describeTasks(listing, shell) {
65
+ if (listing === undefined) {
66
+ return '';
67
+ }
68
+ if (!Array.isArray(listing)) {
69
+ return `The tasks of the build file could not be listed (${listing.error}); name one as the build file does.`;
70
+ }
71
+ const tasks = listing.filter((task) => !shell || task.name !== shell);
72
+ if (tasks.length === 0) {
73
+ return 'The published build file has no tasks to run.';
74
+ }
75
+ const lines = tasks.slice(0, MAX_LISTED).map((task) => {
76
+ const parts = [`- ${task.name}`];
77
+ if (task.description) {
78
+ parts.push(`: ${cut(oneLine(task.description), MAX_DESCRIPTION_CHARS)}`);
79
+ }
80
+ else if (task.command) {
81
+ parts.push(`: runs \`${cut(oneLine(task.command), MAX_COMMAND_CHARS)}\``);
82
+ }
83
+ if (task.args === null) {
84
+ parts.push(' (takes args)');
85
+ }
86
+ else if (task.args !== undefined || task.command === null) {
87
+ parts.push(' (takes no args)');
88
+ }
89
+ return parts.join('');
90
+ });
91
+ if (tasks.length > MAX_LISTED) {
92
+ lines.push(`- and ${tasks.length - MAX_LISTED} more`);
93
+ }
94
+ return ['The tasks of the published build file:', ...lines].join('\n');
95
+ }
96
+ exports.describeTasks = describeTasks;
97
+ function oneLine(text) {
98
+ return text.replace(/\s+/g, ' ').trim();
99
+ }
100
+ function cut(text, max) {
101
+ return text.length > max ? `${text.slice(0, max)}…` : text;
102
+ }
103
+ /**
104
+ * `run_task`, `run_logs` and `check`, with the tasks the description of `run_task` lists. Without a
105
+ * listing it names none, as the MCP tools before a version could be read.
106
+ */
107
+ function createTaskTools(listing, shell) {
108
+ const tasks = describeTasks(listing, shell);
109
+ return [
110
+ {
111
+ name: 'run_task',
112
+ title: 'Run a task',
113
+ description: 'Run a task of the published build file on the sandbox, such as test or build, and answer how it went: ' +
114
+ 'its status, exit code, why it was not taken from the cache, and its last lines. args are appended to ' +
115
+ 'its last command, for example a test file. It runs in the container, with the network and the ' +
116
+ 'options the build file gives the task.' +
117
+ (tasks ? `\n\n${tasks}` : ''),
118
+ parameters: {
119
+ task: { type: 'string', description: 'The name of the task' },
120
+ args: { type: 'string[]', description: 'Arguments for its last command' },
121
+ },
122
+ required: ['task'],
123
+ async call(client, args, signal) {
124
+ const ran = await client.runTask(args.task, { args: args.args?.length ? args.args : undefined, signal });
125
+ return describeRun(args.task, ran);
126
+ },
127
+ },
128
+ {
129
+ name: 'run_logs',
130
+ title: 'Read the log of a run',
131
+ description: 'Read the log of a run as numbered lines: one task, the last lines (tail), a range (from, limit) or ' +
132
+ 'the lines that contain a text (grep, a fixed string) with lines of context around them.',
133
+ parameters: {
134
+ run: { type: 'string', description: 'The id of the run' },
135
+ task: { type: 'string', description: 'Only the lines of this task' },
136
+ tail: {
137
+ type: 'integer',
138
+ description: 'The last lines, or with grep the last matches',
139
+ minimum: 1,
140
+ maximum: 1000,
141
+ },
142
+ from: { type: 'integer', description: 'The first line to read', minimum: 1 },
143
+ limit: { type: 'integer', description: 'How many lines to read', minimum: 1, maximum: 1000 },
144
+ grep: { type: 'string', description: 'Only lines that contain this text' },
145
+ ignore_case: { type: 'boolean', description: 'Ignore case in grep' },
146
+ context: { type: 'integer', description: 'Lines before and after each match', minimum: 0, maximum: 20 },
147
+ },
148
+ required: ['run'],
149
+ async call(client, args) {
150
+ const page = await client.logs(args.run, {
151
+ task: args.task || undefined,
152
+ tail: args.tail || undefined,
153
+ from: args.from || undefined,
154
+ limit: args.limit || undefined,
155
+ grep: args.grep || undefined,
156
+ i: args.ignore_case || undefined,
157
+ context: args.context || undefined,
158
+ });
159
+ const lines = page.lines.map((line) => `${line.line}${line.match === false ? '-' : ':'} ${args.task ? '' : `[${line.task}] `}${line.text}`);
160
+ lines.push('', `(${page.total} lines${args.grep ? ` searched, ${page.lines.filter((line) => line.match !== false).length} match here` : ''}${page.next ? `, read on with from ${page.next}` : ''}${page.truncated ? ', the server cut the log off at its limit' : ''})`);
161
+ return lines.join('\n');
162
+ },
163
+ },
164
+ {
165
+ name: 'check',
166
+ title: 'Run a check',
167
+ description: sandbox_client_1.CHECK_DESCRIPTION,
168
+ parameters: { task: { type: 'string', description: 'The name of the task' } },
169
+ required: ['task'],
170
+ call(client, args, signal) {
171
+ return (0, sandbox_client_1.runCheck)(client, args.task, signal);
172
+ },
173
+ },
174
+ ];
175
+ }
176
+ exports.createTaskTools = createTaskTools;
177
+ // The parameters as a JSON schema, for MCP and opencode 2.
178
+ function taskToolSchema(tool) {
179
+ return {
180
+ type: 'object',
181
+ properties: Object.fromEntries(Object.entries(tool.parameters).map(([name, parameter]) => {
182
+ if (parameter.type === 'string[]') {
183
+ return [name, { type: 'array', items: { type: 'string' }, description: parameter.description }];
184
+ }
185
+ if (parameter.type === 'integer') {
186
+ const { maximum } = parameter;
187
+ return [
188
+ name,
189
+ {
190
+ type: 'integer',
191
+ description: parameter.description,
192
+ minimum: parameter.minimum,
193
+ ...(maximum !== undefined ? { maximum } : {}),
194
+ },
195
+ ];
196
+ }
197
+ return [name, { type: parameter.type, description: parameter.description }];
198
+ })),
199
+ required: tool.required,
200
+ };
201
+ }
202
+ exports.taskToolSchema = taskToolSchema;
203
+ // The parameters with the zod of opencode 1.
204
+ function taskToolArgs(tool, z) {
205
+ return Object.fromEntries(Object.entries(tool.parameters).map(([name, parameter]) => {
206
+ let type;
207
+ if (parameter.type === 'string[]') {
208
+ type = z.array(z.string());
209
+ }
210
+ else if (parameter.type === 'integer') {
211
+ type = z.number().int().min(parameter.minimum);
212
+ type = parameter.maximum !== undefined ? type.max(parameter.maximum) : type;
213
+ }
214
+ else {
215
+ type = parameter.type === 'boolean' ? z.boolean() : z.string();
216
+ }
217
+ return [name, (tool.required.includes(name) ? type : type.optional()).describe(parameter.description)];
218
+ }));
219
+ }
220
+ exports.taskToolArgs = taskToolArgs;
221
+ //# sourceMappingURL=task-tools.js.map
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.replace = exports.apply = exports.statusCodeOf = void 0;
3
+ exports.replaceDeployment = exports.replace = exports.apply = exports.statusCodeOf = void 0;
4
4
  // The client throws an ApiException carrying the HTTP status as `code`; watch
5
5
  // errors carry it as `statusCode`.
6
6
  function statusCodeOf(e) {
@@ -71,4 +71,52 @@ async function replace(instance, spec) {
71
71
  }
72
72
  }
73
73
  exports.replace = replace;
74
+ /**
75
+ * Makes a Deployment exactly as given, as `replace` does, with a JSON patch of all of it:
76
+ * the runRole of a server has `patch` on Deployments, not `update` (see the backend docs).
77
+ * The patch applies only to the version it read, another change in between reads it again.
78
+ */
79
+ async function replaceDeployment(instance, spec) {
80
+ const { name, namespace } = spec.metadata;
81
+ for (let attempt = 1;; attempt++) {
82
+ let current;
83
+ try {
84
+ current = await instance.appsApi.readNamespacedDeployment({ name, namespace });
85
+ }
86
+ catch (e) {
87
+ if (statusCodeOf(e) !== 404) {
88
+ throw e;
89
+ }
90
+ try {
91
+ return await instance.appsApi.createNamespacedDeployment({ namespace, body: spec });
92
+ }
93
+ catch (createError) {
94
+ if (statusCodeOf(createError) !== 409 || attempt >= 3) {
95
+ throw createError;
96
+ }
97
+ continue;
98
+ }
99
+ }
100
+ try {
101
+ return await instance.appsApi.patchNamespacedDeployment({
102
+ name,
103
+ namespace,
104
+ body: [
105
+ { op: 'test', path: '/metadata/resourceVersion', value: current.metadata?.resourceVersion },
106
+ { op: 'add', path: '/metadata/labels', value: spec.metadata.labels ?? {} },
107
+ { op: 'add', path: '/metadata/annotations', value: spec.metadata.annotations ?? {} },
108
+ { op: 'add', path: '/spec', value: spec.spec },
109
+ ],
110
+ });
111
+ }
112
+ catch (e) {
113
+ // changed since it was read: its test fails (422), or a conflict (409)
114
+ const code = statusCodeOf(e);
115
+ if ((code !== 409 && code !== 422) || attempt >= 3) {
116
+ throw e;
117
+ }
118
+ }
119
+ }
120
+ }
121
+ exports.replaceDeployment = replaceDeployment;
74
122
  //# sourceMappingURL=apply.js.map
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.stopJob = exports.deleteJobAndWait = exports.awaitDeployRunningState = exports.deleteJob = exports.awaitJobCompletion = exports.awaitRunningState = void 0;
3
+ exports.stopJob = exports.deleteJobAndWait = exports.watchKeptPods = exports.KEPT_POD_MS = exports.awaitDeployRollout = exports.awaitDeployRunningState = exports.PASSING_FAILURE_MS = exports.deleteJob = exports.awaitJobCompletion = exports.awaitRunningState = void 0;
4
4
  const abort_1 = require("../executer/abort");
5
5
  const abort_event_1 = require("../utils/abort-event");
6
6
  const apply_1 = require("./apply");
7
7
  const sleep_1 = require("../utils/sleep");
8
+ const cluster_state_1 = require("./cluster-state");
8
9
  // The waits below poll instead of using the client's Watch: Watch aborts every
9
10
  // request after 30s, which a pod or deployment still pulling its image outlasts.
10
11
  async function awaitRunningState(instance, env, name, phase, pollInterval = 1000) {
@@ -76,34 +77,192 @@ async function deleteJob(instance, env, name) {
76
77
  }
77
78
  }
78
79
  exports.deleteJob = deleteJob;
79
- // Waits for a ready replica. The deadline only starts once a pod runs, so an
80
- // image pull doesn't count against it (as on docker, where it starts after
81
- // the container started).
80
+ // how long a reason that may still pass (an image pull a registry refused for now, a
81
+ // container the runtime could not create) may last before it fails the service
82
+ exports.PASSING_FAILURE_MS = 60000;
83
+ // Waits for an available replica: a ready one, for a service without a healthcheck one that
84
+ // stayed ready for a while. The deadline only starts once the container runs, and again
85
+ // at each start of it, so an image pull or a back-off doesn't count against it (as on
86
+ // docker, where it starts after the container started). A pod that won't get there without a change (an image
87
+ // that isn't there, a container that crashed once more than its
88
+ // service's `restarts`, 2 by default) fails it at once, with
89
+ // the reason the cluster gives; one that may still pass fails once it lasted a minute.
90
+ // Only the pods of this Deployment count: not the ones of one deleted before it was
91
+ // created again, which may still be there, terminating or not yet.
82
92
  async function awaitDeployRunningState(instance, env, name, readiness, pollInterval = 1000, now = Date.now) {
83
93
  let runningSince = null;
94
+ // how often the container was restarted when last looked at
95
+ let restarts = 0;
96
+ let said;
97
+ // since when a pod has had a failure that may still pass, by pod
98
+ const failingSince = new Map();
84
99
  for (;;) {
85
100
  const deployment = await instance.appsApi.readNamespacedDeployment({ name, namespace: env.namespace });
86
- if ((deployment.status?.readyReplicas ?? 0) > 0) {
101
+ // available: ready for the Deployment's minReadySeconds
102
+ if ((deployment.status?.availableReplicas ?? 0) > 0) {
87
103
  return;
88
104
  }
105
+ const created = deployment.metadata?.creationTimestamp;
106
+ const listed = await instance.coreApi.listNamespacedPod({
107
+ namespace: env.namespace,
108
+ labelSelector: readiness.podSelector,
109
+ });
110
+ const pods = listed.items.filter((pod) => !pod.metadata?.deletionTimestamp &&
111
+ !(created && pod.metadata?.creationTimestamp && pod.metadata.creationTimestamp < created));
112
+ for (const [index, pod] of pods.entries()) {
113
+ const key = pod.metadata?.name ?? `${index}`;
114
+ const failure = (0, cluster_state_1.getPodFailure)(pod, readiness.container, readiness.restarts);
115
+ if (!failure) {
116
+ failingSince.delete(key);
117
+ continue;
118
+ }
119
+ const since = failingSince.get(key) ?? now();
120
+ failingSince.set(key, since);
121
+ if (failure.final || now() - since >= exports.PASSING_FAILURE_MS) {
122
+ throw new Error(`${readiness.subject ?? name} can not become ready, ${failure.text}`);
123
+ }
124
+ }
125
+ const waiting = pods.map((pod) => (0, cluster_state_1.getPodWaiting)(pod, readiness.container)).find((text) => !!text);
126
+ if (waiting && waiting !== said) {
127
+ readiness.onWaiting?.(waiting);
128
+ }
129
+ said = waiting;
89
130
  if (readiness.timeout !== null) {
90
- if (runningSince === null) {
91
- const pods = await instance.coreApi.listNamespacedPod({
92
- namespace: env.namespace,
93
- labelSelector: readiness.podSelector,
94
- });
95
- if (pods.items.some((pod) => pod.status?.phase === 'Running')) {
96
- runningSince = now();
97
- }
131
+ // the deadline bounds each start of the container, as on Docker: a crash is counted by
132
+ // `restarts`, and the back-off before the next start doesn't count against it
133
+ const statuses = pods.flatMap((pod) => (pod.status?.containerStatuses ?? []).filter((status) => !readiness.container || status.name === readiness.container));
134
+ const running = statuses.length > 0
135
+ ? statuses.some((status) => status.state?.running)
136
+ : pods.some((pod) => pod.status?.phase === 'Running');
137
+ const restarted = Math.max(0, ...statuses.map((status) => status.restartCount ?? 0));
138
+ if (!running) {
139
+ runningSince = null;
140
+ }
141
+ else if (runningSince === null || restarted !== restarts) {
142
+ runningSince = now();
98
143
  }
99
144
  else if (now() - runningSince >= readiness.timeout) {
100
145
  throw new Error(readiness.timeoutMessage);
101
146
  }
147
+ restarts = restarted;
102
148
  }
103
149
  await (0, sleep_1.sleep)(pollInterval);
104
150
  }
105
151
  }
106
152
  exports.awaitDeployRunningState = awaitDeployRunningState;
153
+ /**
154
+ * Waits until every pod of a Deployment is of its spec `generation` and available, as a
155
+ * rollout of a changed service does. Fails when Kubernetes gave up on it (its progress
156
+ * deadline), when a new pod (`podSelector`) won't get there without a change (as
157
+ * awaitDeployRunningState tells), or once one ran for the readiness timeout without getting
158
+ * ready.
159
+ */
160
+ async function awaitDeployRollout(instance, env, name, readiness, pollInterval = 1000, now = Date.now) {
161
+ const subject = readiness.subject ?? name;
162
+ const timeoutMessage = readiness.timeoutMessage || `${subject} did not get ready`;
163
+ let runningSince = null;
164
+ let said;
165
+ const failingSince = new Map();
166
+ for (;;) {
167
+ if (readiness.signal?.aborted) {
168
+ throw new abort_1.AbortError();
169
+ }
170
+ const deployment = await instance.appsApi.readNamespacedDeployment({ name, namespace: env.namespace });
171
+ const status = deployment.status;
172
+ if ((status?.observedGeneration ?? 0) >= readiness.generation &&
173
+ (status?.updatedReplicas ?? 0) >= 1 &&
174
+ (status?.availableReplicas ?? 0) >= 1 &&
175
+ (status?.replicas ?? 0) === (status?.updatedReplicas ?? 0)) {
176
+ return;
177
+ }
178
+ const stalled = status?.conditions?.find((condition) => condition.type === 'Progressing' && condition.reason === 'ProgressDeadlineExceeded');
179
+ if (stalled && (status?.observedGeneration ?? 0) >= readiness.generation) {
180
+ throw new Error(`${timeoutMessage}: ${stalled.message ?? 'its rollout made no progress'}`);
181
+ }
182
+ const listed = await instance.coreApi.listNamespacedPod({
183
+ namespace: env.namespace,
184
+ labelSelector: readiness.podSelector,
185
+ });
186
+ const pods = listed.items.filter((pod) => !pod.metadata?.deletionTimestamp);
187
+ for (const [index, pod] of pods.entries()) {
188
+ const key = pod.metadata?.name ?? `${index}`;
189
+ const failure = (0, cluster_state_1.getPodFailure)(pod, readiness.container);
190
+ if (!failure) {
191
+ failingSince.delete(key);
192
+ continue;
193
+ }
194
+ const since = failingSince.get(key) ?? now();
195
+ failingSince.set(key, since);
196
+ if (failure.final || now() - since >= exports.PASSING_FAILURE_MS) {
197
+ throw new Error(`${subject} can not become ready, ${failure.text}`);
198
+ }
199
+ }
200
+ const waiting = pods.map((pod) => (0, cluster_state_1.getPodWaiting)(pod, readiness.container)).find((text) => !!text);
201
+ if (waiting && waiting !== said) {
202
+ readiness.onWaiting?.(waiting);
203
+ }
204
+ said = waiting;
205
+ if (readiness.timeout !== null) {
206
+ if (runningSince === null) {
207
+ if (pods.some((pod) => pod.status?.phase === 'Running')) {
208
+ runningSince = now();
209
+ }
210
+ }
211
+ else if (now() - runningSince >= readiness.timeout) {
212
+ throw new Error(timeoutMessage);
213
+ }
214
+ }
215
+ await sleep(pollInterval, readiness.signal ?? new AbortController().signal);
216
+ }
217
+ }
218
+ exports.awaitDeployRollout = awaitDeployRollout;
219
+ // how long the pod a service that watches its files keeps has to go on running once it got
220
+ // the changed files
221
+ exports.KEPT_POD_MS = 15000;
222
+ /**
223
+ * Watches the pods of a Deployment that stays (a service that watches its files got the
224
+ * changed ones, see syncLiveFiles) for `windowMs`: fails when one of them restarts, fails as
225
+ * awaitDeployRunningState tells, or the Deployment has no available pod any more, so a move
226
+ * whose files break the service does not count as up.
227
+ */
228
+ async function watchKeptPods(instance, env, name, readiness, windowMs = exports.KEPT_POD_MS, pollInterval = 1000, now = Date.now) {
229
+ const subject = readiness.subject ?? name;
230
+ const restarts = new Map();
231
+ const started = now();
232
+ for (;;) {
233
+ if (readiness.signal?.aborted) {
234
+ throw new abort_1.AbortError();
235
+ }
236
+ const listed = await instance.coreApi.listNamespacedPod({
237
+ namespace: env.namespace,
238
+ labelSelector: readiness.podSelector,
239
+ });
240
+ for (const pod of listed.items.filter((pod) => !pod.metadata?.deletionTimestamp)) {
241
+ const key = pod.metadata?.name ?? '';
242
+ const failure = (0, cluster_state_1.getPodFailure)(pod, readiness.container, readiness.restarts);
243
+ if (failure) {
244
+ throw new Error(`${subject} stopped working with the changed files, ${failure.text}`);
245
+ }
246
+ const count = (pod.status?.containerStatuses ?? [])
247
+ .filter((status) => !readiness.container || status.name === readiness.container)
248
+ .reduce((sum, status) => sum + (status.restartCount ?? 0), 0);
249
+ const before = restarts.get(key);
250
+ if (before !== undefined && count > before) {
251
+ throw new Error(`${subject} stopped working with the changed files, its container restarted`);
252
+ }
253
+ restarts.set(key, count);
254
+ }
255
+ const deployment = await instance.appsApi.readNamespacedDeployment({ name, namespace: env.namespace });
256
+ if ((deployment.status?.availableReplicas ?? 0) < 1) {
257
+ throw new Error(`${subject} stopped working with the changed files, it has no ready pod`);
258
+ }
259
+ if (now() - started >= windowMs) {
260
+ return;
261
+ }
262
+ await sleep(pollInterval, readiness.signal ?? new AbortController().signal);
263
+ }
264
+ }
265
+ exports.watchKeptPods = watchKeptPods;
107
266
  // Delete a job and wait until it is gone: its name is reused for the task's next
108
267
  // command (and the next run with the same state), and creating it while the old
109
268
  // one is still being deleted would pick up the old job.