@outerlayer/cli 0.6.0 → 0.8.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +15 -5
  3. package/dist/{agent-setup-USZ2SAXC.js → agent-setup-C7PLRXZO.js} +7 -6
  4. package/dist/{build-PAEKVYVK.js → build-MS34XEGN.js} +3 -1
  5. package/dist/build-info.json +1 -1
  6. package/dist/builder-G52A6GW2.js +13 -0
  7. package/dist/{check-CCVPBN7A.js → check-FOF3PUKI.js} +35 -26
  8. package/dist/{chunk-ZILGTTY6.js → chunk-3S4P55NE.js} +3 -3
  9. package/dist/{chunk-MIGEDH5P.js → chunk-4A5E2G4E.js} +9 -9
  10. package/dist/chunk-4ACZCY4I.js +39 -0
  11. package/dist/{chunk-OIRNH6UO.js → chunk-5FWNEQNZ.js} +3 -3
  12. package/dist/{chunk-BNMCXPG7.js → chunk-5QLNZGAQ.js} +12 -3
  13. package/dist/chunk-5TNS3PL7.js +263 -0
  14. package/dist/chunk-7RQ73DLO.js +23 -0
  15. package/dist/{chunk-VMJ3MMBX.js → chunk-7VNURP6M.js} +538 -46
  16. package/dist/{chunk-EMY4I27X.js → chunk-7ZR5UPMB.js} +555 -5
  17. package/dist/{chunk-UMFWVGJO.js → chunk-AIL2R4SI.js} +4 -3
  18. package/dist/{chunk-NQXFAGHU.js → chunk-AX23QKHS.js} +7 -3
  19. package/dist/{chunk-RZSF5B46.js → chunk-CM5QTKM4.js} +152 -158
  20. package/dist/chunk-CX22S5BA.js +45 -0
  21. package/dist/{chunk-H5NCNVDA.js → chunk-EV3M3EH7.js} +2 -2
  22. package/dist/{chunk-ANK2B36Y.js → chunk-GAYWY4GI.js} +6190 -28615
  23. package/dist/chunk-HKMPVPO7.js +55 -0
  24. package/dist/{chunk-43J3M6KB.js → chunk-IJBVPLZZ.js} +91 -39
  25. package/dist/{chunk-PMAMOSTP.js → chunk-ILEA3YHM.js} +85 -25
  26. package/dist/{chunk-MXB5VB4Y.js → chunk-IRCBTGWF.js} +6 -6
  27. package/dist/{chunk-OUUINEYB.js → chunk-JKIWMRFH.js} +1 -1
  28. package/dist/{chunk-FA2BOX4O.js → chunk-JPR2WHHF.js} +1 -1
  29. package/dist/{chunk-IASWYRVG.js → chunk-K35RW5HP.js} +4 -1
  30. package/dist/{chunk-ZMYPLFG3.js → chunk-M3SQ4YGD.js} +1 -1
  31. package/dist/{chunk-4NLKY4WR.js → chunk-NALVDFIS.js} +2 -2
  32. package/dist/{chunk-AUFG23AA.js → chunk-OD2VTIHN.js} +19 -47
  33. package/dist/{chunk-JWOQNNN2.js → chunk-OEK4JEB3.js} +33 -4
  34. package/dist/chunk-PTW6W5NR.js +127 -0
  35. package/dist/{chunk-3DD6AOWC.js → chunk-PWBILI5P.js} +28 -3
  36. package/dist/{chunk-B2G7JHHB.js → chunk-R7STSBLC.js} +1 -1
  37. package/dist/{chunk-YYYXRJUW.js → chunk-TI7B7UEO.js} +1 -1
  38. package/dist/{chunk-Z62HNKEJ.js → chunk-TTNFGMHO.js} +72 -54
  39. package/dist/{chunk-RBOMWNOG.js → chunk-TUUMW4TC.js} +2 -2
  40. package/dist/{chunk-774HEQL6.js → chunk-U3GOI2B4.js} +91 -8
  41. package/dist/{chunk-F6GFZXZ3.js → chunk-UXYLWVTQ.js} +1 -18
  42. package/dist/{chunk-WO2BXCTQ.js → chunk-VXLJORHL.js} +1 -1
  43. package/dist/{chunk-CBPPJSAR.js → chunk-WVGSB3Z2.js} +2 -2
  44. package/dist/chunk-YNJEOGDL.js +22434 -0
  45. package/dist/{chunk-W4SQCOZU.js → chunk-ZOAEPWGL.js} +1 -1
  46. package/dist/{chunk-3DEVYTNQ.js → chunk-ZYHKJKSU.js} +1 -1
  47. package/dist/{chunk-WIOZAJ2W.js → chunk-ZZXLM64C.js} +1 -1
  48. package/dist/{cli-7WCNPBYC.js → cli-AOJPAVFX.js} +109 -81
  49. package/dist/{command-ARFK3OYF.js → command-SY3SVLAI.js} +67 -27
  50. package/dist/config-Z3USDBLU.js +11 -0
  51. package/dist/{connect-cmd-PCESZLTR.js → connect-cmd-PNZ6IJLJ.js} +4 -4
  52. package/dist/{context-adopt-G5JAELE7.js → context-adopt-CE4KDXS3.js} +5 -4
  53. package/dist/{context-materialize-3HLPZAF6.js → context-materialize-QL33HDJN.js} +5 -4
  54. package/dist/{dist-RDLP4GGJ.js → dist-3ZTZF2TP.js} +2 -1
  55. package/dist/docker-676FD3CQ.js +15 -0
  56. package/dist/{doctor-VOMYOHQU.js → doctor-WR6TRPSU.js} +49 -19
  57. package/dist/{emit-artifact-cmd-TBF46VXZ.js → emit-artifact-cmd-AVM4YXMQ.js} +11 -4
  58. package/dist/{emit-cmd-DCERAU27.js → emit-cmd-2QRMYPYO.js} +4 -3
  59. package/dist/{emit-criteria-cmd-3427XKFW.js → emit-criteria-cmd-LDBE4YDS.js} +10 -1
  60. package/dist/{emit-finding-cmd-GSI75K36.js → emit-finding-cmd-6R5WBPCH.js} +12 -1
  61. package/dist/{emit-result-cmd-3PRQKB55.js → emit-result-cmd-2DMN6O6U.js} +13 -6
  62. package/dist/{exec-client-4XGXV3XN.js → exec-client-NPYNUAF3.js} +4 -3
  63. package/dist/{guest-init-GENRHNP7.js → guest-init-JWNNAH2H.js} +4 -3
  64. package/dist/{hook-fast-NJBBZQ5Q.js → hook-fast-XBOZ3N6L.js} +1 -1
  65. package/dist/{import-capture-cmd-AVZ4VIVJ.js → import-capture-cmd-T4556ELW.js} +3 -2
  66. package/dist/{import-ruler-cmd-GUHL6IY5.js → import-ruler-cmd-SG3UVGI7.js} +2 -1
  67. package/dist/index.js +3 -3
  68. package/dist/init-KDK5QLQZ.js +46 -0
  69. package/dist/{init-OHYFIKZD.js → init-R2FJ5KNS.js} +35 -21
  70. package/dist/{init-cmd-HMFNIFPI.js → init-cmd-BYZYAJ5P.js} +10 -9
  71. package/dist/{install-DSUZE2TJ.js → install-FHTVJZS6.js} +7 -5
  72. package/dist/{install-cmd-453C7IZT.js → install-cmd-DCF7TVXD.js} +3 -2
  73. package/dist/{logs-7RYUH5A6.js → logs-H55NWW4H.js} +1 -0
  74. package/dist/{loop-UAJB34KA.js → loop-AG3PLXPO.js} +35 -26
  75. package/dist/machine-LV6WBRRK.js +45 -0
  76. package/dist/{session-sender-ED26V45B.js → session-sender-Q5HAMOL2.js} +6 -5
  77. package/dist/{starter-pack-LIZYMKYQ.js → starter-pack-MUXMAU4S.js} +4 -3
  78. package/dist/{status-6WNXX2O7.js → status-HH4OSOHR.js} +29 -20
  79. package/dist/{sync-cmd-YONVGUYS.js → sync-cmd-CAEPJR7B.js} +5 -4
  80. package/dist/{watch-HHKAF26L.js → watch-LAAZFBHH.js} +5 -4
  81. package/dist/{work-claim-cmd-LXBZTMJA.js → work-claim-cmd-ARIYZARE.js} +15 -7
  82. package/dist/{work-cmd-OVNF44FY.js → work-cmd-B3FOXZAL.js} +9 -3
  83. package/dist/{work-comment-cmd-6DGQNTAH.js → work-comment-cmd-O5WGRI6K.js} +9 -3
  84. package/dist/{work-open-pr-cmd-FCZHFHZS.js → work-open-pr-cmd-M6CQW7KW.js} +10 -4
  85. package/dist/{work-pr-cmd-QJJVHZDC.js → work-pr-cmd-C4YEGKD6.js} +9 -2
  86. package/package.json +1 -1
  87. package/skill-pack/template/build/scripts/record-criteria.mjs +2 -1
  88. package/dist/chunk-7DBI6SNT.js +0 -55
  89. package/dist/config-HL2AIR3K.js +0 -9
  90. package/dist/docker-NEGDLU6D.js +0 -7
  91. package/dist/init-WVR4BZQ4.js +0 -36
  92. package/dist/machine-2JOWGRSZ.js +0 -35
@@ -1,30 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
3
  import { writeJobFile, readJobFile } from './chunk-T6OIJQRT.js';
4
- import { senderPidfile } from './chunk-ZILGTTY6.js';
5
- import { DOCKERD_LOG } from './chunk-CBPPJSAR.js';
6
- import { installScrubContext } from './chunk-UMFWVGJO.js';
7
- import { AssetError, describeCallFailure, obtainImage, ensureRootfs, gitRemoteFor, startServicesFor, createBuildDisk, machineConfig, refusalCode, resolveAgentUser, AgentUserError, runBuildSteps, removeStagedContext, stageGovernedContext, shellQuote, createRunnerLog, createContainerRuntime, stopContainer, syncInContainer, formatDuration, cleanupContainer, retrySend, formatClock, containerSenderCommand, GH_CONFIG_YML, ghHostsYml, ADDED_USER, isLiteralAddress, signalInContainer, deleteBuildDisk, senderSignalScript } from './chunk-RZSF5B46.js';
8
- import { CLAUDE_CODE_VERSION } from './chunk-Z62HNKEJ.js';
4
+ import { senderPidfile } from './chunk-3S4P55NE.js';
5
+ import { DOCKERD_LOG } from './chunk-WVGSB3Z2.js';
6
+ import { installScrubContext } from './chunk-AIL2R4SI.js';
7
+ import { AssetError, describeCallFailure, obtainImage, ensureRootfs, gitRemoteFor, startServicesFor, createBuildDisk, machineConfig, refusalCode, resolveAgentUser, AgentUserError, runBuildSteps, removeStagedContext, stageGovernedContext, shellQuote, createRunnerLog, createContainerRuntime, stopContainer, cleanupContainer, outOfMemoryReason, formatDuration, retrySend, formatClock, containerSenderCommand, syncInContainer, GH_CONFIG_YML, ghHostsYml, ADDED_USER, isLiteralAddress, signalInContainer, deleteBuildDisk, startCheckContainer, lifecycleSteps, lifecycleFailureReason, senderSignalScript } from './chunk-CM5QTKM4.js';
8
+ import { readBranchHead, fetchCommit } from './chunk-TTNFGMHO.js';
9
9
  import { isSameProcess, startTimeOf } from './chunk-N5FOE5PS.js';
10
- import { childEnvironment } from './chunk-774HEQL6.js';
11
- import { waitForAgent, vmExecArgs } from './chunk-W4SQCOZU.js';
12
- import { socketPathProblem, vsockLinks, linkVsockSockets } from './chunk-B2G7JHHB.js';
13
- import { WorkCommandError, postJson, isSignatureRefusal, getJson } from './chunk-BNMCXPG7.js';
14
- import { planArtifactUpload } from './chunk-3DEVYTNQ.js';
15
- import { scrubText } from './chunk-ANK2B36Y.js';
16
- import { isBuiltinContainer, isBuiltinProcess, isBuiltinVm } from './chunk-3DD6AOWC.js';
17
- import { parseSize } from './chunk-F6GFZXZ3.js';
18
- import { claudeCredential, buildEnvironment, JOB_MOUNT, CHECKOUT_DIR, parseIsolation, proxyEnvironment, containerLocalAddress, destinationEnvironment, CLAUDE_CREDENTIAL_VARIABLES, builtInEnvironment, withHooksPath, startBuildServices, BUILT_IN_NAMES, AGENT_CREDENTIAL_MISSING, sharedUserEnvironment, headerVariables, authorEnvironment, withTunnelRecord, commitIdentityRecord, RELAY_PORT } from './chunk-AUFG23AA.js';
10
+ import { childEnvironment } from './chunk-U3GOI2B4.js';
11
+ import { CLAUDE_CODE_VERSION } from './chunk-CX22S5BA.js';
12
+ import { waitForAgent, vmExecArgs } from './chunk-ZOAEPWGL.js';
13
+ import { socketPathProblem, vsockLinks, linkVsockSockets } from './chunk-R7STSBLC.js';
14
+ import { WorkCommandError, postJson, isSignatureRefusal, getJson } from './chunk-5QLNZGAQ.js';
15
+ import { planArtifactUpload } from './chunk-ZYHKJKSU.js';
16
+ import { scrubText } from './chunk-GAYWY4GI.js';
17
+ import { isBuiltinContainer, isBuiltinProcess, isBuiltinVm, DEFAULT_CHECK_TIME_LIMIT_MINUTES } from './chunk-PWBILI5P.js';
18
+ import { parseSize } from './chunk-7RQ73DLO.js';
19
+ import { claudeCredential, buildEnvironment, JOB_MOUNT, CHECKOUT_DIR, parseIsolation, proxyEnvironment, containerLocalAddress, destinationEnvironment, CLAUDE_CREDENTIAL_VARIABLES, builtInEnvironment, startBuildServices, BUILT_IN_NAMES, AGENT_CREDENTIAL_MISSING, sharedUserEnvironment, headerVariables, withTunnelRecord, RELAY_PORT, checkEnvironment } from './chunk-OD2VTIHN.js';
20
+ import { withHooksPath, authorEnvironment, commitIdentityRecord } from './chunk-HKMPVPO7.js';
21
+ import { parsePolicy } from './chunk-7ZR5UPMB.js';
19
22
  import { readCloudConfig } from './chunk-EABW6AJQ.js';
20
23
  import { resolveVersion } from './chunk-RA3O54FC.js';
21
24
  import { signerFor } from './chunk-M5POMKKW.js';
22
25
  import { runnerDir, buildDir, vsockPath, RUNNER_FILE_MODE } from './chunk-ZM2IMMYN.js';
23
26
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
24
27
  import { spawn, spawnSync } from 'child_process';
25
- import { randomBytes } from 'crypto';
26
- import { mkdirSync, chmodSync, writeFileSync, appendFileSync, statSync, accessSync, constants, openSync, existsSync, readFileSync, closeSync, rmSync, readdirSync } from 'fs';
27
- import { join, isAbsolute, dirname, delimiter } from 'path';
28
+ import { randomBytes, createHash } from 'crypto';
29
+ import { mkdirSync, chmodSync, writeFileSync, appendFileSync, statSync, accessSync, constants, openSync, existsSync, readFileSync, closeSync, rmSync, readdirSync, lstatSync, realpathSync } from 'fs';
30
+ import { join, isAbsolute, dirname, delimiter, sep } from 'path';
28
31
 
29
32
  createRequire(import.meta.url);
30
33
  var DEFAULT_MEMORY = "8g";
@@ -119,7 +122,9 @@ async function provisionMicroVm(input) {
119
122
  repository,
120
123
  imageId: report.imageId,
121
124
  containerEnv: report.apply.containerEnv,
122
- builtIns: true
125
+ builtIns: true,
126
+ lifecycle: report.apply.lifecycle,
127
+ ...report.docker === true ? { docker: true } : {}
123
128
  };
124
129
  const env = buildEnvironment({
125
130
  config,
@@ -143,12 +148,20 @@ async function provisionMicroVm(input) {
143
148
  };
144
149
  input.save(state, environment);
145
150
  const bail = async (reason, aborted = false) => {
151
+ let why = reason;
152
+ if (!aborted && state.cgroup !== void 0) {
153
+ try {
154
+ if (runtime.vm.cgroup.oomKills(state.cgroup) > 0) why = outOfMemoryReason(config.build?.memory);
155
+ } catch (err) {
156
+ log.error(`${tag} could not read the build's memory events: ${describeCallFailure(err)} \xB7 continuing`);
157
+ }
158
+ }
146
159
  try {
147
160
  await cleanupMicroVm(runtime, state);
148
161
  } catch (err) {
149
162
  log.error(`${tag} cleanup failed: ${describeCallFailure(err)}`);
150
163
  }
151
- return aborted ? { ok: false, reason: "ended before provision finished", aborted: true } : fail(reason);
164
+ return aborted ? { ok: false, reason: "ended before provision finished", aborted: true } : fail(why);
152
165
  };
153
166
  if (input.aborted()) return bail("", true);
154
167
  let imageConfig;
@@ -393,6 +406,147 @@ async function copyHookErrors(runtime, state, jobDir) {
393
406
  } catch {
394
407
  }
395
408
  }
409
+ async function startCheckMicroVm(input) {
410
+ const { config, runtime, job, jobDir, tokens, log } = input;
411
+ const { vm } = runtime;
412
+ const tag = `#${job.item}`;
413
+ const built = job.microvm;
414
+ const repository = built?.repository ?? job.repository;
415
+ if (built === void 0 || built.imageId === void 0 || repository === void 0 || job.branch === void 0) {
416
+ return { ok: false, reason: "the build's image, repository or branch is not recorded" };
417
+ }
418
+ const branch = job.branch;
419
+ const runnerDirPath = runnerDir(config.configPath);
420
+ const tooDeep = socketPathProblem(buildDir(config.configPath, "0".repeat(12)));
421
+ if (tooDeep !== void 0) return { ok: false, reason: tooDeep };
422
+ const buildId = randomBytes(6).toString("hex");
423
+ const buildDir2 = buildDir(config.configPath, buildId);
424
+ const agentUser = built.agentUser ?? ADDED_USER;
425
+ const state = {
426
+ buildId,
427
+ buildDir: buildDir2,
428
+ repository,
429
+ imageId: built.imageId,
430
+ agentUser,
431
+ containerEnv: built.containerEnv,
432
+ imageEnv: built.imageEnv,
433
+ lifecycle: built.lifecycle,
434
+ ...built.docker === true ? { docker: true } : {}
435
+ };
436
+ const checksDir = join(jobDir, "checks");
437
+ mkdirSync(checksDir, { recursive: true });
438
+ const consolePath = join(checksDir, "vm-console.log");
439
+ input.save(state);
440
+ const fail = async (reason) => {
441
+ try {
442
+ await cleanupMicroVm(runtime, state);
443
+ } catch (err) {
444
+ log.error(`${tag} cleanup of the check environment failed: ${describeCallFailure(err)}`);
445
+ }
446
+ return { ok: false, reason };
447
+ };
448
+ const abandoned = () => input.aborted?.() === true;
449
+ const ABANDONED = "the check environment was abandoned before it was ready";
450
+ let assets;
451
+ try {
452
+ assets = await vm.assets(runnerDirPath);
453
+ } catch (err) {
454
+ return fail(err instanceof AssetError ? err.message : `the microVM's assets could not be had: ${describeCallFailure(err)}`);
455
+ }
456
+ if (abandoned()) return fail(ABANDONED);
457
+ try {
458
+ state.rootfs = await ensureRootfs({ host: vm.rootfs, runnerDir: runnerDirPath, imageId: built.imageId });
459
+ } catch (err) {
460
+ return fail(`the image could not be converted to a root disk: ${describeCallFailure(err)}`);
461
+ }
462
+ if (abandoned()) return fail(ABANDONED);
463
+ state.buildDisk = join(buildDir2, "build.ext4");
464
+ state.vsockPath = vsockPath(buildDir2);
465
+ input.save(state);
466
+ mkdirSync(buildDir2, { recursive: true, mode: 448 });
467
+ try {
468
+ const gitRemote = gitRemoteFor(runtime, { repository, branch, tokens, buildDir: buildDir2, log: (line) => log.line(`${tag} ${line}`), readOnly: true });
469
+ runtime.tunnels.set(buildId, await startServicesFor(runtime, config, buildDir2, checksDir, void 0, false, gitRemote));
470
+ } catch (err) {
471
+ return fail(`the tunnel could not start: ${describeCallFailure(err)}`);
472
+ }
473
+ if (abandoned()) return fail(ABANDONED);
474
+ const limits = {
475
+ cpus: config.build?.cpus ?? DEFAULT_CPUS,
476
+ memoryBytes: parseSize(config.build?.memory ?? DEFAULT_MEMORY) ?? parseSize(DEFAULT_MEMORY),
477
+ diskBytes: parseSize(config.build?.disk ?? DEFAULT_DISK) ?? parseSize(DEFAULT_DISK)
478
+ };
479
+ const links = vsockLinks({ destinations: config.build?.destinations?.length ?? 0, builtIns: false });
480
+ const configPath = join(buildDir2, "vm.json");
481
+ try {
482
+ linkVsockSockets({ buildDir: buildDir2, vsockPath: state.vsockPath, links });
483
+ await createBuildDisk({ host: vm.rootfs, runnerDir: runnerDirPath, bytes: limits.diskBytes, dest: state.buildDisk });
484
+ writeFileSync(
485
+ configPath,
486
+ JSON.stringify(
487
+ machineConfig(limits, { kernel: assets.kernel, rootfs: state.rootfs, buildDisk: state.buildDisk, vsock: state.vsockPath, relayPorts: links.map((link) => link.port), docker: built.docker }),
488
+ null,
489
+ 2
490
+ ),
491
+ { mode: 384 }
492
+ );
493
+ } catch (err) {
494
+ return fail(`the check environment's disk and machine could not be made: ${describeCallFailure(err)}`);
495
+ }
496
+ if (abandoned()) return fail(ABANDONED);
497
+ try {
498
+ state.cgroup = vm.cgroup.create(buildId, limits.memoryBytes);
499
+ } catch (err) {
500
+ return fail(`the check environment's memory limit could not be set: ${describeCallFailure(err)}`);
501
+ }
502
+ if (state.cgroup !== void 0) input.save(state);
503
+ const boot = async () => {
504
+ try {
505
+ state.firecracker = await vm.firecracker.spawn({ binary: assets.firecracker, configPath, consolePath, cgroup: state.cgroup });
506
+ } catch (err) {
507
+ return `Firecracker did not start: ${describeCallFailure(err)}`;
508
+ }
509
+ input.save(state);
510
+ if (abandoned()) return ABANDONED;
511
+ const ready = await waitForAgent(state.vsockPath, { attempts: BOOT_ATTEMPTS, delayMs: BOOT_POLL_MS, sleep: runtime.sleep });
512
+ if (!ready) {
513
+ const why = vm.firecracker.alive(state.firecracker) ? "its exec agent did not answer" : "it exited before its exec agent answered";
514
+ return `the microVM did not start: ${why}; its console is in ${consolePath}`;
515
+ }
516
+ if (built.docker === true) return waitForDocker(runtime, state, agentUser, () => /* @__PURE__ */ new Date());
517
+ return void 0;
518
+ };
519
+ const failed = await boot();
520
+ if (failed !== void 0) return fail(failed);
521
+ if (abandoned()) return fail(ABANDONED);
522
+ const env = checkEnvironment({ config, item: job.item, repository, hostEnv: runtime.hostEnv, containerEnv: built.containerEnv ?? {} });
523
+ return {
524
+ ok: true,
525
+ environment: {
526
+ state,
527
+ exec: async (argv, workdir, opts) => {
528
+ const ran = await vm.exec(state.vsockPath, { argv, user: agentUser, cwd: workdir, env: execEnvironment(state.imageEnv, env) });
529
+ if (opts?.logPath !== void 0) appendFileSync(opts.logPath, ran.stdout + ran.stderr);
530
+ return ran;
531
+ },
532
+ restart: async () => {
533
+ try {
534
+ await vm.firecracker.stop(state.firecracker);
535
+ rmSync(state.vsockPath, { force: true });
536
+ return await boot() === void 0;
537
+ } catch {
538
+ return false;
539
+ }
540
+ }
541
+ }
542
+ };
543
+ }
544
+ async function stopBuildMicroVm(runtime, state, jobDir) {
545
+ await copyHookErrors(runtime, state, jobDir);
546
+ if (state.firecracker === void 0) return;
547
+ await runtime.vm.firecracker.stop(state.firecracker);
548
+ if (runtime.vm.firecracker.alive(state.firecracker)) throw new Error(`the microVM (pid ${state.firecracker.pid}) is still running after it was stopped`);
549
+ }
396
550
  async function cleanupMicroVm(runtime, state, jobDir) {
397
551
  const failures = [];
398
552
  if (jobDir !== void 0) await copyHookErrors(runtime, state, jobDir);
@@ -422,8 +576,8 @@ async function cleanupMicroVm(runtime, state, jobDir) {
422
576
  async function stopOrphanedMicroVms(input) {
423
577
  const stopped = [];
424
578
  for (const { job } of input.jobs) {
425
- const state = job.microvm;
426
- if (state === void 0) continue;
579
+ const states = [job.microvm, job.checkMicrovm].filter((state) => state !== void 0);
580
+ if (states.length === 0) continue;
427
581
  if (job.pid !== void 0 && job.start !== void 0 && isSameProcess(job.pid, job.start)) {
428
582
  try {
429
583
  process.kill(-job.pid, "SIGKILL");
@@ -432,9 +586,11 @@ async function stopOrphanedMicroVms(input) {
432
586
  const deadline = Date.now() + 2e3;
433
587
  while (isSameProcess(job.pid, job.start) && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 10));
434
588
  }
435
- if (state.firecracker !== void 0 && input.runtime.vm.firecracker.alive(state.firecracker)) {
436
- await input.runtime.vm.firecracker.stop(state.firecracker);
437
- stopped.push(state.buildId);
589
+ for (const state of states) {
590
+ if (state.firecracker !== void 0 && input.runtime.vm.firecracker.alive(state.firecracker)) {
591
+ await input.runtime.vm.firecracker.stop(state.firecracker);
592
+ stopped.push(state.buildId);
593
+ }
438
594
  }
439
595
  }
440
596
  return stopped;
@@ -717,6 +873,297 @@ async function provisionProcess(input) {
717
873
  function cleanupProcess(runtime, state) {
718
874
  return closeHostBuild(runtime, state);
719
875
  }
876
+ var MAX_VALIDATOR_FILES = 20;
877
+ var VALIDATORS_DIR = ".outerlayer/validators";
878
+ var POLICY_FILE = ".outerlayer/policy.yaml";
879
+ var LIMIT_KEY_TAIL = " (runner.checks.timeLimitMinutes)";
880
+ function lastLine(text2) {
881
+ return text2.split(/\r\n|\r|\n/).map((line) => line.trim()).filter((line) => line !== "").at(-1);
882
+ }
883
+ function readInside(root, rel) {
884
+ const path = join(root, rel);
885
+ try {
886
+ if (!lstatSync(path).isFile()) return void 0;
887
+ const inside = realpathSync(path);
888
+ const base = realpathSync(root);
889
+ return inside.startsWith(base + sep) ? readFileSync(inside, "utf8") : void 0;
890
+ } catch {
891
+ return void 0;
892
+ }
893
+ }
894
+ function validatorFilesIn(root) {
895
+ const dir = join(root, VALIDATORS_DIR);
896
+ let names;
897
+ try {
898
+ if (!lstatSync(dir).isDirectory()) return [];
899
+ names = readdirSync(dir);
900
+ } catch {
901
+ return [];
902
+ }
903
+ const files = [];
904
+ for (const name of names.filter((entry) => /\.ya?ml$/.test(entry)).sort().slice(0, MAX_VALIDATOR_FILES)) {
905
+ const path = `${VALIDATORS_DIR}/${name}`;
906
+ const content = readInside(root, path);
907
+ if (content !== void 0) files.push({ path, content });
908
+ }
909
+ return files;
910
+ }
911
+ function checksIn(root, log, tag) {
912
+ const policy = readInside(root, POLICY_FILE);
913
+ const loaded = parsePolicy(policy === void 0 ? null : { path: POLICY_FILE, content: policy }, validatorFilesIn(root));
914
+ for (const problem of loaded.problems) log.error(`${tag} ${problem.file}: ${problem.problem}`);
915
+ const hostChecks = loaded.customs.filter((def) => def.runWhere === "host" && def.level !== "off" && def.declaresEmit !== null && def.command !== void 0);
916
+ return hostChecks.map((def) => ({ name: def.declaresEmit, command: def.command }));
917
+ }
918
+ async function readDefinitions(input, repository, scratch, token) {
919
+ const { config, job, deps } = input;
920
+ const tag = `#${job.item}`;
921
+ const stage = deps.stage ?? stageGovernedContext;
922
+ const controlPlaneUrl = (controlPlane) => deps.runtime.remoteFor?.(controlPlane) ?? `${deps.runtime.github.gitUrl}/${controlPlane}.git`;
923
+ const staged = await stage({
924
+ config,
925
+ repository,
926
+ buildId: `checks-${randomBytes(4).toString("hex")}`,
927
+ buildDir: scratch,
928
+ fetchImpl: deps.fetchImpl,
929
+ controlPlaneUrl
930
+ });
931
+ if (staged.kind === "refused") return { ok: false, reason: staged.reason };
932
+ if (staged.kind === "staged") {
933
+ try {
934
+ return { ok: true, checks: checksIn(join(staged.dir, "tree"), deps.log, tag) };
935
+ } finally {
936
+ rmSync(staged.dir, { recursive: true, force: true });
937
+ }
938
+ }
939
+ const repo = join(scratch, "repo");
940
+ const remote = deps.runtime.remoteFor?.(repository) ?? `${deps.runtime.github.gitUrl}/${repository}.git`;
941
+ const commit = job.environment?.recipe?.sourceCommit ?? await deps.runtime.readHead(remote, scratch);
942
+ if (commit === void 0) return { ok: false, reason: "the repository's default branch could not be read" };
943
+ const fetched = await (deps.fetchCommit ?? fetchCommit)({ remote, dir: repo, commit, token });
944
+ if (!fetched.ok) return { ok: false, reason: fetched.reason };
945
+ return { ok: true, checks: checksIn(repo, deps.log, tag) };
946
+ }
947
+ function timeoutAfter(ms, sleep) {
948
+ if (sleep !== void 0) return { done: sleep(ms).then(() => "timeout"), cancel: () => void 0 };
949
+ let timer;
950
+ const done = new Promise((resolve) => {
951
+ timer = setTimeout(() => resolve("timeout"), ms);
952
+ timer.unref();
953
+ });
954
+ return { done, cancel: () => clearTimeout(timer) };
955
+ }
956
+ function noCleanEnvironment(job) {
957
+ const ran = job.build?.workDir !== void 0 ? "builtin:process" : "a provision hook of the host's own";
958
+ return `the host has no clean environment for checks: this build ran under ${ran}, and checks run only in a new container or microVM from a build's image`;
959
+ }
960
+ async function runHostChecks(input) {
961
+ const { config, job, jobDir, deps } = input;
962
+ const { runtime, log } = deps;
963
+ const tag = `#${job.item}`;
964
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
965
+ const repository = job.repository ?? job.container?.repository;
966
+ if (repository === void 0) return;
967
+ const jobFilePath = join(jobDir, "job.json");
968
+ const tokens = runtime.credentials.get(jobDir);
969
+ let readToken;
970
+ try {
971
+ readToken = await tokens?.token("read");
972
+ } catch (err) {
973
+ log.error(`${tag} no read token for the checks: ${describeCallFailure(err)}`);
974
+ }
975
+ const scratch = join(jobDir, "checks", "definitions");
976
+ rmSync(scratch, { recursive: true, force: true });
977
+ mkdirSync(scratch, { recursive: true });
978
+ let definitions;
979
+ try {
980
+ definitions = await readDefinitions(input, repository, scratch, readToken);
981
+ } catch (err) {
982
+ definitions = { ok: false, reason: describeCallFailure(err) };
983
+ } finally {
984
+ removeStagedContext(scratch);
985
+ }
986
+ if (!definitions.ok) {
987
+ log.line(`${tag} checks not run: the definitions could not be read: ${definitions.reason}`);
988
+ rmSync(scratch, { recursive: true, force: true });
989
+ return;
990
+ }
991
+ const checks = definitions.checks;
992
+ if (checks.length === 0) {
993
+ rmSync(scratch, { recursive: true, force: true });
994
+ return;
995
+ }
996
+ log.line(`${tag} checks started (${checks.map((check) => check.name).join(", ")})`);
997
+ const startedMs = now().getTime();
998
+ let signer;
999
+ const record = async (check, result, sentence, keyTail = "") => {
1000
+ signer ??= await signerFor(config.configPath);
1001
+ const body = sentence === void 0 ? void 0 : cutReasonLine(`${scrubReason(sentence, config) ?? "check failed"}${keyTail}`);
1002
+ const clientEmitId = `host-${createHash("sha256").update(`${job.claimId}:${job.startedAt}:${check.name}`).digest("hex").slice(0, 40)}`;
1003
+ const sent = await retrySend(
1004
+ () => postJson(
1005
+ config.url,
1006
+ "/v1/emitted-results",
1007
+ config.apiKey,
1008
+ config.appId,
1009
+ { schemaVersion: 1, emit: { clientEmitId, name: check.name, result, ...body === void 0 ? {} : { body }, emittedAt: now().toISOString(), item: { number: Number(job.item) } } },
1010
+ deps.fetchImpl,
1011
+ void 0,
1012
+ signer
1013
+ ),
1014
+ { now: deps.now }
1015
+ );
1016
+ if (sent.ok) log.line(`${tag} check ${check.name} ${result}${body === void 0 ? "" : `: ${body}`}`);
1017
+ else log.error(`${tag} check ${check.name} ${result} could not be recorded: ${sent.reason}`);
1018
+ };
1019
+ const failAll = async (reason, keyTail = "") => {
1020
+ log.error(`${tag} checks failed: ${reason}${keyTail}`);
1021
+ for (const check of checks) await record(check, "fail", reason, keyTail);
1022
+ };
1023
+ try {
1024
+ if (job.container === void 0 && job.microvm === void 0) return await failAll(noCleanEnvironment(job));
1025
+ if (tokens === void 0) return await failAll("the build's repository tokens are no longer held, so the branch cannot be fetched");
1026
+ const branch = job.branch;
1027
+ if (branch === void 0) return await failAll("the build pushed no branch: the claim names none");
1028
+ const remote = runtime.remoteFor?.(repository) ?? `${runtime.github.gitUrl}/${repository}.git`;
1029
+ const read = await (deps.readBranch ?? readBranchHead)({ remote, dir: scratch, branch, token: readToken });
1030
+ if (read.kind === "absent") return await failAll(`the build pushed no branch: ${branch} does not exist on ${repository}`);
1031
+ if (read.kind === "unreadable") return await failAll(`the branch ${branch} could not be read: ${read.reason}`);
1032
+ const limitMinutes = config.checks?.timeLimitMinutes ?? DEFAULT_CHECK_TIME_LIMIT_MINUTES;
1033
+ const minutes = `${limitMinutes} ${limitMinutes === 1 ? "minute" : "minutes"}`;
1034
+ const environmentLog = join(jobDir, "checks", "environment.log");
1035
+ if (job.microvm !== void 0) {
1036
+ if (deps.endBuildMicroVm === void 0) return await failAll("the runner has no way to stop the build's microVM, so no check environment was made");
1037
+ try {
1038
+ await deps.endBuildMicroVm();
1039
+ } catch (err) {
1040
+ return await failAll(`the build's microVM could not be stopped, so no check environment was made: ${describeCallFailure(err)}`);
1041
+ }
1042
+ }
1043
+ let abandoned = false;
1044
+ const setup = async () => {
1045
+ const started = job.microvm !== void 0 ? await startCheckMicroVm({
1046
+ config,
1047
+ runtime,
1048
+ job,
1049
+ jobDir,
1050
+ tokens,
1051
+ log,
1052
+ aborted: () => abandoned,
1053
+ save: (state) => {
1054
+ if (abandoned) return;
1055
+ job.checkMicrovm = state;
1056
+ writeJobFile(jobFilePath, job);
1057
+ }
1058
+ }) : await startCheckContainer({
1059
+ config,
1060
+ runtime,
1061
+ job,
1062
+ jobDir,
1063
+ tokens,
1064
+ log,
1065
+ save: (state) => {
1066
+ job.checkContainer = state;
1067
+ writeJobFile(jobFilePath, job);
1068
+ }
1069
+ });
1070
+ if (!started.ok) return `the check environment could not be made: ${started.reason}`;
1071
+ const made = "container" in started ? started.container : started.environment;
1072
+ const steps = [
1073
+ {
1074
+ argv: ["git", "config", "--global", `url.http://127.0.0.1:${RELAY_PORT}/github.com/.insteadOf`, "https://github.com/"],
1075
+ workdir: "/build/work",
1076
+ failed: (status) => `configuring git failed (exit ${status})`
1077
+ },
1078
+ {
1079
+ argv: ["git", "clone", "--filter=blob:none", "--single-branch", "--branch", branch, `https://github.com/${repository}.git`, CHECKOUT_DIR],
1080
+ workdir: "/build/work",
1081
+ failed: (status) => `the clone of ${branch} failed (exit ${status})`
1082
+ }
1083
+ ];
1084
+ for (const step of steps) {
1085
+ if (abandoned) return "the check environment setup ran out of time";
1086
+ const ran = await made.exec(step.argv, step.workdir, { logPath: environmentLog });
1087
+ if (ran.status !== 0) return `the check environment could not be prepared: ${step.failed(ran.status)}`;
1088
+ }
1089
+ for (const step of lifecycleSteps((job.container ?? job.microvm)?.lifecycle ?? {})) {
1090
+ log.line(`${tag} checks lifecycle ${step.name} (${step.source})`);
1091
+ const ran = await made.exec(step.argv, CHECKOUT_DIR, { logPath: environmentLog });
1092
+ if (ran.status !== 0) return `the check environment could not be prepared: ${lifecycleFailureReason(step, ran.status)}`;
1093
+ }
1094
+ return made;
1095
+ };
1096
+ const setupTimer = timeoutAfter(limitMinutes * 6e4, deps.sleep);
1097
+ let settled;
1098
+ try {
1099
+ const outcome = await Promise.race([
1100
+ setup().catch((err) => `the check environment could not be made: ${describeCallFailure(err)}`),
1101
+ setupTimer.done
1102
+ ]);
1103
+ if (outcome === "timeout") {
1104
+ abandoned = true;
1105
+ return await failAll(`the check environment setup ran out of time after ${minutes}`, LIMIT_KEY_TAIL);
1106
+ }
1107
+ settled = outcome;
1108
+ } finally {
1109
+ setupTimer.cancel();
1110
+ }
1111
+ if (typeof settled === "string") return await failAll(settled);
1112
+ const ready = settled;
1113
+ let usable = true;
1114
+ for (const check of checks) {
1115
+ if (!usable) {
1116
+ await record(check, "fail", "the check environment could not be restarted after an earlier check ran out of time");
1117
+ continue;
1118
+ }
1119
+ const checkStartedMs = now().getTime();
1120
+ const running = ready.exec(["/bin/sh", "-c", check.command], CHECKOUT_DIR, { logPath: join(jobDir, "checks", `${check.name}.log`) });
1121
+ running.catch(() => void 0);
1122
+ const timer = timeoutAfter(limitMinutes * 6e4, deps.sleep);
1123
+ let outcome;
1124
+ try {
1125
+ outcome = await Promise.race([running, timer.done]);
1126
+ } catch (err) {
1127
+ timer.cancel();
1128
+ await record(check, "fail", `the command could not be run: ${describeCallFailure(err)}`);
1129
+ continue;
1130
+ }
1131
+ timer.cancel();
1132
+ if (outcome === "timeout") {
1133
+ await record(check, "fail", `ran out of time after ${minutes}`, LIMIT_KEY_TAIL);
1134
+ const restarting = ready.restart().catch(() => false);
1135
+ const restartTimer = timeoutAfter(limitMinutes * 6e4, deps.sleep);
1136
+ usable = await Promise.race([restarting, restartTimer.done]) === true;
1137
+ restartTimer.cancel();
1138
+ continue;
1139
+ }
1140
+ if (outcome.status === 0) {
1141
+ log.line(`${tag} check ${check.name} ran (${formatDuration(now().getTime() - checkStartedMs)})`);
1142
+ await record(check, "pass");
1143
+ continue;
1144
+ }
1145
+ const last = lastLine(outcome.stderr) ?? lastLine(outcome.stdout);
1146
+ const scrubbed = last === void 0 ? void 0 : scrubReason(last, config);
1147
+ await record(check, "fail", `exited ${outcome.status}${scrubbed === void 0 ? " with no output" : `: ${cutReasonLine(scrubbed, 140)}`}`);
1148
+ }
1149
+ } catch (err) {
1150
+ log.error(`${tag} checks stopped: ${describeCallFailure(err)}`);
1151
+ } finally {
1152
+ if (job.checkContainer !== void 0) {
1153
+ await cleanupContainer(runtime, job.checkContainer).catch((err) => log.error(`${tag} removing the check environment failed: ${describeCallFailure(err)}`));
1154
+ delete job.checkContainer;
1155
+ writeJobFile(jobFilePath, job);
1156
+ }
1157
+ if (job.checkMicrovm !== void 0) {
1158
+ await cleanupMicroVm(runtime, job.checkMicrovm).catch((err) => log.error(`${tag} removing the check environment failed: ${describeCallFailure(err)}`));
1159
+ delete job.checkMicrovm;
1160
+ writeJobFile(jobFilePath, job);
1161
+ }
1162
+ rmSync(scratch, { recursive: true, force: true });
1163
+ if (existsSync(scratch)) log.error(`${tag} the checks' scratch directory could not be removed`);
1164
+ log.line(`${tag} checks done (${formatDuration(now().getTime() - startedMs)})`);
1165
+ }
1166
+ }
720
1167
 
721
1168
  // src/runner/job.ts
722
1169
  var LEASE_SECONDS = 900;
@@ -1181,7 +1628,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1181
1628
  await stopProcessGroup(job.pid);
1182
1629
  }
1183
1630
  if (outcome !== void 0 && job.endStage === void 0) {
1184
- const TEARDOWN_STAGES = ["sync", "cleanup", "report"];
1631
+ const TEARDOWN_STAGES = ["checks", "sync", "cleanup", "report"];
1185
1632
  const staleTeardownStage = job.stage !== void 0 && TEARDOWN_STAGES.includes(job.stage);
1186
1633
  job.endStage = ending?.stage ?? (staleTeardownStage ? job.exitCode !== void 0 ? "command" : void 0 : job.stage);
1187
1634
  job.endExitCode = ending?.exitCode ?? (outcome === "ok" || outcome === "failed" ? job.exitCode : void 0);
@@ -1191,11 +1638,74 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1191
1638
  job.reason = scrubReason(ending.reason, config);
1192
1639
  writeJobFile(jobFilePath, job);
1193
1640
  }
1641
+ const uploadSessions = async (workdir) => {
1642
+ if (job.container === void 0 && job.microvm === void 0 && job.build?.exec === void 0 && workdirProblem(workdir) !== void 0) {
1643
+ log.line(`${tag} sync skipped: workdir ${workdir} is gone`);
1644
+ } else if (job.itemKey === void 0) {
1645
+ log.line(`${tag} sync skipped: the job file holds no item key`);
1646
+ } else {
1647
+ log.line(`${tag} sync started`);
1648
+ const startedMs = now().getTime();
1649
+ const sync = build !== void 0 ? await syncInContainer({ config, runtime: build.runtime, job, jobDir: jobDirPath }) : vmBuild !== void 0 ? await syncInMicroVm({ config, runtime: vmBuild.runtime, job, jobDir: jobDirPath }) : job.build?.exec !== void 0 ? await runSyncViaExec(config, job, jobDirPath, spawnFn) : await runSync(config, job.item, job.itemKey, workdir, jobDirPath, spawnFn, job.branch, job.commitIdentity?.author);
1650
+ const took = formatDuration(now().getTime() - startedMs);
1651
+ if (sync.ok) log.line(`${tag} sync ok (${took})`);
1652
+ else log.error(`${tag} sync failed: ${sync.reason ?? "no reason given"} \xB7 continuing`);
1653
+ const left = spooledArtifactIds(deps.home ?? homeFromEnv());
1654
+ if (left.length > 0) {
1655
+ job.spooledArtifacts = left.slice(0, MAX_NAMED_SPOOLED);
1656
+ const named = job.spooledArtifacts.join(", ");
1657
+ log.error(
1658
+ `${tag} ${left.length} spooled artifact(s) not uploaded: ${named}${left.length > MAX_NAMED_SPOOLED ? `, and ${left.length - MAX_NAMED_SPOOLED} more` : ""}`
1659
+ );
1660
+ }
1661
+ }
1662
+ };
1663
+ const endBuildMicroVm = vmBuild === void 0 ? void 0 : async () => {
1664
+ if (job.workdir && job.sessionsSynced !== true) {
1665
+ await uploadSessions(job.workdir);
1666
+ job.sessionsSynced = true;
1667
+ writeJobFile(jobFilePath, job);
1668
+ }
1669
+ await stopBuildMicroVm(vmBuild.runtime, vmBuild.state, jobDirPath);
1670
+ };
1671
+ if (job.checkContainer !== void 0 && job.checksRan === true) {
1672
+ try {
1673
+ await cleanupContainer(deps.container ?? createContainerRuntime(), job.checkContainer);
1674
+ delete job.checkContainer;
1675
+ writeJobFile(jobFilePath, job);
1676
+ } catch (err) {
1677
+ log.error(`${tag} removing an earlier attempt's check environment failed: ${describeCallFailure(err)} \xB7 continuing`);
1678
+ }
1679
+ }
1680
+ if (job.checkMicrovm !== void 0 && job.checksRan === true) {
1681
+ try {
1682
+ await cleanupMicroVm(deps.container ?? createContainerRuntime(), job.checkMicrovm);
1683
+ delete job.checkMicrovm;
1684
+ writeJobFile(jobFilePath, job);
1685
+ } catch (err) {
1686
+ log.error(`${tag} removing an earlier attempt's check environment failed: ${describeCallFailure(err)} \xB7 continuing`);
1687
+ }
1688
+ }
1689
+ if (outcome === "ok" && !job.cleanupRan && job.checksRan !== true) {
1690
+ setStage(job, jobDirPath, "checks");
1691
+ job.checksRan = true;
1692
+ writeJobFile(jobFilePath, job);
1693
+ try {
1694
+ await runHostChecks({
1695
+ config,
1696
+ job,
1697
+ jobDir: jobDirPath,
1698
+ deps: { fetchImpl: deps.fetchImpl, runtime: deps.container ?? createContainerRuntime(), log, now: deps.now, endBuildMicroVm, ...deps.hostChecks }
1699
+ });
1700
+ } catch (err) {
1701
+ log.error(`${tag} the host's checks stopped: ${describeCallFailure(err)} \xB7 continuing`);
1702
+ }
1703
+ }
1194
1704
  let outOfMemory;
1195
1705
  if (vmBuild?.state.cgroup !== void 0 && outcome !== void 0 && outcome !== "ok" && ending?.reason === void 0) {
1196
1706
  try {
1197
1707
  if (vmBuild.runtime.vm.cgroup.oomKills(vmBuild.state.cgroup) > 0) {
1198
- outOfMemory = `the build ran out of memory: the kernel killed its microVM at the limit of runner.build.memory (${config.build?.memory ?? "8g"}) plus 256 MiB for Firecracker. Raise runner.build.memory, or lower runner.concurrency`;
1708
+ outOfMemory = outOfMemoryReason(config.build?.memory);
1199
1709
  }
1200
1710
  } catch (err) {
1201
1711
  log.error(`${tag} could not read the build's memory events: ${describeCallFailure(err)} \xB7 continuing`);
@@ -1210,26 +1720,8 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1210
1720
  if (!job.cleanupRan) {
1211
1721
  if (job.workdir) {
1212
1722
  setStage(job, jobDirPath, "sync");
1213
- if (job.container === void 0 && job.microvm === void 0 && job.build?.exec === void 0 && workdirProblem(job.workdir) !== void 0) {
1214
- log.line(`${tag} sync skipped: workdir ${job.workdir} is gone`);
1215
- } else if (job.itemKey === void 0) {
1216
- log.line(`${tag} sync skipped: the job file holds no item key`);
1217
- } else {
1218
- log.line(`${tag} sync started`);
1219
- const startedMs = now().getTime();
1220
- const sync = build !== void 0 ? await syncInContainer({ config, runtime: build.runtime, job, jobDir: jobDirPath }) : vmBuild !== void 0 ? await syncInMicroVm({ config, runtime: vmBuild.runtime, job, jobDir: jobDirPath }) : job.build?.exec !== void 0 ? await runSyncViaExec(config, job, jobDirPath, spawnFn) : await runSync(config, job.item, job.itemKey, job.workdir, jobDirPath, spawnFn, job.branch, job.commitIdentity?.author);
1221
- const took = formatDuration(now().getTime() - startedMs);
1222
- if (sync.ok) log.line(`${tag} sync ok (${took})`);
1223
- else log.error(`${tag} sync failed: ${sync.reason ?? "no reason given"} \xB7 continuing`);
1224
- const left = spooledArtifactIds(deps.home ?? homeFromEnv());
1225
- if (left.length > 0) {
1226
- job.spooledArtifacts = left.slice(0, MAX_NAMED_SPOOLED);
1227
- const named = job.spooledArtifacts.join(", ");
1228
- log.error(
1229
- `${tag} ${left.length} spooled artifact(s) not uploaded: ${named}${left.length > MAX_NAMED_SPOOLED ? `, and ${left.length - MAX_NAMED_SPOOLED} more` : ""}`
1230
- );
1231
- }
1232
- }
1723
+ if (job.sessionsSynced === true) log.line(`${tag} sync done before the build's microVM stopped`);
1724
+ else await uploadSessions(job.workdir);
1233
1725
  }
1234
1726
  setStage(job, jobDirPath, "cleanup");
1235
1727
  try {