@controlplane/cli
Version:
Control Plane Corporation CLI
160 lines • 7.26 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RemoteBuildOrchestrator = void 0;
const crypto_1 = require("crypto");
const errors_1 = require("./errors");
const service_1 = require("./service");
const watch_1 = require("./watch");
const connect_1 = require("./connect");
const archive_1 = require("./folder/archive");
const scanner_1 = require("./folder/scanner");
const sync_1 = require("./folder/sync");
// ANCHOR - Constants
// The shell's conventional exit codes for a process killed by each signal.
const INTERRUPT_EXIT_CODE = 130;
const TERMINATE_EXIT_CODE = 143;
// ANCHOR - RemoteBuildOrchestrator
/**
* Owns one `cpln image build --remote` run: prepares the source (scanned folder or
* validated repository), submits the build, and either detaches or follows it to
* completion. Every submission carries a fresh request id — a pre-connect and a
* post-connect submission are different intents and must never share an
* idempotency key. On Ctrl-C, temp archives are cleaned and the user is told the
* build continues remotely.
*/
class RemoteBuildOrchestrator {
constructor(session, progress, deps = {}) {
var _a, _b, _c, _d, _e, _f, _g;
this.submitted = null;
this.progress = progress;
this.service = (_a = deps.service) !== null && _a !== void 0 ? _a : service_1.RemoteBuildService.forSession(session);
this.scanner = (_b = deps.scanner) !== null && _b !== void 0 ? _b : new scanner_1.RemoteFolderScanner();
this.sync = (_c = deps.sync) !== null && _c !== void 0 ? _c : new sync_1.RemoteFolderSync(progress);
this.connect = (_d = deps.connect) !== null && _d !== void 0 ? _d : new connect_1.RemoteGitConnect(this.service, progress);
this.watch = (_e = deps.watch) !== null && _e !== void 0 ? _e : new watch_1.RemoteBuildWatch(this.service, progress);
this.newRequestId = (_f = deps.newRequestId) !== null && _f !== void 0 ? _f : crypto_1.randomUUID;
this.exit = (_g = deps.exit) !== null && _g !== void 0 ? _g : process.exit;
this.interrupt = this.handleInterrupt.bind(this);
}
// Public Methods //
/**
* Builds the image remotely and pushes it, returning the outcome: a pushed image,
* or a detached build that finishes in the background.
*
* @param {RemoteBuildRequest} request - The source and target image.
* @returns {Promise<RemoteBuildOutcome>} The build id, image ref, and whether it completed.
*/
async buildAndPush(request) {
this.submitted = null;
process.once('SIGINT', this.interrupt);
process.once('SIGTERM', this.interrupt);
try {
const build = request.kind === 'folder' ? await this.submitFolder(request) : await this.submitRepo(request);
this.submitted = build;
this.progress.onSubmitted(build);
if (request.detach) {
this.progress.onDetached(build);
return { buildId: build.id, imageRef: build.imageRef, completed: false };
}
const pushed = await this.watch.untilDone({
id: build.id,
displayRef: `${request.imageName}:${request.imageTag}`,
org: request.org,
});
return { buildId: pushed.id, imageRef: pushed.imageRef, completed: true };
}
finally {
process.removeListener('SIGINT', this.interrupt);
process.removeListener('SIGTERM', this.interrupt);
}
}
// Private Methods //
/**
* Cleans up temp archives, tells the user the build continues remotely, and exits
* with the signal's conventional code. Reporting is best-effort: a closed output
* stream must not turn the interrupt into a crash.
*
* @param {NodeJS.Signals} signal - The signal that interrupted the run.
* @returns {void}
*/
handleInterrupt(signal) {
archive_1.RemoteFolderArchive.disposeAll();
try {
this.progress.onInterrupted(this.submitted);
}
catch (_a) {
// Nothing can be reported once the output stream is gone; the exit still matters.
}
this.exit(signal === 'SIGTERM' ? TERMINATE_EXIT_CODE : INTERRUPT_EXIT_CODE);
}
/**
* Scans the folder, uploads what the service is missing, and submits the build.
*
* @param {RemoteFolderBuild} request - The folder build request.
* @returns {Promise<BuildRecord>} The started build record.
*/
async submitFolder(request) {
this.progress.onScanStarted(request.dir);
const index = await this.scanner.scan(request.dir);
this.progress.onScanned(index);
const sensitive = (0, scanner_1.credentialLikeFiles)(index);
if (sensitive.length > 0) {
this.progress.onSensitiveFiles(sensitive);
}
const plan = await this.service.planUpload({
org: request.org,
imageName: request.imageName,
manifest: index.manifest,
emptyDirs: index.emptyDirs,
symlinks: index.symlinks,
force: request.noCache === true,
});
this.progress.onPlanned(plan);
await this.sync.push(index, plan);
const submission = {
org: request.org,
imageName: request.imageName,
imageTag: request.imageTag,
requestId: this.newRequestId(),
noCache: request.noCache,
buildArgs: request.buildArgs,
secrets: request.secrets,
sourceKind: plan.mode === 'seed' ? 'r2-seed' : 'r2',
};
return this.service.submitBuild(submission);
}
/**
* Ensures the org's git provider is connected for the repo, then submits the build
* once. A not-connected reply after a verified connection (a rare
* revoke-during-submit race) becomes an actionable error instead of the raw failure.
*
* @param {RemoteRepoBuild} request - The repo build request.
* @returns {Promise<BuildRecord>} The started build record.
*/
async submitRepo(request) {
const repoUrl = this.connect.normalizeRepoUrl(request.repoUrl);
await this.connect.ensureConnected(repoUrl, request.org);
const submission = {
org: request.org,
imageName: request.imageName,
imageTag: request.imageTag,
requestId: this.newRequestId(),
noCache: request.noCache,
buildArgs: request.buildArgs,
secrets: request.secrets,
repoUrl,
branch: request.branch,
};
try {
return await this.service.submitBuild(submission);
}
catch (e) {
if (!(e instanceof errors_1.NotConnectedError)) {
throw e;
}
throw new errors_1.RemoteBuildError('request-failed', `the git provider is connected, but the build service still cannot access ${request.repoUrl}`, 'The connection may be bound to a different account or not cover this repository. Check the installation, then re-run the build.');
}
}
}
exports.RemoteBuildOrchestrator = RemoteBuildOrchestrator;
//# sourceMappingURL=orchestrator.js.map