@snappedly-tools/shipyard 0.4.4 → 0.5.0
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/README.md +99 -146
- package/dist/chunk-56TDSWFU.js +255 -0
- package/dist/chunk-56TDSWFU.js.map +1 -0
- package/dist/chunk-ACD46ZM4.js +136 -0
- package/dist/chunk-ACD46ZM4.js.map +1 -0
- package/dist/{chunk-2GT3OPO5.js → chunk-FDYOTN55.js} +196 -6
- package/dist/chunk-FDYOTN55.js.map +1 -0
- package/dist/chunk-SOJTAJTF.js +78 -0
- package/dist/chunk-SOJTAJTF.js.map +1 -0
- package/dist/chunk-WAXEMZUV.js +877 -0
- package/dist/chunk-WAXEMZUV.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +58 -118
- package/dist/index.js.map +1 -1
- package/dist/main.js +1831 -143
- package/dist/main.js.map +1 -1
- package/dist/sandboxes/docker.js +3 -3
- package/dist/sandboxes/no-sandbox.js +2 -2
- package/dist/sandboxes/vercel.js +15 -35
- package/dist/sandboxes/vercel.js.map +1 -1
- package/dist/templates/parallel-planner/main.mts +11 -8
- package/dist/templates/parallel-planner-with-review/main.mts +23 -13
- package/package.json +3 -2
- package/dist/chunk-2GT3OPO5.js.map +0 -1
- package/dist/chunk-OCJKONZ7.js +0 -475
- package/dist/chunk-OCJKONZ7.js.map +0 -1
- package/dist/chunk-VDHZO5PP.js +0 -113
- package/dist/chunk-VDHZO5PP.js.map +0 -1
- package/dist/chunk-VW4MI5GJ.js +0 -126
- package/dist/chunk-VW4MI5GJ.js.map +0 -1
- package/dist/chunk-WA7QPAXO.js +0 -50
- package/dist/chunk-WA7QPAXO.js.map +0 -1
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
import { assertProtectedDirectoryIdentities, ACTIVATION_LABEL, CONFIG_DIR, RUNNER_DIR, RUNNER_SANDBOX_MASK_DIR, resolveUserMounts, assertIsolatedWorkspaceMounts, processFileMountParents, defaultImageName, registerShutdown, DockerError, repositoryRunnerEnvironment, formatVolumeMount } from './chunk-FDYOTN55.js';
|
|
2
|
+
import { createIsolatedSandboxProvider } from './chunk-KMGNFXKN.js';
|
|
3
|
+
import { MAX_TAIL_CHARS, collectProcessOutput } from './chunk-ACD46ZM4.js';
|
|
4
|
+
import { execFile, execFileSync, spawn } from 'child_process';
|
|
5
|
+
import { resolve, join, posix } from 'path';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import { Effect } from 'effect';
|
|
9
|
+
import { constants } from 'fs';
|
|
10
|
+
import { rm, lstat, realpath, mkdir, readFile, access } from 'fs/promises';
|
|
11
|
+
|
|
12
|
+
var exec = promisify(execFile);
|
|
13
|
+
var copyIntoContainer = async (engine, container, owner, hostPath, sandboxPath) => {
|
|
14
|
+
try {
|
|
15
|
+
await exec(engine, [
|
|
16
|
+
"exec",
|
|
17
|
+
container,
|
|
18
|
+
"mkdir",
|
|
19
|
+
"-p",
|
|
20
|
+
"--",
|
|
21
|
+
posix.dirname(sandboxPath)
|
|
22
|
+
]);
|
|
23
|
+
await exec(engine, ["cp", hostPath, `${container}:${sandboxPath}`]);
|
|
24
|
+
await exec(engine, [
|
|
25
|
+
"exec",
|
|
26
|
+
"--user",
|
|
27
|
+
"0:0",
|
|
28
|
+
container,
|
|
29
|
+
"chown",
|
|
30
|
+
"-hR",
|
|
31
|
+
"--",
|
|
32
|
+
owner,
|
|
33
|
+
sandboxPath
|
|
34
|
+
]);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`${engine} cp (in) failed: ${error instanceof Error ? error.message : String(error)}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var dockerExec = (args) => Effect.async((resume) => {
|
|
42
|
+
execFile(
|
|
43
|
+
"docker",
|
|
44
|
+
args,
|
|
45
|
+
{ maxBuffer: 10 * 1024 * 1024 },
|
|
46
|
+
(error, stdout, stderr) => {
|
|
47
|
+
if (error) {
|
|
48
|
+
resume(
|
|
49
|
+
Effect.fail(
|
|
50
|
+
new DockerError({
|
|
51
|
+
message: `docker ${args[0]} failed: ${stderr?.toString() || error.message}`
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
} else {
|
|
56
|
+
resume(Effect.succeed(stdout.toString()));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
var buildImage = (imageName, dockerfileDir, options) => Effect.gen(function* () {
|
|
62
|
+
const buildArgFlags = Object.entries(options?.buildArgs ?? {}).flatMap(
|
|
63
|
+
([k, v]) => ["--build-arg", `${k}=${v}`]
|
|
64
|
+
);
|
|
65
|
+
if (options?.dockerfile) {
|
|
66
|
+
yield* dockerExec([
|
|
67
|
+
"build",
|
|
68
|
+
"-t",
|
|
69
|
+
imageName,
|
|
70
|
+
...buildArgFlags,
|
|
71
|
+
"-f",
|
|
72
|
+
resolve(options.dockerfile),
|
|
73
|
+
process.cwd()
|
|
74
|
+
]);
|
|
75
|
+
} else {
|
|
76
|
+
yield* dockerExec([
|
|
77
|
+
"build",
|
|
78
|
+
"-t",
|
|
79
|
+
imageName,
|
|
80
|
+
...buildArgFlags,
|
|
81
|
+
resolve(dockerfileDir)
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
var startContainer = (containerName, imageName, env, options) => Effect.gen(function* () {
|
|
86
|
+
const existing = yield* dockerExec([
|
|
87
|
+
"ps",
|
|
88
|
+
"-a",
|
|
89
|
+
"--filter",
|
|
90
|
+
`name=^${containerName}$`,
|
|
91
|
+
"--format",
|
|
92
|
+
"{{.Names}}"
|
|
93
|
+
]);
|
|
94
|
+
if (existing.trim() === containerName) {
|
|
95
|
+
yield* Effect.fail(
|
|
96
|
+
new DockerError({
|
|
97
|
+
message: `Container '${containerName}' already exists. Run cleanup first.`
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const envFlags = Object.entries(env).flatMap(([k, v]) => [
|
|
102
|
+
"-e",
|
|
103
|
+
`${k}=${v}`
|
|
104
|
+
]);
|
|
105
|
+
const selinuxLabel = options?.selinuxLabel ?? "z";
|
|
106
|
+
const volumeFlags = (options?.volumeMounts ?? []).flatMap((mount) => [
|
|
107
|
+
"-v",
|
|
108
|
+
formatVolumeMount(mount, selinuxLabel)
|
|
109
|
+
]);
|
|
110
|
+
const workdirFlags = options?.workdir ? ["-w", options.workdir] : [];
|
|
111
|
+
const userFlags = options?.user ? ["--user", options.user] : [];
|
|
112
|
+
const networks = options?.network ? Array.isArray(options.network) ? options.network : [options.network] : [];
|
|
113
|
+
const networkFlags = networks.flatMap((n) => ["--network", n]);
|
|
114
|
+
const groupAddFlags = (options?.groups ?? []).flatMap((g) => [
|
|
115
|
+
"--group-add",
|
|
116
|
+
String(g)
|
|
117
|
+
]);
|
|
118
|
+
const deviceFlags = (options?.devices ?? []).flatMap((d) => [
|
|
119
|
+
"--device",
|
|
120
|
+
d
|
|
121
|
+
]);
|
|
122
|
+
const cpusFlags = options?.cpus !== void 0 ? ["--cpus", String(options.cpus)] : [];
|
|
123
|
+
const labelFlags = Object.entries(options?.labels ?? {}).flatMap(
|
|
124
|
+
([key, value]) => ["--label", `${key}=${value}`]
|
|
125
|
+
);
|
|
126
|
+
yield* dockerExec([
|
|
127
|
+
"run",
|
|
128
|
+
"-d",
|
|
129
|
+
"--init",
|
|
130
|
+
"--name",
|
|
131
|
+
containerName,
|
|
132
|
+
...labelFlags,
|
|
133
|
+
...envFlags,
|
|
134
|
+
...volumeFlags,
|
|
135
|
+
...workdirFlags,
|
|
136
|
+
...userFlags,
|
|
137
|
+
...networkFlags,
|
|
138
|
+
...groupAddFlags,
|
|
139
|
+
...deviceFlags,
|
|
140
|
+
...cpusFlags,
|
|
141
|
+
imageName
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
var removeContainer = (containerName) => Effect.gen(function* () {
|
|
145
|
+
yield* Effect.ignore(dockerExec(["stop", containerName]));
|
|
146
|
+
yield* Effect.ignore(dockerExec(["rm", containerName]));
|
|
147
|
+
});
|
|
148
|
+
var removeImage = (imageName) => Effect.gen(function* () {
|
|
149
|
+
yield* dockerExec(["rmi", imageName]);
|
|
150
|
+
});
|
|
151
|
+
var execFileAsync = promisify(execFile);
|
|
152
|
+
var RUNNER_INSTALL_METADATA = ".shipyard-install.json";
|
|
153
|
+
var RUNNER_CONTROLLER_LOCK = ".shipyard-controller.lock";
|
|
154
|
+
var RUNNER_WORK_DIR = "_work";
|
|
155
|
+
var REPOSITORY_RUNNER_OWNER_LABEL = "com.snappedly.shipyard.repository-runner-owner";
|
|
156
|
+
var REPOSITORY_RUNNER_OWNER_ENV = "SHIPYARD_RUNNER_OWNER";
|
|
157
|
+
var REGISTRATION_FILES = [
|
|
158
|
+
".credentials",
|
|
159
|
+
".credentials_rsaparams",
|
|
160
|
+
".runner"
|
|
161
|
+
];
|
|
162
|
+
var RunnerLifecycleError = class extends Error {
|
|
163
|
+
name = "RunnerLifecycleError";
|
|
164
|
+
};
|
|
165
|
+
var defaultAdapters = {
|
|
166
|
+
environment: () => process.env,
|
|
167
|
+
exists: async (path) => {
|
|
168
|
+
try {
|
|
169
|
+
await access(path, constants.F_OK);
|
|
170
|
+
return true;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
readText: (path) => readFile(path, "utf8"),
|
|
176
|
+
makeDirectory: async (path) => {
|
|
177
|
+
await mkdir(path, { recursive: true, mode: 448 });
|
|
178
|
+
},
|
|
179
|
+
inspectDirectory: async (path) => {
|
|
180
|
+
const [stat, resolved] = await Promise.all([lstat(path), realpath(path)]);
|
|
181
|
+
return {
|
|
182
|
+
realPath: resolved,
|
|
183
|
+
directory: stat.isDirectory(),
|
|
184
|
+
symbolicLink: stat.isSymbolicLink()
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
remove: (path) => rm(path, { recursive: true, force: true }),
|
|
188
|
+
run: async (command, args, options) => {
|
|
189
|
+
const result = await execFileAsync(command, [...args], {
|
|
190
|
+
cwd: options.cwd,
|
|
191
|
+
env: options.env,
|
|
192
|
+
maxBuffer: 10 * 1024 * 1024
|
|
193
|
+
});
|
|
194
|
+
return { stdout: result.stdout, stderr: result.stderr };
|
|
195
|
+
},
|
|
196
|
+
processIdentity: async (pid) => {
|
|
197
|
+
try {
|
|
198
|
+
const result = await execFileAsync("ps", [
|
|
199
|
+
"-p",
|
|
200
|
+
String(pid),
|
|
201
|
+
"-o",
|
|
202
|
+
"lstart="
|
|
203
|
+
]);
|
|
204
|
+
const identity = result.stdout.trim().replace(/\s+/g, " ");
|
|
205
|
+
return identity.length > 0 ? identity : void 0;
|
|
206
|
+
} catch {
|
|
207
|
+
return void 0;
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
isProcessRunning: (pid) => {
|
|
211
|
+
try {
|
|
212
|
+
process.kill(pid, 0);
|
|
213
|
+
return true;
|
|
214
|
+
} catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
signalProcess: (pid, signal) => process.kill(pid, signal),
|
|
219
|
+
pause: (milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds))
|
|
220
|
+
};
|
|
221
|
+
var lockOwnsProcess = async (lock, adapters) => lock !== void 0 && Number.isInteger(lock.pid) && typeof lock.processStartedAt === "string" && lock.processStartedAt.length > 0 && adapters.isProcessRunning(lock.pid) && await adapters.processIdentity(lock.pid) === lock.processStartedAt;
|
|
222
|
+
var lifecycleFailure = (purpose, error) => error instanceof RunnerLifecycleError ? error : new RunnerLifecycleError(
|
|
223
|
+
`${purpose} failed: ${error instanceof Error ? error.message : String(error)}`
|
|
224
|
+
);
|
|
225
|
+
var runCommand = async (adapters, purpose, command, args, options) => {
|
|
226
|
+
try {
|
|
227
|
+
return await adapters.run(command, args, options);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
throw lifecycleFailure(purpose, error);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
var readJson = async (path, adapters) => {
|
|
233
|
+
if (!await adapters.exists(path)) return void 0;
|
|
234
|
+
try {
|
|
235
|
+
return JSON.parse(await adapters.readText(path));
|
|
236
|
+
} catch (error) {
|
|
237
|
+
throw lifecycleFailure(`Reading repository runner state at ${path}`, error);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
var requireInstallMetadata = async (runnerDir, adapters) => {
|
|
241
|
+
const metadata = await readJson(
|
|
242
|
+
join(runnerDir, RUNNER_INSTALL_METADATA),
|
|
243
|
+
adapters
|
|
244
|
+
);
|
|
245
|
+
if (metadata?.schemaVersion !== 1 || typeof metadata.repository !== "string" || typeof metadata.repositoryUrl !== "string" || typeof metadata.name !== "string" || metadata.label !== ACTIVATION_LABEL || typeof metadata.version !== "string") {
|
|
246
|
+
throw new RunnerLifecycleError(
|
|
247
|
+
`The existing directory at ${runnerDir} is not a valid Shipyard repository runner installation. Refusing to replace it.`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return metadata;
|
|
251
|
+
};
|
|
252
|
+
var parseRemoteRunners = (stdout) => stdout.split(/\r?\n/).filter((line) => line.length > 0).map((line) => {
|
|
253
|
+
const [name = "", status = "", labels = ""] = line.split(" ");
|
|
254
|
+
return {
|
|
255
|
+
name,
|
|
256
|
+
status,
|
|
257
|
+
labels: new Set(labels.split(",").filter(Boolean))
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
var listRemoteRunners = async (repository, repoDir, environment, adapters) => {
|
|
261
|
+
const response = await runCommand(
|
|
262
|
+
adapters,
|
|
263
|
+
"Checking registered GitHub repository runners",
|
|
264
|
+
"gh",
|
|
265
|
+
[
|
|
266
|
+
"api",
|
|
267
|
+
"--paginate",
|
|
268
|
+
`repos/${repository}/actions/runners`,
|
|
269
|
+
"--jq",
|
|
270
|
+
'.runners[] | [.name, .status, ([.labels[].name] | join(","))] | @tsv'
|
|
271
|
+
],
|
|
272
|
+
{ cwd: repoDir, env: environment }
|
|
273
|
+
);
|
|
274
|
+
return parseRemoteRunners(response.stdout);
|
|
275
|
+
};
|
|
276
|
+
var validateExistingRepositoryRunner = async (options, adapters = defaultAdapters) => {
|
|
277
|
+
if (!await adapters.exists(options.maskDir)) {
|
|
278
|
+
await adapters.makeDirectory(options.maskDir);
|
|
279
|
+
}
|
|
280
|
+
await assertProtectedDirectoryIdentities(
|
|
281
|
+
options.runnerDir,
|
|
282
|
+
options.maskDir,
|
|
283
|
+
adapters.inspectDirectory
|
|
284
|
+
).catch((error) => {
|
|
285
|
+
throw lifecycleFailure("Validating protected runner directories", error);
|
|
286
|
+
});
|
|
287
|
+
const metadata = await requireInstallMetadata(options.runnerDir, adapters);
|
|
288
|
+
if (metadata.repository !== options.repository) {
|
|
289
|
+
throw new RunnerLifecycleError(
|
|
290
|
+
`The existing repository runner belongs to ${metadata.repository}, not ${options.repository}. Refusing to replace it.`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if (metadata.name !== options.runnerName) {
|
|
294
|
+
throw new RunnerLifecycleError(
|
|
295
|
+
`The existing installation belongs to runner ${metadata.name}, not ${options.runnerName}. A Mac rename does not rename a registered runner; remove and reinstall it explicitly.`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
for (const required of [".credentials", "run.sh"]) {
|
|
299
|
+
if (!await adapters.exists(join(options.runnerDir, required))) {
|
|
300
|
+
throw new RunnerLifecycleError(
|
|
301
|
+
`The existing repository runner is missing ${required}. Refusing to replace its registration; remove it explicitly first.`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const remoteRunners = await listRemoteRunners(
|
|
306
|
+
options.repository,
|
|
307
|
+
options.repoDir,
|
|
308
|
+
adapters.environment(),
|
|
309
|
+
adapters
|
|
310
|
+
);
|
|
311
|
+
const labelled = remoteRunners.filter(
|
|
312
|
+
({ labels }) => labels.has(ACTIVATION_LABEL)
|
|
313
|
+
);
|
|
314
|
+
if (labelled.length > 1) {
|
|
315
|
+
throw new RunnerLifecycleError(
|
|
316
|
+
`Multiple repository runners carry the ${ACTIVATION_LABEL} label (${labelled.map(({ name }) => name).join(", ")}). Remove duplicates before continuing.`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
const matching = remoteRunners.find(({ name }) => name === metadata.name);
|
|
320
|
+
if (!matching) {
|
|
321
|
+
throw new RunnerLifecycleError(
|
|
322
|
+
`Runner ${metadata.name} is installed locally but is no longer registered with GitHub. Run \`shipyard runner start\` to repair it, or remove it explicitly.`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
if (!matching.labels.has(ACTIVATION_LABEL)) {
|
|
326
|
+
throw new RunnerLifecycleError(
|
|
327
|
+
`Registered runner ${metadata.name} does not carry the ${ACTIVATION_LABEL} label. Refusing to replace a customized registration.`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
return metadata;
|
|
331
|
+
};
|
|
332
|
+
var removeRegistrationFiles = async (runnerDir, adapters) => {
|
|
333
|
+
for (const name of REGISTRATION_FILES) {
|
|
334
|
+
const path = join(runnerDir, name);
|
|
335
|
+
if (await adapters.exists(path)) await adapters.remove(path);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
var readOneTimeToken = (stdout, purpose) => {
|
|
339
|
+
try {
|
|
340
|
+
const parsed = JSON.parse(stdout);
|
|
341
|
+
if (typeof parsed.token !== "string" || parsed.token.length === 0) {
|
|
342
|
+
throw new Error("response did not contain a token");
|
|
343
|
+
}
|
|
344
|
+
return parsed.token;
|
|
345
|
+
} catch (error) {
|
|
346
|
+
throw lifecycleFailure(purpose, error);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
var requestRegistrationToken = async (repository, repoDir, environment, adapters) => {
|
|
350
|
+
await runCommand(
|
|
351
|
+
adapters,
|
|
352
|
+
"Checking current administrative GitHub authorization",
|
|
353
|
+
"gh",
|
|
354
|
+
["auth", "status", "--hostname", "github.com"],
|
|
355
|
+
{ cwd: repoDir, env: environment }
|
|
356
|
+
);
|
|
357
|
+
const response = await runCommand(
|
|
358
|
+
adapters,
|
|
359
|
+
"Requesting a one-time repository runner registration token",
|
|
360
|
+
"gh",
|
|
361
|
+
[
|
|
362
|
+
"api",
|
|
363
|
+
"--method",
|
|
364
|
+
"POST",
|
|
365
|
+
`repos/${repository}/actions/runners/registration-token`
|
|
366
|
+
],
|
|
367
|
+
{ cwd: repoDir, env: environment }
|
|
368
|
+
);
|
|
369
|
+
return readOneTimeToken(response.stdout, "Reading the registration token");
|
|
370
|
+
};
|
|
371
|
+
var recoverRepositoryRunner = async (options, adapters = defaultAdapters) => {
|
|
372
|
+
const lockPath = join(options.runnerDir, RUNNER_CONTROLLER_LOCK);
|
|
373
|
+
const lock = await readJson(lockPath, adapters);
|
|
374
|
+
if (await lockOwnsProcess(lock, adapters)) {
|
|
375
|
+
throw new RunnerLifecycleError(
|
|
376
|
+
`The repository runner is already running with process ${lock.pid}.`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
if (lock) await adapters.remove(lockPath);
|
|
380
|
+
const workDir = join(options.runnerDir, RUNNER_WORK_DIR);
|
|
381
|
+
if (await adapters.exists(workDir)) await adapters.remove(workDir);
|
|
382
|
+
if (await adapters.exists(options.maskDir))
|
|
383
|
+
await adapters.remove(options.maskDir);
|
|
384
|
+
await adapters.makeDirectory(options.maskDir);
|
|
385
|
+
await assertProtectedDirectoryIdentities(
|
|
386
|
+
options.runnerDir,
|
|
387
|
+
options.maskDir,
|
|
388
|
+
adapters.inspectDirectory
|
|
389
|
+
).catch((error) => {
|
|
390
|
+
throw lifecycleFailure("Validating protected runner directories", error);
|
|
391
|
+
});
|
|
392
|
+
await removeOwnedRepositoryRunnerContainers(
|
|
393
|
+
{
|
|
394
|
+
repoDir: options.repoDir,
|
|
395
|
+
repository: options.metadata.repository,
|
|
396
|
+
environment: options.dockerEnvironment ?? options.runnerEnvironment
|
|
397
|
+
},
|
|
398
|
+
adapters
|
|
399
|
+
);
|
|
400
|
+
const adminEnvironment = adapters.environment();
|
|
401
|
+
const remoteRunners = await listRemoteRunners(
|
|
402
|
+
options.metadata.repository,
|
|
403
|
+
options.repoDir,
|
|
404
|
+
adminEnvironment,
|
|
405
|
+
adapters
|
|
406
|
+
);
|
|
407
|
+
const labelled = remoteRunners.filter(
|
|
408
|
+
({ labels }) => labels.has(ACTIVATION_LABEL)
|
|
409
|
+
);
|
|
410
|
+
if (labelled.length > 1) {
|
|
411
|
+
throw new RunnerLifecycleError(
|
|
412
|
+
`Multiple repository runners carry the ${ACTIVATION_LABEL} label (${labelled.map(({ name }) => name).join(", ")}). Remove duplicates before continuing.`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
const matching = remoteRunners.find(
|
|
416
|
+
({ name }) => name === options.metadata.name
|
|
417
|
+
);
|
|
418
|
+
if (matching) {
|
|
419
|
+
if (!matching.labels.has(ACTIVATION_LABEL)) {
|
|
420
|
+
throw new RunnerLifecycleError(
|
|
421
|
+
`Registered runner ${matching.name} does not carry the ${ACTIVATION_LABEL} label. Refusing to replace a customized registration.`
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
if (!await adapters.exists(join(options.runnerDir, ".credentials"))) {
|
|
425
|
+
throw new RunnerLifecycleError(
|
|
426
|
+
`Registered runner ${matching.name} is missing local credentials. Remove and reinstall it explicitly.`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
return { reRegistered: false };
|
|
430
|
+
}
|
|
431
|
+
const collision = labelled[0];
|
|
432
|
+
if (collision) {
|
|
433
|
+
throw new RunnerLifecycleError(
|
|
434
|
+
`Repository runner ${collision.name} already carries the ${ACTIVATION_LABEL} label. Refusing to create a conflicting registration.`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
const registrationToken = await requestRegistrationToken(
|
|
438
|
+
options.metadata.repository,
|
|
439
|
+
options.repoDir,
|
|
440
|
+
adminEnvironment,
|
|
441
|
+
adapters
|
|
442
|
+
);
|
|
443
|
+
await removeRegistrationFiles(options.runnerDir, adapters);
|
|
444
|
+
try {
|
|
445
|
+
await adapters.run(
|
|
446
|
+
"./config.sh",
|
|
447
|
+
[
|
|
448
|
+
"--url",
|
|
449
|
+
options.metadata.repositoryUrl,
|
|
450
|
+
"--token",
|
|
451
|
+
registrationToken,
|
|
452
|
+
"--name",
|
|
453
|
+
options.metadata.name,
|
|
454
|
+
"--labels",
|
|
455
|
+
ACTIVATION_LABEL,
|
|
456
|
+
"--work",
|
|
457
|
+
RUNNER_WORK_DIR,
|
|
458
|
+
"--unattended"
|
|
459
|
+
],
|
|
460
|
+
{ cwd: options.runnerDir, env: options.runnerEnvironment }
|
|
461
|
+
);
|
|
462
|
+
} catch {
|
|
463
|
+
await removeRegistrationFiles(options.runnerDir, adapters);
|
|
464
|
+
throw new RunnerLifecycleError(
|
|
465
|
+
`Re-registering ${options.metadata.name} failed. Partial local registration files were removed; check GitHub Settings > Actions > Runners for an orphan registration, then retry \`shipyard runner start\`. The one-time token was not stored.`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
return { reRegistered: true };
|
|
469
|
+
};
|
|
470
|
+
var removeOwnedRepositoryRunnerContainers = async (options, adapters = defaultAdapters) => {
|
|
471
|
+
const ownedContainers = await runCommand(
|
|
472
|
+
adapters,
|
|
473
|
+
"Finding stale repository-runner containers",
|
|
474
|
+
"docker",
|
|
475
|
+
[
|
|
476
|
+
"ps",
|
|
477
|
+
"-aq",
|
|
478
|
+
"--filter",
|
|
479
|
+
`label=${REPOSITORY_RUNNER_OWNER_LABEL}=${options.repository}`
|
|
480
|
+
],
|
|
481
|
+
{ cwd: options.repoDir, env: options.environment }
|
|
482
|
+
);
|
|
483
|
+
const containerIds = ownedContainers.stdout.split(/\s+/).filter(Boolean);
|
|
484
|
+
if (containerIds.length === 0) return;
|
|
485
|
+
await runCommand(
|
|
486
|
+
adapters,
|
|
487
|
+
"Removing stale repository-runner containers",
|
|
488
|
+
"docker",
|
|
489
|
+
["rm", "-f", ...containerIds],
|
|
490
|
+
{ cwd: options.repoDir, env: options.environment }
|
|
491
|
+
);
|
|
492
|
+
};
|
|
493
|
+
var waitForProcessToStop = async (pid, adapters) => {
|
|
494
|
+
adapters.signalProcess(pid, "SIGTERM");
|
|
495
|
+
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
496
|
+
if (!adapters.isProcessRunning(pid)) return;
|
|
497
|
+
await adapters.pause(100);
|
|
498
|
+
}
|
|
499
|
+
throw new RunnerLifecycleError(
|
|
500
|
+
`Repository runner process ${pid} did not stop within 5 seconds. Local runner files were preserved.`
|
|
501
|
+
);
|
|
502
|
+
};
|
|
503
|
+
var unregisterRepositoryRunner = async (metadata, repoDir, runnerDir, environment, adapters) => {
|
|
504
|
+
await runCommand(
|
|
505
|
+
adapters,
|
|
506
|
+
"Checking current administrative GitHub authorization",
|
|
507
|
+
"gh",
|
|
508
|
+
["auth", "status", "--hostname", "github.com"],
|
|
509
|
+
{ cwd: repoDir, env: environment }
|
|
510
|
+
);
|
|
511
|
+
const response = await runCommand(
|
|
512
|
+
adapters,
|
|
513
|
+
"Requesting a one-time repository runner removal token",
|
|
514
|
+
"gh",
|
|
515
|
+
[
|
|
516
|
+
"api",
|
|
517
|
+
"--method",
|
|
518
|
+
"POST",
|
|
519
|
+
`repos/${metadata.repository}/actions/runners/remove-token`
|
|
520
|
+
],
|
|
521
|
+
{ cwd: repoDir, env: environment }
|
|
522
|
+
);
|
|
523
|
+
const token = readOneTimeToken(response.stdout, "Reading the removal token");
|
|
524
|
+
try {
|
|
525
|
+
await adapters.run(
|
|
526
|
+
"./config.sh",
|
|
527
|
+
["remove", "--token", token, "--unattended"],
|
|
528
|
+
{ cwd: runnerDir, env: repositoryRunnerEnvironment(environment) }
|
|
529
|
+
);
|
|
530
|
+
} catch {
|
|
531
|
+
throw new RunnerLifecycleError(
|
|
532
|
+
`Unregistering ${metadata.name} from GitHub failed. Local runner files were preserved; retry after restoring repository administration access.`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
var removeRepositoryRunner = async (options, adapters = defaultAdapters) => {
|
|
537
|
+
const configDir = join(options.repoDir, CONFIG_DIR);
|
|
538
|
+
const runnerDir = join(configDir, RUNNER_DIR);
|
|
539
|
+
const maskDir = join(configDir, RUNNER_SANDBOX_MASK_DIR);
|
|
540
|
+
if (!await adapters.exists(runnerDir)) {
|
|
541
|
+
throw new RunnerLifecycleError(
|
|
542
|
+
`No repository runner is installed at ${runnerDir}.`
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
let metadata;
|
|
546
|
+
try {
|
|
547
|
+
metadata = await requireInstallMetadata(runnerDir, adapters);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (!options.force) throw error;
|
|
550
|
+
await adapters.remove(runnerDir);
|
|
551
|
+
if (await adapters.exists(maskDir)) await adapters.remove(maskDir);
|
|
552
|
+
return {
|
|
553
|
+
removed: true,
|
|
554
|
+
forced: true,
|
|
555
|
+
manualCleanup: "Check GitHub Settings > Actions > Runners and manually remove any orphan repository runner registration."
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
const lock = await readJson(
|
|
559
|
+
join(runnerDir, RUNNER_CONTROLLER_LOCK),
|
|
560
|
+
adapters
|
|
561
|
+
);
|
|
562
|
+
if (await lockOwnsProcess(lock, adapters)) {
|
|
563
|
+
await waitForProcessToStop(lock.pid, adapters);
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
await unregisterRepositoryRunner(
|
|
567
|
+
metadata,
|
|
568
|
+
options.repoDir,
|
|
569
|
+
runnerDir,
|
|
570
|
+
adapters.environment(),
|
|
571
|
+
adapters
|
|
572
|
+
);
|
|
573
|
+
} catch (error) {
|
|
574
|
+
if (!options.force) {
|
|
575
|
+
const failure = lifecycleFailure("Removing the repository runner", error);
|
|
576
|
+
throw new RunnerLifecycleError(
|
|
577
|
+
`${failure.message} Local runner files were preserved.`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
await adapters.remove(runnerDir);
|
|
581
|
+
if (await adapters.exists(maskDir)) await adapters.remove(maskDir);
|
|
582
|
+
return {
|
|
583
|
+
removed: true,
|
|
584
|
+
forced: true,
|
|
585
|
+
manualCleanup: `Remove runner ${metadata.name} from ${metadata.repository} in GitHub Settings > Actions > Runners.`
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
if (await adapters.exists(maskDir)) await adapters.remove(maskDir);
|
|
589
|
+
await adapters.remove(runnerDir);
|
|
590
|
+
return { removed: true, forced: false };
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// src/sandboxes/docker.ts
|
|
594
|
+
var docker = (options) => {
|
|
595
|
+
const configuredImageName = options?.imageName;
|
|
596
|
+
const selinuxLabel = options?.selinuxLabel ?? "z";
|
|
597
|
+
const maxOutputTailChars = options?.maxOutputTailChars ?? MAX_TAIL_CHARS;
|
|
598
|
+
const sandboxHomedir = "/home/agent";
|
|
599
|
+
const userMounts = options?.mounts ? resolveUserMounts(options.mounts, sandboxHomedir) : [];
|
|
600
|
+
assertIsolatedWorkspaceMounts(userMounts);
|
|
601
|
+
const parentDirsToCreate = processFileMountParents(
|
|
602
|
+
userMounts,
|
|
603
|
+
sandboxHomedir
|
|
604
|
+
);
|
|
605
|
+
return createIsolatedSandboxProvider({
|
|
606
|
+
name: "docker",
|
|
607
|
+
env: options?.env,
|
|
608
|
+
create: async (createOptions) => {
|
|
609
|
+
const containerName = `shipyard-${randomUUID()}`;
|
|
610
|
+
const worktreePath = "/home/agent/workspace";
|
|
611
|
+
const allMounts = userMounts;
|
|
612
|
+
const volumeMounts = allMounts.map((m) => ({
|
|
613
|
+
hostPath: m.hostPath,
|
|
614
|
+
sandboxPath: m.sandboxPath,
|
|
615
|
+
readonly: m.readonly
|
|
616
|
+
}));
|
|
617
|
+
const imageName = configuredImageName ?? defaultImageName(createOptions.hostRepoPath ?? process.cwd());
|
|
618
|
+
const containerUid = options?.containerUid ?? process.getuid?.() ?? 1e3;
|
|
619
|
+
const containerGid = options?.containerGid ?? process.getgid?.() ?? 1e3;
|
|
620
|
+
await checkImageUid(imageName, containerUid);
|
|
621
|
+
await Effect.runPromise(
|
|
622
|
+
startContainer(
|
|
623
|
+
containerName,
|
|
624
|
+
imageName,
|
|
625
|
+
{
|
|
626
|
+
...createOptions.env,
|
|
627
|
+
HOME: "/home/agent"
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
volumeMounts,
|
|
631
|
+
workdir: worktreePath,
|
|
632
|
+
user: `${containerUid}:${containerGid}`,
|
|
633
|
+
network: options?.network,
|
|
634
|
+
groups: options?.groups,
|
|
635
|
+
devices: options?.devices,
|
|
636
|
+
cpus: options?.cpus,
|
|
637
|
+
selinuxLabel,
|
|
638
|
+
labels: process.env[REPOSITORY_RUNNER_OWNER_ENV] ? {
|
|
639
|
+
[REPOSITORY_RUNNER_OWNER_LABEL]: process.env[REPOSITORY_RUNNER_OWNER_ENV]
|
|
640
|
+
} : void 0
|
|
641
|
+
}
|
|
642
|
+
)
|
|
643
|
+
);
|
|
644
|
+
for (const dir of parentDirsToCreate) {
|
|
645
|
+
await new Promise((resolve2, reject) => {
|
|
646
|
+
execFile(
|
|
647
|
+
"docker",
|
|
648
|
+
[
|
|
649
|
+
"exec",
|
|
650
|
+
"--user",
|
|
651
|
+
"0:0",
|
|
652
|
+
containerName,
|
|
653
|
+
"sh",
|
|
654
|
+
"-c",
|
|
655
|
+
`mkdir -p "$1" && chown "$2" "$1"`,
|
|
656
|
+
"sh",
|
|
657
|
+
dir,
|
|
658
|
+
`${containerUid}:${containerGid}`
|
|
659
|
+
],
|
|
660
|
+
(error) => {
|
|
661
|
+
if (error) {
|
|
662
|
+
reject(
|
|
663
|
+
new Error(
|
|
664
|
+
`Failed to create parent directory '${dir}' in container: ${error.message}`
|
|
665
|
+
)
|
|
666
|
+
);
|
|
667
|
+
} else {
|
|
668
|
+
resolve2();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
const removeContainerSync = () => {
|
|
675
|
+
try {
|
|
676
|
+
execFileSync("docker", ["rm", "-f", containerName], {
|
|
677
|
+
stdio: "ignore"
|
|
678
|
+
});
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
const unregisterShutdown = registerShutdown(removeContainerSync);
|
|
683
|
+
const handle = {
|
|
684
|
+
worktreePath,
|
|
685
|
+
exec: (command, opts) => {
|
|
686
|
+
const effectiveCommand = opts?.sudo ? `sudo ${command}` : command;
|
|
687
|
+
const args = ["exec"];
|
|
688
|
+
if (opts?.stdin !== void 0) args.push("-i");
|
|
689
|
+
if (opts?.cwd) args.push("-w", opts.cwd);
|
|
690
|
+
args.push(containerName, "sh", "-c", effectiveCommand);
|
|
691
|
+
return new Promise((resolve2, reject) => {
|
|
692
|
+
const proc = spawn("docker", args, {
|
|
693
|
+
signal: opts?.signal,
|
|
694
|
+
stdio: [
|
|
695
|
+
opts?.stdin !== void 0 ? "pipe" : "ignore",
|
|
696
|
+
"pipe",
|
|
697
|
+
"pipe"
|
|
698
|
+
]
|
|
699
|
+
});
|
|
700
|
+
if (opts?.stdin !== void 0) {
|
|
701
|
+
proc.stdin.write(opts.stdin);
|
|
702
|
+
proc.stdin.end();
|
|
703
|
+
}
|
|
704
|
+
proc.on("error", (error) => {
|
|
705
|
+
reject(new Error(`docker exec failed: ${error.message}`));
|
|
706
|
+
});
|
|
707
|
+
collectProcessOutput(
|
|
708
|
+
{
|
|
709
|
+
stdout: proc.stdout,
|
|
710
|
+
stderr: proc.stderr,
|
|
711
|
+
kill: () => proc.kill("SIGKILL"),
|
|
712
|
+
onClose: (listener) => proc.on("close", listener)
|
|
713
|
+
},
|
|
714
|
+
{ ...opts, maxOutputTailChars },
|
|
715
|
+
resolve2,
|
|
716
|
+
reject
|
|
717
|
+
);
|
|
718
|
+
});
|
|
719
|
+
},
|
|
720
|
+
interactiveExec: (args, opts) => {
|
|
721
|
+
return new Promise((resolve2, reject) => {
|
|
722
|
+
opts.signal?.throwIfAborted();
|
|
723
|
+
const executionId = randomUUID();
|
|
724
|
+
const pidFile = `/tmp/shipyard-interactive-${executionId}.pid`;
|
|
725
|
+
const cancelFile = `/tmp/shipyard-interactive-${executionId}.cancel`;
|
|
726
|
+
const dockerArgs = ["exec"];
|
|
727
|
+
if ("isTTY" in opts.stdin && opts.stdin.isTTY) {
|
|
728
|
+
dockerArgs.push("-it");
|
|
729
|
+
} else {
|
|
730
|
+
dockerArgs.push("-i");
|
|
731
|
+
}
|
|
732
|
+
if (opts.cwd) dockerArgs.push("-w", opts.cwd);
|
|
733
|
+
dockerArgs.push(
|
|
734
|
+
containerName,
|
|
735
|
+
"sh",
|
|
736
|
+
"-c",
|
|
737
|
+
'pid_file=$1; cancel_file=$2; shift 2; command -v setsid >/dev/null 2>&1 && command -v ps >/dev/null 2>&1 && command -v awk >/dev/null 2>&1 || { printf "%s\\n" "setsid, ps, and awk are required for cancellable interactive execution" >&2; exit 127; }; if [ -e "$cancel_file" ]; then rm -f "$pid_file" "$cancel_file"; exit 130; fi; setsid "$@" & child=$!; printf "%s\\n" "$child" > "$pid_file"; wait "$child"; status=$?; rm -f "$pid_file" "$cancel_file"; exit "$status"',
|
|
738
|
+
"shipyard-interactive",
|
|
739
|
+
pidFile,
|
|
740
|
+
cancelFile,
|
|
741
|
+
...args
|
|
742
|
+
);
|
|
743
|
+
const proc = spawn("docker", dockerArgs, {
|
|
744
|
+
stdio: [opts.stdin, opts.stdout, opts.stderr]
|
|
745
|
+
});
|
|
746
|
+
let settled = false;
|
|
747
|
+
let cancelling = false;
|
|
748
|
+
const finish = (result) => {
|
|
749
|
+
if (settled) return;
|
|
750
|
+
settled = true;
|
|
751
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
752
|
+
if ("error" in result) reject(result.error);
|
|
753
|
+
else resolve2(result);
|
|
754
|
+
};
|
|
755
|
+
const onAbort = () => {
|
|
756
|
+
if (cancelling || settled) return;
|
|
757
|
+
cancelling = true;
|
|
758
|
+
const cancellationScript = `touch "$1"; attempts=0; while [ ! -s "$2" ] && [ "$attempts" -lt 50 ]; do sleep 0.1; attempts=$((attempts + 1)); done; if [ -s "$2" ]; then pid=$(cat "$2"); group_running() { ps -eo pgid=,stat= | awk -v pg="$pid" '$1 == pg && $2 !~ /^Z/ { found=1 } END { exit found ? 0 : 1 }'; }; kill -TERM "-$pid" 2>/dev/null || true; attempts=0; while group_running && [ "$attempts" -lt 20 ]; do sleep 0.1; attempts=$((attempts + 1)); done; kill -KILL "-$pid" 2>/dev/null || true; attempts=0; while group_running && [ "$attempts" -lt 20 ]; do sleep 0.1; attempts=$((attempts + 1)); done; if group_running; then exit 1; fi; rm -f "$1" "$2"; fi`;
|
|
759
|
+
execFile(
|
|
760
|
+
"docker",
|
|
761
|
+
[
|
|
762
|
+
"exec",
|
|
763
|
+
containerName,
|
|
764
|
+
"sh",
|
|
765
|
+
"-c",
|
|
766
|
+
cancellationScript,
|
|
767
|
+
"shipyard-cancel",
|
|
768
|
+
cancelFile,
|
|
769
|
+
pidFile
|
|
770
|
+
],
|
|
771
|
+
{ timeout: 7e3 },
|
|
772
|
+
(error) => {
|
|
773
|
+
proc.kill("SIGKILL");
|
|
774
|
+
if (error) {
|
|
775
|
+
finish({
|
|
776
|
+
error: new Error(
|
|
777
|
+
`Failed to terminate interactive process in Docker sandbox: ${error.message}`
|
|
778
|
+
)
|
|
779
|
+
});
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
finish({
|
|
783
|
+
error: opts.signal?.reason ?? new Error("Interactive execution aborted")
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
);
|
|
787
|
+
};
|
|
788
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
789
|
+
if (opts.signal?.aborted) onAbort();
|
|
790
|
+
proc.on("error", (error) => {
|
|
791
|
+
if (!cancelling) {
|
|
792
|
+
finish({
|
|
793
|
+
error: new Error(`docker exec failed: ${error.message}`)
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
proc.on("close", (code) => {
|
|
798
|
+
if (cancelling) return;
|
|
799
|
+
execFile(
|
|
800
|
+
"docker",
|
|
801
|
+
["exec", containerName, "rm", "-f", pidFile, cancelFile],
|
|
802
|
+
() => {
|
|
803
|
+
}
|
|
804
|
+
);
|
|
805
|
+
finish({ exitCode: code ?? 0 });
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
},
|
|
809
|
+
copyIn: (hostPath, sandboxPath) => copyIntoContainer(
|
|
810
|
+
"docker",
|
|
811
|
+
containerName,
|
|
812
|
+
`${containerUid}:${containerGid}`,
|
|
813
|
+
hostPath,
|
|
814
|
+
sandboxPath
|
|
815
|
+
),
|
|
816
|
+
copyFileOut: (sandboxPath, hostPath) => new Promise((resolve2, reject) => {
|
|
817
|
+
execFile(
|
|
818
|
+
"docker",
|
|
819
|
+
["cp", `${containerName}:${sandboxPath}`, hostPath],
|
|
820
|
+
(error) => {
|
|
821
|
+
if (error) {
|
|
822
|
+
reject(new Error(`docker cp (out) failed: ${error.message}`));
|
|
823
|
+
} else {
|
|
824
|
+
resolve2();
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
);
|
|
828
|
+
}),
|
|
829
|
+
close: async () => {
|
|
830
|
+
unregisterShutdown();
|
|
831
|
+
await Effect.runPromise(removeContainer(containerName));
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
return handle;
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
};
|
|
838
|
+
var checkImageUid = (imageName, expectedUid) => new Promise((resolve2, reject) => {
|
|
839
|
+
execFile(
|
|
840
|
+
"docker",
|
|
841
|
+
["image", "inspect", imageName, "--format", "{{.Config.User}}"],
|
|
842
|
+
(error, stdout) => {
|
|
843
|
+
if (error) {
|
|
844
|
+
reject(
|
|
845
|
+
new Error(
|
|
846
|
+
`Image '${imageName}' not found locally. Build it first with 'shipyard docker build-image'.`
|
|
847
|
+
)
|
|
848
|
+
);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const imageUser = (stdout ?? "").toString().trim();
|
|
852
|
+
if (!imageUser) {
|
|
853
|
+
resolve2();
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const uidPart = imageUser.split(":")[0];
|
|
857
|
+
const imageUid = parseInt(uidPart, 10);
|
|
858
|
+
if (isNaN(imageUid)) {
|
|
859
|
+
resolve2();
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (imageUid !== expectedUid) {
|
|
863
|
+
reject(
|
|
864
|
+
new Error(
|
|
865
|
+
`UID mismatch: image '${imageName}' was built with UID ${imageUid}, but the expected UID is ${expectedUid}. Rebuild the image with 'shipyard docker build-image', or pass containerUid: ${imageUid} to docker() to match the image.`
|
|
866
|
+
)
|
|
867
|
+
);
|
|
868
|
+
} else {
|
|
869
|
+
resolve2();
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
);
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
export { REPOSITORY_RUNNER_OWNER_ENV, RUNNER_CONTROLLER_LOCK, RUNNER_INSTALL_METADATA, RUNNER_WORK_DIR, RunnerLifecycleError, buildImage, docker, recoverRepositoryRunner, removeImage, removeOwnedRepositoryRunnerContainers, removeRepositoryRunner, validateExistingRepositoryRunner };
|
|
876
|
+
//# sourceMappingURL=chunk-WAXEMZUV.js.map
|
|
877
|
+
//# sourceMappingURL=chunk-WAXEMZUV.js.map
|