hammerkit 1.27.0-next.9 → 2.0.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.schema.json +176 -6
- package/dist/cache/backends/local-cache-backend.js +6 -0
- package/dist/cache/backends/registry/registry-client.js +1 -0
- package/dist/cache/cache-sync.js +1 -1
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +2 -0
- package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
- package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
- package/dist/dashboard/favicon.svg +1 -0
- package/dist/dashboard/index.html +15 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/archive-task-entry.js +4 -2
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/deferred-restore.js +36 -0
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/docker-task.js +21 -1
- package/dist/executer/execute-work-service.js +2 -0
- package/dist/executer/execute-work-task.js +11 -4
- package/dist/executer/execution-steps.js +25 -4
- package/dist/executer/reset-work-tree.js +4 -0
- package/dist/executer/scheduler/enqueue-next.js +57 -6
- package/dist/executer/scheduler/state-key.js +42 -5
- package/dist/executer/watch-loop.js +6 -3
- package/dist/harness/connect.js +20 -6
- package/dist/harness/mcp-command.js +22 -6
- package/dist/harness/mcp.js +63 -114
- package/dist/harness/opencode.js +158 -50
- package/dist/harness/pi.js +29 -0
- package/dist/harness/sandbox-client.js +194 -23
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +49 -1
- package/dist/kubernetes/await-running-state.js +172 -13
- package/dist/kubernetes/cluster-state.js +245 -0
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +133 -9
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +2 -0
- package/dist/kubernetes/ensure-persistent-data.js +211 -6
- package/dist/kubernetes/follow-pod-logs.js +182 -0
- package/dist/kubernetes/job-placement.js +36 -12
- package/dist/kubernetes/kubernetes-instance.js +1 -0
- package/dist/kubernetes/network-policy.js +2 -1
- package/dist/kubernetes/remove-stale-items.js +46 -0
- package/dist/kubernetes/restore-kubernetes-data.js +9 -5
- package/dist/kubernetes/sandbox-runner.js +628 -0
- package/dist/kubernetes/service-rollouts.js +74 -0
- package/dist/kubernetes/store-kubernetes-data.js +5 -2
- package/dist/kubernetes/volumes.js +94 -9
- package/dist/main.js +2 -0
- package/dist/optimizer/calculate-checksum.js +3 -2
- package/dist/optimizer/check-key.js +94 -0
- package/dist/optimizer/check-keys.js +64 -0
- package/dist/optimizer/get-work-cache-stats.js +17 -3
- package/dist/optimizer/work-task-cache-description.js +49 -1
- package/dist/planner/service-definition.js +9 -4
- package/dist/planner/utils/append-work-dependencies.js +5 -3
- package/dist/planner/utils/append-work-service.js +18 -1
- package/dist/planner/utils/append-work-task.js +13 -3
- package/dist/planner/utils/get-work-runtime.js +6 -1
- package/dist/planner/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/planner/validate.js +22 -0
- package/dist/planner/work-network.js +3 -1
- package/dist/planner/work-runtime-aggregate.js +32 -0
- package/dist/planner/work-runtime-kubernetes.js +136 -13
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +27 -9
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +69 -3
- package/dist/remote/remote-commands.js +33 -7
- package/dist/remote/remote-status.js +112 -0
- package/dist/remote/remote-work-commands.js +562 -45
- package/dist/remote/server-client.js +15 -2
- package/dist/remote/token-source.js +64 -26
- package/dist/run-program.js +9 -0
- package/dist/schema/build-file-base-task-schema.js +3 -3
- package/dist/schema/build-file-container-service-schema.js +10 -0
- package/dist/schema/build-file-container-task-schema.js +16 -1
- package/dist/schema/build-file-include-schema.js +18 -4
- package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
- package/dist/schema/build-file-task-schema.js +5 -1
- package/dist/schema/capabilities-schema.js +56 -0
- package/dist/schema/reference-parser.js +119 -11
- package/dist/schema/removed-properties.js +20 -0
- package/dist/schema/schema-parser.js +94 -41
- package/dist/schema/work-scope-parser.js +33 -10
- package/dist/secrets/command-secret-provider.js +3 -1
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/secrets/secret-catalog.js +43 -19
- package/dist/server/accounts-api.js +4 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/capacity-format.js +36 -0
- package/dist/server/capacity.js +300 -0
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/dashboard.js +196 -0
- package/dist/server/definitions-api.js +66 -3
- package/dist/server/endpoints.js +794 -0
- package/dist/server/executor.js +164 -9
- package/dist/server/kubernetes-backend.js +415 -48
- package/dist/server/kubernetes-sandbox-storage.js +46 -2
- package/dist/server/overview.js +271 -0
- package/dist/server/plan.js +170 -121
- package/dist/server/policy.js +34 -2
- package/dist/server/proposals-api.js +17 -8
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-logs.js +4 -0
- package/dist/server/run-plan.js +38 -4
- package/dist/server/run-request.js +58 -3
- package/dist/server/run-worker.js +262 -13
- package/dist/server/runs.js +962 -36
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-config.js +246 -10
- package/dist/server/server.js +429 -32
- package/dist/server/service-runs.js +82 -0
- package/dist/server/timings.js +132 -0
- package/dist/server/wait-reason.js +21 -0
- package/dist/utils/tar.js +28 -1
- package/dist/utils/terminal-prompt.js +53 -0
- package/package.json +8 -2
package/dist/server/executor.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HostExecutor = exports.volumeScope = exports.cacheNamespace = exports.defaultWorker = void 0;
|
|
3
|
+
exports.HostExecutor = exports.removeServiceRunVolumes = exports.serviceRunScopes = exports.volumeScope = exports.cacheNamespace = exports.defaultWorker = void 0;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const crypto_1 = require("crypto");
|
|
6
|
+
const checks_1 = require("./checks");
|
|
6
7
|
const fs_1 = require("fs");
|
|
7
8
|
const promises_1 = require("fs/promises");
|
|
8
9
|
const path_1 = require("path");
|
|
@@ -11,6 +12,7 @@ const resolve_git_source_1 = require("../includes/resolve-git-source");
|
|
|
11
12
|
const git_checkout_1 = require("./git-checkout");
|
|
12
13
|
const run_plan_1 = require("./run-plan");
|
|
13
14
|
const work_network_1 = require("../planner/work-network");
|
|
15
|
+
const execute_docker_1 = require("../executer/execute-docker");
|
|
14
16
|
// `.git` as a file system may read it: any case, and a name Windows reads without its
|
|
15
17
|
// trailing dots and spaces, or by its short name
|
|
16
18
|
function isGitDirectory(segment) {
|
|
@@ -35,19 +37,65 @@ exports.defaultWorker = defaultWorker;
|
|
|
35
37
|
// share one, those of an upload or a sandbox only their owner's. Somebody who can only
|
|
36
38
|
// upload, or an agent on a sandbox, can not leave an entry for a run from a reviewed
|
|
37
39
|
// repository to find.
|
|
40
|
+
//
|
|
41
|
+
// A check on a sandbox runs on a copy of the files a commit of them would hold, so it is a
|
|
42
|
+
// run of the repository like a ref run of that commit, and shares its cache: what its
|
|
43
|
+
// owner may write in the sandbox is not part of it. It leaves entries a run from main can
|
|
44
|
+
// find, keyed by the files it ran on, as a ref run of a branch does: so only a caller who
|
|
45
|
+
// may push a branch of the repository and run from it may check (server.ts).
|
|
38
46
|
function cacheNamespace(run) {
|
|
39
|
-
const
|
|
40
|
-
|
|
47
|
+
const repository = run.source.type === 'ref' || (run.source.type === 'sandbox' && run.source.check);
|
|
48
|
+
const key = repository
|
|
49
|
+
? `ref\0${run.source.repo}`
|
|
50
|
+
: `${run.source.type}\0${run.owner.iss}\0${run.owner.sub}`;
|
|
51
|
+
return `${repository ? 'ref' : run.source.type}-${(0, crypto_1.createHash)('sha256').update(key).digest('hex').slice(0, 24)}`;
|
|
41
52
|
}
|
|
42
53
|
exports.cacheNamespace = cacheNamespace;
|
|
43
54
|
// The docker volumes of a run's services are its caller's, within the cache's source trust:
|
|
44
55
|
// two callers of one repository share a cache, a task is keyed by what it reads, but not the
|
|
45
|
-
// data their services keep.
|
|
46
|
-
|
|
56
|
+
// data their services keep. A service run's are its own: they stay while it sleeps and go
|
|
57
|
+
// when it ends (removeServiceRunVolumes). They carry the server's key (see serviceRunScopes),
|
|
58
|
+
// so a server sweeps its own only, next to another one on the same docker.
|
|
59
|
+
function volumeScope(run, server = '') {
|
|
60
|
+
if (run.up) {
|
|
61
|
+
return `${serviceRunScopes(server)}${(0, crypto_1.createHash)('sha256').update(run.id).digest('hex').slice(0, 24)}`;
|
|
62
|
+
}
|
|
47
63
|
const key = `${cacheNamespace(run)}\0${run.owner.iss}\0${run.owner.sub}`;
|
|
48
64
|
return `${run.source.type}-${(0, crypto_1.createHash)('sha256').update(key).digest('hex').slice(0, 24)}`;
|
|
49
65
|
}
|
|
50
66
|
exports.volumeScope = volumeScope;
|
|
67
|
+
// the start of the volume scopes of the service runs of a server, by the directory its runs
|
|
68
|
+
// work in
|
|
69
|
+
function serviceRunScopes(server) {
|
|
70
|
+
return `up-${(0, crypto_1.createHash)('sha256').update(server).digest('hex').slice(0, 8)}-`;
|
|
71
|
+
}
|
|
72
|
+
exports.serviceRunScopes = serviceRunScopes;
|
|
73
|
+
// the volume of a service of a service run, with the scope of its run (see scopeServiceVolumes)
|
|
74
|
+
const SERVICE_RUN_VOLUME = /^hammerkit-(up-[0-9a-f]{8}-[0-9a-f]{24})-/;
|
|
75
|
+
/**
|
|
76
|
+
* Removes the docker volumes of the services of service runs whose scope `remove` names: of
|
|
77
|
+
* one run that ended, or at a start, of every run but those that sleep with their volumes.
|
|
78
|
+
* One a container still uses stays, and is said.
|
|
79
|
+
*/
|
|
80
|
+
async function removeServiceRunVolumes(docker, remove, log) {
|
|
81
|
+
const listed = await docker.listVolumes({ filters: { name: ['hammerkit-up-'] } });
|
|
82
|
+
const removed = [];
|
|
83
|
+
for (const volume of listed.Volumes ?? []) {
|
|
84
|
+
const scope = SERVICE_RUN_VOLUME.exec(volume.Name)?.[1];
|
|
85
|
+
if (!scope || !remove(scope)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
await docker.getVolume(volume.Name).remove();
|
|
90
|
+
removed.push(volume.Name);
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
log(`the volume ${volume.Name} could not be removed: ${e instanceof Error ? e.message : String(e)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return removed;
|
|
97
|
+
}
|
|
98
|
+
exports.removeServiceRunVolumes = removeServiceRunVolumes;
|
|
51
99
|
/**
|
|
52
100
|
* Runs the tasks of a run on this machine with the engine of `hammerkit run`, in a
|
|
53
101
|
* process of its own.
|
|
@@ -65,6 +113,56 @@ exports.volumeScope = volumeScope;
|
|
|
65
113
|
class HostExecutor {
|
|
66
114
|
constructor(options) {
|
|
67
115
|
this.options = options;
|
|
116
|
+
// the directories of the executions that run now
|
|
117
|
+
this.directories = new Set();
|
|
118
|
+
}
|
|
119
|
+
// the docker daemon the runs' services are on, as their processes reach it
|
|
120
|
+
docker() {
|
|
121
|
+
return (0, execute_docker_1.createDockerClient)(this.options.planning.environment.processEnvs.DOCKER_HOST);
|
|
122
|
+
}
|
|
123
|
+
// the volumes of a service run's services, which its end removes
|
|
124
|
+
async removeVolumes(run, context) {
|
|
125
|
+
const scope = volumeScope(run, this.options.directory);
|
|
126
|
+
const say = (text) => {
|
|
127
|
+
this.options.log?.(`run=${run.id} ${text}`);
|
|
128
|
+
context?.emit({ type: 'log', task: 'hammerkit', stream: 'stderr', text });
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
await removeServiceRunVolumes(this.docker(), (candidate) => candidate === scope, say);
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
say(`the volumes of the services could not be removed: ${e instanceof Error ? e.message : String(e)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** A service run that slept with its volumes ended without waking: they go. */
|
|
138
|
+
async forget(run) {
|
|
139
|
+
if (run.up && !this.options.placement) {
|
|
140
|
+
await this.removeVolumes(run);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* At the start of the server: the volumes of service runs go, but of those that sleep with
|
|
145
|
+
* them. A server without docker has none.
|
|
146
|
+
*/
|
|
147
|
+
restored(runs) {
|
|
148
|
+
if (this.options.placement) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const own = serviceRunScopes(this.options.directory);
|
|
152
|
+
const kept = new Set(runs.filter((run) => run.up).map((run) => volumeScope(run, this.options.directory)));
|
|
153
|
+
const log = (line) => this.options.log?.(line);
|
|
154
|
+
removeServiceRunVolumes(this.docker(), (scope) => scope.startsWith(own) && !kept.has(scope), log)
|
|
155
|
+
.then((removed) => {
|
|
156
|
+
if (removed.length > 0) {
|
|
157
|
+
log(`removed ${removed.length} volumes of service runs from before the server started`);
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
.catch((e) => {
|
|
161
|
+
const code = e.code;
|
|
162
|
+
if (code !== 'ENOENT' && code !== 'ECONNREFUSED') {
|
|
163
|
+
log(`the volumes of earlier service runs could not be removed: ${e instanceof Error ? e.message : e}`);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
68
166
|
}
|
|
69
167
|
async execute(run, context) {
|
|
70
168
|
const { definitions, planning, repos } = this.options;
|
|
@@ -110,7 +208,8 @@ class HostExecutor {
|
|
|
110
208
|
abort();
|
|
111
209
|
}
|
|
112
210
|
context.signal.addEventListener('abort', abort, { once: true });
|
|
113
|
-
|
|
211
|
+
// a service run that moves executes beside itself, each in a directory of its own
|
|
212
|
+
const root = this.directoryOf(run);
|
|
114
213
|
let attachment;
|
|
115
214
|
try {
|
|
116
215
|
// where the process of the run starts: the checkout of a ref, or for a sandbox the
|
|
@@ -121,13 +220,42 @@ class HostExecutor {
|
|
|
121
220
|
let claim;
|
|
122
221
|
// the files of the sandbox the tasks work on
|
|
123
222
|
let work;
|
|
124
|
-
if (source.type === '
|
|
223
|
+
if (source.type === 'sandbox' && source.check) {
|
|
224
|
+
// a check works on the files a push of the sandbox commits, copied out of it: never
|
|
225
|
+
// on the sandbox, whose ignored files (node_modules, outputs) its owner writes
|
|
226
|
+
const { registry, network } = sandboxes;
|
|
227
|
+
cwd = (0, path_1.join)(root, 'source');
|
|
228
|
+
try {
|
|
229
|
+
await registry.exportTree(source.id, source.tree, cwd, abortCtrl.signal);
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
throw new Error(`the files of the sandbox ${source.id} could not be copied for the check, submit it again: ${e instanceof Error ? e.message : String(e)}`);
|
|
233
|
+
}
|
|
234
|
+
// a repository of its own, as a checkout of the commit is: the root of the project,
|
|
235
|
+
// which the paths of the tasks' keys are relative to, is the one of git
|
|
236
|
+
await (0, git_checkout_1.git)(['init', '--quiet', cwd], {}, abortCtrl.signal, `the check of sandbox ${source.id}`, 'init');
|
|
237
|
+
context.emit({
|
|
238
|
+
type: 'log',
|
|
239
|
+
task: 'hammerkit',
|
|
240
|
+
stream: 'stdout',
|
|
241
|
+
text: `check of sandbox ${source.id} of ${source.repo}: files ${source.tree}, fetched at ${source.commit}`,
|
|
242
|
+
});
|
|
243
|
+
await this.writeBundle(cwd, bundle.files);
|
|
244
|
+
input = {
|
|
245
|
+
entry: (0, path_1.join)(cwd, version.entry),
|
|
246
|
+
...(planning.policy.network ? { network: (0, work_network_1.toNetworkSchema)(planning.policy.network) } : {}),
|
|
247
|
+
sandboxNetwork: network,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
else if (source.type === 'ref') {
|
|
125
251
|
cwd = (0, path_1.join)(root, 'source');
|
|
126
252
|
const { repo, ref, commit: pinned } = source;
|
|
127
253
|
const credentials = await repos.credentials(repo);
|
|
128
254
|
let commit;
|
|
129
255
|
try {
|
|
130
|
-
commit
|
|
256
|
+
// a woken service run starts the commit it started first, where the ref is now or not
|
|
257
|
+
const fetched = run.wakes ? pinned : ref;
|
|
258
|
+
commit = await (0, git_checkout_1.checkoutRef)(repos.url(repo), fetched, cwd, credentials.env, abortCtrl.signal, pinned);
|
|
131
259
|
}
|
|
132
260
|
catch (e) {
|
|
133
261
|
throw repos.scrub(e, credentials.values);
|
|
@@ -187,27 +315,51 @@ class HostExecutor {
|
|
|
187
315
|
tasks: run.tasks,
|
|
188
316
|
options: run.options,
|
|
189
317
|
accounts: run.accounts,
|
|
190
|
-
volumeScope: volumeScope(run),
|
|
318
|
+
volumeScope: volumeScope(run, this.options.directory),
|
|
319
|
+
...((0, checks_1.isCheckRun)(run) ? { check: true } : {}),
|
|
191
320
|
catalog: planning.environment.hosted?.catalog,
|
|
321
|
+
...(run.up
|
|
322
|
+
? {
|
|
323
|
+
up: {
|
|
324
|
+
services: run.up.services,
|
|
325
|
+
endpoints: run.up.endpoints.map(({ service, port }) => ({ service, port })),
|
|
326
|
+
},
|
|
327
|
+
}
|
|
328
|
+
: {}),
|
|
192
329
|
...(placed ? { kubernetes: placed.kubernetes } : {}),
|
|
330
|
+
...(!placed && this.options.capabilityRuntime ? { capabilityRuntime: this.options.capabilityRuntime } : {}),
|
|
193
331
|
}, cwd, { ...this.processEnvironment(home, cache), ...placed?.env }, context, abortCtrl.signal);
|
|
194
332
|
}
|
|
195
333
|
finally {
|
|
196
334
|
if (placement && placed) {
|
|
197
335
|
await placement.release(run, context);
|
|
198
336
|
}
|
|
337
|
+
else if (run.up && !context.keep?.()) {
|
|
338
|
+
// the services are gone with the process, their volumes stay only while the run sleeps
|
|
339
|
+
await this.removeVolumes(run, context);
|
|
340
|
+
}
|
|
199
341
|
}
|
|
200
342
|
}
|
|
201
343
|
finally {
|
|
202
344
|
context.signal.removeEventListener('abort', abort);
|
|
203
345
|
attachment?.release?.();
|
|
204
346
|
await this.removeRunDirectory(run, root, context);
|
|
347
|
+
this.directories.delete(root);
|
|
205
348
|
if (source.type === 'sandbox') {
|
|
206
349
|
// what the run wrote counts against the owner's limits, from their next run or create
|
|
207
350
|
sandboxes?.registry.measure(source.id).catch(() => undefined);
|
|
208
351
|
}
|
|
209
352
|
}
|
|
210
353
|
}
|
|
354
|
+
directoryOf(run) {
|
|
355
|
+
for (let execution = 1;; execution++) {
|
|
356
|
+
const root = (0, path_1.join)(this.options.directory, execution === 1 ? run.id : `${run.id}-${execution}`);
|
|
357
|
+
if (!this.directories.has(root)) {
|
|
358
|
+
this.directories.add(root);
|
|
359
|
+
return root;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
211
363
|
// A task of the run may still hold a file of its directory for a moment after the run
|
|
212
364
|
// ended, which Windows refuses to remove. A directory that stays is reported, to the run
|
|
213
365
|
// by its error code and to the operator with its path; it does not change how the run ended.
|
|
@@ -282,6 +434,9 @@ class HostExecutor {
|
|
|
282
434
|
if (message.kind === 'event') {
|
|
283
435
|
context.emit(message.event);
|
|
284
436
|
}
|
|
437
|
+
else if (message.kind === 'up') {
|
|
438
|
+
context.up?.(message.targets);
|
|
439
|
+
}
|
|
285
440
|
else if (message.kind === 'result') {
|
|
286
441
|
result = message;
|
|
287
442
|
// nothing left to tell the process
|