Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/api/src/lib/ssh-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ import {
type HostChannelHealth,
type SshConfig,
} from "@repo/adapters";
import { formatDuration, systemDebug } from "@/lib/system-debug";
import { decryptSecretField } from "@/lib/credential-encryption";
import { operatorSshKeyRoots, resolveSafeSshKeyPath } from "@/lib/ssh-key-path";
import { isLocalHostRow } from "@/lib/box-org";
import { formatDuration, systemDebug } from "./system-debug";
import { decryptSecretField } from "./credential-encryption";
import { operatorSshKeyRoots, resolveSafeSshKeyPath } from "./ssh-key-path";
import { isLocalHostRow } from "./box-org";
import { safeErrorMessage } from "@repo/core";

const execFileAsync = promisify(execFile);
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/modules/deployments/build-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,7 @@ async function executeBuildAndDeploy(project: Project, dep: Deployment, buildSes
isDesktop: plat.target === "desktop",
forwardGitCredentials: snapshot.forwardGitCredentials,
repoIsGithub: !!project.gitOwner,
isLocalHost: resolved.platform.localHost,
});
const cloneOnServer = clonePlan.runsOnServer;
// The relay needs a real SSH reverse tunnel — `reverseForward` exists on every
Expand Down
9 changes: 9 additions & 0 deletions apps/api/src/modules/deployments/clone-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,13 @@ describe("resolveClonePlan — relayEligible (forward is the default on desktop)
}).relayEligible,
).toBe(false);
});
it("is NOT eligible on a local host server (no remote host)", () => {
expect(
resolveClonePlan({
...base,
isLocalHost: true,
forwardGitCredentials: true,
}).relayEligible,
).toBe(false);
});
});
21 changes: 16 additions & 5 deletions apps/api/src/modules/deployments/clone-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export interface ClonePlanInput {
* adapter re-validates the URL (github + https) before downloading and falls
* back to clone. Local/imported projects → false → unchanged. */
repoIsGithub?: boolean;
/** Target server is the local host ("This Server" row, socket DooD).
* Docker builds on the local host build over the local Docker socket and
* prepare context on the API host — they never clone on a remote server. */
isLocalHost?: boolean;
}

export interface ClonePlan {
Expand Down Expand Up @@ -89,28 +93,35 @@ export function relayConfigEligible(input: {

export function resolveClonePlan(input: ClonePlanInput): ClonePlan {
const onServer = input.effectiveTarget === "server";
const isLocalHost = input.isLocalHost === true || input.effectiveTarget === "local";

// Docker on the local host (socket DooD) or a local build (buildStrategy === "local"
// without cloneStrategy === "server") builds via createDockerBuildContext on the API host
// and NEVER clones on a remote host via SSH.
const isLocalDockerBuild =
!input.runtimeIsBare &&
(isLocalHost || (input.buildStrategy === "local" && input.cloneStrategy !== "server"));

// Docker acquires source ON THE SERVER when the deploy opted in
// (cloneStrategy="server") OR the repo is a GitHub HTTPS remote — the server
// downloads the tarball directly, skipping the orchestrator clone + context
// (cloneStrategy="server") OR the repo is a GitHub HTTPS remote on a remote server build —
// the server downloads the tarball directly, skipping the orchestrator clone + context
// transfer. Bare has its own always-on-server path (below), so it's excluded
// here. Whether it truly runs on the server still hinges on a shippable
// credential; effectiveCloneOnServer degrades to an api-host clone otherwise
// (allowApiHostFallback is driven by dockerClonesOnServer).
const dockerServerSide =
onServer &&
!input.runtimeIsBare &&
!isLocalDockerBuild &&
(input.cloneStrategy === "server" || input.repoIsGithub === true);

// Pipeline: the clone runs on the server (bare always; docker per above).
const runsOnServer =
onServer && !!input.serverId && (input.runtimeIsBare || dockerServerSide);
onServer && !isLocalDockerBuild && !!input.serverId && (input.runtimeIsBare || dockerServerSide);

// Preflight warn-case + api-host-fallback gate: DOCKER (non-bare) acquiring on
// the server. Bare is handled by the separate hard-fail remote-build checks.
const dockerClonesOnServer = dockerServerSide;

// The clone's credential purpose follows WHERE THE CLONE RUNS, not where the
// build runs: a local build clones on this machine, and a server deploy that
// isn't cloning on the server clones on the api-host (both local → gh OK).
// Everything else (on-server clone, cloud workspace clone) is off-host → remote.
Expand Down
9 changes: 8 additions & 1 deletion apps/api/src/modules/deployments/preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { isStaticService, type DeployableService } from "../../lib/deployable-se
import { isFullyPinned, snapshotNeedsGitSource } from "./pinned-artifacts";
import { snapshotToClass } from "./deployment-class";
import { relayConfigEligible, resolveClonePlan } from "./clone-plan";
import { isLocalHostRow } from "../../lib/box-org";
import { hasLocalGitIdentity } from "../github/github.local-auth";
import { isPublicRepo } from "../github/github.http";
import { getRoutingBaseDomain } from "../../lib/routing-domains";
Expand Down Expand Up @@ -1438,12 +1439,17 @@ export async function runPreflightChecks(
// Does this machine meet what the app says it needs? Cloud is sized from the
// tier table, not from host hardware, so there is nothing to match there (and
// nothing to probe — a multi-tenant control plane must not dial a tenant's box).
let isLocalTarget = effectiveTarget === "local";
if (snapshot.serverId) {
const server = await repos.server?.get?.(snapshot.serverId).catch(() => null);
if (server) isLocalTarget = await isLocalHostRow(server);
}
if (opts?.appTemplateId && snapshot.organizationId && effectiveTarget !== "cloud") {
const hostCapacity = await checkHostCapacity(
snapshot.organizationId,
opts.appTemplateId,
snapshot.serverId,
effectiveTarget === "local",
isLocalTarget,
opts.firstDeploy ?? false,
);
if (hostCapacity) checks.push(hostCapacity);
Expand Down Expand Up @@ -1564,6 +1570,7 @@ export async function runPreflightChecks(
// tarball on the server for them. Same structured signal the pipeline uses
// (`!!project.gitOwner`) so the two decisions can't drift.
repoIsGithub: !!opts?.gitOwner,
isLocalHost: isLocalTarget,
}).dockerClonesOnServer;
if (dockerClonesOnServer) {
checks.push(
Expand Down
27 changes: 27 additions & 0 deletions apps/api/test/modules/deployments/clone-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,33 @@ describe("resolveClonePlan", () => {
expect(plan.runsLocally).toBe(true);
expect(plan.cloneBuildStrategy).toBe("local");
});
it("docker + server + repoIsGithub + buildStrategy=local → clone runs locally with local credential", () => {
const plan = resolveClonePlan({
...base,
effectiveTarget: "server",
buildStrategy: "local",
repoIsGithub: true,
});
expect(plan.runsOnServer).toBe(false);
expect(plan.dockerClonesOnServer).toBe(false);
expect(plan.runsLocally).toBe(true);
expect(plan.cloneBuildStrategy).toBe("local");
});

it("docker + server (localHost / This Server) + repoIsGithub → clones locally with local credential, not on server", () => {
const plan = resolveClonePlan({
...base,
effectiveTarget: "server",
isLocalHost: true,
repoIsGithub: true,
cloneStrategy: "server",
});
expect(plan.runsOnServer).toBe(false);
expect(plan.dockerClonesOnServer).toBe(false);
expect(plan.runsLocally).toBe(true);
expect(plan.cloneBuildStrategy).toBe("local");
});


it("docker + server + api-host clone → api-host clone (local credential), not on server", () => {
const plan = resolveClonePlan({ ...base, cloneStrategy: "api-host" });
Expand Down