11import { ComputeClient , stripImageDigest } from "@internal/compute" ;
2+ import type { TemplateCreateResultEntry } from "@internal/compute" ;
3+ import { MachinePresetName } from "@trigger.dev/core/v3" ;
24import { machinePresetFromName } from "~/v3/machinePresets.server" ;
35import { env } from "~/env.server" ;
46import { logger } from "~/services/logger.server" ;
@@ -10,8 +12,16 @@ import { resolveComputeAccess } from "../regionAccess.server";
1012
1113type TemplateCreationMode = "required" | "shadow" | "skip" ;
1214
15+ type ResolvedPreset = {
16+ name : MachinePresetName ;
17+ cpu : number ;
18+ memory_gb : number ;
19+ } ;
20+
1321export class ComputeTemplateCreationService {
1422 private client : ComputeClient | undefined ;
23+ private presets : ResolvedPreset [ ] ;
24+ private requiredPresets : Set < MachinePresetName > ;
1525
1626 constructor ( ) {
1727 if ( env . COMPUTE_GATEWAY_URL ) {
@@ -21,6 +31,24 @@ export class ComputeTemplateCreationService {
2131 timeoutMs : 5 * 60 * 1000 , // 5 minutes
2232 } ) ;
2333 }
34+
35+ this . presets = env . COMPUTE_TEMPLATE_MACHINE_PRESETS . map ( ( name ) => {
36+ const machine = machinePresetFromName ( name ) ;
37+ return { name, cpu : machine . cpu , memory_gb : machine . memory } ;
38+ } ) ;
39+
40+ // Sanity-check that required is a subset of the configured presets.
41+ // env-layer parsing already validated each name; this catches the cross-
42+ // field invariant that env-level transforms can't easily express.
43+ const presetNames = new Set ( this . presets . map ( ( p ) => p . name ) ) ;
44+ for ( const required of env . COMPUTE_TEMPLATE_MACHINE_PRESETS_REQUIRED ) {
45+ if ( ! presetNames . has ( required ) ) {
46+ throw new Error (
47+ `COMPUTE_TEMPLATE_MACHINE_PRESETS_REQUIRED includes "${ required } " which is not in COMPUTE_TEMPLATE_MACHINE_PRESETS`
48+ ) ;
49+ }
50+ }
51+ this . requiredPresets = new Set ( env . COMPUTE_TEMPLATE_MACHINE_PRESETS_REQUIRED ) ;
2452 }
2553
2654 /**
@@ -48,12 +76,12 @@ export class ComputeTemplateCreationService {
4876
4977 if ( mode === "shadow" ) {
5078 this . createTemplate ( options . imageReference , { background : true } )
51- . then ( ( result ) => {
52- if ( ! result . success ) {
79+ . then ( ( outcome ) => {
80+ if ( outcome . error ) {
5381 logger . error ( "Shadow template creation failed" , {
5482 id : options . deploymentFriendlyId ,
5583 imageReference : options . imageReference ,
56- error : result . error ,
84+ error : outcome . error ,
5785 } ) ;
5886 }
5987 } )
@@ -81,31 +109,39 @@ export class ComputeTemplateCreationService {
81109 logger . info ( "Creating compute template (required mode)" , {
82110 id : options . deploymentFriendlyId ,
83111 imageReference : options . imageReference ,
112+ presets : this . presets . map ( ( p ) => p . name ) ,
113+ requiredPresets : [ ...this . requiredPresets ] ,
84114 } ) ;
85115
86- const result = await this . createTemplate ( options . imageReference ) ;
116+ const outcome = await this . createTemplate ( options . imageReference ) ;
117+ const failureMessage = this . failureMessageForRequiredMode (
118+ outcome ,
119+ options . deploymentFriendlyId ,
120+ options . imageReference
121+ ) ;
87122
88- if ( ! result . success ) {
123+ if ( failureMessage ) {
89124 logger . error ( "Compute template creation failed" , {
90125 id : options . deploymentFriendlyId ,
91126 imageReference : options . imageReference ,
92- error : result . error ,
127+ error : failureMessage ,
93128 } ) ;
94129
95130 const failService = new FailDeploymentService ( ) ;
96131 await failService . call ( options . authenticatedEnv , options . deploymentFriendlyId , {
97132 error : {
98133 name : "TemplateCreationFailed" ,
99- message : `Failed to create compute template: ${ result . error } ` ,
134+ message : `Failed to create compute template: ${ failureMessage } ` ,
100135 } ,
101136 } ) ;
102137
103- throw new ServiceValidationError ( `Compute template creation failed: ${ result . error } ` ) ;
138+ throw new ServiceValidationError ( `Compute template creation failed: ${ failureMessage } ` ) ;
104139 }
105140
106141 logger . info ( "Compute template created" , {
107142 id : options . deploymentFriendlyId ,
108143 imageReference : options . imageReference ,
144+ results : outcome . results . length ,
109145 } ) ;
110146 }
111147
@@ -154,29 +190,108 @@ export class ComputeTemplateCreationService {
154190 async createTemplate (
155191 imageReference : string ,
156192 options ?: { background ?: boolean }
157- ) : Promise < { success : boolean ; error ?: string } > {
193+ ) : Promise < CreateTemplateOutcome > {
158194 if ( ! this . client ) {
159- return { success : false , error : "Compute gateway not configured" } ;
195+ return { error : "Compute gateway not configured" , results : [ ] } ;
160196 }
161197
162198 try {
163- // Templates are resource-agnostic - these values don't affect template content.
164- const machine = machinePresetFromName ( "small-1x" ) ;
199+ const machineConfigs = this . presets . map ( ( p ) => ( {
200+ cpu : p . cpu ,
201+ memory_gb : p . memory_gb ,
202+ } ) ) ;
165203
166- await this . client . templates . create ( {
204+ const response = await this . client . templates . create ( {
167205 image : stripImageDigest ( imageReference ) ,
168- cpu : machine . cpu ,
169- memory_gb : machine . memory ,
206+ machine_configs : machineConfigs ,
170207 background : options ?. background ,
171208 } ) ;
172- return { success : true } ;
209+
210+ // Background mode (202 Accepted): no body to inspect.
211+ if ( options ?. background || ! response ) {
212+ return { results : [ ] } ;
213+ }
214+
215+ return {
216+ error : response . error ,
217+ results : response . results ,
218+ } ;
173219 } catch ( error ) {
174220 const message = error instanceof Error ? error . message : "Unknown error" ;
175221 logger . error ( "Failed to create compute template" , {
176222 imageReference,
177223 error : message ,
178224 } ) ;
179- return { success : false , error : message } ;
225+ return { error : message , results : [ ] } ;
226+ }
227+ }
228+
229+ // Returns a human-readable failure message if any required preset failed
230+ // or the request itself failed. Optional preset failures are logged and
231+ // do not contribute to the message. Returns undefined on success.
232+ //
233+ // Result-to-preset matching is by index: response.results[i] corresponds
234+ // to this.presets[i]. While the multi-config compute path is being built
235+ // out, the gateway may return fewer entries than presets requested - any
236+ // unbuilt required preset is still treated as a failure.
237+ private failureMessageForRequiredMode (
238+ outcome : CreateTemplateOutcome ,
239+ deploymentFriendlyId : string ,
240+ imageReference : string
241+ ) : string | undefined {
242+ if ( this . presets . length === 0 ) {
243+ return undefined ;
180244 }
245+
246+ const failures : string [ ] = [ ] ;
247+
248+ this . presets . forEach ( ( preset , i ) => {
249+ const isRequired = this . requiredPresets . has ( preset . name ) ;
250+ const result = outcome . results [ i ] ;
251+
252+ if ( ! result ) {
253+ if ( isRequired ) {
254+ failures . push ( `${ preset . name } : not built` ) ;
255+ } else {
256+ logger . warn ( "Optional compute template preset not built" , {
257+ id : deploymentFriendlyId ,
258+ imageReference,
259+ preset : preset . name ,
260+ } ) ;
261+ }
262+ return ;
263+ }
264+
265+ if ( result . error ) {
266+ if ( isRequired ) {
267+ failures . push ( `${ preset . name } : ${ result . error } ` ) ;
268+ } else {
269+ logger . warn ( "Optional compute template preset failed" , {
270+ id : deploymentFriendlyId ,
271+ imageReference,
272+ preset : preset . name ,
273+ error : result . error ,
274+ } ) ;
275+ }
276+ }
277+ } ) ;
278+
279+ // Top-level error not tied to a specific preset (e.g. gateway unreachable,
280+ // image pull failed). Surface it if no per-preset failure was attributed.
281+ if ( outcome . error && failures . length === 0 ) {
282+ failures . push ( outcome . error ) ;
283+ }
284+
285+ return failures . length > 0 ? failures . join ( "; " ) : undefined ;
181286 }
182287}
288+
289+ type CreateTemplateOutcome = {
290+ // Top-level error - request didn't reach gateway, image pull/squashfs
291+ // failed, or any per-result failure that was promoted to a request-level
292+ // failure by the gateway. Empty if all succeeded.
293+ error ?: string ;
294+ // Per-preset results in input order. Empty for background mode or when
295+ // the request didn't reach the gateway.
296+ results : TemplateCreateResultEntry [ ] ;
297+ } ;
0 commit comments