hammerkit 1.25.0 → 1.27.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 +53 -1
- package/dist/cache/backends/local-cache-backend.js +10 -2
- package/dist/cache/explain.js +13 -11
- package/dist/cli.js +3 -0
- package/dist/docker/item-network.js +471 -0
- package/dist/docker/pull.js +4 -2
- package/dist/docker/remove-orphaned-containers.js +13 -6
- package/dist/environment/replace-env-variables.js +8 -6
- package/dist/executer/await-completed-dependencies.js +2 -1
- package/dist/executer/check-readiness.js +2 -2
- package/dist/executer/container-task-state.js +8 -6
- package/dist/executer/docker-service.js +24 -13
- package/dist/executer/docker-task.js +18 -29
- package/dist/executer/event-cache.js +4 -0
- package/dist/executer/execute-command.js +18 -5
- package/dist/executer/execute-docker.js +4 -1
- package/dist/executer/execution-steps.js +33 -7
- package/dist/executer/get-docker-executor.js +14 -7
- package/dist/executer/scheduler/enqueue-next.js +17 -3
- package/dist/executer/watch-loop.js +2 -2
- package/dist/executer/watch-state-key.js +1 -1
- package/dist/file/within-root.js +42 -0
- package/dist/harness/connect.js +99 -0
- package/dist/harness/format.js +73 -0
- package/dist/harness/mcp-command.js +59 -0
- package/dist/harness/mcp.js +432 -0
- package/dist/harness/opencode.js +492 -0
- package/dist/harness/pi.js +217 -0
- package/dist/harness/sandbox-client.js +373 -0
- package/dist/index.js +33 -36
- package/dist/kubernetes/apply.js +31 -1
- package/dist/kubernetes/await-running-state.js +33 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +44 -22
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +131 -4
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +25 -4
- package/dist/kubernetes/ensure-persistent-data.js +48 -7
- package/dist/kubernetes/get-service-ip.js +15 -1
- package/dist/kubernetes/job-placement.js +99 -0
- package/dist/kubernetes/job-secrets.js +306 -0
- package/dist/kubernetes/kept-claims.js +191 -0
- package/dist/kubernetes/kubernetes-instance.js +10 -2
- package/dist/kubernetes/network-gate.js +71 -0
- package/dist/kubernetes/network-policy.js +526 -0
- package/dist/kubernetes/resources.js +13 -2
- package/dist/kubernetes/restore-kubernetes-data.js +19 -4
- package/dist/kubernetes/secrets.js +2 -0
- package/dist/kubernetes/store-kubernetes-data.js +1 -1
- package/dist/kubernetes/volumes.js +100 -11
- package/dist/main.js +41 -0
- package/dist/network/egress-proxy.js +368 -0
- package/dist/network/port-relay.js +35 -0
- package/dist/optimizer/get-work-cache-stats.js +16 -0
- package/dist/optimizer/work-task-cache-description.js +3 -2
- package/dist/planner/service-definition.js +8 -2
- package/dist/planner/utils/append-work-environment.js +2 -0
- package/dist/planner/utils/append-work-service.js +4 -0
- package/dist/planner/utils/append-work-task.js +17 -1
- package/dist/planner/utils/assert-item-paths.js +50 -0
- package/dist/planner/utils/hosted-volumes.js +35 -0
- package/dist/planner/utils/task-arguments.js +155 -0
- package/dist/planner/work-cache-id.js +15 -1
- package/dist/planner/work-network.js +193 -0
- package/dist/planner/work-runtime-docker.js +43 -0
- package/dist/planner/work-runtime-kubernetes.js +336 -102
- package/dist/planner/work-runtime-local.js +16 -8
- package/dist/planner/work-service.js +7 -3
- package/dist/planner/work-state-record.js +30 -0
- package/dist/program.js +80 -7
- package/dist/remote/annotations.js +103 -0
- package/dist/remote/bundle-text.js +24 -0
- package/dist/remote/bundle.js +9 -1
- package/dist/remote/remote-commands.js +4 -1
- package/dist/remote/remote-work-commands.js +182 -69
- package/dist/remote/server-client.js +18 -5
- package/dist/remote/source-manifest.js +15 -2
- package/dist/remote/token-source.js +67 -0
- package/dist/schema/build-file-base-task-schema.js +3 -0
- package/dist/schema/build-file-container-service-schema.js +6 -0
- package/dist/schema/build-file-container-task-schema.js +3 -0
- package/dist/schema/build-file-environment-schema.js +9 -0
- package/dist/schema/network-schema.js +78 -0
- package/dist/schema/reference-parser.js +1 -0
- package/dist/schema/schema-parser.js +16 -3
- package/dist/schema/work-scope-parser.js +13 -0
- package/dist/secrets/command-secret-provider.js +73 -10
- package/dist/secrets/github-app-token-command.js +23 -0
- package/dist/secrets/github-app-token.js +191 -0
- package/dist/server/accounts-api.js +9 -2
- package/dist/server/definitions-api.js +69 -47
- package/dist/server/definitions.js +76 -51
- package/dist/server/executor.js +156 -41
- package/dist/server/git-checkout.js +72 -8
- package/dist/server/kubernetes-backend.js +554 -0
- package/dist/server/kubernetes-sandbox-storage.js +996 -0
- package/dist/server/oauth.js +1 -0
- package/dist/server/plan.js +51 -1
- package/dist/server/policy.js +104 -15
- package/dist/server/proposals-api.js +24 -40
- package/dist/server/proposals.js +12 -25
- package/dist/server/repo-access.js +37 -6
- package/dist/server/run-logs.js +217 -0
- package/dist/server/run-plan.js +51 -2
- package/dist/server/run-request.js +23 -10
- package/dist/server/run-store.js +5 -0
- package/dist/server/run-worker-command.js +13 -1
- package/dist/server/run-worker.js +121 -24
- package/dist/server/runs.js +184 -10
- package/dist/server/sandbox-docker.js +62 -0
- package/dist/server/sandbox-files-api.js +269 -0
- package/dist/server/sandbox-files.js +1054 -0
- package/dist/server/sandbox-patch.js +481 -0
- package/dist/server/sandbox-pod-command.js +15 -0
- package/dist/server/sandbox-pod.js +146 -0
- package/dist/server/sandbox-storage.js +462 -0
- package/dist/server/sandboxes-api.js +210 -0
- package/dist/server/sandboxes.js +519 -0
- package/dist/server/server-config.js +412 -22
- package/dist/server/server.js +204 -39
- package/dist/utils/run-id.js +12 -0
- package/harness/opencode.ts +11 -0
- package/harness/pi.ts +7 -0
- package/package.json +8 -2
- package/dist/remote/text-diff.js +0 -89
package/dist/server/executor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HostExecutor = exports.defaultWorker = void 0;
|
|
3
|
+
exports.HostExecutor = 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
6
|
const fs_1 = require("fs");
|
|
@@ -10,13 +10,15 @@ const readline_1 = require("readline");
|
|
|
10
10
|
const resolve_git_source_1 = require("../includes/resolve-git-source");
|
|
11
11
|
const git_checkout_1 = require("./git-checkout");
|
|
12
12
|
const run_plan_1 = require("./run-plan");
|
|
13
|
+
const work_network_1 = require("../planner/work-network");
|
|
13
14
|
// `.git` as a file system may read it: any case, and a name Windows reads without its
|
|
14
15
|
// trailing dots and spaces, or by its short name
|
|
15
16
|
function isGitDirectory(segment) {
|
|
16
17
|
const name = segment.toLowerCase().replace(/[. ]+$/, '');
|
|
17
18
|
return name === '.git' || name === 'git~1';
|
|
18
19
|
}
|
|
19
|
-
// A cancelled run gets this long to stop its tasks and remove its containers
|
|
20
|
+
// A cancelled run gets this long to stop its tasks and remove its containers, after that
|
|
21
|
+
// its process is killed
|
|
20
22
|
const STOP_GRACE_MS = 15000;
|
|
21
23
|
// what is kept of the error output of a process that died without saying why
|
|
22
24
|
const MAX_STDERR = 2 * 1024;
|
|
@@ -30,19 +32,29 @@ function defaultWorker() {
|
|
|
30
32
|
}
|
|
31
33
|
exports.defaultWorker = defaultWorker;
|
|
32
34
|
// The cache a run shares with others is the one of its source: the runs of a repository
|
|
33
|
-
// share one, those of an upload only
|
|
34
|
-
// not leave an entry for a run from a reviewed
|
|
35
|
+
// share one, those of an upload or a sandbox only their owner's. Somebody who can only
|
|
36
|
+
// upload, or an agent on a sandbox, can not leave an entry for a run from a reviewed
|
|
37
|
+
// repository to find.
|
|
35
38
|
function cacheNamespace(run) {
|
|
36
|
-
const key = run.source.type === 'ref' ? `ref\0${run.source.repo}` :
|
|
39
|
+
const key = run.source.type === 'ref' ? `ref\0${run.source.repo}` : `${run.source.type}\0${run.owner.iss}\0${run.owner.sub}`;
|
|
37
40
|
return `${run.source.type}-${(0, crypto_1.createHash)('sha256').update(key).digest('hex').slice(0, 24)}`;
|
|
38
41
|
}
|
|
42
|
+
exports.cacheNamespace = cacheNamespace;
|
|
43
|
+
// The docker volumes of a run's services are its caller's, within the cache's source trust:
|
|
44
|
+
// 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
|
+
function volumeScope(run) {
|
|
47
|
+
const key = `${cacheNamespace(run)}\0${run.owner.iss}\0${run.owner.sub}`;
|
|
48
|
+
return `${run.source.type}-${(0, crypto_1.createHash)('sha256').update(key).digest('hex').slice(0, 24)}`;
|
|
49
|
+
}
|
|
50
|
+
exports.volumeScope = volumeScope;
|
|
39
51
|
/**
|
|
40
52
|
* Runs the tasks of a run on this machine with the engine of `hammerkit run`, in a
|
|
41
53
|
* process of its own.
|
|
42
54
|
*
|
|
43
|
-
* The working directory is made for the run and removed afterwards
|
|
44
|
-
* file is the published version the run is pinned to, written over the
|
|
45
|
-
* source, so the commands are the published ones whatever the source says. The process
|
|
55
|
+
* The working directory is made for the run and removed afterwards, or reported if it can
|
|
56
|
+
* not be. Its build file is the published version the run is pinned to, written over the
|
|
57
|
+
* code of the source, so the commands are the published ones whatever the source says. The process
|
|
46
58
|
* starts with an environment the server builds: the variables `runs.env` lists, a home
|
|
47
59
|
* directory that is the run's alone, and a cache directory per source trust. So what
|
|
48
60
|
* the engine reads of its process (the docker host, the registry logins in the home
|
|
@@ -56,21 +68,25 @@ class HostExecutor {
|
|
|
56
68
|
}
|
|
57
69
|
async execute(run, context) {
|
|
58
70
|
const { definitions, planning, repos } = this.options;
|
|
59
|
-
const version = definitions.find(run.definition
|
|
71
|
+
const version = definitions.find(run.definition);
|
|
60
72
|
const bundle = version ? await definitions.files(version) : undefined;
|
|
61
73
|
if (!version || !bundle) {
|
|
62
|
-
throw new Error(`the build file ${run.definition
|
|
74
|
+
throw new Error(`the build file ${run.definition} is no longer on this server`);
|
|
63
75
|
}
|
|
64
76
|
// pulled back after the run was accepted, while it waited for an approval or in the queue
|
|
65
77
|
if (version.yanked) {
|
|
66
|
-
throw new Error(`the build file ${run.definition
|
|
78
|
+
throw new Error(`the build file ${run.definition} was yanked${version.yanked.reason ? `: ${version.yanked.reason}` : ''}`);
|
|
67
79
|
}
|
|
68
|
-
if (run.source.type
|
|
80
|
+
if (run.source.type === 'upload') {
|
|
69
81
|
throw new Error('runs from an upload are not supported by this server yet, use a git ref');
|
|
70
82
|
}
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
const source = run.source;
|
|
84
|
+
const sandboxes = this.options.sandboxes;
|
|
85
|
+
if (source.type === 'sandbox' && !sandboxes) {
|
|
86
|
+
throw new Error('this server keeps no sandboxes');
|
|
87
|
+
}
|
|
88
|
+
if (source.type === 'ref' && !repos.readable(source.repo)) {
|
|
89
|
+
throw new Error(`this server can not read ${source.repo}, the operator lists it in sources`);
|
|
74
90
|
}
|
|
75
91
|
const worker = this.options.worker ?? defaultWorker();
|
|
76
92
|
if (!worker) {
|
|
@@ -78,11 +94,16 @@ class HostExecutor {
|
|
|
78
94
|
}
|
|
79
95
|
// what was approved is what runs: the accounts are read again from the files with
|
|
80
96
|
// the environment of the run, and the run may use no more than it was admitted for
|
|
81
|
-
const
|
|
97
|
+
const policy = source.type === 'sandbox' && sandboxes ? sandboxes.policy : planning.policy;
|
|
98
|
+
const planned = await (0, run_plan_1.planRun)(bundle, run, planning.environment, policy);
|
|
82
99
|
const extra = planned.accounts.filter((account) => !run.accounts.includes(account));
|
|
83
100
|
if (extra.length > 0) {
|
|
84
101
|
throw new Error(`the run would use ${extra.join(', ')}, which it was not admitted for`);
|
|
85
102
|
}
|
|
103
|
+
const refused = (0, run_plan_1.refuseArguments)(run, planned.plan);
|
|
104
|
+
if (refused) {
|
|
105
|
+
throw new Error(refused);
|
|
106
|
+
}
|
|
86
107
|
const abortCtrl = new AbortController();
|
|
87
108
|
const abort = () => abortCtrl.abort();
|
|
88
109
|
if (context.signal.aborted) {
|
|
@@ -90,34 +111,119 @@ class HostExecutor {
|
|
|
90
111
|
}
|
|
91
112
|
context.signal.addEventListener('abort', abort, { once: true });
|
|
92
113
|
const root = (0, path_1.join)(this.options.directory, run.id);
|
|
93
|
-
|
|
114
|
+
let attachment;
|
|
94
115
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
116
|
+
// where the process of the run starts: the checkout of a ref, or for a sandbox the
|
|
117
|
+
// published files, so what the process and the secret providers it starts read of
|
|
118
|
+
// their directory is never what a task wrote
|
|
119
|
+
let cwd;
|
|
120
|
+
let input;
|
|
121
|
+
let claim;
|
|
122
|
+
// the files of the sandbox the tasks work on
|
|
123
|
+
let work;
|
|
124
|
+
if (source.type === 'ref') {
|
|
125
|
+
cwd = (0, path_1.join)(root, 'source');
|
|
126
|
+
const { repo, ref, commit: pinned } = source;
|
|
127
|
+
const credentials = await repos.credentials(repo);
|
|
128
|
+
let commit;
|
|
129
|
+
try {
|
|
130
|
+
commit = await (0, git_checkout_1.checkoutRef)(repos.url(repo), ref, cwd, credentials.env, abortCtrl.signal, pinned);
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
throw repos.scrub(e, credentials.values);
|
|
134
|
+
}
|
|
135
|
+
context.emit({ type: 'log', task: 'hammerkit', stream: 'stdout', text: `checked out ${repo} at ${commit}` });
|
|
136
|
+
await this.writeBundle(cwd, bundle.files);
|
|
137
|
+
// what the version was checked against at publish holds while it runs too
|
|
138
|
+
input = {
|
|
139
|
+
entry: (0, path_1.join)(cwd, version.entry),
|
|
140
|
+
...(planning.policy.network ? { network: (0, work_network_1.toNetworkSchema)(planning.policy.network) } : {}),
|
|
141
|
+
};
|
|
99
142
|
}
|
|
100
|
-
|
|
101
|
-
|
|
143
|
+
else {
|
|
144
|
+
// the published files are read from a directory of the run and work on the
|
|
145
|
+
// sandbox, whose own build files are just files
|
|
146
|
+
const { registry, network } = sandboxes;
|
|
147
|
+
// read before the run attaches: until then the walk of the last run's measure holds
|
|
148
|
+
const state = await registry.state(source.id, abortCtrl.signal);
|
|
149
|
+
attachment = await registry.attach(source.id, (0, path_1.join)(root, 'sandbox'));
|
|
150
|
+
work = attachment.directory;
|
|
151
|
+
claim = attachment.claim;
|
|
152
|
+
// what was approved is what runs: the files may have changed since, by a run
|
|
153
|
+
// before this one on the sandbox
|
|
154
|
+
if (run.approvals?.length && state.tree !== source.tree) {
|
|
155
|
+
throw new Error(`the files of the sandbox ${source.id} changed since the run was approved, they are ${state.tree} now and were ${source.tree}, submit the run again`);
|
|
156
|
+
}
|
|
157
|
+
if (claim && !this.options.placement) {
|
|
158
|
+
throw new Error(`the sandbox ${source.id} is a claim on a cluster, this server runs tasks on its own docker`);
|
|
159
|
+
}
|
|
160
|
+
context.emit({
|
|
161
|
+
type: 'log',
|
|
162
|
+
task: 'hammerkit',
|
|
163
|
+
stream: 'stdout',
|
|
164
|
+
text: `sandbox ${source.id} of ${source.repo} at ${state.commit}, files ${state.tree}${state.clean ? '' : ' with changes'}`,
|
|
165
|
+
});
|
|
166
|
+
const published = (0, path_1.join)(root, 'bundle');
|
|
167
|
+
await (0, promises_1.mkdir)(published, { recursive: true });
|
|
168
|
+
await this.writeBundle(published, bundle.files);
|
|
169
|
+
cwd = published;
|
|
170
|
+
input = {
|
|
171
|
+
entry: (0, path_1.join)(published, version.entry),
|
|
172
|
+
directory: { read: published, work },
|
|
173
|
+
// the worker takes the lower of sandboxes.network and runs.network
|
|
174
|
+
...(planning.policy.network ? { network: (0, work_network_1.toNetworkSchema)(planning.policy.network) } : {}),
|
|
175
|
+
sandboxNetwork: network,
|
|
176
|
+
};
|
|
102
177
|
}
|
|
103
|
-
context.emit({ type: 'log', task: 'hammerkit', stream: 'stdout', text: `checked out ${repo} at ${commit}` });
|
|
104
|
-
await this.writeBundle(checkout, bundle.files);
|
|
105
178
|
const home = (0, path_1.join)(root, 'home');
|
|
106
179
|
await (0, promises_1.mkdir)(home, { recursive: true });
|
|
107
180
|
const cache = (0, path_1.join)(this.options.cache, cacheNamespace(run));
|
|
108
181
|
await (0, promises_1.mkdir)(cache, { recursive: true });
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
182
|
+
const placement = this.options.placement;
|
|
183
|
+
const placed = placement ? await placement.prepare(run, context, { root: work ?? cwd, claim }) : undefined;
|
|
184
|
+
try {
|
|
185
|
+
await this.runInProcess(worker, {
|
|
186
|
+
...input,
|
|
187
|
+
tasks: run.tasks,
|
|
188
|
+
options: run.options,
|
|
189
|
+
accounts: run.accounts,
|
|
190
|
+
volumeScope: volumeScope(run),
|
|
191
|
+
catalog: planning.environment.hosted?.catalog,
|
|
192
|
+
...(placed ? { kubernetes: placed.kubernetes } : {}),
|
|
193
|
+
}, cwd, { ...this.processEnvironment(home, cache), ...placed?.env }, context, abortCtrl.signal);
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
if (placement && placed) {
|
|
197
|
+
await placement.release(run, context);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
117
200
|
}
|
|
118
201
|
finally {
|
|
119
202
|
context.signal.removeEventListener('abort', abort);
|
|
120
|
-
|
|
203
|
+
attachment?.release?.();
|
|
204
|
+
await this.removeRunDirectory(run, root, context);
|
|
205
|
+
if (source.type === 'sandbox') {
|
|
206
|
+
// what the run wrote counts against the owner's limits, from their next run or create
|
|
207
|
+
sandboxes?.registry.measure(source.id).catch(() => undefined);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// A task of the run may still hold a file of its directory for a moment after the run
|
|
212
|
+
// ended, which Windows refuses to remove. A directory that stays is reported, to the run
|
|
213
|
+
// by its error code and to the operator with its path; it does not change how the run ended.
|
|
214
|
+
async removeRunDirectory(run, root, context) {
|
|
215
|
+
try {
|
|
216
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
const error = e;
|
|
220
|
+
context.emit({
|
|
221
|
+
type: 'log',
|
|
222
|
+
task: 'hammerkit',
|
|
223
|
+
stream: 'stderr',
|
|
224
|
+
text: `the working directory of the run could not be removed: ${error.code ?? 'unknown error'}`,
|
|
225
|
+
});
|
|
226
|
+
this.options.log?.(`run=${run.id} could not remove its directory: ${error.message}`);
|
|
121
227
|
}
|
|
122
228
|
}
|
|
123
229
|
// What the process of a run sees: the variables the operator listed, and a home
|
|
@@ -136,9 +242,11 @@ class HostExecutor {
|
|
|
136
242
|
env.HAMMERKIT_DIR = cache;
|
|
137
243
|
return env;
|
|
138
244
|
}
|
|
139
|
-
// Starts the process, hands it the run on its standard input and relays what it reports.
|
|
140
|
-
// is
|
|
141
|
-
//
|
|
245
|
+
// Starts the process, hands it the run on its standard input and relays what it reports.
|
|
246
|
+
// A cancelled run is told so on the standard input, the same on every platform, so the
|
|
247
|
+
// engine stops its tasks and removes its containers; a signal on Windows would end the
|
|
248
|
+
// process at once and leave its tasks running in the directory of the run. The process is
|
|
249
|
+
// killed if it has not stopped after the grace period.
|
|
142
250
|
runInProcess(command, input, cwd, env, context, signal) {
|
|
143
251
|
return new Promise((resolvePromise, reject) => {
|
|
144
252
|
const child = (0, child_process_1.spawn)(command[0], command.slice(1), {
|
|
@@ -147,12 +255,19 @@ class HostExecutor {
|
|
|
147
255
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
148
256
|
windowsHide: true,
|
|
149
257
|
});
|
|
258
|
+
child.stdin.on('error', () => undefined);
|
|
259
|
+
// the run first, so a cancel comes after it; kept open, it is how a cancel reaches the process
|
|
260
|
+
child.stdin.write(`${JSON.stringify(input)}\n`);
|
|
150
261
|
let result;
|
|
151
262
|
let stderr = '';
|
|
152
263
|
let killTimer;
|
|
153
264
|
const stop = () => {
|
|
154
|
-
child.
|
|
155
|
-
|
|
265
|
+
child.stdin.write('cancel\n', (e) => {
|
|
266
|
+
if (e) {
|
|
267
|
+
child.kill();
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
killTimer = setTimeout(() => child.kill('SIGKILL'), this.options.stopGraceMs ?? STOP_GRACE_MS);
|
|
156
271
|
killTimer.unref();
|
|
157
272
|
};
|
|
158
273
|
if (signal.aborted) {
|
|
@@ -169,6 +284,8 @@ class HostExecutor {
|
|
|
169
284
|
}
|
|
170
285
|
else if (message.kind === 'result') {
|
|
171
286
|
result = message;
|
|
287
|
+
// nothing left to tell the process
|
|
288
|
+
child.stdin.end();
|
|
172
289
|
}
|
|
173
290
|
}
|
|
174
291
|
catch {
|
|
@@ -178,8 +295,6 @@ class HostExecutor {
|
|
|
178
295
|
child.stderr.on('data', (chunk) => {
|
|
179
296
|
stderr = (stderr + chunk.toString('utf8')).slice(-MAX_STDERR);
|
|
180
297
|
});
|
|
181
|
-
child.stdin.on('error', () => undefined);
|
|
182
|
-
child.stdin.end(JSON.stringify(input));
|
|
183
298
|
child.on('error', (e) => {
|
|
184
299
|
signal.removeEventListener('abort', stop);
|
|
185
300
|
clearTimeout(killTimer);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveRef = exports.RefError = exports.checkoutRef = exports.isValidRef = void 0;
|
|
3
|
+
exports.git = exports.remoteBranch = exports.isValidBranch = exports.assertBranchName = exports.resolveRef = exports.GitOutputTooLarge = exports.RefError = exports.checkoutRef = exports.isValidRef = void 0;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const promises_1 = require("fs/promises");
|
|
6
6
|
const util_1 = require("util");
|
|
@@ -42,13 +42,20 @@ exports.checkoutRef = checkoutRef;
|
|
|
42
42
|
class RefError extends Error {
|
|
43
43
|
}
|
|
44
44
|
exports.RefError = RefError;
|
|
45
|
+
class GitOutputTooLarge extends Error {
|
|
46
|
+
}
|
|
47
|
+
exports.GitOutputTooLarge = GitOutputTooLarge;
|
|
45
48
|
const COMMIT = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
46
49
|
/**
|
|
47
50
|
* The commit a branch or tag of a repository points to now: `refs/heads/<ref>`, else
|
|
48
51
|
* `refs/tags/<ref>` (the commit of an annotated tag). A name that is both a branch and
|
|
49
|
-
* a tag is ambiguous. A full commit hash is taken as it is.
|
|
52
|
+
* a tag is ambiguous. A full commit hash is taken as it is. With `branch`, only a
|
|
53
|
+
* branch: a sandbox starts from one.
|
|
50
54
|
*/
|
|
51
|
-
async function resolveRef(repository, ref, env, signal) {
|
|
55
|
+
async function resolveRef(repository, ref, env, signal, only) {
|
|
56
|
+
if (only === 'branch') {
|
|
57
|
+
return resolveBranch(repository, ref, env, signal);
|
|
58
|
+
}
|
|
52
59
|
if (!isValidRef(ref)) {
|
|
53
60
|
throw new RefError(`${JSON.stringify(ref.slice(0, 80))} is not a branch, tag or commit`);
|
|
54
61
|
}
|
|
@@ -84,7 +91,52 @@ async function resolveRef(repository, ref, env, signal) {
|
|
|
84
91
|
return commit;
|
|
85
92
|
}
|
|
86
93
|
exports.resolveRef = resolveRef;
|
|
87
|
-
async function
|
|
94
|
+
async function resolveBranch(repository, branch, env, signal) {
|
|
95
|
+
assertBranchName(branch);
|
|
96
|
+
const commit = await remoteBranch(repository, branch, env, signal);
|
|
97
|
+
if (!commit) {
|
|
98
|
+
throw new RefError(`there is no branch ${branch}`);
|
|
99
|
+
}
|
|
100
|
+
return commit;
|
|
101
|
+
}
|
|
102
|
+
// A name a sandbox can start from: a branch, never a tag or a commit, written without `refs/heads/`.
|
|
103
|
+
function assertBranchName(branch) {
|
|
104
|
+
if (!isValidRef(branch) || branch.startsWith('refs/')) {
|
|
105
|
+
throw new RefError(`${JSON.stringify(branch.slice(0, 80))} is not a branch`);
|
|
106
|
+
}
|
|
107
|
+
if (COMMIT.test(branch.toLowerCase())) {
|
|
108
|
+
throw new RefError(`a sandbox starts from a branch, ${branch} is a commit`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.assertBranchName = assertBranchName;
|
|
112
|
+
// A branch git takes as a name of its own: a ref, and no part of it starts or ends with a
|
|
113
|
+
// dot or ends with `.lock` (`git check-ref-format` refuses those).
|
|
114
|
+
function isValidBranch(branch) {
|
|
115
|
+
return (isValidRef(branch) &&
|
|
116
|
+
branch
|
|
117
|
+
.split('/')
|
|
118
|
+
.every((part) => !part.startsWith('.') && !part.startsWith('-') && !part.endsWith('.') && !part.endsWith('.lock')));
|
|
119
|
+
}
|
|
120
|
+
exports.isValidBranch = isValidBranch;
|
|
121
|
+
// The commit a branch of a repository points to now, none when there is no such branch.
|
|
122
|
+
async function remoteBranch(repository, branch, env, signal) {
|
|
123
|
+
const output = await git(['ls-remote', '--heads', '--', repository, `refs/heads/${branch}`], env, signal, `the branches of ${repository}`);
|
|
124
|
+
for (const line of output.split('\n')) {
|
|
125
|
+
const [commit, name] = line.split('\t');
|
|
126
|
+
if (commit && name === `refs/heads/${branch}`) {
|
|
127
|
+
return commit;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
exports.remoteBranch = remoteBranch;
|
|
133
|
+
// Runs git without a prompt and with the network transports only. Errors say what could
|
|
134
|
+
// not be done (`unable to <action> <what>`) with the last line git printed.
|
|
135
|
+
async function git(args, baseEnv, signal, what, action = 'fetch',
|
|
136
|
+
// what git reads on stdin, the exit codes that answer rather than fail (check-ignore exits
|
|
137
|
+
// 1 when nothing is ignored), and what git may print (a diff of a sandbox can be larger
|
|
138
|
+
// than the default)
|
|
139
|
+
options = {}) {
|
|
88
140
|
const env = {};
|
|
89
141
|
for (const [key, value] of Object.entries(baseEnv)) {
|
|
90
142
|
if (value !== undefined) {
|
|
@@ -94,22 +146,34 @@ async function git(args, baseEnv, signal, what) {
|
|
|
94
146
|
env.GIT_TERMINAL_PROMPT = '0';
|
|
95
147
|
env.GIT_ALLOW_PROTOCOL = ALLOWED_PROTOCOLS;
|
|
96
148
|
try {
|
|
97
|
-
|
|
149
|
+
// warnings of a large tree must not fail the command
|
|
150
|
+
const running = execFileAsync('git', args, { env, signal, maxBuffer: options.maxBuffer ?? 64 * 1024 * 1024 });
|
|
151
|
+
// a git that exits before it read its input fails the command, not the server (EPIPE)
|
|
152
|
+
running.child.stdin?.on('error', () => undefined);
|
|
153
|
+
running.child.stdin?.end(options.input);
|
|
154
|
+
return (await running).stdout;
|
|
98
155
|
}
|
|
99
156
|
catch (e) {
|
|
100
157
|
const error = e;
|
|
158
|
+
if (typeof error.code === 'number' && options.exitCodes?.includes(error.code)) {
|
|
159
|
+
return error.stdout ?? '';
|
|
160
|
+
}
|
|
101
161
|
if (error.code === 'ABORT_ERR') {
|
|
102
|
-
throw new Error(
|
|
162
|
+
throw new Error(`${action === 'fetch' ? 'fetching' : `${action} of`} ${what} was cancelled`);
|
|
103
163
|
}
|
|
104
164
|
if (error.code === 'ENOENT') {
|
|
105
|
-
throw new Error(`unable to
|
|
165
|
+
throw new Error(`unable to ${action} ${what}: git is not installed`);
|
|
166
|
+
}
|
|
167
|
+
if (error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
|
|
168
|
+
throw new GitOutputTooLarge(`unable to ${action} ${what}: git printed more than ${options.maxBuffer ?? 64 * 1024 * 1024} bytes`);
|
|
106
169
|
}
|
|
107
170
|
const reason = (error.stderr ?? '')
|
|
108
171
|
.split('\n')
|
|
109
172
|
.map((line) => line.trim())
|
|
110
173
|
.filter((line) => line.length > 0)
|
|
111
174
|
.pop() ?? error.message;
|
|
112
|
-
throw new Error(`unable to
|
|
175
|
+
throw new Error(`unable to ${action} ${what}: ${reason}`);
|
|
113
176
|
}
|
|
114
177
|
}
|
|
178
|
+
exports.git = git;
|
|
115
179
|
//# sourceMappingURL=git-checkout.js.map
|