UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

160 lines 7.26 kB
"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