-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathinitializeDeployment.server.ts
More file actions
295 lines (264 loc) · 11.1 KB
/
initializeDeployment.server.ts
File metadata and controls
295 lines (264 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
import {
BuildServerMetadata,
type InitializeDeploymentRequestBody,
type ExternalBuildData,
} from "@trigger.dev/core/v3";
import { customAlphabet } from "nanoid";
import { env } from "~/env.server";
import { type AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import { createRemoteImageBuild, remoteBuildsEnabled } from "../remoteImageBuilder.server";
import { calculateNextBuildVersion } from "../utils/calculateNextBuildVersion";
import { BaseService, ServiceValidationError } from "./baseService.server";
import { TimeoutDeploymentService } from "./timeoutDeployment.server";
import { getDeploymentImageRef } from "../getDeploymentImageRef.server";
import { tryCatch } from "@trigger.dev/core";
import { getRegistryConfig } from "../registryConfig.server";
import { DeploymentService } from "./deployment.server";
import { errAsync } from "neverthrow";
const nanoid = customAlphabet("1234567890abcdefghijklmnopqrstuvwxyz", 8);
export class InitializeDeploymentService extends BaseService {
public async call(
environment: AuthenticatedEnvironment,
payload: InitializeDeploymentRequestBody
) {
return this.traceWithEnv("call", environment, async () => {
if (payload.gitMeta?.commitSha?.startsWith("deployment_")) {
// When we introduced automatic deployments via the build server, we slightly changed the deployment flow
// mainly in the initialization and starting step: now deployments are first initialized in the `PENDING` status
// and updated to `BUILDING` once the build server dequeues the build job.
// Newer versions of the `deploy` command in the CLI will automatically attach to the existing deployment
// and continue with the build process. For older versions, we can't change the command's client-side behavior,
// so we need to handle this case here in the initialization endpoint. As we control the env variables which
// the git meta is extracted from in the build server, we can use those to pass the existing deployment ID
// to this endpoint. This doesn't affect the git meta on the deployment as it is set prior to this step using the
// /start endpoint. It's a rather hacky solution, but it will do for now as it enables us to avoid degrading the
// build server experience for users with older CLI versions. We'll eventually be able to remove this workaround
// once we stop supporting 3.x CLI versions.
const existingDeploymentId = payload.gitMeta.commitSha;
const existingDeployment = await this._prisma.workerDeployment.findFirst({
where: {
environmentId: environment.id,
friendlyId: existingDeploymentId,
},
});
if (!existingDeployment) {
throw new ServiceValidationError(
"Existing deployment not found during deployment initialization"
);
}
return {
deployment: existingDeployment,
imageRef: existingDeployment.imageReference ?? "",
};
}
if (payload.type === "UNMANAGED") {
throw new ServiceValidationError("UNMANAGED deployments are not supported");
}
// Upgrade the project to engine "V2" if it's not already. This should cover cases where people deploy to V2 without running dev first.
if (payload.type === "MANAGED" && environment.project.engine === "V1") {
await this._prisma.project.update({
where: {
id: environment.project.id,
},
data: {
engine: "V2",
},
});
}
const latestDeployment = await this._prisma.workerDeployment.findFirst({
where: {
environmentId: environment.id,
},
orderBy: {
createdAt: "desc",
},
take: 1,
});
const nextVersion = calculateNextBuildVersion(latestDeployment?.version);
if (payload.selfHosted && remoteBuildsEnabled()) {
throw new ServiceValidationError(
"Self-hosted deployments are not supported on this instance"
);
}
// For the `PENDING` initial status, defer the creation of the Depot build until the deployment is started to avoid token expiration issues.
// For local and native builds we don't need to generate the Depot tokens. We still need to create an empty object sadly due to a bug in older CLI versions.
const generateExternalBuildToken =
payload.initialStatus === "PENDING" || payload.isNativeBuild || payload.isLocalBuild;
const externalBuildData = generateExternalBuildToken
? ({
projectId: "-",
buildToken: "-",
buildId: "-",
} satisfies ExternalBuildData)
: await createRemoteImageBuild(environment.project);
const triggeredBy = payload.userId
? await this._prisma.user.findFirst({
where: {
id: payload.userId,
orgMemberships: {
some: {
organizationId: environment.project.organizationId,
},
},
},
})
: undefined;
const isV4Deployment = payload.type === "MANAGED";
const registryConfig = getRegistryConfig(isV4Deployment);
const deploymentShortCode = nanoid(8);
const [imageRefError, imageRefResult] = await tryCatch(
getDeploymentImageRef({
registry: registryConfig,
projectRef: environment.project.externalRef,
nextVersion,
environmentType: environment.type,
deploymentShortCode,
})
);
if (imageRefError) {
logger.error("Failed to get deployment image ref", {
environmentId: environment.id,
projectId: environment.projectId,
version: nextVersion,
triggeredById: triggeredBy?.id,
type: payload.type,
cause: imageRefError.message,
});
throw new ServiceValidationError("Failed to get deployment image ref");
}
const { imageRef, isEcr, repoCreated } = imageRefResult;
// We keep using `BUILDING` as the initial status if not explicitly set
// to avoid changing the behavior for deployments not created in the build server.
// Native builds always start in the `PENDING` status.
const initialStatus =
payload.initialStatus ?? (payload.isNativeBuild ? "PENDING" : "BUILDING");
const deploymentService = new DeploymentService();
const s2StreamOrFail = await deploymentService
.createEventStream(environment.project, { shortCode: deploymentShortCode })
.andThen(({ basin, stream }) =>
deploymentService.getEventStreamAccessToken(environment.project).map((accessToken) => ({
basin,
stream,
accessToken,
}))
);
if (s2StreamOrFail.isErr()) {
logger.error(
"Failed to create S2 event stream on deployment initialization, continuing without logs stream",
{
environmentId: environment.id,
projectId: environment.projectId,
error: s2StreamOrFail.error,
}
);
}
const eventStream = s2StreamOrFail.isOk()
? {
s2: {
basin: s2StreamOrFail.value.basin,
stream: s2StreamOrFail.value.stream,
accessToken: s2StreamOrFail.value.accessToken,
},
}
: undefined;
logger.debug("Creating deployment", {
environmentId: environment.id,
projectId: environment.projectId,
version: nextVersion,
triggeredById: triggeredBy?.id,
type: payload.type,
imageRef,
isEcr,
repoCreated,
initialStatus,
artifactKey: payload.isNativeBuild ? payload.artifactKey : undefined,
isNativeBuild: payload.isNativeBuild,
});
const buildServerMetadata: BuildServerMetadata | undefined =
payload.isNativeBuild || payload.buildId
? {
buildId: payload.buildId,
...(payload.isNativeBuild
? {
isNativeBuild: payload.isNativeBuild,
artifactKey: payload.artifactKey,
skipPromotion: payload.skipPromotion,
configFilePath: payload.configFilePath,
skipEnqueue: payload.skipEnqueue,
}
: {}),
}
: undefined;
const deployment = await this._prisma.workerDeployment.create({
data: {
friendlyId: generateFriendlyId("deployment"),
contentHash: payload.contentHash,
shortCode: deploymentShortCode,
version: nextVersion,
status: initialStatus,
environmentId: environment.id,
projectId: environment.projectId,
externalBuildData,
buildServerMetadata,
triggeredById: triggeredBy?.id,
type: payload.type,
imageReference: imageRef,
imagePlatform: env.DEPLOY_IMAGE_PLATFORM,
git: payload.gitMeta ?? undefined,
commitSHA: payload.gitMeta?.commitSha ?? undefined,
runtime: payload.runtime ?? undefined,
triggeredVia: payload.triggeredVia ?? undefined,
startedAt: initialStatus === "BUILDING" ? new Date() : undefined,
},
});
const timeoutMs =
deployment.status === "PENDING" ? env.DEPLOY_QUEUE_TIMEOUT_MS : env.DEPLOY_TIMEOUT_MS;
await TimeoutDeploymentService.enqueue(
deployment.id,
deployment.status,
"Building timed out",
new Date(Date.now() + timeoutMs)
);
// For github integration there is no artifactKey, hence we skip it here
if (payload.isNativeBuild && payload.artifactKey && !payload.skipEnqueue) {
const result = await deploymentService
.enqueueBuild(environment, deployment, payload.artifactKey, {
skipPromotion: payload.skipPromotion,
configFilePath: payload.configFilePath,
})
.orElse((error) => {
logger.error("Failed to enqueue build", {
environmentId: environment.id,
projectId: environment.projectId,
deploymentId: deployment.id,
error: error.cause,
});
return deploymentService
.cancelDeployment(environment, deployment.friendlyId, {
canceledReason: "Failed to enqueue build, please try again shortly.",
})
.orTee((cancelError) =>
logger.error("Failed to cancel deployment after failed build enqueue", {
environmentId: environment.id,
projectId: environment.projectId,
deploymentId: deployment.id,
error: cancelError,
})
)
.andThen(() => errAsync(error))
.orElse(() => errAsync(error));
});
if (result.isErr()) {
throw Error("Failed to enqueue build");
}
}
return {
deployment,
imageRef,
eventStream,
};
});
}
}