@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.
- package/dist/railway/volume.cjs +1 -1
- package/dist/railway/volume.cjs.map +1 -1
- package/dist/railway/volume.mjs +1 -1
- package/dist/railway/volume.mjs.map +1 -1
- package/dist/vercel/deploy.cjs +73 -4
- package/dist/vercel/deploy.cjs.map +1 -1
- package/dist/vercel/deploy.d.cts +52 -4
- package/dist/vercel/deploy.d.cts.map +1 -1
- package/dist/vercel/deploy.d.mts +52 -4
- package/dist/vercel/deploy.d.mts.map +1 -1
- package/dist/vercel/deploy.mjs +72 -5
- package/dist/vercel/deploy.mjs.map +1 -1
- package/package.json +1 -1
package/dist/railway/volume.cjs
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/railway/volume.mjs
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/vercel/deploy.cjs
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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:
|
|
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`
|
|
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"}
|
package/dist/vercel/deploy.d.cts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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":";;;;;;;
|
|
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"}
|
package/dist/vercel/deploy.d.mts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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":";;;;;;;
|
|
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"}
|
package/dist/vercel/deploy.mjs
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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:
|
|
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`
|
|
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"}
|