@infracraft/pulumi 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -134,7 +134,7 @@ var RailwayVolume = class extends _pulumi_pulumi.ComponentResource {
134
134
  serviceId: service.id,
135
135
  environmentId: environment.id,
136
136
  mountPath: args.mountPath
137
- }, { parent: this });
137
+ }, _pulumi_pulumi.mergeOptions(pulumiOpts, { parent: this }));
138
138
  this.registerOutputs({});
139
139
  }
140
140
  };
@@ -1 +1 @@
1
- {"version":3,"file":"volume.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/volume.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway volume dynamic provider. */\nexport interface RailwayVolumeInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the volume to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Absolute path inside the container where the volume is mounted (e.g. `\"/data\"`). */\n\tmountPath: string;\n}\n\n/** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */\ninterface RailwayVolumeOutputs extends RailwayVolumeInputs {\n\t/** Railway-assigned volume UUID (set after create or adopt). */\n\tvolumeId: string;\n}\n\nconst VOLUME_CREATE = `\n mutation($input: VolumeCreateInput!) {\n volumeCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst VOLUME_DELETE = `\n mutation($volumeId: String!) {\n volumeDelete(volumeId: $volumeId)\n }\n`;\n\nconst VOLUMES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n volumes {\n edges {\n node {\n id\n name\n volumeInstances {\n edges {\n node {\n id\n mountPath\n serviceId\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Finds an existing volume attached to a specific service within a project.\n */\nasync function findVolumeByService(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n): Promise<string | undefined> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tvolumes: {\n\t\t\t\tedges: Array<{\n\t\t\t\t\tnode: {\n\t\t\t\t\t\tid: string;\n\t\t\t\t\t\tvolumeInstances: {\n\t\t\t\t\t\t\tedges: Array<{\n\t\t\t\t\t\t\t\tnode: { serviceId: string };\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t};\n\t}>(VOLUMES_QUERY, { projectId });\n\n\tconst match = result.project.volumes.edges.find((edge) =>\n\t\tedge.node.volumeInstances.edges.some(\n\t\t\t(vi) => vi.node.serviceId === serviceId,\n\t\t),\n\t);\n\n\treturn match?.node.id;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway persistent volumes.\n *\n * Uses adopt-or-create on `create()`: finds an existing volume by service\n * before creating a new one. Volumes are immutable after creation — changing\n * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.\n */\nclass RailwayVolumeResourceProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tlet volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t);\n\n\t\tif (volumeId) {\n\t\t\tpulumi.log.info(`Adopting existing volume for service (${volumeId})`);\n\t\t} else {\n\t\t\tconst result = await client.query<{\n\t\t\t\tvolumeCreate: { id: string };\n\t\t\t}>(VOLUME_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tmountPath: inputs.mountPath,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tvolumeId = result.volumeCreate.id;\n\t\t}\n\n\t\treturn {\n\t\t\tid: volumeId,\n\t\t\touts: { ...inputs, volumeId },\n\t\t};\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.serviceId,\n\t\t);\n\n\t\tif (!volumeId) {\n\t\t\tthrow new Error(\"Railway volume not found during refresh\");\n\t\t}\n\n\t\treturn { id: volumeId, props: { ...props, volumeId } };\n\t}\n\n\tasync delete(id: string, props: RailwayVolumeOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(VOLUME_DELETE, { volumeId: id });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway volume (may already be deleted)\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayVolumeOutputs,\n\t\tnews: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.mountPath !== news.mountPath) {\n\t\t\treplaces.push(\"mountPath\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayVolumeResource extends pulumi.dynamic.Resource {\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tmountPath: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayVolumeResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, volumeId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */\ntype RailwayVolumeOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayVolume. */\nexport interface RailwayVolumeArgs {\n\t/** Absolute path inside the container where the volume is mounted. */\n\tmountPath: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway persistent volume with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new RailwayVolume(\"api-data\", {\n * mountPath: \"/data\",\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayVolume extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayVolumeArgs,\n\t\topts: RailwayVolumeOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Volume\", name, {}, pulumiOpts);\n\n\t\tnew RailwayVolumeResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tmountPath: args.mountPath,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;;AA+BA,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,gBAAgB;;;;;AAMtB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,eAAe,oBACd,QACA,WACA,WAC8B;CAwB9B,QANc,MAjBO,OAAO,MAezB,eAAe,EAAE,UAAU,CAAC,GAEV,QAAQ,QAAQ,MAAM,MAAM,SAChD,KAAK,KAAK,gBAAgB,MAAM,MAC9B,OAAO,GAAG,KAAK,cAAc,SAC/B,CAGU,GAAG,KAAK;AACpB;;;;;;;;AASA,IAAM,gCAAN,MAA+E;CAC9E,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAIA,qCAAc,OAAO,KAAK;EAE7C,IAAI,WAAW,MAAM,oBACpB,QACA,OAAO,WACP,OAAO,SACR;EAEA,IAAI,UACH,eAAO,IAAI,KAAK,yCAAyC,SAAS,EAAE;OAapE,YAAW,MAXU,OAAO,MAEzB,eAAe,EACjB,OAAO;GACN,WAAW,OAAO;GAClB,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,WAAW,OAAO;EACnB,EACD,CAAC,GAEiB,aAAa;EAGhC,OAAO;GACN,IAAI;GACJ,MAAM;IAAE,GAAG;IAAQ;GAAS;EAC7B;CACD;CAEA,MAAM,KACL,KACA,OACqC;EAGrC,MAAM,WAAW,MAAM,oBACtB,IAHkBA,qCAAc,MAAM,KAGjC,GACL,MAAM,WACN,MAAM,SACP;EAEA,IAAI,CAAC,UACJ,MAAM,IAAI,MAAM,yCAAyC;EAG1D,OAAO;GAAE,IAAI;GAAU,OAAO;IAAE,GAAG;IAAO;GAAS;EAAE;CACtD;CAEA,MAAM,OAAO,IAAY,OAA4C;EACpE,MAAM,SAAS,IAAIA,qCAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MAAM,eAAe,EAAE,UAAU,GAAG,CAAC;EACnD,QAAQ;GACP,eAAO,IAAI,KACV,0DACD;EACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,wBAAN,cAAoCC,eAAO,QAAQ,SAAS;CAC3D,YACC,MACA,MAOA,MACC;EACD,MACC,IAAI,8BAA8B,GAClC,MACA;GAAE,GAAG;GAAM,UAAU;EAAU,GAC/B,IACD;CACD;AACD;;;;;;;;;;;AAoCA,IAAa,gBAAb,cAAmCA,eAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,SAAS,GAAG,eAAe;EAEnE,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,IAAI,sBACH,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,WAAW,KAAK;EACjB,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
1
+ {"version":3,"file":"volume.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/volume.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway volume dynamic provider. */\nexport interface RailwayVolumeInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the volume to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Absolute path inside the container where the volume is mounted (e.g. `\"/data\"`). */\n\tmountPath: string;\n}\n\n/** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */\ninterface RailwayVolumeOutputs extends RailwayVolumeInputs {\n\t/** Railway-assigned volume UUID (set after create or adopt). */\n\tvolumeId: string;\n}\n\nconst VOLUME_CREATE = `\n mutation($input: VolumeCreateInput!) {\n volumeCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst VOLUME_DELETE = `\n mutation($volumeId: String!) {\n volumeDelete(volumeId: $volumeId)\n }\n`;\n\nconst VOLUMES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n volumes {\n edges {\n node {\n id\n name\n volumeInstances {\n edges {\n node {\n id\n mountPath\n serviceId\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Finds an existing volume attached to a specific service within a project.\n */\nasync function findVolumeByService(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n): Promise<string | undefined> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tvolumes: {\n\t\t\t\tedges: Array<{\n\t\t\t\t\tnode: {\n\t\t\t\t\t\tid: string;\n\t\t\t\t\t\tvolumeInstances: {\n\t\t\t\t\t\t\tedges: Array<{\n\t\t\t\t\t\t\t\tnode: { serviceId: string };\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t};\n\t}>(VOLUMES_QUERY, { projectId });\n\n\tconst match = result.project.volumes.edges.find((edge) =>\n\t\tedge.node.volumeInstances.edges.some(\n\t\t\t(vi) => vi.node.serviceId === serviceId,\n\t\t),\n\t);\n\n\treturn match?.node.id;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway persistent volumes.\n *\n * Uses adopt-or-create on `create()`: finds an existing volume by service\n * before creating a new one. Volumes are immutable after creation — changing\n * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.\n */\nclass RailwayVolumeResourceProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tlet volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t);\n\n\t\tif (volumeId) {\n\t\t\tpulumi.log.info(`Adopting existing volume for service (${volumeId})`);\n\t\t} else {\n\t\t\tconst result = await client.query<{\n\t\t\t\tvolumeCreate: { id: string };\n\t\t\t}>(VOLUME_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tmountPath: inputs.mountPath,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tvolumeId = result.volumeCreate.id;\n\t\t}\n\n\t\treturn {\n\t\t\tid: volumeId,\n\t\t\touts: { ...inputs, volumeId },\n\t\t};\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.serviceId,\n\t\t);\n\n\t\tif (!volumeId) {\n\t\t\tthrow new Error(\"Railway volume not found during refresh\");\n\t\t}\n\n\t\treturn { id: volumeId, props: { ...props, volumeId } };\n\t}\n\n\tasync delete(id: string, props: RailwayVolumeOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(VOLUME_DELETE, { volumeId: id });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway volume (may already be deleted)\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayVolumeOutputs,\n\t\tnews: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.mountPath !== news.mountPath) {\n\t\t\treplaces.push(\"mountPath\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayVolumeResource extends pulumi.dynamic.Resource {\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tmountPath: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayVolumeResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, volumeId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */\ntype RailwayVolumeOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayVolume. */\nexport interface RailwayVolumeArgs {\n\t/** Absolute path inside the container where the volume is mounted. */\n\tmountPath: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway persistent volume with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new RailwayVolume(\"api-data\", {\n * mountPath: \"/data\",\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayVolume extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayVolumeArgs,\n\t\topts: RailwayVolumeOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Volume\", name, {}, pulumiOpts);\n\n\t\tnew RailwayVolumeResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tmountPath: args.mountPath,\n\t\t\t},\n\t\t\t// Forward the consumer's resource options to the underlying resource. Pulumi\n\t\t\t// auto-inherits `provider`/`protect` from the parent component, but NOT options\n\t\t\t// like `retainOnDelete` — without this pass-through, setting `retainOnDelete` on a\n\t\t\t// RailwayVolume would silently never reach the actual cloud volume.\n\t\t\tpulumi.mergeOptions(pulumiOpts, { parent: this }),\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;;AA+BA,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,gBAAgB;;;;;AAMtB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,eAAe,oBACd,QACA,WACA,WAC8B;CAwB9B,QANc,MAjBO,OAAO,MAezB,eAAe,EAAE,UAAU,CAAC,GAEV,QAAQ,QAAQ,MAAM,MAAM,SAChD,KAAK,KAAK,gBAAgB,MAAM,MAC9B,OAAO,GAAG,KAAK,cAAc,SAC/B,CAGU,GAAG,KAAK;AACpB;;;;;;;;AASA,IAAM,gCAAN,MAA+E;CAC9E,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAIA,qCAAc,OAAO,KAAK;EAE7C,IAAI,WAAW,MAAM,oBACpB,QACA,OAAO,WACP,OAAO,SACR;EAEA,IAAI,UACH,eAAO,IAAI,KAAK,yCAAyC,SAAS,EAAE;OAapE,YAAW,MAXU,OAAO,MAEzB,eAAe,EACjB,OAAO;GACN,WAAW,OAAO;GAClB,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,WAAW,OAAO;EACnB,EACD,CAAC,GAEiB,aAAa;EAGhC,OAAO;GACN,IAAI;GACJ,MAAM;IAAE,GAAG;IAAQ;GAAS;EAC7B;CACD;CAEA,MAAM,KACL,KACA,OACqC;EAGrC,MAAM,WAAW,MAAM,oBACtB,IAHkBA,qCAAc,MAAM,KAGjC,GACL,MAAM,WACN,MAAM,SACP;EAEA,IAAI,CAAC,UACJ,MAAM,IAAI,MAAM,yCAAyC;EAG1D,OAAO;GAAE,IAAI;GAAU,OAAO;IAAE,GAAG;IAAO;GAAS;EAAE;CACtD;CAEA,MAAM,OAAO,IAAY,OAA4C;EACpE,MAAM,SAAS,IAAIA,qCAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MAAM,eAAe,EAAE,UAAU,GAAG,CAAC;EACnD,QAAQ;GACP,eAAO,IAAI,KACV,0DACD;EACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,wBAAN,cAAoCC,eAAO,QAAQ,SAAS;CAC3D,YACC,MACA,MAOA,MACC;EACD,MACC,IAAI,8BAA8B,GAClC,MACA;GAAE,GAAG;GAAM,UAAU;EAAU,GAC/B,IACD;CACD;AACD;;;;;;;;;;;AAoCA,IAAa,gBAAb,cAAmCA,eAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,SAAS,GAAG,eAAe;EAEnE,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,IAAI,sBACH,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,WAAW,KAAK;EACjB,GAKAA,eAAO,aAAa,YAAY,EAAE,QAAQ,KAAK,CAAC,CACjD;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
@@ -132,7 +132,7 @@ var RailwayVolume = class extends pulumi.ComponentResource {
132
132
  serviceId: service.id,
133
133
  environmentId: environment.id,
134
134
  mountPath: args.mountPath
135
- }, { parent: this });
135
+ }, pulumi.mergeOptions(pulumiOpts, { parent: this }));
136
136
  this.registerOutputs({});
137
137
  }
138
138
  };
@@ -1 +1 @@
1
- {"version":3,"file":"volume.mjs","names":[],"sources":["../../src/railway/volume.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway volume dynamic provider. */\nexport interface RailwayVolumeInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the volume to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Absolute path inside the container where the volume is mounted (e.g. `\"/data\"`). */\n\tmountPath: string;\n}\n\n/** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */\ninterface RailwayVolumeOutputs extends RailwayVolumeInputs {\n\t/** Railway-assigned volume UUID (set after create or adopt). */\n\tvolumeId: string;\n}\n\nconst VOLUME_CREATE = `\n mutation($input: VolumeCreateInput!) {\n volumeCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst VOLUME_DELETE = `\n mutation($volumeId: String!) {\n volumeDelete(volumeId: $volumeId)\n }\n`;\n\nconst VOLUMES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n volumes {\n edges {\n node {\n id\n name\n volumeInstances {\n edges {\n node {\n id\n mountPath\n serviceId\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Finds an existing volume attached to a specific service within a project.\n */\nasync function findVolumeByService(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n): Promise<string | undefined> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tvolumes: {\n\t\t\t\tedges: Array<{\n\t\t\t\t\tnode: {\n\t\t\t\t\t\tid: string;\n\t\t\t\t\t\tvolumeInstances: {\n\t\t\t\t\t\t\tedges: Array<{\n\t\t\t\t\t\t\t\tnode: { serviceId: string };\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t};\n\t}>(VOLUMES_QUERY, { projectId });\n\n\tconst match = result.project.volumes.edges.find((edge) =>\n\t\tedge.node.volumeInstances.edges.some(\n\t\t\t(vi) => vi.node.serviceId === serviceId,\n\t\t),\n\t);\n\n\treturn match?.node.id;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway persistent volumes.\n *\n * Uses adopt-or-create on `create()`: finds an existing volume by service\n * before creating a new one. Volumes are immutable after creation — changing\n * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.\n */\nclass RailwayVolumeResourceProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tlet volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t);\n\n\t\tif (volumeId) {\n\t\t\tpulumi.log.info(`Adopting existing volume for service (${volumeId})`);\n\t\t} else {\n\t\t\tconst result = await client.query<{\n\t\t\t\tvolumeCreate: { id: string };\n\t\t\t}>(VOLUME_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tmountPath: inputs.mountPath,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tvolumeId = result.volumeCreate.id;\n\t\t}\n\n\t\treturn {\n\t\t\tid: volumeId,\n\t\t\touts: { ...inputs, volumeId },\n\t\t};\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.serviceId,\n\t\t);\n\n\t\tif (!volumeId) {\n\t\t\tthrow new Error(\"Railway volume not found during refresh\");\n\t\t}\n\n\t\treturn { id: volumeId, props: { ...props, volumeId } };\n\t}\n\n\tasync delete(id: string, props: RailwayVolumeOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(VOLUME_DELETE, { volumeId: id });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway volume (may already be deleted)\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayVolumeOutputs,\n\t\tnews: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.mountPath !== news.mountPath) {\n\t\t\treplaces.push(\"mountPath\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayVolumeResource extends pulumi.dynamic.Resource {\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tmountPath: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayVolumeResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, volumeId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */\ntype RailwayVolumeOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayVolume. */\nexport interface RailwayVolumeArgs {\n\t/** Absolute path inside the container where the volume is mounted. */\n\tmountPath: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway persistent volume with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new RailwayVolume(\"api-data\", {\n * mountPath: \"/data\",\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayVolume extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayVolumeArgs,\n\t\topts: RailwayVolumeOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Volume\", name, {}, pulumiOpts);\n\n\t\tnew RailwayVolumeResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tmountPath: args.mountPath,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;AA+BA,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,gBAAgB;;;;;AAMtB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,eAAe,oBACd,QACA,WACA,WAC8B;CAwB9B,QANc,MAjBO,OAAO,MAezB,eAAe,EAAE,UAAU,CAAC,GAEV,QAAQ,QAAQ,MAAM,MAAM,SAChD,KAAK,KAAK,gBAAgB,MAAM,MAC9B,OAAO,GAAG,KAAK,cAAc,SAC/B,CAGU,GAAG,KAAK;AACpB;;;;;;;;AASA,IAAM,gCAAN,MAA+E;CAC9E,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAI,cAAc,OAAO,KAAK;EAE7C,IAAI,WAAW,MAAM,oBACpB,QACA,OAAO,WACP,OAAO,SACR;EAEA,IAAI,UACH,OAAO,IAAI,KAAK,yCAAyC,SAAS,EAAE;OAapE,YAAW,MAXU,OAAO,MAEzB,eAAe,EACjB,OAAO;GACN,WAAW,OAAO;GAClB,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,WAAW,OAAO;EACnB,EACD,CAAC,GAEiB,aAAa;EAGhC,OAAO;GACN,IAAI;GACJ,MAAM;IAAE,GAAG;IAAQ;GAAS;EAC7B;CACD;CAEA,MAAM,KACL,KACA,OACqC;EAGrC,MAAM,WAAW,MAAM,oBACtB,IAHkB,cAAc,MAAM,KAGjC,GACL,MAAM,WACN,MAAM,SACP;EAEA,IAAI,CAAC,UACJ,MAAM,IAAI,MAAM,yCAAyC;EAG1D,OAAO;GAAE,IAAI;GAAU,OAAO;IAAE,GAAG;IAAO;GAAS;EAAE;CACtD;CAEA,MAAM,OAAO,IAAY,OAA4C;EACpE,MAAM,SAAS,IAAI,cAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MAAM,eAAe,EAAE,UAAU,GAAG,CAAC;EACnD,QAAQ;GACP,OAAO,IAAI,KACV,0DACD;EACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,wBAAN,cAAoC,OAAO,QAAQ,SAAS;CAC3D,YACC,MACA,MAOA,MACC;EACD,MACC,IAAI,8BAA8B,GAClC,MACA;GAAE,GAAG;GAAM,UAAU;EAAU,GAC/B,IACD;CACD;AACD;;;;;;;;;;;AAoCA,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,SAAS,GAAG,eAAe;EAEnE,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,IAAI,sBACH,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,WAAW,KAAK;EACjB,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
1
+ {"version":3,"file":"volume.mjs","names":[],"sources":["../../src/railway/volume.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway volume dynamic provider. */\nexport interface RailwayVolumeInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the volume to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Absolute path inside the container where the volume is mounted (e.g. `\"/data\"`). */\n\tmountPath: string;\n}\n\n/** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */\ninterface RailwayVolumeOutputs extends RailwayVolumeInputs {\n\t/** Railway-assigned volume UUID (set after create or adopt). */\n\tvolumeId: string;\n}\n\nconst VOLUME_CREATE = `\n mutation($input: VolumeCreateInput!) {\n volumeCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst VOLUME_DELETE = `\n mutation($volumeId: String!) {\n volumeDelete(volumeId: $volumeId)\n }\n`;\n\nconst VOLUMES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n volumes {\n edges {\n node {\n id\n name\n volumeInstances {\n edges {\n node {\n id\n mountPath\n serviceId\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Finds an existing volume attached to a specific service within a project.\n */\nasync function findVolumeByService(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n): Promise<string | undefined> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tvolumes: {\n\t\t\t\tedges: Array<{\n\t\t\t\t\tnode: {\n\t\t\t\t\t\tid: string;\n\t\t\t\t\t\tvolumeInstances: {\n\t\t\t\t\t\t\tedges: Array<{\n\t\t\t\t\t\t\t\tnode: { serviceId: string };\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t};\n\t}>(VOLUMES_QUERY, { projectId });\n\n\tconst match = result.project.volumes.edges.find((edge) =>\n\t\tedge.node.volumeInstances.edges.some(\n\t\t\t(vi) => vi.node.serviceId === serviceId,\n\t\t),\n\t);\n\n\treturn match?.node.id;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway persistent volumes.\n *\n * Uses adopt-or-create on `create()`: finds an existing volume by service\n * before creating a new one. Volumes are immutable after creation — changing\n * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.\n */\nclass RailwayVolumeResourceProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tlet volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t);\n\n\t\tif (volumeId) {\n\t\t\tpulumi.log.info(`Adopting existing volume for service (${volumeId})`);\n\t\t} else {\n\t\t\tconst result = await client.query<{\n\t\t\t\tvolumeCreate: { id: string };\n\t\t\t}>(VOLUME_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tmountPath: inputs.mountPath,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tvolumeId = result.volumeCreate.id;\n\t\t}\n\n\t\treturn {\n\t\t\tid: volumeId,\n\t\t\touts: { ...inputs, volumeId },\n\t\t};\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.serviceId,\n\t\t);\n\n\t\tif (!volumeId) {\n\t\t\tthrow new Error(\"Railway volume not found during refresh\");\n\t\t}\n\n\t\treturn { id: volumeId, props: { ...props, volumeId } };\n\t}\n\n\tasync delete(id: string, props: RailwayVolumeOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(VOLUME_DELETE, { volumeId: id });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway volume (may already be deleted)\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayVolumeOutputs,\n\t\tnews: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.mountPath !== news.mountPath) {\n\t\t\treplaces.push(\"mountPath\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayVolumeResource extends pulumi.dynamic.Resource {\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tmountPath: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayVolumeResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, volumeId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */\ntype RailwayVolumeOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayVolume. */\nexport interface RailwayVolumeArgs {\n\t/** Absolute path inside the container where the volume is mounted. */\n\tmountPath: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway persistent volume with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new RailwayVolume(\"api-data\", {\n * mountPath: \"/data\",\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayVolume extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayVolumeArgs,\n\t\topts: RailwayVolumeOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Volume\", name, {}, pulumiOpts);\n\n\t\tnew RailwayVolumeResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tmountPath: args.mountPath,\n\t\t\t},\n\t\t\t// Forward the consumer's resource options to the underlying resource. Pulumi\n\t\t\t// auto-inherits `provider`/`protect` from the parent component, but NOT options\n\t\t\t// like `retainOnDelete` — without this pass-through, setting `retainOnDelete` on a\n\t\t\t// RailwayVolume would silently never reach the actual cloud volume.\n\t\t\tpulumi.mergeOptions(pulumiOpts, { parent: this }),\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;AA+BA,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,gBAAgB;;;;;AAMtB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,eAAe,oBACd,QACA,WACA,WAC8B;CAwB9B,QANc,MAjBO,OAAO,MAezB,eAAe,EAAE,UAAU,CAAC,GAEV,QAAQ,QAAQ,MAAM,MAAM,SAChD,KAAK,KAAK,gBAAgB,MAAM,MAC9B,OAAO,GAAG,KAAK,cAAc,SAC/B,CAGU,GAAG,KAAK;AACpB;;;;;;;;AASA,IAAM,gCAAN,MAA+E;CAC9E,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAI,cAAc,OAAO,KAAK;EAE7C,IAAI,WAAW,MAAM,oBACpB,QACA,OAAO,WACP,OAAO,SACR;EAEA,IAAI,UACH,OAAO,IAAI,KAAK,yCAAyC,SAAS,EAAE;OAapE,YAAW,MAXU,OAAO,MAEzB,eAAe,EACjB,OAAO;GACN,WAAW,OAAO;GAClB,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,WAAW,OAAO;EACnB,EACD,CAAC,GAEiB,aAAa;EAGhC,OAAO;GACN,IAAI;GACJ,MAAM;IAAE,GAAG;IAAQ;GAAS;EAC7B;CACD;CAEA,MAAM,KACL,KACA,OACqC;EAGrC,MAAM,WAAW,MAAM,oBACtB,IAHkB,cAAc,MAAM,KAGjC,GACL,MAAM,WACN,MAAM,SACP;EAEA,IAAI,CAAC,UACJ,MAAM,IAAI,MAAM,yCAAyC;EAG1D,OAAO;GAAE,IAAI;GAAU,OAAO;IAAE,GAAG;IAAO;GAAS;EAAE;CACtD;CAEA,MAAM,OAAO,IAAY,OAA4C;EACpE,MAAM,SAAS,IAAI,cAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MAAM,eAAe,EAAE,UAAU,GAAG,CAAC;EACnD,QAAQ;GACP,OAAO,IAAI,KACV,0DACD;EACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,wBAAN,cAAoC,OAAO,QAAQ,SAAS;CAC3D,YACC,MACA,MAOA,MACC;EACD,MACC,IAAI,8BAA8B,GAClC,MACA;GAAE,GAAG;GAAM,UAAU;EAAU,GAC/B,IACD;CACD;AACD;;;;;;;;;;;AAoCA,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,SAAS,GAAG,eAAe;EAEnE,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,IAAI,sBACH,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,WAAW,KAAK;EACjB,GAKA,OAAO,aAAa,YAAY,EAAE,QAAQ,KAAK,CAAC,CACjD;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_chunk = require('../chunk-BVYJZCqc.cjs');
3
+ const require_git_guard = require('../git-guard.cjs');
3
4
  const require_stable_dir = require('../stable-dir.cjs');
4
5
  let _pulumi_command = require("@pulumi/command");
5
6
  _pulumi_command = require_chunk.__toESM(_pulumi_command, 1);
@@ -7,21 +8,87 @@ let _pulumi_pulumi = require("@pulumi/pulumi");
7
8
  _pulumi_pulumi = require_chunk.__toESM(_pulumi_pulumi, 1);
8
9
 
9
10
  //#region src/vercel/deploy.ts
11
+ /** mkdir-based lock serializing the brief window when `.vercelignore` is written. */
12
+ const LOCK_DIR = "/tmp/.vercel-upload-lock";
13
+ /** Where a committed `.vercelignore` is parked while the engine owns the file. */
14
+ const IGNORE_BACKUP = ".vercelignore.infracraft-bak";
10
15
  /**
11
- * Deploys a Vercel project via `vercel deploy --prod --yes` CLI.
16
+ * Patterns the engine ALWAYS writes to `.vercelignore`, independent of `excludePaths`.
17
+ *
18
+ * `gitGuard` hides the real `.git` by renaming it to {@link GUARD_DIR} (or a
19
+ * {@link LEGACY_GUARD_DIRS} name) for the duration of a deploy, leaving a stub `.git`.
20
+ * Vercel default-ignores `.git` but NOT the renamed guard dir, and — unlike the Railway
21
+ * CLI — Vercel never reads `.gitignore` (where gitGuard also records the guard dir).
22
+ * Without this exclusion the renamed real `.git`, with its 100MB+ pack files, is
23
+ * uploaded and trips Vercel's 100MB-per-file limit. The transient `.vercelignore*` (the
24
+ * generated file and its {@link IGNORE_BACKUP}) is excluded too, so the engine's own
25
+ * scratch files never ship.
26
+ */
27
+ const ALWAYS_IGNORE = [
28
+ require_git_guard.GUARD_DIR,
29
+ ...require_git_guard.LEGACY_GUARD_DIRS,
30
+ ".vercelignore*"
31
+ ];
32
+ /**
33
+ * Builds the newline-joined `.vercelignore` body for the deploy.
34
+ *
35
+ * Prepends {@link ALWAYS_IGNORE} (the guard dir and scratch files) to the consumer's
36
+ * `excludePaths`, applying the workspace-preserving `apps/<name>` rule so other apps'
37
+ * code is dropped while their `package.json` stays for the build's dependency graph.
38
+ *
39
+ * @param excludePaths Consumer-supplied paths to exclude (optional)
40
+ * @returns The ignore body, one pattern per line
41
+ * @example
42
+ * ```typescript
43
+ * buildVercelIgnore(["apps/mesh", "docs"]);
44
+ * // ".git-infracraft-pulumi-guard\n…\napps/mesh/**\n!apps/mesh/package.json\ndocs"
45
+ * ```
46
+ */
47
+ function buildVercelIgnore(excludePaths) {
48
+ const excludeLines = (excludePaths ?? []).map((entry) => entry.startsWith("apps/") ? `${entry}/**\n!${entry}/package.json` : entry);
49
+ return [...ALWAYS_IGNORE, ...excludeLines].join("\n");
50
+ }
51
+ /**
52
+ * Builds the `create` shell command that wraps `vercel deploy` in the ignore engine.
53
+ *
54
+ * The engine acquires a mkdir lock, parks any committed `.vercelignore` at
55
+ * {@link IGNORE_BACKUP}, writes the generated body (`printf` expands the encoded `\n`
56
+ * sequences), runs the deploy, and — in a background timer so a slow remote build does
57
+ * not block sibling deploys — restores the parked file and releases the lock. The
58
+ * deploy's exit status is captured into `EXIT` before `wait`, so the Pulumi resource
59
+ * fails when the deploy fails.
60
+ *
61
+ * @param excludePaths Consumer-supplied paths to exclude (optional)
62
+ * @returns A single-line shell command (no raw newlines)
63
+ */
64
+ function buildVercelDeployCommand(excludePaths) {
65
+ return `while ! mkdir ${LOCK_DIR} 2>/dev/null; do sleep 1; done; if [ -f .vercelignore ]; then mv .vercelignore ${IGNORE_BACKUP}; fi; printf '${buildVercelIgnore(excludePaths).replace(/\n/g, "\\n")}\\n' > .vercelignore; { sleep 8; ${`rm -f .vercelignore; [ -f ${IGNORE_BACKUP} ] && mv ${IGNORE_BACKUP} .vercelignore`}; rmdir ${LOCK_DIR} 2>/dev/null; } & vercel deploy --prod --yes; EXIT=$?; wait; exit $EXIT`;
66
+ }
67
+ /**
68
+ * Deploys a Vercel project via `vercel deploy --prod --yes`, generating a
69
+ * `.vercelignore` around the upload — the same exclude engine as `RailwayDeploy`.
12
70
  *
13
71
  * Triggers on source hash (computed from the app directory) and env content hash
14
72
  * (from `VercelVariable.contentHash`). When an env value changes — whether from
15
73
  * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes
16
74
  * and a redeploy is triggered.
17
75
  *
76
+ * The upload is wrapped in an ignore engine mirroring Railway's: a mkdir lock
77
+ * serializes the brief window in which `.vercelignore` must be consistent, the file is
78
+ * written (gitGuard guard dir + {@link VercelDeployArgs.excludePaths}), then a
79
+ * background timer restores the repository and releases the lock so concurrent deploys
80
+ * stream. Any committed `.vercelignore` is parked at {@link IGNORE_BACKUP} and restored
81
+ * afterward, so this never destroys a consumer's file — and because a committed
82
+ * `.vercelignore` is git-tracked, a hard-killed run is recoverable with
83
+ * `git checkout .vercelignore`.
84
+ *
18
85
  * @example
19
86
  * ```typescript
20
87
  * new VercelDeploy("nexus-deploy", {
21
88
  * projectId: vercelProject.id,
22
- * rootDirectory: "apps/nexus",
23
89
  * monorepoRoot,
24
- * env: { NEXT_PUBLIC_API_URL: meshUrl },
90
+ * triggers: [sourceHash, envHash],
91
+ * excludePaths: ["apps/mesh", "docs"],
25
92
  * }, { provider });
26
93
  * ```
27
94
  */
@@ -32,7 +99,7 @@ var VercelDeploy = class extends _pulumi_pulumi.ComponentResource {
32
99
  const projectId = project ? project.id : args.projectId;
33
100
  if (!projectId) throw new Error("VercelDeploy: either `args.projectId` or `opts.project` must be provided");
34
101
  const deployCmd = new _pulumi_command.local.Command(`${name}-deploy`, {
35
- create: "vercel deploy --prod --yes",
102
+ create: buildVercelDeployCommand(args.excludePaths),
36
103
  triggers: args.triggers,
37
104
  dir: require_stable_dir.stableDir(args.monorepoRoot),
38
105
  environment: {
@@ -48,4 +115,6 @@ var VercelDeploy = class extends _pulumi_pulumi.ComponentResource {
48
115
 
49
116
  //#endregion
50
117
  exports.VercelDeploy = VercelDeploy;
118
+ exports.buildVercelDeployCommand = buildVercelDeployCommand;
119
+ exports.buildVercelIgnore = buildVercelIgnore;
51
120
  //# sourceMappingURL=deploy.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.cjs","names":["pulumi","command","stableDir"],"sources":["../../src/vercel/deploy.ts"],"sourcesContent":["import * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { stableDir } from \"../stable-dir\";\nimport type { VercelProject } from \"./project\";\nimport type { VercelProvider } from \"./provider\";\n\n/** Options type for VercelDeploy — replaces Pulumi's native `provider` field. */\ntype VercelDeployOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n\n\t/**\n\t * VercelProject resource to source the project ID from.\n\t * When provided, `args.projectId` is optional and ignored if both are given.\n\t */\n\tproject?: VercelProject;\n};\n\n/** Args for VercelDeploy. */\nexport interface VercelDeployArgs {\n\t/**\n\t * Vercel project ID.\n\t * Required when `opts.project` is not provided.\n\t */\n\tprojectId?: pulumi.Input<string>;\n\n\t/**\n\t * Absolute path to the monorepo root (working directory for `vercel deploy`).\n\t * Stored relative to the Pulumi program directory so the command stays stable\n\t * across machines and CI (see {@link stableDir}).\n\t */\n\tmonorepoRoot: string;\n\n\t/** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n}\n\n/**\n * Deploys a Vercel project via `vercel deploy --prod --yes` CLI.\n *\n * Triggers on source hash (computed from the app directory) and env content hash\n * (from `VercelVariable.contentHash`). When an env value changes — whether from\n * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes\n * and a redeploy is triggered.\n *\n * @example\n * ```typescript\n * new VercelDeploy(\"nexus-deploy\", {\n * projectId: vercelProject.id,\n * rootDirectory: \"apps/nexus\",\n * monorepoRoot,\n * env: { NEXT_PUBLIC_API_URL: meshUrl },\n * }, { provider });\n * ```\n */\nexport class VercelDeploy extends pulumi.ComponentResource {\n\t/**\n\t * The production deployment URL printed by `vercel deploy` (its final stdout line).\n\t * Surfaces the deployed link in stack outputs after `pulumi up` without visiting the dashboard.\n\t */\n\tpublic readonly deploymentUrl: pulumi.Output<string>;\n\n\tconstructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions) {\n\t\tconst { provider, project, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst projectId = project\n\t\t\t? project.id\n\t\t\t: (args.projectId as pulumi.Input<string>);\n\n\t\tif (!projectId) {\n\t\t\tthrow new Error(\n\t\t\t\t\"VercelDeploy: either `args.projectId` or `opts.project` must be provided\",\n\t\t\t);\n\t\t}\n\n\t\tconst deployCmd = new command.local.Command(\n\t\t\t`${name}-deploy`,\n\t\t\t{\n\t\t\t\tcreate: \"vercel deploy --prod --yes\",\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\tdir: stableDir(args.monorepoRoot),\n\t\t\t\tenvironment: {\n\t\t\t\t\tVERCEL_TOKEN: provider.token,\n\t\t\t\t\tVERCEL_ORG_ID: provider.teamId,\n\t\t\t\t\tVERCEL_PROJECT_ID: projectId,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.deploymentUrl = deployCmd.stdout.apply(\n\t\t\t(out) => out.trim().split(\"\\n\").pop() ?? \"\",\n\t\t);\n\n\t\tthis.registerOutputs({ deploymentUrl: this.deploymentUrl });\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,IAAa,eAAb,cAAkCA,eAAO,kBAAkB;CAO1D,YAAY,MAAc,MAAwB,MAA2B;EAC5E,MAAM,EAAE,UAAU,SAAS,GAAG,eAAe;EAE7C,MAAM,4BAA4B,MAAM,CAAC,GAAG,UAAU;EAEtD,MAAM,YAAY,UACf,QAAQ,KACP,KAAK;EAET,IAAI,CAAC,WACJ,MAAM,IAAI,MACT,0EACD;EAGD,MAAM,YAAY,IAAIC,gBAAQ,MAAM,QACnC,GAAG,KAAK,UACR;GACC,QAAQ;GACR,UAAU,KAAK;GACf,KAAKC,6BAAU,KAAK,YAAY;GAChC,aAAa;IACZ,cAAc,SAAS;IACvB,eAAe,SAAS;IACxB,mBAAmB;GACpB;EACD,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,UAAU,OAAO,OACpC,QAAQ,IAAI,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,KAAK,EAC1C;EAEA,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,CAAC;CAC3D;AACD"}
1
+ {"version":3,"file":"deploy.cjs","names":["GUARD_DIR","LEGACY_GUARD_DIRS","pulumi","command","stableDir"],"sources":["../../src/vercel/deploy.ts"],"sourcesContent":["import * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { GUARD_DIR, LEGACY_GUARD_DIRS } from \"../git-guard\";\nimport { stableDir } from \"../stable-dir\";\nimport type { VercelProject } from \"./project\";\nimport type { VercelProvider } from \"./provider\";\n\n/** Options type for VercelDeploy — replaces Pulumi's native `provider` field. */\ntype VercelDeployOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n\n\t/**\n\t * VercelProject resource to source the project ID from.\n\t * When provided, `args.projectId` is optional and ignored if both are given.\n\t */\n\tproject?: VercelProject;\n};\n\n/** Args for VercelDeploy. */\nexport interface VercelDeployArgs {\n\t/**\n\t * Vercel project ID.\n\t * Required when `opts.project` is not provided.\n\t */\n\tprojectId?: pulumi.Input<string>;\n\n\t/**\n\t * Absolute path to the monorepo root (working directory for `vercel deploy`).\n\t * Stored relative to the Pulumi program directory so the command stays stable\n\t * across machines and CI (see {@link stableDir}).\n\t */\n\tmonorepoRoot: string;\n\n\t/** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\n\t/**\n\t * Paths to exclude from the upload via a generated `.vercelignore`, mirroring\n\t * `RailwayDeployArgs.excludePaths`. An `apps/<name>` entry excludes that app's\n\t * code but keeps its `package.json`, so the workspace graph still resolves during\n\t * the monorepo build. The gitGuard guard dir is always excluded regardless of this\n\t * list — see {@link buildVercelIgnore}.\n\t */\n\texcludePaths?: string[];\n}\n\n/** mkdir-based lock serializing the brief window when `.vercelignore` is written. */\nconst LOCK_DIR = \"/tmp/.vercel-upload-lock\";\n\n/** Where a committed `.vercelignore` is parked while the engine owns the file. */\nconst IGNORE_BACKUP = \".vercelignore.infracraft-bak\";\n\n/**\n * Patterns the engine ALWAYS writes to `.vercelignore`, independent of `excludePaths`.\n *\n * `gitGuard` hides the real `.git` by renaming it to {@link GUARD_DIR} (or a\n * {@link LEGACY_GUARD_DIRS} name) for the duration of a deploy, leaving a stub `.git`.\n * Vercel default-ignores `.git` but NOT the renamed guard dir, and — unlike the Railway\n * CLI — Vercel never reads `.gitignore` (where gitGuard also records the guard dir).\n * Without this exclusion the renamed real `.git`, with its 100MB+ pack files, is\n * uploaded and trips Vercel's 100MB-per-file limit. The transient `.vercelignore*` (the\n * generated file and its {@link IGNORE_BACKUP}) is excluded too, so the engine's own\n * scratch files never ship.\n */\nconst ALWAYS_IGNORE: readonly string[] = [\n\tGUARD_DIR,\n\t...LEGACY_GUARD_DIRS,\n\t\".vercelignore*\",\n];\n\n/**\n * Builds the newline-joined `.vercelignore` body for the deploy.\n *\n * Prepends {@link ALWAYS_IGNORE} (the guard dir and scratch files) to the consumer's\n * `excludePaths`, applying the workspace-preserving `apps/<name>` rule so other apps'\n * code is dropped while their `package.json` stays for the build's dependency graph.\n *\n * @param excludePaths Consumer-supplied paths to exclude (optional)\n * @returns The ignore body, one pattern per line\n * @example\n * ```typescript\n * buildVercelIgnore([\"apps/mesh\", \"docs\"]);\n * // \".git-infracraft-pulumi-guard\\n…\\napps/mesh/**\\n!apps/mesh/package.json\\ndocs\"\n * ```\n */\nexport function buildVercelIgnore(excludePaths?: string[]): string {\n\tconst excludeLines = (excludePaths ?? []).map((entry) =>\n\t\tentry.startsWith(\"apps/\") ? `${entry}/**\\n!${entry}/package.json` : entry,\n\t);\n\n\treturn [...ALWAYS_IGNORE, ...excludeLines].join(\"\\n\");\n}\n\n/**\n * Builds the `create` shell command that wraps `vercel deploy` in the ignore engine.\n *\n * The engine acquires a mkdir lock, parks any committed `.vercelignore` at\n * {@link IGNORE_BACKUP}, writes the generated body (`printf` expands the encoded `\\n`\n * sequences), runs the deploy, and — in a background timer so a slow remote build does\n * not block sibling deploys — restores the parked file and releases the lock. The\n * deploy's exit status is captured into `EXIT` before `wait`, so the Pulumi resource\n * fails when the deploy fails.\n *\n * @param excludePaths Consumer-supplied paths to exclude (optional)\n * @returns A single-line shell command (no raw newlines)\n */\nexport function buildVercelDeployCommand(excludePaths?: string[]): string {\n\tconst ignoreBody = buildVercelIgnore(excludePaths).replace(/\\n/g, \"\\\\n\");\n\n\tconst restore = `rm -f .vercelignore; [ -f ${IGNORE_BACKUP} ] && mv ${IGNORE_BACKUP} .vercelignore`;\n\n\treturn (\n\t\t`while ! mkdir ${LOCK_DIR} 2>/dev/null; do sleep 1; done; ` +\n\t\t`if [ -f .vercelignore ]; then mv .vercelignore ${IGNORE_BACKUP}; fi; ` +\n\t\t`printf '${ignoreBody}\\\\n' > .vercelignore; ` +\n\t\t`{ sleep 8; ${restore}; rmdir ${LOCK_DIR} 2>/dev/null; } & ` +\n\t\t`vercel deploy --prod --yes; EXIT=$?; wait; exit $EXIT`\n\t);\n}\n\n/**\n * Deploys a Vercel project via `vercel deploy --prod --yes`, generating a\n * `.vercelignore` around the upload — the same exclude engine as `RailwayDeploy`.\n *\n * Triggers on source hash (computed from the app directory) and env content hash\n * (from `VercelVariable.contentHash`). When an env value changes — whether from\n * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes\n * and a redeploy is triggered.\n *\n * The upload is wrapped in an ignore engine mirroring Railway's: a mkdir lock\n * serializes the brief window in which `.vercelignore` must be consistent, the file is\n * written (gitGuard guard dir + {@link VercelDeployArgs.excludePaths}), then a\n * background timer restores the repository and releases the lock so concurrent deploys\n * stream. Any committed `.vercelignore` is parked at {@link IGNORE_BACKUP} and restored\n * afterward, so this never destroys a consumer's file — and because a committed\n * `.vercelignore` is git-tracked, a hard-killed run is recoverable with\n * `git checkout .vercelignore`.\n *\n * @example\n * ```typescript\n * new VercelDeploy(\"nexus-deploy\", {\n * projectId: vercelProject.id,\n * monorepoRoot,\n * triggers: [sourceHash, envHash],\n * excludePaths: [\"apps/mesh\", \"docs\"],\n * }, { provider });\n * ```\n */\nexport class VercelDeploy extends pulumi.ComponentResource {\n\t/**\n\t * The production deployment URL printed by `vercel deploy` (its final stdout line).\n\t * Surfaces the deployed link in stack outputs after `pulumi up` without visiting the dashboard.\n\t */\n\tpublic readonly deploymentUrl: pulumi.Output<string>;\n\n\tconstructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions) {\n\t\tconst { provider, project, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst projectId = project\n\t\t\t? project.id\n\t\t\t: (args.projectId as pulumi.Input<string>);\n\n\t\tif (!projectId) {\n\t\t\tthrow new Error(\n\t\t\t\t\"VercelDeploy: either `args.projectId` or `opts.project` must be provided\",\n\t\t\t);\n\t\t}\n\n\t\tconst deployCmd = new command.local.Command(\n\t\t\t`${name}-deploy`,\n\t\t\t{\n\t\t\t\tcreate: buildVercelDeployCommand(args.excludePaths),\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\tdir: stableDir(args.monorepoRoot),\n\t\t\t\tenvironment: {\n\t\t\t\t\tVERCEL_TOKEN: provider.token,\n\t\t\t\t\tVERCEL_ORG_ID: provider.teamId,\n\t\t\t\t\tVERCEL_PROJECT_ID: projectId,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.deploymentUrl = deployCmd.stdout.apply(\n\t\t\t(out) => out.trim().split(\"\\n\").pop() ?? \"\",\n\t\t);\n\n\t\tthis.registerOutputs({ deploymentUrl: this.deploymentUrl });\n\t}\n}\n"],"mappings":";;;;;;;;;;;AAgDA,MAAM,WAAW;;AAGjB,MAAM,gBAAgB;;;;;;;;;;;;;AActB,MAAM,gBAAmC;CACxCA;CACA,GAAGC;CACH;AACD;;;;;;;;;;;;;;;;AAiBA,SAAgB,kBAAkB,cAAiC;CAClE,MAAM,gBAAgB,gBAAgB,CAAC,GAAG,KAAK,UAC9C,MAAM,WAAW,OAAO,IAAI,GAAG,MAAM,QAAQ,MAAM,iBAAiB,KACrE;CAEA,OAAO,CAAC,GAAG,eAAe,GAAG,YAAY,EAAE,KAAK,IAAI;AACrD;;;;;;;;;;;;;;AAeA,SAAgB,yBAAyB,cAAiC;CAKzE,OACC,iBAAiB,SAAS,iFACwB,cAAc,gBAN9C,kBAAkB,YAAY,EAAE,QAAQ,OAAO,KAO7C,EAAE,mCACR,6BAN8B,cAAc,WAAW,cAAc,gBAM7D,UAAU,SAAS;AAG3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,IAAa,eAAb,cAAkCC,eAAO,kBAAkB;CAO1D,YAAY,MAAc,MAAwB,MAA2B;EAC5E,MAAM,EAAE,UAAU,SAAS,GAAG,eAAe;EAE7C,MAAM,4BAA4B,MAAM,CAAC,GAAG,UAAU;EAEtD,MAAM,YAAY,UACf,QAAQ,KACP,KAAK;EAET,IAAI,CAAC,WACJ,MAAM,IAAI,MACT,0EACD;EAGD,MAAM,YAAY,IAAIC,gBAAQ,MAAM,QACnC,GAAG,KAAK,UACR;GACC,QAAQ,yBAAyB,KAAK,YAAY;GAClD,UAAU,KAAK;GACf,KAAKC,6BAAU,KAAK,YAAY;GAChC,aAAa;IACZ,cAAc,SAAS;IACvB,eAAe,SAAS;IACxB,mBAAmB;GACpB;EACD,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,UAAU,OAAO,OACpC,QAAQ,IAAI,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,KAAK,EAC1C;EAEA,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,CAAC;CAC3D;AACD"}
@@ -28,22 +28,70 @@ interface VercelDeployArgs {
28
28
  monorepoRoot: string;
29
29
  /** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */
30
30
  triggers: pulumi.Input<pulumi.Input<string>[]>;
31
+ /**
32
+ * Paths to exclude from the upload via a generated `.vercelignore`, mirroring
33
+ * `RailwayDeployArgs.excludePaths`. An `apps/<name>` entry excludes that app's
34
+ * code but keeps its `package.json`, so the workspace graph still resolves during
35
+ * the monorepo build. The gitGuard guard dir is always excluded regardless of this
36
+ * list — see {@link buildVercelIgnore}.
37
+ */
38
+ excludePaths?: string[];
31
39
  }
32
40
  /**
33
- * Deploys a Vercel project via `vercel deploy --prod --yes` CLI.
41
+ * Builds the newline-joined `.vercelignore` body for the deploy.
42
+ *
43
+ * Prepends {@link ALWAYS_IGNORE} (the guard dir and scratch files) to the consumer's
44
+ * `excludePaths`, applying the workspace-preserving `apps/<name>` rule so other apps'
45
+ * code is dropped while their `package.json` stays for the build's dependency graph.
46
+ *
47
+ * @param excludePaths Consumer-supplied paths to exclude (optional)
48
+ * @returns The ignore body, one pattern per line
49
+ * @example
50
+ * ```typescript
51
+ * buildVercelIgnore(["apps/mesh", "docs"]);
52
+ * // ".git-infracraft-pulumi-guard\n…\napps/mesh/**\n!apps/mesh/package.json\ndocs"
53
+ * ```
54
+ */
55
+ declare function buildVercelIgnore(excludePaths?: string[]): string;
56
+ /**
57
+ * Builds the `create` shell command that wraps `vercel deploy` in the ignore engine.
58
+ *
59
+ * The engine acquires a mkdir lock, parks any committed `.vercelignore` at
60
+ * {@link IGNORE_BACKUP}, writes the generated body (`printf` expands the encoded `\n`
61
+ * sequences), runs the deploy, and — in a background timer so a slow remote build does
62
+ * not block sibling deploys — restores the parked file and releases the lock. The
63
+ * deploy's exit status is captured into `EXIT` before `wait`, so the Pulumi resource
64
+ * fails when the deploy fails.
65
+ *
66
+ * @param excludePaths Consumer-supplied paths to exclude (optional)
67
+ * @returns A single-line shell command (no raw newlines)
68
+ */
69
+ declare function buildVercelDeployCommand(excludePaths?: string[]): string;
70
+ /**
71
+ * Deploys a Vercel project via `vercel deploy --prod --yes`, generating a
72
+ * `.vercelignore` around the upload — the same exclude engine as `RailwayDeploy`.
34
73
  *
35
74
  * Triggers on source hash (computed from the app directory) and env content hash
36
75
  * (from `VercelVariable.contentHash`). When an env value changes — whether from
37
76
  * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes
38
77
  * and a redeploy is triggered.
39
78
  *
79
+ * The upload is wrapped in an ignore engine mirroring Railway's: a mkdir lock
80
+ * serializes the brief window in which `.vercelignore` must be consistent, the file is
81
+ * written (gitGuard guard dir + {@link VercelDeployArgs.excludePaths}), then a
82
+ * background timer restores the repository and releases the lock so concurrent deploys
83
+ * stream. Any committed `.vercelignore` is parked at {@link IGNORE_BACKUP} and restored
84
+ * afterward, so this never destroys a consumer's file — and because a committed
85
+ * `.vercelignore` is git-tracked, a hard-killed run is recoverable with
86
+ * `git checkout .vercelignore`.
87
+ *
40
88
  * @example
41
89
  * ```typescript
42
90
  * new VercelDeploy("nexus-deploy", {
43
91
  * projectId: vercelProject.id,
44
- * rootDirectory: "apps/nexus",
45
92
  * monorepoRoot,
46
- * env: { NEXT_PUBLIC_API_URL: meshUrl },
93
+ * triggers: [sourceHash, envHash],
94
+ * excludePaths: ["apps/mesh", "docs"],
47
95
  * }, { provider });
48
96
  * ```
49
97
  */
@@ -56,5 +104,5 @@ declare class VercelDeploy extends pulumi.ComponentResource {
56
104
  constructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions);
57
105
  }
58
106
  //#endregion
59
- export { VercelDeploy, VercelDeployArgs };
107
+ export { VercelDeploy, VercelDeployArgs, buildVercelDeployCommand, buildVercelIgnore };
60
108
  //# sourceMappingURL=deploy.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/vercel/deploy.ts"],"mappings":";;;;;;;KAOK,mBAAA,GAAsB,IAAA,CAAK,MAAA,CAAO,wBAAA;uCAEtC,QAAA,EAAU,cAAA;EAFa;;;;EAQvB,OAAA,GAAU,aAAA;AAAA;;UAIM,gBAAA;EAZU;;;;EAiB1B,SAAA,GAAY,MAAA,CAAO,KAAA;EATnB;;;AAAuB;AAIxB;EAYC,YAAA;;EAGA,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;;;AAAK;AAqBpC;;;;cAAa,YAAA,SAAqB,MAAA,CAAO,iBAAA;EAOgB;;;;EAAA,SAFxC,aAAA,EAAe,MAAA,CAAO,MAAA;cAE1B,IAAA,UAAc,IAAA,EAAM,gBAAA,EAAkB,IAAA,EAAM,mBAAA;AAAA"}
1
+ {"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/vercel/deploy.ts"],"mappings":";;;;;;;KAQK,mBAAA,GAAsB,IAAA,CAAK,MAAA,CAAO,wBAAA;uCAEtC,QAAA,EAAU,cAAA;EAFa;;;;EAQvB,OAAA,GAAU,aAAA;AAAA;;UAIM,gBAAA;EAZU;;;;EAiB1B,SAAA,GAAY,MAAA,CAAO,KAAA;EATnB;;;AAAuB;AAIxB;EAYC,YAAA;;EAGA,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAAP;;;;;;;EASvB,YAAA;AAAA;;;;;;;AAAY;AA0Cb;;;;AAAyD;AAqBzD;;;iBArBgB,iBAAA,CAAkB,YAAuB;AAqBO;AA0ChE;;;;;;;;;;;;AA1CgE,iBAAhD,wBAAA,CAAyB,YAAuB;;;;;;;;;AAiDY;;;;;;;;;;;;;;;;;;;;cAP/D,YAAA,SAAqB,MAAA,CAAO,iBAAA;;;;;WAKxB,aAAA,EAAe,MAAA,CAAO,MAAA;cAE1B,IAAA,UAAc,IAAA,EAAM,gBAAA,EAAkB,IAAA,EAAM,mBAAA;AAAA"}
@@ -28,22 +28,70 @@ interface VercelDeployArgs {
28
28
  monorepoRoot: string;
29
29
  /** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */
30
30
  triggers: pulumi.Input<pulumi.Input<string>[]>;
31
+ /**
32
+ * Paths to exclude from the upload via a generated `.vercelignore`, mirroring
33
+ * `RailwayDeployArgs.excludePaths`. An `apps/<name>` entry excludes that app's
34
+ * code but keeps its `package.json`, so the workspace graph still resolves during
35
+ * the monorepo build. The gitGuard guard dir is always excluded regardless of this
36
+ * list — see {@link buildVercelIgnore}.
37
+ */
38
+ excludePaths?: string[];
31
39
  }
32
40
  /**
33
- * Deploys a Vercel project via `vercel deploy --prod --yes` CLI.
41
+ * Builds the newline-joined `.vercelignore` body for the deploy.
42
+ *
43
+ * Prepends {@link ALWAYS_IGNORE} (the guard dir and scratch files) to the consumer's
44
+ * `excludePaths`, applying the workspace-preserving `apps/<name>` rule so other apps'
45
+ * code is dropped while their `package.json` stays for the build's dependency graph.
46
+ *
47
+ * @param excludePaths Consumer-supplied paths to exclude (optional)
48
+ * @returns The ignore body, one pattern per line
49
+ * @example
50
+ * ```typescript
51
+ * buildVercelIgnore(["apps/mesh", "docs"]);
52
+ * // ".git-infracraft-pulumi-guard\n…\napps/mesh/**\n!apps/mesh/package.json\ndocs"
53
+ * ```
54
+ */
55
+ declare function buildVercelIgnore(excludePaths?: string[]): string;
56
+ /**
57
+ * Builds the `create` shell command that wraps `vercel deploy` in the ignore engine.
58
+ *
59
+ * The engine acquires a mkdir lock, parks any committed `.vercelignore` at
60
+ * {@link IGNORE_BACKUP}, writes the generated body (`printf` expands the encoded `\n`
61
+ * sequences), runs the deploy, and — in a background timer so a slow remote build does
62
+ * not block sibling deploys — restores the parked file and releases the lock. The
63
+ * deploy's exit status is captured into `EXIT` before `wait`, so the Pulumi resource
64
+ * fails when the deploy fails.
65
+ *
66
+ * @param excludePaths Consumer-supplied paths to exclude (optional)
67
+ * @returns A single-line shell command (no raw newlines)
68
+ */
69
+ declare function buildVercelDeployCommand(excludePaths?: string[]): string;
70
+ /**
71
+ * Deploys a Vercel project via `vercel deploy --prod --yes`, generating a
72
+ * `.vercelignore` around the upload — the same exclude engine as `RailwayDeploy`.
34
73
  *
35
74
  * Triggers on source hash (computed from the app directory) and env content hash
36
75
  * (from `VercelVariable.contentHash`). When an env value changes — whether from
37
76
  * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes
38
77
  * and a redeploy is triggered.
39
78
  *
79
+ * The upload is wrapped in an ignore engine mirroring Railway's: a mkdir lock
80
+ * serializes the brief window in which `.vercelignore` must be consistent, the file is
81
+ * written (gitGuard guard dir + {@link VercelDeployArgs.excludePaths}), then a
82
+ * background timer restores the repository and releases the lock so concurrent deploys
83
+ * stream. Any committed `.vercelignore` is parked at {@link IGNORE_BACKUP} and restored
84
+ * afterward, so this never destroys a consumer's file — and because a committed
85
+ * `.vercelignore` is git-tracked, a hard-killed run is recoverable with
86
+ * `git checkout .vercelignore`.
87
+ *
40
88
  * @example
41
89
  * ```typescript
42
90
  * new VercelDeploy("nexus-deploy", {
43
91
  * projectId: vercelProject.id,
44
- * rootDirectory: "apps/nexus",
45
92
  * monorepoRoot,
46
- * env: { NEXT_PUBLIC_API_URL: meshUrl },
93
+ * triggers: [sourceHash, envHash],
94
+ * excludePaths: ["apps/mesh", "docs"],
47
95
  * }, { provider });
48
96
  * ```
49
97
  */
@@ -56,5 +104,5 @@ declare class VercelDeploy extends pulumi.ComponentResource {
56
104
  constructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions);
57
105
  }
58
106
  //#endregion
59
- export { VercelDeploy, VercelDeployArgs };
107
+ export { VercelDeploy, VercelDeployArgs, buildVercelDeployCommand, buildVercelIgnore };
60
108
  //# sourceMappingURL=deploy.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/vercel/deploy.ts"],"mappings":";;;;;;;KAOK,mBAAA,GAAsB,IAAA,CAAK,MAAA,CAAO,wBAAA;uCAEtC,QAAA,EAAU,cAAA;EAFa;;;;EAQvB,OAAA,GAAU,aAAA;AAAA;;UAIM,gBAAA;EAZU;;;;EAiB1B,SAAA,GAAY,MAAA,CAAO,KAAA;EATnB;;;AAAuB;AAIxB;EAYC,YAAA;;EAGA,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;;;AAAK;AAqBpC;;;;cAAa,YAAA,SAAqB,MAAA,CAAO,iBAAA;EAOgB;;;;EAAA,SAFxC,aAAA,EAAe,MAAA,CAAO,MAAA;cAE1B,IAAA,UAAc,IAAA,EAAM,gBAAA,EAAkB,IAAA,EAAM,mBAAA;AAAA"}
1
+ {"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/vercel/deploy.ts"],"mappings":";;;;;;;KAQK,mBAAA,GAAsB,IAAA,CAAK,MAAA,CAAO,wBAAA;uCAEtC,QAAA,EAAU,cAAA;EAFa;;;;EAQvB,OAAA,GAAU,aAAA;AAAA;;UAIM,gBAAA;EAZU;;;;EAiB1B,SAAA,GAAY,MAAA,CAAO,KAAA;EATnB;;;AAAuB;AAIxB;EAYC,YAAA;;EAGA,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAAP;;;;;;;EASvB,YAAA;AAAA;;;;;;;AAAY;AA0Cb;;;;AAAyD;AAqBzD;;;iBArBgB,iBAAA,CAAkB,YAAuB;AAqBO;AA0ChE;;;;;;;;;;;;AA1CgE,iBAAhD,wBAAA,CAAyB,YAAuB;;;;;;;;;AAiDY;;;;;;;;;;;;;;;;;;;;cAP/D,YAAA,SAAqB,MAAA,CAAO,iBAAA;;;;;WAKxB,aAAA,EAAe,MAAA,CAAO,MAAA;cAE1B,IAAA,UAAc,IAAA,EAAM,gBAAA,EAAkB,IAAA,EAAM,mBAAA;AAAA"}
@@ -1,24 +1,91 @@
1
1
  import { t as __name } from "../chunk-OPjESj5l.mjs";
2
+ import { GUARD_DIR, LEGACY_GUARD_DIRS } from "../git-guard.mjs";
2
3
  import { stableDir } from "../stable-dir.mjs";
3
4
  import * as command from "@pulumi/command";
4
5
  import * as pulumi from "@pulumi/pulumi";
5
6
 
6
7
  //#region src/vercel/deploy.ts
8
+ /** mkdir-based lock serializing the brief window when `.vercelignore` is written. */
9
+ const LOCK_DIR = "/tmp/.vercel-upload-lock";
10
+ /** Where a committed `.vercelignore` is parked while the engine owns the file. */
11
+ const IGNORE_BACKUP = ".vercelignore.infracraft-bak";
7
12
  /**
8
- * Deploys a Vercel project via `vercel deploy --prod --yes` CLI.
13
+ * Patterns the engine ALWAYS writes to `.vercelignore`, independent of `excludePaths`.
14
+ *
15
+ * `gitGuard` hides the real `.git` by renaming it to {@link GUARD_DIR} (or a
16
+ * {@link LEGACY_GUARD_DIRS} name) for the duration of a deploy, leaving a stub `.git`.
17
+ * Vercel default-ignores `.git` but NOT the renamed guard dir, and — unlike the Railway
18
+ * CLI — Vercel never reads `.gitignore` (where gitGuard also records the guard dir).
19
+ * Without this exclusion the renamed real `.git`, with its 100MB+ pack files, is
20
+ * uploaded and trips Vercel's 100MB-per-file limit. The transient `.vercelignore*` (the
21
+ * generated file and its {@link IGNORE_BACKUP}) is excluded too, so the engine's own
22
+ * scratch files never ship.
23
+ */
24
+ const ALWAYS_IGNORE = [
25
+ GUARD_DIR,
26
+ ...LEGACY_GUARD_DIRS,
27
+ ".vercelignore*"
28
+ ];
29
+ /**
30
+ * Builds the newline-joined `.vercelignore` body for the deploy.
31
+ *
32
+ * Prepends {@link ALWAYS_IGNORE} (the guard dir and scratch files) to the consumer's
33
+ * `excludePaths`, applying the workspace-preserving `apps/<name>` rule so other apps'
34
+ * code is dropped while their `package.json` stays for the build's dependency graph.
35
+ *
36
+ * @param excludePaths Consumer-supplied paths to exclude (optional)
37
+ * @returns The ignore body, one pattern per line
38
+ * @example
39
+ * ```typescript
40
+ * buildVercelIgnore(["apps/mesh", "docs"]);
41
+ * // ".git-infracraft-pulumi-guard\n…\napps/mesh/**\n!apps/mesh/package.json\ndocs"
42
+ * ```
43
+ */
44
+ function buildVercelIgnore(excludePaths) {
45
+ const excludeLines = (excludePaths ?? []).map((entry) => entry.startsWith("apps/") ? `${entry}/**\n!${entry}/package.json` : entry);
46
+ return [...ALWAYS_IGNORE, ...excludeLines].join("\n");
47
+ }
48
+ /**
49
+ * Builds the `create` shell command that wraps `vercel deploy` in the ignore engine.
50
+ *
51
+ * The engine acquires a mkdir lock, parks any committed `.vercelignore` at
52
+ * {@link IGNORE_BACKUP}, writes the generated body (`printf` expands the encoded `\n`
53
+ * sequences), runs the deploy, and — in a background timer so a slow remote build does
54
+ * not block sibling deploys — restores the parked file and releases the lock. The
55
+ * deploy's exit status is captured into `EXIT` before `wait`, so the Pulumi resource
56
+ * fails when the deploy fails.
57
+ *
58
+ * @param excludePaths Consumer-supplied paths to exclude (optional)
59
+ * @returns A single-line shell command (no raw newlines)
60
+ */
61
+ function buildVercelDeployCommand(excludePaths) {
62
+ return `while ! mkdir ${LOCK_DIR} 2>/dev/null; do sleep 1; done; if [ -f .vercelignore ]; then mv .vercelignore ${IGNORE_BACKUP}; fi; printf '${buildVercelIgnore(excludePaths).replace(/\n/g, "\\n")}\\n' > .vercelignore; { sleep 8; ${`rm -f .vercelignore; [ -f ${IGNORE_BACKUP} ] && mv ${IGNORE_BACKUP} .vercelignore`}; rmdir ${LOCK_DIR} 2>/dev/null; } & vercel deploy --prod --yes; EXIT=$?; wait; exit $EXIT`;
63
+ }
64
+ /**
65
+ * Deploys a Vercel project via `vercel deploy --prod --yes`, generating a
66
+ * `.vercelignore` around the upload — the same exclude engine as `RailwayDeploy`.
9
67
  *
10
68
  * Triggers on source hash (computed from the app directory) and env content hash
11
69
  * (from `VercelVariable.contentHash`). When an env value changes — whether from
12
70
  * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes
13
71
  * and a redeploy is triggered.
14
72
  *
73
+ * The upload is wrapped in an ignore engine mirroring Railway's: a mkdir lock
74
+ * serializes the brief window in which `.vercelignore` must be consistent, the file is
75
+ * written (gitGuard guard dir + {@link VercelDeployArgs.excludePaths}), then a
76
+ * background timer restores the repository and releases the lock so concurrent deploys
77
+ * stream. Any committed `.vercelignore` is parked at {@link IGNORE_BACKUP} and restored
78
+ * afterward, so this never destroys a consumer's file — and because a committed
79
+ * `.vercelignore` is git-tracked, a hard-killed run is recoverable with
80
+ * `git checkout .vercelignore`.
81
+ *
15
82
  * @example
16
83
  * ```typescript
17
84
  * new VercelDeploy("nexus-deploy", {
18
85
  * projectId: vercelProject.id,
19
- * rootDirectory: "apps/nexus",
20
86
  * monorepoRoot,
21
- * env: { NEXT_PUBLIC_API_URL: meshUrl },
87
+ * triggers: [sourceHash, envHash],
88
+ * excludePaths: ["apps/mesh", "docs"],
22
89
  * }, { provider });
23
90
  * ```
24
91
  */
@@ -29,7 +96,7 @@ var VercelDeploy = class extends pulumi.ComponentResource {
29
96
  const projectId = project ? project.id : args.projectId;
30
97
  if (!projectId) throw new Error("VercelDeploy: either `args.projectId` or `opts.project` must be provided");
31
98
  const deployCmd = new command.local.Command(`${name}-deploy`, {
32
- create: "vercel deploy --prod --yes",
99
+ create: buildVercelDeployCommand(args.excludePaths),
33
100
  triggers: args.triggers,
34
101
  dir: stableDir(args.monorepoRoot),
35
102
  environment: {
@@ -44,5 +111,5 @@ var VercelDeploy = class extends pulumi.ComponentResource {
44
111
  };
45
112
 
46
113
  //#endregion
47
- export { VercelDeploy };
114
+ export { VercelDeploy, buildVercelDeployCommand, buildVercelIgnore };
48
115
  //# sourceMappingURL=deploy.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.mjs","names":[],"sources":["../../src/vercel/deploy.ts"],"sourcesContent":["import * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { stableDir } from \"../stable-dir\";\nimport type { VercelProject } from \"./project\";\nimport type { VercelProvider } from \"./provider\";\n\n/** Options type for VercelDeploy — replaces Pulumi's native `provider` field. */\ntype VercelDeployOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n\n\t/**\n\t * VercelProject resource to source the project ID from.\n\t * When provided, `args.projectId` is optional and ignored if both are given.\n\t */\n\tproject?: VercelProject;\n};\n\n/** Args for VercelDeploy. */\nexport interface VercelDeployArgs {\n\t/**\n\t * Vercel project ID.\n\t * Required when `opts.project` is not provided.\n\t */\n\tprojectId?: pulumi.Input<string>;\n\n\t/**\n\t * Absolute path to the monorepo root (working directory for `vercel deploy`).\n\t * Stored relative to the Pulumi program directory so the command stays stable\n\t * across machines and CI (see {@link stableDir}).\n\t */\n\tmonorepoRoot: string;\n\n\t/** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n}\n\n/**\n * Deploys a Vercel project via `vercel deploy --prod --yes` CLI.\n *\n * Triggers on source hash (computed from the app directory) and env content hash\n * (from `VercelVariable.contentHash`). When an env value changes — whether from\n * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes\n * and a redeploy is triggered.\n *\n * @example\n * ```typescript\n * new VercelDeploy(\"nexus-deploy\", {\n * projectId: vercelProject.id,\n * rootDirectory: \"apps/nexus\",\n * monorepoRoot,\n * env: { NEXT_PUBLIC_API_URL: meshUrl },\n * }, { provider });\n * ```\n */\nexport class VercelDeploy extends pulumi.ComponentResource {\n\t/**\n\t * The production deployment URL printed by `vercel deploy` (its final stdout line).\n\t * Surfaces the deployed link in stack outputs after `pulumi up` without visiting the dashboard.\n\t */\n\tpublic readonly deploymentUrl: pulumi.Output<string>;\n\n\tconstructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions) {\n\t\tconst { provider, project, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst projectId = project\n\t\t\t? project.id\n\t\t\t: (args.projectId as pulumi.Input<string>);\n\n\t\tif (!projectId) {\n\t\t\tthrow new Error(\n\t\t\t\t\"VercelDeploy: either `args.projectId` or `opts.project` must be provided\",\n\t\t\t);\n\t\t}\n\n\t\tconst deployCmd = new command.local.Command(\n\t\t\t`${name}-deploy`,\n\t\t\t{\n\t\t\t\tcreate: \"vercel deploy --prod --yes\",\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\tdir: stableDir(args.monorepoRoot),\n\t\t\t\tenvironment: {\n\t\t\t\t\tVERCEL_TOKEN: provider.token,\n\t\t\t\t\tVERCEL_ORG_ID: provider.teamId,\n\t\t\t\t\tVERCEL_PROJECT_ID: projectId,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.deploymentUrl = deployCmd.stdout.apply(\n\t\t\t(out) => out.trim().split(\"\\n\").pop() ?? \"\",\n\t\t);\n\n\t\tthis.registerOutputs({ deploymentUrl: this.deploymentUrl });\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAuDA,IAAa,eAAb,cAAkC,OAAO,kBAAkB;CAO1D,YAAY,MAAc,MAAwB,MAA2B;EAC5E,MAAM,EAAE,UAAU,SAAS,GAAG,eAAe;EAE7C,MAAM,4BAA4B,MAAM,CAAC,GAAG,UAAU;EAEtD,MAAM,YAAY,UACf,QAAQ,KACP,KAAK;EAET,IAAI,CAAC,WACJ,MAAM,IAAI,MACT,0EACD;EAGD,MAAM,YAAY,IAAI,QAAQ,MAAM,QACnC,GAAG,KAAK,UACR;GACC,QAAQ;GACR,UAAU,KAAK;GACf,KAAK,UAAU,KAAK,YAAY;GAChC,aAAa;IACZ,cAAc,SAAS;IACvB,eAAe,SAAS;IACxB,mBAAmB;GACpB;EACD,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,UAAU,OAAO,OACpC,QAAQ,IAAI,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,KAAK,EAC1C;EAEA,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,CAAC;CAC3D;AACD"}
1
+ {"version":3,"file":"deploy.mjs","names":[],"sources":["../../src/vercel/deploy.ts"],"sourcesContent":["import * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { GUARD_DIR, LEGACY_GUARD_DIRS } from \"../git-guard\";\nimport { stableDir } from \"../stable-dir\";\nimport type { VercelProject } from \"./project\";\nimport type { VercelProvider } from \"./provider\";\n\n/** Options type for VercelDeploy — replaces Pulumi's native `provider` field. */\ntype VercelDeployOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n\n\t/**\n\t * VercelProject resource to source the project ID from.\n\t * When provided, `args.projectId` is optional and ignored if both are given.\n\t */\n\tproject?: VercelProject;\n};\n\n/** Args for VercelDeploy. */\nexport interface VercelDeployArgs {\n\t/**\n\t * Vercel project ID.\n\t * Required when `opts.project` is not provided.\n\t */\n\tprojectId?: pulumi.Input<string>;\n\n\t/**\n\t * Absolute path to the monorepo root (working directory for `vercel deploy`).\n\t * Stored relative to the Pulumi program directory so the command stays stable\n\t * across machines and CI (see {@link stableDir}).\n\t */\n\tmonorepoRoot: string;\n\n\t/** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\n\t/**\n\t * Paths to exclude from the upload via a generated `.vercelignore`, mirroring\n\t * `RailwayDeployArgs.excludePaths`. An `apps/<name>` entry excludes that app's\n\t * code but keeps its `package.json`, so the workspace graph still resolves during\n\t * the monorepo build. The gitGuard guard dir is always excluded regardless of this\n\t * list — see {@link buildVercelIgnore}.\n\t */\n\texcludePaths?: string[];\n}\n\n/** mkdir-based lock serializing the brief window when `.vercelignore` is written. */\nconst LOCK_DIR = \"/tmp/.vercel-upload-lock\";\n\n/** Where a committed `.vercelignore` is parked while the engine owns the file. */\nconst IGNORE_BACKUP = \".vercelignore.infracraft-bak\";\n\n/**\n * Patterns the engine ALWAYS writes to `.vercelignore`, independent of `excludePaths`.\n *\n * `gitGuard` hides the real `.git` by renaming it to {@link GUARD_DIR} (or a\n * {@link LEGACY_GUARD_DIRS} name) for the duration of a deploy, leaving a stub `.git`.\n * Vercel default-ignores `.git` but NOT the renamed guard dir, and — unlike the Railway\n * CLI — Vercel never reads `.gitignore` (where gitGuard also records the guard dir).\n * Without this exclusion the renamed real `.git`, with its 100MB+ pack files, is\n * uploaded and trips Vercel's 100MB-per-file limit. The transient `.vercelignore*` (the\n * generated file and its {@link IGNORE_BACKUP}) is excluded too, so the engine's own\n * scratch files never ship.\n */\nconst ALWAYS_IGNORE: readonly string[] = [\n\tGUARD_DIR,\n\t...LEGACY_GUARD_DIRS,\n\t\".vercelignore*\",\n];\n\n/**\n * Builds the newline-joined `.vercelignore` body for the deploy.\n *\n * Prepends {@link ALWAYS_IGNORE} (the guard dir and scratch files) to the consumer's\n * `excludePaths`, applying the workspace-preserving `apps/<name>` rule so other apps'\n * code is dropped while their `package.json` stays for the build's dependency graph.\n *\n * @param excludePaths Consumer-supplied paths to exclude (optional)\n * @returns The ignore body, one pattern per line\n * @example\n * ```typescript\n * buildVercelIgnore([\"apps/mesh\", \"docs\"]);\n * // \".git-infracraft-pulumi-guard\\n…\\napps/mesh/**\\n!apps/mesh/package.json\\ndocs\"\n * ```\n */\nexport function buildVercelIgnore(excludePaths?: string[]): string {\n\tconst excludeLines = (excludePaths ?? []).map((entry) =>\n\t\tentry.startsWith(\"apps/\") ? `${entry}/**\\n!${entry}/package.json` : entry,\n\t);\n\n\treturn [...ALWAYS_IGNORE, ...excludeLines].join(\"\\n\");\n}\n\n/**\n * Builds the `create` shell command that wraps `vercel deploy` in the ignore engine.\n *\n * The engine acquires a mkdir lock, parks any committed `.vercelignore` at\n * {@link IGNORE_BACKUP}, writes the generated body (`printf` expands the encoded `\\n`\n * sequences), runs the deploy, and — in a background timer so a slow remote build does\n * not block sibling deploys — restores the parked file and releases the lock. The\n * deploy's exit status is captured into `EXIT` before `wait`, so the Pulumi resource\n * fails when the deploy fails.\n *\n * @param excludePaths Consumer-supplied paths to exclude (optional)\n * @returns A single-line shell command (no raw newlines)\n */\nexport function buildVercelDeployCommand(excludePaths?: string[]): string {\n\tconst ignoreBody = buildVercelIgnore(excludePaths).replace(/\\n/g, \"\\\\n\");\n\n\tconst restore = `rm -f .vercelignore; [ -f ${IGNORE_BACKUP} ] && mv ${IGNORE_BACKUP} .vercelignore`;\n\n\treturn (\n\t\t`while ! mkdir ${LOCK_DIR} 2>/dev/null; do sleep 1; done; ` +\n\t\t`if [ -f .vercelignore ]; then mv .vercelignore ${IGNORE_BACKUP}; fi; ` +\n\t\t`printf '${ignoreBody}\\\\n' > .vercelignore; ` +\n\t\t`{ sleep 8; ${restore}; rmdir ${LOCK_DIR} 2>/dev/null; } & ` +\n\t\t`vercel deploy --prod --yes; EXIT=$?; wait; exit $EXIT`\n\t);\n}\n\n/**\n * Deploys a Vercel project via `vercel deploy --prod --yes`, generating a\n * `.vercelignore` around the upload — the same exclude engine as `RailwayDeploy`.\n *\n * Triggers on source hash (computed from the app directory) and env content hash\n * (from `VercelVariable.contentHash`). When an env value changes — whether from\n * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes\n * and a redeploy is triggered.\n *\n * The upload is wrapped in an ignore engine mirroring Railway's: a mkdir lock\n * serializes the brief window in which `.vercelignore` must be consistent, the file is\n * written (gitGuard guard dir + {@link VercelDeployArgs.excludePaths}), then a\n * background timer restores the repository and releases the lock so concurrent deploys\n * stream. Any committed `.vercelignore` is parked at {@link IGNORE_BACKUP} and restored\n * afterward, so this never destroys a consumer's file — and because a committed\n * `.vercelignore` is git-tracked, a hard-killed run is recoverable with\n * `git checkout .vercelignore`.\n *\n * @example\n * ```typescript\n * new VercelDeploy(\"nexus-deploy\", {\n * projectId: vercelProject.id,\n * monorepoRoot,\n * triggers: [sourceHash, envHash],\n * excludePaths: [\"apps/mesh\", \"docs\"],\n * }, { provider });\n * ```\n */\nexport class VercelDeploy extends pulumi.ComponentResource {\n\t/**\n\t * The production deployment URL printed by `vercel deploy` (its final stdout line).\n\t * Surfaces the deployed link in stack outputs after `pulumi up` without visiting the dashboard.\n\t */\n\tpublic readonly deploymentUrl: pulumi.Output<string>;\n\n\tconstructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions) {\n\t\tconst { provider, project, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst projectId = project\n\t\t\t? project.id\n\t\t\t: (args.projectId as pulumi.Input<string>);\n\n\t\tif (!projectId) {\n\t\t\tthrow new Error(\n\t\t\t\t\"VercelDeploy: either `args.projectId` or `opts.project` must be provided\",\n\t\t\t);\n\t\t}\n\n\t\tconst deployCmd = new command.local.Command(\n\t\t\t`${name}-deploy`,\n\t\t\t{\n\t\t\t\tcreate: buildVercelDeployCommand(args.excludePaths),\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\tdir: stableDir(args.monorepoRoot),\n\t\t\t\tenvironment: {\n\t\t\t\t\tVERCEL_TOKEN: provider.token,\n\t\t\t\t\tVERCEL_ORG_ID: provider.teamId,\n\t\t\t\t\tVERCEL_PROJECT_ID: projectId,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.deploymentUrl = deployCmd.stdout.apply(\n\t\t\t(out) => out.trim().split(\"\\n\").pop() ?? \"\",\n\t\t);\n\n\t\tthis.registerOutputs({ deploymentUrl: this.deploymentUrl });\n\t}\n}\n"],"mappings":";;;;;;;;AAgDA,MAAM,WAAW;;AAGjB,MAAM,gBAAgB;;;;;;;;;;;;;AActB,MAAM,gBAAmC;CACxC;CACA,GAAG;CACH;AACD;;;;;;;;;;;;;;;;AAiBA,SAAgB,kBAAkB,cAAiC;CAClE,MAAM,gBAAgB,gBAAgB,CAAC,GAAG,KAAK,UAC9C,MAAM,WAAW,OAAO,IAAI,GAAG,MAAM,QAAQ,MAAM,iBAAiB,KACrE;CAEA,OAAO,CAAC,GAAG,eAAe,GAAG,YAAY,EAAE,KAAK,IAAI;AACrD;;;;;;;;;;;;;;AAeA,SAAgB,yBAAyB,cAAiC;CAKzE,OACC,iBAAiB,SAAS,iFACwB,cAAc,gBAN9C,kBAAkB,YAAY,EAAE,QAAQ,OAAO,KAO7C,EAAE,mCACR,6BAN8B,cAAc,WAAW,cAAc,gBAM7D,UAAU,SAAS;AAG3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,IAAa,eAAb,cAAkC,OAAO,kBAAkB;CAO1D,YAAY,MAAc,MAAwB,MAA2B;EAC5E,MAAM,EAAE,UAAU,SAAS,GAAG,eAAe;EAE7C,MAAM,4BAA4B,MAAM,CAAC,GAAG,UAAU;EAEtD,MAAM,YAAY,UACf,QAAQ,KACP,KAAK;EAET,IAAI,CAAC,WACJ,MAAM,IAAI,MACT,0EACD;EAGD,MAAM,YAAY,IAAI,QAAQ,MAAM,QACnC,GAAG,KAAK,UACR;GACC,QAAQ,yBAAyB,KAAK,YAAY;GAClD,UAAU,KAAK;GACf,KAAK,UAAU,KAAK,YAAY;GAChC,aAAa;IACZ,cAAc,SAAS;IACvB,eAAe,SAAS;IACxB,mBAAmB;GACpB;EACD,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,UAAU,OAAO,OACpC,QAAQ,IAAI,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,KAAK,EAC1C;EAEA,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,CAAC;CAC3D;AACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infracraft/pulumi",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {