Skip to content

Commit 4544598

Browse files
committed
fix(ci): preserve custom SSH keys in API create flow
1 parent c0b31a0 commit 4544598

3 files changed

Lines changed: 141 additions & 45 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Effect } from "effect"
2+
3+
import { request } from "./api-http.js"
4+
import { asObject, type JsonRequest, type JsonValue } from "./api-json.js"
5+
import { decodeProjectDetails } from "./api-project-codec.js"
6+
import type { CreateCommand } from "./frontend-lib/core/domain.js"
7+
8+
type ResolvedCreateRequestPaths = {
9+
readonly authorizedKeysPath: string
10+
readonly authorizedKeysContents?: string | undefined
11+
}
12+
13+
const projectPath = (projectId: string, suffix = ""): string => `/projects/${encodeURIComponent(projectId)}${suffix}`
14+
15+
export const decodeProjectResponse = (payload: JsonValue) => {
16+
const object = asObject(payload)
17+
return object === null
18+
? decodeProjectDetails(payload)
19+
: decodeProjectDetails(object["project"] ?? payload)
20+
}
21+
22+
export const createProjectRequestNeedsFollowUpUp = (
23+
command: CreateCommand,
24+
resolvedPaths: ResolvedCreateRequestPaths
25+
): boolean => command.runUp && resolvedPaths.authorizedKeysContents !== undefined
26+
27+
export const createProjectRequestAllowsImmediateUp = (
28+
command: CreateCommand,
29+
resolvedPaths: ResolvedCreateRequestPaths
30+
): boolean => command.runUp && resolvedPaths.authorizedKeysContents === undefined
31+
32+
export const buildCreateProjectRequest = (
33+
command: CreateCommand,
34+
resolvedPaths: ResolvedCreateRequestPaths,
35+
shouldRunUpInCreateRequest: boolean
36+
) => {
37+
const config = command.config
38+
return {
39+
repoUrl: config.repoUrl,
40+
repoRef: config.repoRef,
41+
targetDir: config.targetDir,
42+
sshPort: String(config.sshPort),
43+
sshUser: config.sshUser,
44+
containerName: config.containerName,
45+
serviceName: config.serviceName,
46+
volumeName: config.volumeName,
47+
authorizedKeysPath: resolvedPaths.authorizedKeysPath,
48+
authorizedKeysContents: resolvedPaths.authorizedKeysContents,
49+
envGlobalPath: config.envGlobalPath,
50+
envProjectPath: config.envProjectPath,
51+
codexAuthPath: config.codexAuthPath,
52+
codexHome: config.codexHome,
53+
cpuLimit: config.cpuLimit,
54+
ramLimit: config.ramLimit,
55+
dockerNetworkMode: config.dockerNetworkMode,
56+
dockerSharedNetworkName: config.dockerSharedNetworkName,
57+
enableMcpPlaywright: config.enableMcpPlaywright,
58+
outDir: command.outDir,
59+
gitTokenLabel: config.gitTokenLabel,
60+
skipGithubAuth: config.skipGithubAuth,
61+
useManagedAuthorizedKeys: true,
62+
codexTokenLabel: config.codexAuthLabel,
63+
claudeTokenLabel: config.claudeAuthLabel,
64+
agentAutoMode: config.agentAuto ? (config.agentMode ?? "auto") : undefined,
65+
up: shouldRunUpInCreateRequest,
66+
openSsh: false,
67+
force: command.force,
68+
forceEnv: command.forceEnv,
69+
waitForClone: command.waitForClone
70+
} satisfies JsonRequest
71+
}
72+
73+
export const upCreatedProjectWithAuthorizedKeys = (
74+
projectId: string,
75+
authorizedKeysContents: string
76+
) =>
77+
request("POST", projectPath(projectId, "/up"), {
78+
authorizedKeysContents,
79+
useManagedAuthorizedKeys: true
80+
}).pipe(
81+
Effect.map((payload) => decodeProjectResponse(payload))
82+
)

packages/app/src/docker-git/api-client-helpers.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as FileSystem from "@effect/platform/FileSystem"
12
import * as Path from "@effect/platform/Path"
23
import { Effect } from "effect"
34

@@ -34,14 +35,28 @@ const resolveClientCreatePath = (
3435
? targetPath
3536
: resolvePathFromCwd(path, cwd, targetPath)
3637

38+
const missingAuthorizedKeysContents = (): string | undefined => undefined
39+
3740
export const resolveCreateRequestPaths = (command: CreateCommand) =>
3841
Effect.gen(function*(_) {
42+
const fs = yield* _(FileSystem.FileSystem)
3943
const path = yield* _(Path.Path)
4044
const cwd = process.cwd()
45+
const authorizedKeysPath = command.config.authorizedKeysPath === defaultTemplateConfig.authorizedKeysPath
46+
? command.config.authorizedKeysPath
47+
: resolveClientCreatePath(path, cwd, command.config.authorizedKeysPath)
48+
const authorizedKeysContents = authorizedKeysPath === defaultTemplateConfig.authorizedKeysPath
49+
? undefined
50+
: yield* _(
51+
fs.exists(authorizedKeysPath).pipe(
52+
Effect.flatMap((exists) =>
53+
exists ? fs.readFileString(authorizedKeysPath) : Effect.sync(missingAuthorizedKeysContents)
54+
)
55+
)
56+
)
4157

4258
return {
43-
authorizedKeysPath: command.config.authorizedKeysPath === defaultTemplateConfig.authorizedKeysPath
44-
? command.config.authorizedKeysPath
45-
: resolveClientCreatePath(path, cwd, command.config.authorizedKeysPath)
59+
authorizedKeysPath,
60+
authorizedKeysContents
4661
}
4762
})

packages/app/src/docker-git/api-client.ts

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
import * as FileSystem from "@effect/platform/FileSystem"
2-
import * as Path from "@effect/platform/Path"
1+
import * as FsPlatform from "@effect/platform/FileSystem"
2+
import * as PathPlatform from "@effect/platform/Path"
33
import { Effect } from "effect"
44

5+
import {
6+
buildCreateProjectRequest,
7+
createProjectRequestAllowsImmediateUp,
8+
createProjectRequestNeedsFollowUpUp,
9+
decodeProjectResponse,
10+
upCreatedProjectWithAuthorizedKeys
11+
} from "./api-client-create.js"
512
import { readProjectOutput, resolveCreateRequestPaths } from "./api-client-helpers.js"
613
import { request, requestTextStream, requestVoid } from "./api-http.js"
7-
import { asArray, asObject, type JsonRequest } from "./api-json.js"
14+
import { asArray, asObject } from "./api-json.js"
815
import { decodeProjectDetails, decodeProjectSummary } from "./api-project-codec.js"
916
import { decodeTerminalSession } from "./api-terminal-codec.js"
1017
import type {
@@ -85,46 +92,38 @@ export const getProject = (projectId: string) =>
8592
})
8693
)
8794

95+
const createProjectWithResolvedPaths = (
96+
command: CreateCommand,
97+
resolvedPaths: {
98+
readonly authorizedKeysPath: string
99+
readonly authorizedKeysContents?: string | undefined
100+
}
101+
) =>
102+
Effect.gen(function*(_) {
103+
const createRequest = buildCreateProjectRequest(
104+
command,
105+
resolvedPaths,
106+
createProjectRequestAllowsImmediateUp(command, resolvedPaths)
107+
)
108+
const payload = yield* _(request("POST", "/projects", createRequest))
109+
const createdProject = decodeProjectResponse(payload)
110+
if (
111+
createdProject === null ||
112+
resolvedPaths.authorizedKeysContents === undefined ||
113+
!createProjectRequestNeedsFollowUpUp(command, resolvedPaths)
114+
) {
115+
return createdProject
116+
}
117+
118+
return yield* _(
119+
upCreatedProjectWithAuthorizedKeys(createdProject.projectDir, resolvedPaths.authorizedKeysContents)
120+
)
121+
})
122+
88123
export const createProject = (command: CreateCommand) =>
89124
Effect.gen(function*(_) {
90-
const config = command.config
91125
const resolvedPaths = yield* _(resolveCreateRequestPaths(command))
92-
const body = {
93-
repoUrl: config.repoUrl,
94-
repoRef: config.repoRef,
95-
targetDir: config.targetDir,
96-
sshPort: String(config.sshPort),
97-
sshUser: config.sshUser,
98-
containerName: config.containerName,
99-
serviceName: config.serviceName,
100-
volumeName: config.volumeName,
101-
authorizedKeysPath: resolvedPaths.authorizedKeysPath,
102-
envGlobalPath: config.envGlobalPath,
103-
envProjectPath: config.envProjectPath,
104-
codexAuthPath: config.codexAuthPath,
105-
codexHome: config.codexHome,
106-
cpuLimit: config.cpuLimit,
107-
ramLimit: config.ramLimit,
108-
dockerNetworkMode: config.dockerNetworkMode,
109-
dockerSharedNetworkName: config.dockerSharedNetworkName,
110-
enableMcpPlaywright: config.enableMcpPlaywright,
111-
outDir: command.outDir,
112-
gitTokenLabel: config.gitTokenLabel,
113-
skipGithubAuth: config.skipGithubAuth,
114-
useManagedAuthorizedKeys: true,
115-
codexTokenLabel: config.codexAuthLabel,
116-
claudeTokenLabel: config.claudeAuthLabel,
117-
agentAutoMode: config.agentAuto ? (config.agentMode ?? "auto") : undefined,
118-
up: command.runUp,
119-
openSsh: false,
120-
force: command.force,
121-
forceEnv: command.forceEnv,
122-
waitForClone: command.waitForClone
123-
} satisfies JsonRequest
124-
125-
const payload = yield* _(request("POST", "/projects", body))
126-
const object = asObject(payload)
127-
return object === null ? decodeProjectDetails(payload) : decodeProjectDetails(object["project"] ?? payload)
126+
return yield* _(createProjectWithResolvedPaths(command, resolvedPaths))
128127
})
129128

130129
export const deleteProject = (projectId: string) => requestVoid("DELETE", projectPath(projectId))
@@ -286,8 +285,8 @@ export const codexLogin = (command: AuthCodexLoginCommand) =>
286285

287286
const readCodexAuthText = (command: AuthCodexImportCommand) =>
288287
Effect.gen(function*(_) {
289-
const fs = yield* _(FileSystem.FileSystem)
290-
const path = yield* _(Path.Path)
288+
const fs = yield* _(FsPlatform.FileSystem)
289+
const path = yield* _(PathPlatform.Path)
291290
const resolvedCodexAuthDir = resolvePathFromCwd(path, process.cwd(), command.codexAuthPath)
292291
const authFilePath = path.join(resolvedCodexAuthDir, "auth.json")
293292
return yield* _(fs.readFileString(authFilePath))

0 commit comments

Comments
 (0)