@infracraft/pulumi 1.22.0 → 1.23.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/commands/deploy.cjs +1 -1
- package/dist/commands/deploy.mjs +1 -1
- package/dist/railway/service.cjs +25 -0
- package/dist/railway/service.cjs.map +1 -1
- package/dist/railway/service.d.cts.map +1 -1
- package/dist/railway/service.d.mts.map +1 -1
- package/dist/railway/service.mjs +25 -0
- package/dist/railway/service.mjs.map +1 -1
- package/dist/railway/volume.cjs +21 -6
- package/dist/railway/volume.cjs.map +1 -1
- package/dist/railway/volume.d.cts.map +1 -1
- package/dist/railway/volume.d.mts.map +1 -1
- package/dist/railway/volume.mjs +21 -6
- package/dist/railway/volume.mjs.map +1 -1
- package/dist/{sandbox-Bh7tQ1Jy.cjs → sandbox-BBnhmyM_.cjs} +7 -6
- package/dist/sandbox-BBnhmyM_.cjs.map +1 -0
- package/dist/{sandbox-8xQPhxw7.mjs → sandbox-BQtqMu-5.mjs} +7 -6
- package/dist/sandbox-BQtqMu-5.mjs.map +1 -0
- package/dist/sandbox.cjs +1 -1
- package/dist/sandbox.d.cts +5 -3
- package/dist/sandbox.d.cts.map +1 -1
- package/dist/sandbox.d.mts +5 -3
- package/dist/sandbox.d.mts.map +1 -1
- package/dist/sandbox.mjs +1 -1
- package/package.json +1 -1
- package/dist/sandbox-8xQPhxw7.mjs.map +0 -1
- package/dist/sandbox-Bh7tQ1Jy.cjs.map +0 -1
package/dist/commands/deploy.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
const require_chunk = require('../chunk-BVYJZCqc.cjs');
|
|
3
3
|
const require_git_guard = require('../git-guard.cjs');
|
|
4
|
-
const require_sandbox = require('../sandbox-
|
|
4
|
+
const require_sandbox = require('../sandbox-BBnhmyM_.cjs');
|
|
5
5
|
let _pulumi_pulumi = require("@pulumi/pulumi");
|
|
6
6
|
_pulumi_pulumi = require_chunk.__toESM(_pulumi_pulumi, 1);
|
|
7
7
|
let _pulumi_command = require("@pulumi/command");
|
package/dist/commands/deploy.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as __name } from "../chunk-OPjESj5l.mjs";
|
|
2
2
|
import { isGitGuard } from "../git-guard.mjs";
|
|
3
|
-
import { a as buildSandboxScript, n as isDeploySandbox, r as SandboxMode } from "../sandbox-
|
|
3
|
+
import { a as buildSandboxScript, n as isDeploySandbox, r as SandboxMode } from "../sandbox-BQtqMu-5.mjs";
|
|
4
4
|
import * as pulumi from "@pulumi/pulumi";
|
|
5
5
|
import * as command from "@pulumi/command";
|
|
6
6
|
|
package/dist/railway/service.cjs
CHANGED
|
@@ -87,12 +87,35 @@ const SERVICE_CONNECT = `
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
`;
|
|
90
|
+
const SERVICE_INSTANCE_DEPLOY = `
|
|
91
|
+
mutation($serviceId: String!, $environmentId: String!) {
|
|
92
|
+
serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
/**
|
|
96
|
+
* Triggers a deployment of the service instance in the target environment.
|
|
97
|
+
* Image-sourced services never deploy there on their own: `serviceCreate`'s
|
|
98
|
+
* auto-deploy only reaches the project's DEFAULT environment, and
|
|
99
|
+
* `serviceInstanceUpdate` applies config without redeploying — so a service
|
|
100
|
+
* in any other environment stays undeployed forever and its private DNS name
|
|
101
|
+
* never registers. `environmentTriggersDeploy` is no alternative: it returns
|
|
102
|
+
* success without creating anything for a service that has never deployed in
|
|
103
|
+
* that environment (proven live, 2026-07-06).
|
|
104
|
+
*/
|
|
105
|
+
async function deployServiceInstance(client, serviceId, environmentId) {
|
|
106
|
+
const result = await client.query(SERVICE_INSTANCE_DEPLOY, {
|
|
107
|
+
serviceId,
|
|
108
|
+
environmentId
|
|
109
|
+
});
|
|
110
|
+
_pulumi_pulumi.log.info(`[infracraft] serviceInstanceDeployV2 created deployment ${result.serviceInstanceDeployV2}`);
|
|
111
|
+
}
|
|
90
112
|
/**
|
|
91
113
|
* Applies service instance configuration (builder, commands, healthcheck).
|
|
92
114
|
* Retries without healthcheck fields if the first call fails.
|
|
93
115
|
*/
|
|
94
116
|
async function applyInstanceConfig(client, serviceId, environmentId, inputs) {
|
|
95
117
|
const instanceInput = {};
|
|
118
|
+
if (inputs.source) instanceInput.source = { image: inputs.source.image };
|
|
96
119
|
if (inputs.builder) instanceInput.builder = inputs.builder;
|
|
97
120
|
if (inputs.buildCommand) instanceInput.buildCommand = inputs.buildCommand;
|
|
98
121
|
if (inputs.startCommand) instanceInput.startCommand = inputs.startCommand;
|
|
@@ -154,6 +177,7 @@ var RailwayServiceResourceProvider = class {
|
|
|
154
177
|
});
|
|
155
178
|
}
|
|
156
179
|
await applyInstanceConfig(client, serviceId, inputs.environmentId, inputs);
|
|
180
|
+
if (inputs.source) await deployServiceInstance(client, serviceId, inputs.environmentId);
|
|
157
181
|
const outs = {
|
|
158
182
|
...inputs,
|
|
159
183
|
serviceId
|
|
@@ -176,6 +200,7 @@ var RailwayServiceResourceProvider = class {
|
|
|
176
200
|
input: updateInput
|
|
177
201
|
});
|
|
178
202
|
await applyInstanceConfig(client, id, news.environmentId, news);
|
|
203
|
+
if (news.source) await deployServiceInstance(client, id, news.environmentId);
|
|
179
204
|
return { outs: {
|
|
180
205
|
...news,
|
|
181
206
|
serviceId: id
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/service.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\";\n\n/**\n * Railway build system. Enum keys are UPPERCASE per convention; values are\n * Railway's required UPPERCASE wire literals.\n * Note: HEROKU and PAKETO were deprecated Feb 21 2025 and auto-migrated to\n * NIXPACKS by Railway, but remain in the schema and are accepted by the API.\n */\nexport enum RailwayBuilder {\n\tRAILPACK = \"RAILPACK\",\n\tNIXPACKS = \"NIXPACKS\",\n\tDOCKERFILE = \"DOCKERFILE\",\n\tHEROKU = \"HEROKU\",\n\tPAKETO = \"PAKETO\",\n}\n\n/**\n * Railway service restart policy. Controls when Railway restarts the service\n * container after it exits. Default is ON_FAILURE.\n */\nexport enum RailwayRestartPolicy {\n\tON_FAILURE = \"ON_FAILURE\",\n\tALWAYS = \"ALWAYS\",\n\tNEVER = \"NEVER\",\n}\n\n/** Docker image source for a Railway service (e.g. `redis:8-alpine`). */\ninterface RailwayServiceSource {\n\t/** Full Docker image reference including tag. */\n\timage: string;\n}\n\n/** Resolved inputs for the Railway service dynamic provider. */\ninterface RailwayServiceInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Human-readable service name used for adopt-or-create matching. */\n\tname: string;\n\n\t/** SVG icon URL displayed in the Railway dashboard. */\n\ticon?: string;\n\n\t/** Docker image source for image-based services. */\n\tsource?: RailwayServiceSource;\n\n\t/** Build system to use when building the service. */\n\tbuilder?: RailwayBuilder;\n\n\t/** Shell command executed during the build phase. */\n\tbuildCommand?: string;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: string;\n\n\t/** Restart behavior for the service container. */\n\trestartPolicyType?: RailwayRestartPolicy;\n\n\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\thealthcheckPath?: string;\n\n\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\thealthcheckTimeout?: number;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: string;\n}\n\n/** Persisted state for the Railway service, extending inputs with the Railway-assigned ID. */\ninterface RailwayServiceOutputs extends RailwayServiceInputs {\n\t/** Railway-assigned service UUID. */\n\tserviceId: string;\n}\n\nconst SERVICES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n services {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n`;\n\nconst SERVICE_QUERY = `\n query($serviceId: String!) {\n service(id: $serviceId) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_CREATE = `\n mutation($input: ServiceCreateInput!) {\n serviceCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_UPDATE = `\n mutation($id: String!, $input: ServiceUpdateInput!) {\n serviceUpdate(id: $id, input: $input) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_INSTANCE_UPDATE = `\n mutation(\n $serviceId: String!\n $environmentId: String!\n $input: ServiceInstanceUpdateInput!\n ) {\n serviceInstanceUpdate(\n serviceId: $serviceId\n environmentId: $environmentId\n input: $input\n )\n }\n`;\n\nconst SERVICE_CONNECT = `\n mutation($id: String!, $input: ServiceConnectInput!) {\n serviceConnect(id: $id, input: $input) {\n id\n }\n }\n`;\n\n/**\n * Applies service instance configuration (builder, commands, healthcheck).\n * Retries without healthcheck fields if the first call fails.\n */\nasync function applyInstanceConfig(\n\tclient: RailwayClient,\n\tserviceId: string,\n\tenvironmentId: string,\n\tinputs: RailwayServiceInputs,\n): Promise<void> {\n\tconst instanceInput: Record<string, unknown> = {};\n\n\tif (inputs.builder) {\n\t\tinstanceInput.builder = inputs.builder;\n\t}\n\n\tif (inputs.buildCommand) {\n\t\tinstanceInput.buildCommand = inputs.buildCommand;\n\t}\n\n\tif (inputs.startCommand) {\n\t\tinstanceInput.startCommand = inputs.startCommand;\n\t}\n\n\tif (inputs.restartPolicyType) {\n\t\tinstanceInput.restartPolicyType = inputs.restartPolicyType;\n\t}\n\n\tif (inputs.healthcheckPath) {\n\t\tinstanceInput.healthcheckPath = inputs.healthcheckPath;\n\t}\n\n\tif (inputs.healthcheckTimeout) {\n\t\tinstanceInput.healthcheckTimeout = inputs.healthcheckTimeout;\n\t}\n\n\tif (inputs.preDeployCommand) {\n\t\tinstanceInput.preDeployCommand = inputs.preDeployCommand;\n\t}\n\n\tif (Object.keys(instanceInput).length === 0) {\n\t\treturn;\n\t}\n\n\ttry {\n\t\tawait client.query(SERVICE_INSTANCE_UPDATE, {\n\t\t\tserviceId,\n\t\t\tenvironmentId,\n\t\t\tinput: instanceInput,\n\t\t});\n\t} catch (error) {\n\t\tpulumi.log.warn(\n\t\t\t`serviceInstanceUpdate failed, retrying without healthcheck fields: ${error}`,\n\t\t);\n\n\t\tdelete instanceInput.healthcheckPath;\n\t\tdelete instanceInput.healthcheckTimeout;\n\n\t\tif (Object.keys(instanceInput).length > 0) {\n\t\t\tawait client.query(SERVICE_INSTANCE_UPDATE, {\n\t\t\t\tserviceId,\n\t\t\t\tenvironmentId,\n\t\t\t\tinput: instanceInput,\n\t\t\t});\n\t\t}\n\t}\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway services.\n *\n * Uses adopt-or-create on `create()`: queries services by project ID and name\n * before creating a new one. Service instance configuration (builder, commands,\n * healthcheck) is applied via `serviceInstanceUpdate` after create or update.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class RailwayServiceResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\t/**\n\t * Creates or adopts a Railway service by name, then applies instance config.\n\t */\n\tasync create(\n\t\tinputs: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst result = await client.query<{\n\t\t\tproject: {\n\t\t\t\tservices: { edges: Array<{ node: { id: string; name: string } }> };\n\t\t\t};\n\t\t}>(SERVICES_QUERY, { projectId: inputs.projectId });\n\n\t\tlet serviceId = result.project.services.edges.find(\n\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t)?.node.id;\n\n\t\tif (serviceId) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopted existing Railway service \"${inputs.name}\" (${serviceId})`,\n\t\t\t);\n\t\t} else {\n\t\t\tconst createInput: Record<string, unknown> = {\n\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\tname: inputs.name,\n\t\t\t};\n\n\t\t\tif (inputs.source) {\n\t\t\t\tcreateInput.source = { image: inputs.source.image };\n\t\t\t}\n\n\t\t\tconst created = await client.query<{\n\t\t\t\tserviceCreate: { id: string; name: string };\n\t\t\t}>(SERVICE_CREATE, { input: createInput });\n\n\t\t\tserviceId = created.serviceCreate.id;\n\n\t\t\tpulumi.log.info(\n\t\t\t\t`Created Railway service \"${inputs.name}\" (${serviceId})`,\n\t\t\t);\n\n\t\t\tif (inputs.source) {\n\t\t\t\tawait client.query(SERVICE_CONNECT, {\n\t\t\t\t\tid: serviceId,\n\t\t\t\t\tinput: { image: inputs.source.image },\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (inputs.icon) {\n\t\t\t\tawait client.query(SERVICE_UPDATE, {\n\t\t\t\t\tid: serviceId,\n\t\t\t\t\tinput: { icon: inputs.icon },\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tawait applyInstanceConfig(client, serviceId, inputs.environmentId, inputs);\n\n\t\tconst outs: RailwayServiceOutputs = { ...inputs, serviceId };\n\n\t\treturn { id: serviceId, outs };\n\t}\n\n\t/**\n\t * Updates service name/icon and re-applies instance configuration.\n\t */\n\tasync update(\n\t\tid: string,\n\t\tolds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst client = new RailwayClient(news.token);\n\n\t\tconst updateInput: Record<string, unknown> = {};\n\n\t\tif (olds.name !== news.name) {\n\t\t\tupdateInput.name = news.name;\n\t\t}\n\n\t\tif (news.icon && olds.icon !== news.icon) {\n\t\t\tupdateInput.icon = news.icon;\n\t\t}\n\n\t\tif (Object.keys(updateInput).length > 0) {\n\t\t\tawait client.query(SERVICE_UPDATE, { id, input: updateInput });\n\t\t}\n\n\t\tawait applyInstanceConfig(client, id, news.environmentId, news);\n\n\t\tconst outs: RailwayServiceOutputs = { ...news, serviceId: id };\n\n\t\treturn { outs };\n\t}\n\n\t/**\n\t * Reads current state for `pulumi refresh` by querying the service by ID.\n\t */\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayServiceOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query<{ service: { id: string; name: string } }>(\n\t\t\t\tSERVICE_QUERY,\n\t\t\t\t{ serviceId: id },\n\t\t\t);\n\t\t} catch {\n\t\t\tthrow new Error(`Railway service \"${props.name}\" (${id}) not found`);\n\t\t}\n\n\t\treturn { id, props: { ...props, serviceId: id } };\n\t}\n\n\t/**\n\t * Deletion is a no-op. A Railway service is a project-level resource shared\n\t * across environments (forked environments adopt it by name), so a single\n\t * environment's destroy must never delete it. Deleting the *environment*\n\t * removes that environment's service instances instead.\n\t */\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Railway service deletion skipped — services are project-level; delete the environment to remove its instances\",\n\t\t);\n\t}\n\n\t/**\n\t * Compares old and new inputs to determine what changed.\n\t *\n\t * ProjectId and environmentId changes trigger replacement.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\t\tconst changes: string[] = [];\n\n\t\tif (olds.name !== news.name) {\n\t\t\tchanges.push(\"name\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\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.builder !== news.builder) {\n\t\t\tchanges.push(\"builder\");\n\t\t}\n\n\t\tif (olds.buildCommand !== news.buildCommand) {\n\t\t\tchanges.push(\"buildCommand\");\n\t\t}\n\n\t\tif (olds.startCommand !== news.startCommand) {\n\t\t\tchanges.push(\"startCommand\");\n\t\t}\n\n\t\tif (olds.restartPolicyType !== news.restartPolicyType) {\n\t\t\tchanges.push(\"restartPolicyType\");\n\t\t}\n\n\t\tif (olds.healthcheckPath !== news.healthcheckPath) {\n\t\t\tchanges.push(\"healthcheckPath\");\n\t\t}\n\n\t\tif (olds.healthcheckTimeout !== news.healthcheckTimeout) {\n\t\t\tchanges.push(\"healthcheckTimeout\");\n\t\t}\n\n\t\tif (olds.preDeployCommand !== news.preDeployCommand) {\n\t\t\tchanges.push(\"preDeployCommand\");\n\t\t}\n\n\t\tif (olds.icon !== news.icon) {\n\t\t\tchanges.push(\"icon\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0 || changes.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 RailwayServiceResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly serviceId: pulumi.Output<string>;\n\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\tenvironmentId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\ticon?: pulumi.Input<string>;\n\t\t\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\t\t\tbuilder?: pulumi.Input<RailwayBuilder>;\n\t\t\tbuildCommand?: pulumi.Input<string>;\n\t\t\tstartCommand?: pulumi.Input<string>;\n\t\t\trestartPolicyType?: pulumi.Input<RailwayRestartPolicy>;\n\t\t\thealthcheckPath?: pulumi.Input<string>;\n\t\t\thealthcheckTimeout?: pulumi.Input<number>;\n\t\t\tpreDeployCommand?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayServiceResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, serviceId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayService — replaces Pulumi's native `provider` field. */\ntype RailwayServiceOptions = 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\n/** Args for RailwayService. */\nexport interface RailwayServiceArgs {\n\t/** Human-readable service name used for adopt-or-create matching. */\n\tname: pulumi.Input<string>;\n\n\t/** SVG icon URL displayed in the Railway dashboard. */\n\ticon?: pulumi.Input<string>;\n\n\t/** Docker image source for image-based services. */\n\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\n\t/** Build system to use when building the service. */\n\tbuilder?: pulumi.Input<RailwayBuilder>;\n\n\t/** Shell command executed during the build phase. */\n\tbuildCommand?: pulumi.Input<string>;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: pulumi.Input<string>;\n\n\t/** Restart behavior for the service container. */\n\trestartPolicyType?: pulumi.Input<RailwayRestartPolicy>;\n\n\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\thealthcheckPath?: pulumi.Input<string>;\n\n\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\thealthcheckTimeout?: pulumi.Input<number>;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway service with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * const service = new RailwayService(\"api\", {\n * name: \"api\",\n * builder: RailwayBuilder.RAILPACK,\n * startCommand: \"node dist/index.js\",\n * healthcheckPath: \"/health\",\n * }, { provider, project, environment });\n *\n * // Use serviceId downstream\n * new RailwayVariable(\"api-vars\", {\n * variables: { DATABASE_URL: dbUrl },\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayService extends pulumi.ComponentResource {\n\t/** Railway service UUID. */\n\tpublic readonly id: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayServiceArgs,\n\t\topts: RailwayServiceOptions,\n\t) {\n\t\tconst { provider, project, environment, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Service\", name, {}, pulumiOpts);\n\n\t\tconst resource = new RailwayServiceResource(\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\tenvironmentId: environment.id,\n\t\t\t\t...args,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.id = resource.serviceId;\n\n\t\tthis.registerOutputs({ id: this.id });\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/service.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\";\n\n/**\n * Railway build system. Enum keys are UPPERCASE per convention; values are\n * Railway's required UPPERCASE wire literals.\n * Note: HEROKU and PAKETO were deprecated Feb 21 2025 and auto-migrated to\n * NIXPACKS by Railway, but remain in the schema and are accepted by the API.\n */\nexport enum RailwayBuilder {\n\tRAILPACK = \"RAILPACK\",\n\tNIXPACKS = \"NIXPACKS\",\n\tDOCKERFILE = \"DOCKERFILE\",\n\tHEROKU = \"HEROKU\",\n\tPAKETO = \"PAKETO\",\n}\n\n/**\n * Railway service restart policy. Controls when Railway restarts the service\n * container after it exits. Default is ON_FAILURE.\n */\nexport enum RailwayRestartPolicy {\n\tON_FAILURE = \"ON_FAILURE\",\n\tALWAYS = \"ALWAYS\",\n\tNEVER = \"NEVER\",\n}\n\n/** Docker image source for a Railway service (e.g. `redis:8-alpine`). */\ninterface RailwayServiceSource {\n\t/** Full Docker image reference including tag. */\n\timage: string;\n}\n\n/** Resolved inputs for the Railway service dynamic provider. */\ninterface RailwayServiceInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Human-readable service name used for adopt-or-create matching. */\n\tname: string;\n\n\t/** SVG icon URL displayed in the Railway dashboard. */\n\ticon?: string;\n\n\t/** Docker image source for image-based services. */\n\tsource?: RailwayServiceSource;\n\n\t/** Build system to use when building the service. */\n\tbuilder?: RailwayBuilder;\n\n\t/** Shell command executed during the build phase. */\n\tbuildCommand?: string;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: string;\n\n\t/** Restart behavior for the service container. */\n\trestartPolicyType?: RailwayRestartPolicy;\n\n\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\thealthcheckPath?: string;\n\n\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\thealthcheckTimeout?: number;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: string;\n}\n\n/** Persisted state for the Railway service, extending inputs with the Railway-assigned ID. */\ninterface RailwayServiceOutputs extends RailwayServiceInputs {\n\t/** Railway-assigned service UUID. */\n\tserviceId: string;\n}\n\nconst SERVICES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n services {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n`;\n\nconst SERVICE_QUERY = `\n query($serviceId: String!) {\n service(id: $serviceId) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_CREATE = `\n mutation($input: ServiceCreateInput!) {\n serviceCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_UPDATE = `\n mutation($id: String!, $input: ServiceUpdateInput!) {\n serviceUpdate(id: $id, input: $input) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_INSTANCE_UPDATE = `\n mutation(\n $serviceId: String!\n $environmentId: String!\n $input: ServiceInstanceUpdateInput!\n ) {\n serviceInstanceUpdate(\n serviceId: $serviceId\n environmentId: $environmentId\n input: $input\n )\n }\n`;\n\nconst SERVICE_CONNECT = `\n mutation($id: String!, $input: ServiceConnectInput!) {\n serviceConnect(id: $id, input: $input) {\n id\n }\n }\n`;\n\nconst SERVICE_INSTANCE_DEPLOY = `\n mutation($serviceId: String!, $environmentId: String!) {\n serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)\n }\n`;\n\n/**\n * Triggers a deployment of the service instance in the target environment.\n * Image-sourced services never deploy there on their own: `serviceCreate`'s\n * auto-deploy only reaches the project's DEFAULT environment, and\n * `serviceInstanceUpdate` applies config without redeploying — so a service\n * in any other environment stays undeployed forever and its private DNS name\n * never registers. `environmentTriggersDeploy` is no alternative: it returns\n * success without creating anything for a service that has never deployed in\n * that environment (proven live, 2026-07-06).\n */\nasync function deployServiceInstance(\n\tclient: RailwayClient,\n\tserviceId: string,\n\tenvironmentId: string,\n): Promise<void> {\n\tconst result = await client.query<{ serviceInstanceDeployV2: string }>(\n\t\tSERVICE_INSTANCE_DEPLOY,\n\t\t{ serviceId, environmentId },\n\t);\n\n\tpulumi.log.info(\n\t\t`[infracraft] serviceInstanceDeployV2 created deployment ${result.serviceInstanceDeployV2}`,\n\t);\n}\n\n/**\n * Applies service instance configuration (builder, commands, healthcheck).\n * Retries without healthcheck fields if the first call fails.\n */\nasync function applyInstanceConfig(\n\tclient: RailwayClient,\n\tserviceId: string,\n\tenvironmentId: string,\n\tinputs: RailwayServiceInputs,\n): Promise<void> {\n\tconst instanceInput: Record<string, unknown> = {};\n\n\t// Source must be applied PER ENVIRONMENT: `ServiceCreateInput.source` only\n\t// configures the instance of the environment passed at create time, and every\n\t// other environment's instance is born with source=null — deploy triggers\n\t// then no-op silently because there is nothing to deploy.\n\tif (inputs.source) {\n\t\tinstanceInput.source = { image: inputs.source.image };\n\t}\n\n\tif (inputs.builder) {\n\t\tinstanceInput.builder = inputs.builder;\n\t}\n\n\tif (inputs.buildCommand) {\n\t\tinstanceInput.buildCommand = inputs.buildCommand;\n\t}\n\n\tif (inputs.startCommand) {\n\t\tinstanceInput.startCommand = inputs.startCommand;\n\t}\n\n\tif (inputs.restartPolicyType) {\n\t\tinstanceInput.restartPolicyType = inputs.restartPolicyType;\n\t}\n\n\tif (inputs.healthcheckPath) {\n\t\tinstanceInput.healthcheckPath = inputs.healthcheckPath;\n\t}\n\n\tif (inputs.healthcheckTimeout) {\n\t\tinstanceInput.healthcheckTimeout = inputs.healthcheckTimeout;\n\t}\n\n\tif (inputs.preDeployCommand) {\n\t\tinstanceInput.preDeployCommand = inputs.preDeployCommand;\n\t}\n\n\tif (Object.keys(instanceInput).length === 0) {\n\t\treturn;\n\t}\n\n\ttry {\n\t\tawait client.query(SERVICE_INSTANCE_UPDATE, {\n\t\t\tserviceId,\n\t\t\tenvironmentId,\n\t\t\tinput: instanceInput,\n\t\t});\n\t} catch (error) {\n\t\tpulumi.log.warn(\n\t\t\t`serviceInstanceUpdate failed, retrying without healthcheck fields: ${error}`,\n\t\t);\n\n\t\tdelete instanceInput.healthcheckPath;\n\t\tdelete instanceInput.healthcheckTimeout;\n\n\t\tif (Object.keys(instanceInput).length > 0) {\n\t\t\tawait client.query(SERVICE_INSTANCE_UPDATE, {\n\t\t\t\tserviceId,\n\t\t\t\tenvironmentId,\n\t\t\t\tinput: instanceInput,\n\t\t\t});\n\t\t}\n\t}\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway services.\n *\n * Uses adopt-or-create on `create()`: queries services by project ID and name\n * before creating a new one. Service instance configuration (builder, commands,\n * healthcheck) is applied via `serviceInstanceUpdate` after create or update.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class RailwayServiceResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\t/**\n\t * Creates or adopts a Railway service by name, then applies instance config.\n\t */\n\tasync create(\n\t\tinputs: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst result = await client.query<{\n\t\t\tproject: {\n\t\t\t\tservices: { edges: Array<{ node: { id: string; name: string } }> };\n\t\t\t};\n\t\t}>(SERVICES_QUERY, { projectId: inputs.projectId });\n\n\t\tlet serviceId = result.project.services.edges.find(\n\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t)?.node.id;\n\n\t\tif (serviceId) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopted existing Railway service \"${inputs.name}\" (${serviceId})`,\n\t\t\t);\n\t\t} else {\n\t\t\tconst createInput: Record<string, unknown> = {\n\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\tname: inputs.name,\n\t\t\t};\n\n\t\t\tif (inputs.source) {\n\t\t\t\tcreateInput.source = { image: inputs.source.image };\n\t\t\t}\n\n\t\t\tconst created = await client.query<{\n\t\t\t\tserviceCreate: { id: string; name: string };\n\t\t\t}>(SERVICE_CREATE, { input: createInput });\n\n\t\t\tserviceId = created.serviceCreate.id;\n\n\t\t\tpulumi.log.info(\n\t\t\t\t`Created Railway service \"${inputs.name}\" (${serviceId})`,\n\t\t\t);\n\n\t\t\tif (inputs.source) {\n\t\t\t\tawait client.query(SERVICE_CONNECT, {\n\t\t\t\t\tid: serviceId,\n\t\t\t\t\tinput: { image: inputs.source.image },\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (inputs.icon) {\n\t\t\t\tawait client.query(SERVICE_UPDATE, {\n\t\t\t\t\tid: serviceId,\n\t\t\t\t\tinput: { icon: inputs.icon },\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tawait applyInstanceConfig(client, serviceId, inputs.environmentId, inputs);\n\n\t\t// Image services have no `railway up` step (see RailwayDeploy for code\n\t\t// services) — the provider owns their deploy.\n\t\tif (inputs.source) {\n\t\t\tawait deployServiceInstance(client, serviceId, inputs.environmentId);\n\t\t}\n\n\t\tconst outs: RailwayServiceOutputs = { ...inputs, serviceId };\n\n\t\treturn { id: serviceId, outs };\n\t}\n\n\t/**\n\t * Updates service name/icon and re-applies instance configuration.\n\t */\n\tasync update(\n\t\tid: string,\n\t\tolds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst client = new RailwayClient(news.token);\n\n\t\tconst updateInput: Record<string, unknown> = {};\n\n\t\tif (olds.name !== news.name) {\n\t\t\tupdateInput.name = news.name;\n\t\t}\n\n\t\tif (news.icon && olds.icon !== news.icon) {\n\t\t\tupdateInput.icon = news.icon;\n\t\t}\n\n\t\tif (Object.keys(updateInput).length > 0) {\n\t\t\tawait client.query(SERVICE_UPDATE, { id, input: updateInput });\n\t\t}\n\n\t\tawait applyInstanceConfig(client, id, news.environmentId, news);\n\n\t\t// Instance config changes (source, startCommand, …) only take effect on\n\t\t// the next deployment; image services get none unless the provider\n\t\t// triggers it.\n\t\tif (news.source) {\n\t\t\tawait deployServiceInstance(client, id, news.environmentId);\n\t\t}\n\n\t\tconst outs: RailwayServiceOutputs = { ...news, serviceId: id };\n\n\t\treturn { outs };\n\t}\n\n\t/**\n\t * Reads current state for `pulumi refresh` by querying the service by ID.\n\t */\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayServiceOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query<{ service: { id: string; name: string } }>(\n\t\t\t\tSERVICE_QUERY,\n\t\t\t\t{ serviceId: id },\n\t\t\t);\n\t\t} catch {\n\t\t\tthrow new Error(`Railway service \"${props.name}\" (${id}) not found`);\n\t\t}\n\n\t\treturn { id, props: { ...props, serviceId: id } };\n\t}\n\n\t/**\n\t * Deletion is a no-op. A Railway service is a project-level resource shared\n\t * across environments (forked environments adopt it by name), so a single\n\t * environment's destroy must never delete it. Deleting the *environment*\n\t * removes that environment's service instances instead.\n\t */\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Railway service deletion skipped — services are project-level; delete the environment to remove its instances\",\n\t\t);\n\t}\n\n\t/**\n\t * Compares old and new inputs to determine what changed.\n\t *\n\t * ProjectId and environmentId changes trigger replacement.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\t\tconst changes: string[] = [];\n\n\t\tif (olds.name !== news.name) {\n\t\t\tchanges.push(\"name\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\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.builder !== news.builder) {\n\t\t\tchanges.push(\"builder\");\n\t\t}\n\n\t\tif (olds.buildCommand !== news.buildCommand) {\n\t\t\tchanges.push(\"buildCommand\");\n\t\t}\n\n\t\tif (olds.startCommand !== news.startCommand) {\n\t\t\tchanges.push(\"startCommand\");\n\t\t}\n\n\t\tif (olds.restartPolicyType !== news.restartPolicyType) {\n\t\t\tchanges.push(\"restartPolicyType\");\n\t\t}\n\n\t\tif (olds.healthcheckPath !== news.healthcheckPath) {\n\t\t\tchanges.push(\"healthcheckPath\");\n\t\t}\n\n\t\tif (olds.healthcheckTimeout !== news.healthcheckTimeout) {\n\t\t\tchanges.push(\"healthcheckTimeout\");\n\t\t}\n\n\t\tif (olds.preDeployCommand !== news.preDeployCommand) {\n\t\t\tchanges.push(\"preDeployCommand\");\n\t\t}\n\n\t\tif (olds.icon !== news.icon) {\n\t\t\tchanges.push(\"icon\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0 || changes.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 RailwayServiceResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly serviceId: pulumi.Output<string>;\n\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\tenvironmentId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\ticon?: pulumi.Input<string>;\n\t\t\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\t\t\tbuilder?: pulumi.Input<RailwayBuilder>;\n\t\t\tbuildCommand?: pulumi.Input<string>;\n\t\t\tstartCommand?: pulumi.Input<string>;\n\t\t\trestartPolicyType?: pulumi.Input<RailwayRestartPolicy>;\n\t\t\thealthcheckPath?: pulumi.Input<string>;\n\t\t\thealthcheckTimeout?: pulumi.Input<number>;\n\t\t\tpreDeployCommand?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayServiceResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, serviceId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayService — replaces Pulumi's native `provider` field. */\ntype RailwayServiceOptions = 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\n/** Args for RailwayService. */\nexport interface RailwayServiceArgs {\n\t/** Human-readable service name used for adopt-or-create matching. */\n\tname: pulumi.Input<string>;\n\n\t/** SVG icon URL displayed in the Railway dashboard. */\n\ticon?: pulumi.Input<string>;\n\n\t/** Docker image source for image-based services. */\n\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\n\t/** Build system to use when building the service. */\n\tbuilder?: pulumi.Input<RailwayBuilder>;\n\n\t/** Shell command executed during the build phase. */\n\tbuildCommand?: pulumi.Input<string>;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: pulumi.Input<string>;\n\n\t/** Restart behavior for the service container. */\n\trestartPolicyType?: pulumi.Input<RailwayRestartPolicy>;\n\n\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\thealthcheckPath?: pulumi.Input<string>;\n\n\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\thealthcheckTimeout?: pulumi.Input<number>;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway service with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * const service = new RailwayService(\"api\", {\n * name: \"api\",\n * builder: RailwayBuilder.RAILPACK,\n * startCommand: \"node dist/index.js\",\n * healthcheckPath: \"/health\",\n * }, { provider, project, environment });\n *\n * // Use serviceId downstream\n * new RailwayVariable(\"api-vars\", {\n * variables: { DATABASE_URL: dbUrl },\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayService extends pulumi.ComponentResource {\n\t/** Railway service UUID. */\n\tpublic readonly id: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayServiceArgs,\n\t\topts: RailwayServiceOptions,\n\t) {\n\t\tconst { provider, project, environment, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Service\", name, {}, pulumiOpts);\n\n\t\tconst resource = new RailwayServiceResource(\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\tenvironmentId: environment.id,\n\t\t\t\t...args,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.id = resource.serviceId;\n\n\t\tthis.registerOutputs({ id: this.id });\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.cts","names":[],"sources":["../../src/railway/service.ts"],"mappings":";;;;;;;;;;AAYA;;;aAAY,cAAA;EACX,QAAA;EACA,QAAA;EACA,UAAA;EACA,MAAA;EACA,MAAA;AAAA;AAAM;AAOP;;;AAPO,aAOK,oBAAA;EACX,UAAA;EACA,MAAA;EACA,KAAA;AAAA;AAAK;AAAA,UAII,oBAAA;EAAoB;EAE7B,KAAK;AAAA;AAAA;AAAA,UAII,oBAAA;EAAoB;EAE7B,KAAA;EAeS;EAZT,SAAA;EAwBoB;EArBpB,aAAA;EAqBwC;EAlBxC,IAAA;EANA;EASA,IAAA;EAHA;EAMA,MAAA,GAAS,oBAAA;EAAT;EAGA,OAAA,GAAU,cAAA;EAAV;EAGA,YAAA;EAAA;EAGA,YAAA;EAGA;EAAA,iBAAA,GAAoB,oBAAA;EAGpB;EAAA,eAAA;EAMA;EAHA,kBAAA;EAGgB;EAAhB,gBAAA;AAAA;;UAIS,qBAAA,SAA8B,oBAAoB;EAElD;EAAT,SAAS;AAAA;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"service.d.cts","names":[],"sources":["../../src/railway/service.ts"],"mappings":";;;;;;;;;;AAYA;;;aAAY,cAAA;EACX,QAAA;EACA,QAAA;EACA,UAAA;EACA,MAAA;EACA,MAAA;AAAA;AAAM;AAOP;;;AAPO,aAOK,oBAAA;EACX,UAAA;EACA,MAAA;EACA,KAAA;AAAA;AAAK;AAAA,UAII,oBAAA;EAAoB;EAE7B,KAAK;AAAA;AAAA;AAAA,UAII,oBAAA;EAAoB;EAE7B,KAAA;EAeS;EAZT,SAAA;EAwBoB;EArBpB,aAAA;EAqBwC;EAlBxC,IAAA;EANA;EASA,IAAA;EAHA;EAMA,MAAA,GAAS,oBAAA;EAAT;EAGA,OAAA,GAAU,cAAA;EAAV;EAGA,YAAA;EAAA;EAGA,YAAA;EAGA;EAAA,iBAAA,GAAoB,oBAAA;EAGpB;EAAA,eAAA;EAMA;EAHA,kBAAA;EAGgB;EAAhB,gBAAA;AAAA;;UAIS,qBAAA,SAA8B,oBAAoB;EAElD;EAAT,SAAS;AAAA;;;;;;;;;;cAuLG,8BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAqHvB;;;EAhHG,MAAA,CACL,MAAA,EAAQ,oBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAkJf;;;EA7EL,MAAA,CACL,EAAA,UACA,IAAA,EAAM,qBAAA,EACN,IAAA,EAAM,oBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAhFgB;;;EAkHpC,IAAA,CACL,EAAA,UACA,KAAA,EAAO,qBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAhHpB;;;;;;EAqIA,MAAA,CAAA,GAAU,OAAA;EA9DV;;;;;EAyEA,IAAA,CACL,GAAA,UACA,IAAA,EAAM,qBAAA,EACN,IAAA,EAAM,oBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAyFtB,qBAAA,GAAwB,IAAA,CAC5B,MAAA,CAAO,wBAAA;EAnKmB,sCAuK1B,QAAA,EAAU,eAAA,EApIT;EAuID,OAAA,EAAS,cAAA,EAtIR;EAyID,WAAA,EAAa,kBAAA;AAAA;;UAIG,kBAAA;EAvHV;EAyHN,IAAA,EAAM,MAAA,CAAO,KAAA;EA9GP;EAiHN,IAAA,GAAO,MAAA,CAAO,KAAA;EA/GP;EAkHP,MAAA,GAAS,MAAA,CAAO,KAAA;IAAQ,KAAA,EAAO,MAAA,CAAO,KAAA;EAAA;EAhHnC;EAmHH,OAAA,GAAU,MAAA,CAAO,KAAA,CAAM,cAAA;EAnHL;EAsHlB,YAAA,GAAe,MAAA,CAAO,KAAA;EAtHc;EAyHpC,YAAA,GAAe,MAAA,CAAO,KAAA;EAhClB;EAmCJ,iBAAA,GAAoB,MAAA,CAAO,KAAA,CAAM,oBAAA;;EAGjC,eAAA,GAAkB,MAAA,CAAO,KAAA;EAtCG;EAyC5B,kBAAA,GAAqB,MAAA,CAAO,KAAA;EAjCnB;EAoCT,gBAAA,GAAmB,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;AAjCK;AAIhC;;;;;;cAkDa,cAAA,SAAuB,MAAA,CAAO,iBAAA;EAvCnB;EAAA,SAyCP,EAAA,EAAI,MAAA,CAAO,MAAA;cAG1B,IAAA,UACA,IAAA,EAAM,kBAAA,EACN,IAAA,EAAM,qBAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.mts","names":[],"sources":["../../src/railway/service.ts"],"mappings":";;;;;;;;;;AAYA;;;aAAY,cAAA;EACX,QAAA;EACA,QAAA;EACA,UAAA;EACA,MAAA;EACA,MAAA;AAAA;AAAM;AAOP;;;AAPO,aAOK,oBAAA;EACX,UAAA;EACA,MAAA;EACA,KAAA;AAAA;AAAK;AAAA,UAII,oBAAA;EAAoB;EAE7B,KAAK;AAAA;AAAA;AAAA,UAII,oBAAA;EAAoB;EAE7B,KAAA;EAeS;EAZT,SAAA;EAwBoB;EArBpB,aAAA;EAqBwC;EAlBxC,IAAA;EANA;EASA,IAAA;EAHA;EAMA,MAAA,GAAS,oBAAA;EAAT;EAGA,OAAA,GAAU,cAAA;EAAV;EAGA,YAAA;EAAA;EAGA,YAAA;EAGA;EAAA,iBAAA,GAAoB,oBAAA;EAGpB;EAAA,eAAA;EAMA;EAHA,kBAAA;EAGgB;EAAhB,gBAAA;AAAA;;UAIS,qBAAA,SAA8B,oBAAoB;EAElD;EAAT,SAAS;AAAA;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"service.d.mts","names":[],"sources":["../../src/railway/service.ts"],"mappings":";;;;;;;;;;AAYA;;;aAAY,cAAA;EACX,QAAA;EACA,QAAA;EACA,UAAA;EACA,MAAA;EACA,MAAA;AAAA;AAAM;AAOP;;;AAPO,aAOK,oBAAA;EACX,UAAA;EACA,MAAA;EACA,KAAA;AAAA;AAAK;AAAA,UAII,oBAAA;EAAoB;EAE7B,KAAK;AAAA;AAAA;AAAA,UAII,oBAAA;EAAoB;EAE7B,KAAA;EAeS;EAZT,SAAA;EAwBoB;EArBpB,aAAA;EAqBwC;EAlBxC,IAAA;EANA;EASA,IAAA;EAHA;EAMA,MAAA,GAAS,oBAAA;EAAT;EAGA,OAAA,GAAU,cAAA;EAAV;EAGA,YAAA;EAAA;EAGA,YAAA;EAGA;EAAA,iBAAA,GAAoB,oBAAA;EAGpB;EAAA,eAAA;EAMA;EAHA,kBAAA;EAGgB;EAAhB,gBAAA;AAAA;;UAIS,qBAAA,SAA8B,oBAAoB;EAElD;EAAT,SAAS;AAAA;;;;;;;;;;cAuLG,8BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAqHvB;;;EAhHG,MAAA,CACL,MAAA,EAAQ,oBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAkJf;;;EA7EL,MAAA,CACL,EAAA,UACA,IAAA,EAAM,qBAAA,EACN,IAAA,EAAM,oBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAhFgB;;;EAkHpC,IAAA,CACL,EAAA,UACA,KAAA,EAAO,qBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EAhHpB;;;;;;EAqIA,MAAA,CAAA,GAAU,OAAA;EA9DV;;;;;EAyEA,IAAA,CACL,GAAA,UACA,IAAA,EAAM,qBAAA,EACN,IAAA,EAAM,oBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAyFtB,qBAAA,GAAwB,IAAA,CAC5B,MAAA,CAAO,wBAAA;EAnKmB,sCAuK1B,QAAA,EAAU,eAAA,EApIT;EAuID,OAAA,EAAS,cAAA,EAtIR;EAyID,WAAA,EAAa,kBAAA;AAAA;;UAIG,kBAAA;EAvHV;EAyHN,IAAA,EAAM,MAAA,CAAO,KAAA;EA9GP;EAiHN,IAAA,GAAO,MAAA,CAAO,KAAA;EA/GP;EAkHP,MAAA,GAAS,MAAA,CAAO,KAAA;IAAQ,KAAA,EAAO,MAAA,CAAO,KAAA;EAAA;EAhHnC;EAmHH,OAAA,GAAU,MAAA,CAAO,KAAA,CAAM,cAAA;EAnHL;EAsHlB,YAAA,GAAe,MAAA,CAAO,KAAA;EAtHc;EAyHpC,YAAA,GAAe,MAAA,CAAO,KAAA;EAhClB;EAmCJ,iBAAA,GAAoB,MAAA,CAAO,KAAA,CAAM,oBAAA;;EAGjC,eAAA,GAAkB,MAAA,CAAO,KAAA;EAtCG;EAyC5B,kBAAA,GAAqB,MAAA,CAAO,KAAA;EAjCnB;EAoCT,gBAAA,GAAmB,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;AAjCK;AAIhC;;;;;;cAkDa,cAAA,SAAuB,MAAA,CAAO,iBAAA;EAvCnB;EAAA,SAyCP,EAAA,EAAI,MAAA,CAAO,MAAA;cAG1B,IAAA,UACA,IAAA,EAAM,kBAAA,EACN,IAAA,EAAM,qBAAA;AAAA"}
|
package/dist/railway/service.mjs
CHANGED
|
@@ -85,12 +85,35 @@ const SERVICE_CONNECT = `
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
`;
|
|
88
|
+
const SERVICE_INSTANCE_DEPLOY = `
|
|
89
|
+
mutation($serviceId: String!, $environmentId: String!) {
|
|
90
|
+
serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)
|
|
91
|
+
}
|
|
92
|
+
`;
|
|
93
|
+
/**
|
|
94
|
+
* Triggers a deployment of the service instance in the target environment.
|
|
95
|
+
* Image-sourced services never deploy there on their own: `serviceCreate`'s
|
|
96
|
+
* auto-deploy only reaches the project's DEFAULT environment, and
|
|
97
|
+
* `serviceInstanceUpdate` applies config without redeploying — so a service
|
|
98
|
+
* in any other environment stays undeployed forever and its private DNS name
|
|
99
|
+
* never registers. `environmentTriggersDeploy` is no alternative: it returns
|
|
100
|
+
* success without creating anything for a service that has never deployed in
|
|
101
|
+
* that environment (proven live, 2026-07-06).
|
|
102
|
+
*/
|
|
103
|
+
async function deployServiceInstance(client, serviceId, environmentId) {
|
|
104
|
+
const result = await client.query(SERVICE_INSTANCE_DEPLOY, {
|
|
105
|
+
serviceId,
|
|
106
|
+
environmentId
|
|
107
|
+
});
|
|
108
|
+
pulumi.log.info(`[infracraft] serviceInstanceDeployV2 created deployment ${result.serviceInstanceDeployV2}`);
|
|
109
|
+
}
|
|
88
110
|
/**
|
|
89
111
|
* Applies service instance configuration (builder, commands, healthcheck).
|
|
90
112
|
* Retries without healthcheck fields if the first call fails.
|
|
91
113
|
*/
|
|
92
114
|
async function applyInstanceConfig(client, serviceId, environmentId, inputs) {
|
|
93
115
|
const instanceInput = {};
|
|
116
|
+
if (inputs.source) instanceInput.source = { image: inputs.source.image };
|
|
94
117
|
if (inputs.builder) instanceInput.builder = inputs.builder;
|
|
95
118
|
if (inputs.buildCommand) instanceInput.buildCommand = inputs.buildCommand;
|
|
96
119
|
if (inputs.startCommand) instanceInput.startCommand = inputs.startCommand;
|
|
@@ -152,6 +175,7 @@ var RailwayServiceResourceProvider = class {
|
|
|
152
175
|
});
|
|
153
176
|
}
|
|
154
177
|
await applyInstanceConfig(client, serviceId, inputs.environmentId, inputs);
|
|
178
|
+
if (inputs.source) await deployServiceInstance(client, serviceId, inputs.environmentId);
|
|
155
179
|
const outs = {
|
|
156
180
|
...inputs,
|
|
157
181
|
serviceId
|
|
@@ -174,6 +198,7 @@ var RailwayServiceResourceProvider = class {
|
|
|
174
198
|
input: updateInput
|
|
175
199
|
});
|
|
176
200
|
await applyInstanceConfig(client, id, news.environmentId, news);
|
|
201
|
+
if (news.source) await deployServiceInstance(client, id, news.environmentId);
|
|
177
202
|
return { outs: {
|
|
178
203
|
...news,
|
|
179
204
|
serviceId: id
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.mjs","names":[],"sources":["../../src/railway/service.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\";\n\n/**\n * Railway build system. Enum keys are UPPERCASE per convention; values are\n * Railway's required UPPERCASE wire literals.\n * Note: HEROKU and PAKETO were deprecated Feb 21 2025 and auto-migrated to\n * NIXPACKS by Railway, but remain in the schema and are accepted by the API.\n */\nexport enum RailwayBuilder {\n\tRAILPACK = \"RAILPACK\",\n\tNIXPACKS = \"NIXPACKS\",\n\tDOCKERFILE = \"DOCKERFILE\",\n\tHEROKU = \"HEROKU\",\n\tPAKETO = \"PAKETO\",\n}\n\n/**\n * Railway service restart policy. Controls when Railway restarts the service\n * container after it exits. Default is ON_FAILURE.\n */\nexport enum RailwayRestartPolicy {\n\tON_FAILURE = \"ON_FAILURE\",\n\tALWAYS = \"ALWAYS\",\n\tNEVER = \"NEVER\",\n}\n\n/** Docker image source for a Railway service (e.g. `redis:8-alpine`). */\ninterface RailwayServiceSource {\n\t/** Full Docker image reference including tag. */\n\timage: string;\n}\n\n/** Resolved inputs for the Railway service dynamic provider. */\ninterface RailwayServiceInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Human-readable service name used for adopt-or-create matching. */\n\tname: string;\n\n\t/** SVG icon URL displayed in the Railway dashboard. */\n\ticon?: string;\n\n\t/** Docker image source for image-based services. */\n\tsource?: RailwayServiceSource;\n\n\t/** Build system to use when building the service. */\n\tbuilder?: RailwayBuilder;\n\n\t/** Shell command executed during the build phase. */\n\tbuildCommand?: string;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: string;\n\n\t/** Restart behavior for the service container. */\n\trestartPolicyType?: RailwayRestartPolicy;\n\n\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\thealthcheckPath?: string;\n\n\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\thealthcheckTimeout?: number;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: string;\n}\n\n/** Persisted state for the Railway service, extending inputs with the Railway-assigned ID. */\ninterface RailwayServiceOutputs extends RailwayServiceInputs {\n\t/** Railway-assigned service UUID. */\n\tserviceId: string;\n}\n\nconst SERVICES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n services {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n`;\n\nconst SERVICE_QUERY = `\n query($serviceId: String!) {\n service(id: $serviceId) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_CREATE = `\n mutation($input: ServiceCreateInput!) {\n serviceCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_UPDATE = `\n mutation($id: String!, $input: ServiceUpdateInput!) {\n serviceUpdate(id: $id, input: $input) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_INSTANCE_UPDATE = `\n mutation(\n $serviceId: String!\n $environmentId: String!\n $input: ServiceInstanceUpdateInput!\n ) {\n serviceInstanceUpdate(\n serviceId: $serviceId\n environmentId: $environmentId\n input: $input\n )\n }\n`;\n\nconst SERVICE_CONNECT = `\n mutation($id: String!, $input: ServiceConnectInput!) {\n serviceConnect(id: $id, input: $input) {\n id\n }\n }\n`;\n\n/**\n * Applies service instance configuration (builder, commands, healthcheck).\n * Retries without healthcheck fields if the first call fails.\n */\nasync function applyInstanceConfig(\n\tclient: RailwayClient,\n\tserviceId: string,\n\tenvironmentId: string,\n\tinputs: RailwayServiceInputs,\n): Promise<void> {\n\tconst instanceInput: Record<string, unknown> = {};\n\n\tif (inputs.builder) {\n\t\tinstanceInput.builder = inputs.builder;\n\t}\n\n\tif (inputs.buildCommand) {\n\t\tinstanceInput.buildCommand = inputs.buildCommand;\n\t}\n\n\tif (inputs.startCommand) {\n\t\tinstanceInput.startCommand = inputs.startCommand;\n\t}\n\n\tif (inputs.restartPolicyType) {\n\t\tinstanceInput.restartPolicyType = inputs.restartPolicyType;\n\t}\n\n\tif (inputs.healthcheckPath) {\n\t\tinstanceInput.healthcheckPath = inputs.healthcheckPath;\n\t}\n\n\tif (inputs.healthcheckTimeout) {\n\t\tinstanceInput.healthcheckTimeout = inputs.healthcheckTimeout;\n\t}\n\n\tif (inputs.preDeployCommand) {\n\t\tinstanceInput.preDeployCommand = inputs.preDeployCommand;\n\t}\n\n\tif (Object.keys(instanceInput).length === 0) {\n\t\treturn;\n\t}\n\n\ttry {\n\t\tawait client.query(SERVICE_INSTANCE_UPDATE, {\n\t\t\tserviceId,\n\t\t\tenvironmentId,\n\t\t\tinput: instanceInput,\n\t\t});\n\t} catch (error) {\n\t\tpulumi.log.warn(\n\t\t\t`serviceInstanceUpdate failed, retrying without healthcheck fields: ${error}`,\n\t\t);\n\n\t\tdelete instanceInput.healthcheckPath;\n\t\tdelete instanceInput.healthcheckTimeout;\n\n\t\tif (Object.keys(instanceInput).length > 0) {\n\t\t\tawait client.query(SERVICE_INSTANCE_UPDATE, {\n\t\t\t\tserviceId,\n\t\t\t\tenvironmentId,\n\t\t\t\tinput: instanceInput,\n\t\t\t});\n\t\t}\n\t}\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway services.\n *\n * Uses adopt-or-create on `create()`: queries services by project ID and name\n * before creating a new one. Service instance configuration (builder, commands,\n * healthcheck) is applied via `serviceInstanceUpdate` after create or update.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class RailwayServiceResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\t/**\n\t * Creates or adopts a Railway service by name, then applies instance config.\n\t */\n\tasync create(\n\t\tinputs: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst result = await client.query<{\n\t\t\tproject: {\n\t\t\t\tservices: { edges: Array<{ node: { id: string; name: string } }> };\n\t\t\t};\n\t\t}>(SERVICES_QUERY, { projectId: inputs.projectId });\n\n\t\tlet serviceId = result.project.services.edges.find(\n\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t)?.node.id;\n\n\t\tif (serviceId) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopted existing Railway service \"${inputs.name}\" (${serviceId})`,\n\t\t\t);\n\t\t} else {\n\t\t\tconst createInput: Record<string, unknown> = {\n\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\tname: inputs.name,\n\t\t\t};\n\n\t\t\tif (inputs.source) {\n\t\t\t\tcreateInput.source = { image: inputs.source.image };\n\t\t\t}\n\n\t\t\tconst created = await client.query<{\n\t\t\t\tserviceCreate: { id: string; name: string };\n\t\t\t}>(SERVICE_CREATE, { input: createInput });\n\n\t\t\tserviceId = created.serviceCreate.id;\n\n\t\t\tpulumi.log.info(\n\t\t\t\t`Created Railway service \"${inputs.name}\" (${serviceId})`,\n\t\t\t);\n\n\t\t\tif (inputs.source) {\n\t\t\t\tawait client.query(SERVICE_CONNECT, {\n\t\t\t\t\tid: serviceId,\n\t\t\t\t\tinput: { image: inputs.source.image },\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (inputs.icon) {\n\t\t\t\tawait client.query(SERVICE_UPDATE, {\n\t\t\t\t\tid: serviceId,\n\t\t\t\t\tinput: { icon: inputs.icon },\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tawait applyInstanceConfig(client, serviceId, inputs.environmentId, inputs);\n\n\t\tconst outs: RailwayServiceOutputs = { ...inputs, serviceId };\n\n\t\treturn { id: serviceId, outs };\n\t}\n\n\t/**\n\t * Updates service name/icon and re-applies instance configuration.\n\t */\n\tasync update(\n\t\tid: string,\n\t\tolds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst client = new RailwayClient(news.token);\n\n\t\tconst updateInput: Record<string, unknown> = {};\n\n\t\tif (olds.name !== news.name) {\n\t\t\tupdateInput.name = news.name;\n\t\t}\n\n\t\tif (news.icon && olds.icon !== news.icon) {\n\t\t\tupdateInput.icon = news.icon;\n\t\t}\n\n\t\tif (Object.keys(updateInput).length > 0) {\n\t\t\tawait client.query(SERVICE_UPDATE, { id, input: updateInput });\n\t\t}\n\n\t\tawait applyInstanceConfig(client, id, news.environmentId, news);\n\n\t\tconst outs: RailwayServiceOutputs = { ...news, serviceId: id };\n\n\t\treturn { outs };\n\t}\n\n\t/**\n\t * Reads current state for `pulumi refresh` by querying the service by ID.\n\t */\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayServiceOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query<{ service: { id: string; name: string } }>(\n\t\t\t\tSERVICE_QUERY,\n\t\t\t\t{ serviceId: id },\n\t\t\t);\n\t\t} catch {\n\t\t\tthrow new Error(`Railway service \"${props.name}\" (${id}) not found`);\n\t\t}\n\n\t\treturn { id, props: { ...props, serviceId: id } };\n\t}\n\n\t/**\n\t * Deletion is a no-op. A Railway service is a project-level resource shared\n\t * across environments (forked environments adopt it by name), so a single\n\t * environment's destroy must never delete it. Deleting the *environment*\n\t * removes that environment's service instances instead.\n\t */\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Railway service deletion skipped — services are project-level; delete the environment to remove its instances\",\n\t\t);\n\t}\n\n\t/**\n\t * Compares old and new inputs to determine what changed.\n\t *\n\t * ProjectId and environmentId changes trigger replacement.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\t\tconst changes: string[] = [];\n\n\t\tif (olds.name !== news.name) {\n\t\t\tchanges.push(\"name\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\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.builder !== news.builder) {\n\t\t\tchanges.push(\"builder\");\n\t\t}\n\n\t\tif (olds.buildCommand !== news.buildCommand) {\n\t\t\tchanges.push(\"buildCommand\");\n\t\t}\n\n\t\tif (olds.startCommand !== news.startCommand) {\n\t\t\tchanges.push(\"startCommand\");\n\t\t}\n\n\t\tif (olds.restartPolicyType !== news.restartPolicyType) {\n\t\t\tchanges.push(\"restartPolicyType\");\n\t\t}\n\n\t\tif (olds.healthcheckPath !== news.healthcheckPath) {\n\t\t\tchanges.push(\"healthcheckPath\");\n\t\t}\n\n\t\tif (olds.healthcheckTimeout !== news.healthcheckTimeout) {\n\t\t\tchanges.push(\"healthcheckTimeout\");\n\t\t}\n\n\t\tif (olds.preDeployCommand !== news.preDeployCommand) {\n\t\t\tchanges.push(\"preDeployCommand\");\n\t\t}\n\n\t\tif (olds.icon !== news.icon) {\n\t\t\tchanges.push(\"icon\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0 || changes.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 RailwayServiceResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly serviceId: pulumi.Output<string>;\n\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\tenvironmentId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\ticon?: pulumi.Input<string>;\n\t\t\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\t\t\tbuilder?: pulumi.Input<RailwayBuilder>;\n\t\t\tbuildCommand?: pulumi.Input<string>;\n\t\t\tstartCommand?: pulumi.Input<string>;\n\t\t\trestartPolicyType?: pulumi.Input<RailwayRestartPolicy>;\n\t\t\thealthcheckPath?: pulumi.Input<string>;\n\t\t\thealthcheckTimeout?: pulumi.Input<number>;\n\t\t\tpreDeployCommand?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayServiceResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, serviceId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayService — replaces Pulumi's native `provider` field. */\ntype RailwayServiceOptions = 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\n/** Args for RailwayService. */\nexport interface RailwayServiceArgs {\n\t/** Human-readable service name used for adopt-or-create matching. */\n\tname: pulumi.Input<string>;\n\n\t/** SVG icon URL displayed in the Railway dashboard. */\n\ticon?: pulumi.Input<string>;\n\n\t/** Docker image source for image-based services. */\n\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\n\t/** Build system to use when building the service. */\n\tbuilder?: pulumi.Input<RailwayBuilder>;\n\n\t/** Shell command executed during the build phase. */\n\tbuildCommand?: pulumi.Input<string>;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: pulumi.Input<string>;\n\n\t/** Restart behavior for the service container. */\n\trestartPolicyType?: pulumi.Input<RailwayRestartPolicy>;\n\n\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\thealthcheckPath?: pulumi.Input<string>;\n\n\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\thealthcheckTimeout?: pulumi.Input<number>;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway service with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * const service = new RailwayService(\"api\", {\n * name: \"api\",\n * builder: RailwayBuilder.RAILPACK,\n * startCommand: \"node dist/index.js\",\n * healthcheckPath: \"/health\",\n * }, { provider, project, environment });\n *\n * // Use serviceId downstream\n * new RailwayVariable(\"api-vars\", {\n * variables: { DATABASE_URL: dbUrl },\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayService extends pulumi.ComponentResource {\n\t/** Railway service UUID. */\n\tpublic readonly id: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayServiceArgs,\n\t\topts: RailwayServiceOptions,\n\t) {\n\t\tconst { provider, project, environment, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Service\", name, {}, pulumiOpts);\n\n\t\tconst resource = new RailwayServiceResource(\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\tenvironmentId: environment.id,\n\t\t\t\t...args,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.id = resource.serviceId;\n\n\t\tthis.registerOutputs({ id: this.id });\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.mjs","names":[],"sources":["../../src/railway/service.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\";\n\n/**\n * Railway build system. Enum keys are UPPERCASE per convention; values are\n * Railway's required UPPERCASE wire literals.\n * Note: HEROKU and PAKETO were deprecated Feb 21 2025 and auto-migrated to\n * NIXPACKS by Railway, but remain in the schema and are accepted by the API.\n */\nexport enum RailwayBuilder {\n\tRAILPACK = \"RAILPACK\",\n\tNIXPACKS = \"NIXPACKS\",\n\tDOCKERFILE = \"DOCKERFILE\",\n\tHEROKU = \"HEROKU\",\n\tPAKETO = \"PAKETO\",\n}\n\n/**\n * Railway service restart policy. Controls when Railway restarts the service\n * container after it exits. Default is ON_FAILURE.\n */\nexport enum RailwayRestartPolicy {\n\tON_FAILURE = \"ON_FAILURE\",\n\tALWAYS = \"ALWAYS\",\n\tNEVER = \"NEVER\",\n}\n\n/** Docker image source for a Railway service (e.g. `redis:8-alpine`). */\ninterface RailwayServiceSource {\n\t/** Full Docker image reference including tag. */\n\timage: string;\n}\n\n/** Resolved inputs for the Railway service dynamic provider. */\ninterface RailwayServiceInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Human-readable service name used for adopt-or-create matching. */\n\tname: string;\n\n\t/** SVG icon URL displayed in the Railway dashboard. */\n\ticon?: string;\n\n\t/** Docker image source for image-based services. */\n\tsource?: RailwayServiceSource;\n\n\t/** Build system to use when building the service. */\n\tbuilder?: RailwayBuilder;\n\n\t/** Shell command executed during the build phase. */\n\tbuildCommand?: string;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: string;\n\n\t/** Restart behavior for the service container. */\n\trestartPolicyType?: RailwayRestartPolicy;\n\n\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\thealthcheckPath?: string;\n\n\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\thealthcheckTimeout?: number;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: string;\n}\n\n/** Persisted state for the Railway service, extending inputs with the Railway-assigned ID. */\ninterface RailwayServiceOutputs extends RailwayServiceInputs {\n\t/** Railway-assigned service UUID. */\n\tserviceId: string;\n}\n\nconst SERVICES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n services {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n`;\n\nconst SERVICE_QUERY = `\n query($serviceId: String!) {\n service(id: $serviceId) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_CREATE = `\n mutation($input: ServiceCreateInput!) {\n serviceCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_UPDATE = `\n mutation($id: String!, $input: ServiceUpdateInput!) {\n serviceUpdate(id: $id, input: $input) {\n id\n name\n }\n }\n`;\n\nconst SERVICE_INSTANCE_UPDATE = `\n mutation(\n $serviceId: String!\n $environmentId: String!\n $input: ServiceInstanceUpdateInput!\n ) {\n serviceInstanceUpdate(\n serviceId: $serviceId\n environmentId: $environmentId\n input: $input\n )\n }\n`;\n\nconst SERVICE_CONNECT = `\n mutation($id: String!, $input: ServiceConnectInput!) {\n serviceConnect(id: $id, input: $input) {\n id\n }\n }\n`;\n\nconst SERVICE_INSTANCE_DEPLOY = `\n mutation($serviceId: String!, $environmentId: String!) {\n serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)\n }\n`;\n\n/**\n * Triggers a deployment of the service instance in the target environment.\n * Image-sourced services never deploy there on their own: `serviceCreate`'s\n * auto-deploy only reaches the project's DEFAULT environment, and\n * `serviceInstanceUpdate` applies config without redeploying — so a service\n * in any other environment stays undeployed forever and its private DNS name\n * never registers. `environmentTriggersDeploy` is no alternative: it returns\n * success without creating anything for a service that has never deployed in\n * that environment (proven live, 2026-07-06).\n */\nasync function deployServiceInstance(\n\tclient: RailwayClient,\n\tserviceId: string,\n\tenvironmentId: string,\n): Promise<void> {\n\tconst result = await client.query<{ serviceInstanceDeployV2: string }>(\n\t\tSERVICE_INSTANCE_DEPLOY,\n\t\t{ serviceId, environmentId },\n\t);\n\n\tpulumi.log.info(\n\t\t`[infracraft] serviceInstanceDeployV2 created deployment ${result.serviceInstanceDeployV2}`,\n\t);\n}\n\n/**\n * Applies service instance configuration (builder, commands, healthcheck).\n * Retries without healthcheck fields if the first call fails.\n */\nasync function applyInstanceConfig(\n\tclient: RailwayClient,\n\tserviceId: string,\n\tenvironmentId: string,\n\tinputs: RailwayServiceInputs,\n): Promise<void> {\n\tconst instanceInput: Record<string, unknown> = {};\n\n\t// Source must be applied PER ENVIRONMENT: `ServiceCreateInput.source` only\n\t// configures the instance of the environment passed at create time, and every\n\t// other environment's instance is born with source=null — deploy triggers\n\t// then no-op silently because there is nothing to deploy.\n\tif (inputs.source) {\n\t\tinstanceInput.source = { image: inputs.source.image };\n\t}\n\n\tif (inputs.builder) {\n\t\tinstanceInput.builder = inputs.builder;\n\t}\n\n\tif (inputs.buildCommand) {\n\t\tinstanceInput.buildCommand = inputs.buildCommand;\n\t}\n\n\tif (inputs.startCommand) {\n\t\tinstanceInput.startCommand = inputs.startCommand;\n\t}\n\n\tif (inputs.restartPolicyType) {\n\t\tinstanceInput.restartPolicyType = inputs.restartPolicyType;\n\t}\n\n\tif (inputs.healthcheckPath) {\n\t\tinstanceInput.healthcheckPath = inputs.healthcheckPath;\n\t}\n\n\tif (inputs.healthcheckTimeout) {\n\t\tinstanceInput.healthcheckTimeout = inputs.healthcheckTimeout;\n\t}\n\n\tif (inputs.preDeployCommand) {\n\t\tinstanceInput.preDeployCommand = inputs.preDeployCommand;\n\t}\n\n\tif (Object.keys(instanceInput).length === 0) {\n\t\treturn;\n\t}\n\n\ttry {\n\t\tawait client.query(SERVICE_INSTANCE_UPDATE, {\n\t\t\tserviceId,\n\t\t\tenvironmentId,\n\t\t\tinput: instanceInput,\n\t\t});\n\t} catch (error) {\n\t\tpulumi.log.warn(\n\t\t\t`serviceInstanceUpdate failed, retrying without healthcheck fields: ${error}`,\n\t\t);\n\n\t\tdelete instanceInput.healthcheckPath;\n\t\tdelete instanceInput.healthcheckTimeout;\n\n\t\tif (Object.keys(instanceInput).length > 0) {\n\t\t\tawait client.query(SERVICE_INSTANCE_UPDATE, {\n\t\t\t\tserviceId,\n\t\t\t\tenvironmentId,\n\t\t\t\tinput: instanceInput,\n\t\t\t});\n\t\t}\n\t}\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway services.\n *\n * Uses adopt-or-create on `create()`: queries services by project ID and name\n * before creating a new one. Service instance configuration (builder, commands,\n * healthcheck) is applied via `serviceInstanceUpdate` after create or update.\n *\n * @internal Exported only for unit testing; not part of the public API surface.\n */\nexport class RailwayServiceResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\n\t/**\n\t * Creates or adopts a Railway service by name, then applies instance config.\n\t */\n\tasync create(\n\t\tinputs: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tconst result = await client.query<{\n\t\t\tproject: {\n\t\t\t\tservices: { edges: Array<{ node: { id: string; name: string } }> };\n\t\t\t};\n\t\t}>(SERVICES_QUERY, { projectId: inputs.projectId });\n\n\t\tlet serviceId = result.project.services.edges.find(\n\t\t\t(edge) => edge.node.name === inputs.name,\n\t\t)?.node.id;\n\n\t\tif (serviceId) {\n\t\t\tpulumi.log.info(\n\t\t\t\t`Adopted existing Railway service \"${inputs.name}\" (${serviceId})`,\n\t\t\t);\n\t\t} else {\n\t\t\tconst createInput: Record<string, unknown> = {\n\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\tname: inputs.name,\n\t\t\t};\n\n\t\t\tif (inputs.source) {\n\t\t\t\tcreateInput.source = { image: inputs.source.image };\n\t\t\t}\n\n\t\t\tconst created = await client.query<{\n\t\t\t\tserviceCreate: { id: string; name: string };\n\t\t\t}>(SERVICE_CREATE, { input: createInput });\n\n\t\t\tserviceId = created.serviceCreate.id;\n\n\t\t\tpulumi.log.info(\n\t\t\t\t`Created Railway service \"${inputs.name}\" (${serviceId})`,\n\t\t\t);\n\n\t\t\tif (inputs.source) {\n\t\t\t\tawait client.query(SERVICE_CONNECT, {\n\t\t\t\t\tid: serviceId,\n\t\t\t\t\tinput: { image: inputs.source.image },\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (inputs.icon) {\n\t\t\t\tawait client.query(SERVICE_UPDATE, {\n\t\t\t\t\tid: serviceId,\n\t\t\t\t\tinput: { icon: inputs.icon },\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tawait applyInstanceConfig(client, serviceId, inputs.environmentId, inputs);\n\n\t\t// Image services have no `railway up` step (see RailwayDeploy for code\n\t\t// services) — the provider owns their deploy.\n\t\tif (inputs.source) {\n\t\t\tawait deployServiceInstance(client, serviceId, inputs.environmentId);\n\t\t}\n\n\t\tconst outs: RailwayServiceOutputs = { ...inputs, serviceId };\n\n\t\treturn { id: serviceId, outs };\n\t}\n\n\t/**\n\t * Updates service name/icon and re-applies instance configuration.\n\t */\n\tasync update(\n\t\tid: string,\n\t\tolds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.UpdateResult> {\n\t\tconst client = new RailwayClient(news.token);\n\n\t\tconst updateInput: Record<string, unknown> = {};\n\n\t\tif (olds.name !== news.name) {\n\t\t\tupdateInput.name = news.name;\n\t\t}\n\n\t\tif (news.icon && olds.icon !== news.icon) {\n\t\t\tupdateInput.icon = news.icon;\n\t\t}\n\n\t\tif (Object.keys(updateInput).length > 0) {\n\t\t\tawait client.query(SERVICE_UPDATE, { id, input: updateInput });\n\t\t}\n\n\t\tawait applyInstanceConfig(client, id, news.environmentId, news);\n\n\t\t// Instance config changes (source, startCommand, …) only take effect on\n\t\t// the next deployment; image services get none unless the provider\n\t\t// triggers it.\n\t\tif (news.source) {\n\t\t\tawait deployServiceInstance(client, id, news.environmentId);\n\t\t}\n\n\t\tconst outs: RailwayServiceOutputs = { ...news, serviceId: id };\n\n\t\treturn { outs };\n\t}\n\n\t/**\n\t * Reads current state for `pulumi refresh` by querying the service by ID.\n\t */\n\tasync read(\n\t\tid: string,\n\t\tprops: RailwayServiceOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query<{ service: { id: string; name: string } }>(\n\t\t\t\tSERVICE_QUERY,\n\t\t\t\t{ serviceId: id },\n\t\t\t);\n\t\t} catch {\n\t\t\tthrow new Error(`Railway service \"${props.name}\" (${id}) not found`);\n\t\t}\n\n\t\treturn { id, props: { ...props, serviceId: id } };\n\t}\n\n\t/**\n\t * Deletion is a no-op. A Railway service is a project-level resource shared\n\t * across environments (forked environments adopt it by name), so a single\n\t * environment's destroy must never delete it. Deleting the *environment*\n\t * removes that environment's service instances instead.\n\t */\n\tasync delete(): Promise<void> {\n\t\tpulumi.log.warn(\n\t\t\t\"Railway service deletion skipped — services are project-level; delete the environment to remove its instances\",\n\t\t);\n\t}\n\n\t/**\n\t * Compares old and new inputs to determine what changed.\n\t *\n\t * ProjectId and environmentId changes trigger replacement.\n\t */\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayServiceOutputs,\n\t\tnews: RailwayServiceInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\t\tconst changes: string[] = [];\n\n\t\tif (olds.name !== news.name) {\n\t\t\tchanges.push(\"name\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\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.builder !== news.builder) {\n\t\t\tchanges.push(\"builder\");\n\t\t}\n\n\t\tif (olds.buildCommand !== news.buildCommand) {\n\t\t\tchanges.push(\"buildCommand\");\n\t\t}\n\n\t\tif (olds.startCommand !== news.startCommand) {\n\t\t\tchanges.push(\"startCommand\");\n\t\t}\n\n\t\tif (olds.restartPolicyType !== news.restartPolicyType) {\n\t\t\tchanges.push(\"restartPolicyType\");\n\t\t}\n\n\t\tif (olds.healthcheckPath !== news.healthcheckPath) {\n\t\t\tchanges.push(\"healthcheckPath\");\n\t\t}\n\n\t\tif (olds.healthcheckTimeout !== news.healthcheckTimeout) {\n\t\t\tchanges.push(\"healthcheckTimeout\");\n\t\t}\n\n\t\tif (olds.preDeployCommand !== news.preDeployCommand) {\n\t\t\tchanges.push(\"preDeployCommand\");\n\t\t}\n\n\t\tif (olds.icon !== news.icon) {\n\t\t\tchanges.push(\"icon\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0 || changes.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 RailwayServiceResource extends pulumi.dynamic.Resource {\n\tpublic declare readonly serviceId: pulumi.Output<string>;\n\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\tenvironmentId: pulumi.Input<string>;\n\t\t\tname: pulumi.Input<string>;\n\t\t\ticon?: pulumi.Input<string>;\n\t\t\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\t\t\tbuilder?: pulumi.Input<RailwayBuilder>;\n\t\t\tbuildCommand?: pulumi.Input<string>;\n\t\t\tstartCommand?: pulumi.Input<string>;\n\t\t\trestartPolicyType?: pulumi.Input<RailwayRestartPolicy>;\n\t\t\thealthcheckPath?: pulumi.Input<string>;\n\t\t\thealthcheckTimeout?: pulumi.Input<number>;\n\t\t\tpreDeployCommand?: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayServiceResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, serviceId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayService — replaces Pulumi's native `provider` field. */\ntype RailwayServiceOptions = 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\n/** Args for RailwayService. */\nexport interface RailwayServiceArgs {\n\t/** Human-readable service name used for adopt-or-create matching. */\n\tname: pulumi.Input<string>;\n\n\t/** SVG icon URL displayed in the Railway dashboard. */\n\ticon?: pulumi.Input<string>;\n\n\t/** Docker image source for image-based services. */\n\tsource?: pulumi.Input<{ image: pulumi.Input<string> }>;\n\n\t/** Build system to use when building the service. */\n\tbuilder?: pulumi.Input<RailwayBuilder>;\n\n\t/** Shell command executed during the build phase. */\n\tbuildCommand?: pulumi.Input<string>;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: pulumi.Input<string>;\n\n\t/** Restart behavior for the service container. */\n\trestartPolicyType?: pulumi.Input<RailwayRestartPolicy>;\n\n\t/** HTTP path polled for health checks (e.g. `\"/health-check\"`). */\n\thealthcheckPath?: pulumi.Input<string>;\n\n\t/** Seconds to wait for a healthy response before marking unhealthy. */\n\thealthcheckTimeout?: pulumi.Input<number>;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway service with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * const service = new RailwayService(\"api\", {\n * name: \"api\",\n * builder: RailwayBuilder.RAILPACK,\n * startCommand: \"node dist/index.js\",\n * healthcheckPath: \"/health\",\n * }, { provider, project, environment });\n *\n * // Use serviceId downstream\n * new RailwayVariable(\"api-vars\", {\n * variables: { DATABASE_URL: dbUrl },\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayService extends pulumi.ComponentResource {\n\t/** Railway service UUID. */\n\tpublic readonly id: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayServiceArgs,\n\t\topts: RailwayServiceOptions,\n\t) {\n\t\tconst { provider, project, environment, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Service\", name, {}, pulumiOpts);\n\n\t\tconst resource = new RailwayServiceResource(\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\tenvironmentId: environment.id,\n\t\t\t\t...args,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.id = resource.serviceId;\n\n\t\tthis.registerOutputs({ id: this.id });\n\t}\n}\n"],"mappings":"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"}
|
package/dist/railway/volume.cjs
CHANGED
|
@@ -13,6 +13,11 @@ const VOLUME_CREATE = `
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
`;
|
|
16
|
+
const VOLUME_ATTACH_DEPLOY = `
|
|
17
|
+
mutation($serviceId: String!, $environmentId: String!) {
|
|
18
|
+
serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)
|
|
19
|
+
}
|
|
20
|
+
`;
|
|
16
21
|
const VOLUME_DELETE = `
|
|
17
22
|
mutation($volumeId: String!) {
|
|
18
23
|
volumeDelete(volumeId: $volumeId)
|
|
@@ -59,12 +64,22 @@ var RailwayVolumeResourceProvider = class {
|
|
|
59
64
|
const client = new require_railway_client.RailwayClient(inputs.token);
|
|
60
65
|
let volumeId = await findVolumeByService(client, inputs.projectId, inputs.serviceId);
|
|
61
66
|
if (volumeId) _pulumi_pulumi.log.info(`Adopting existing volume for service (${volumeId})`);
|
|
62
|
-
else
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
else {
|
|
68
|
+
volumeId = (await client.query(VOLUME_CREATE, { input: {
|
|
69
|
+
projectId: inputs.projectId,
|
|
70
|
+
serviceId: inputs.serviceId,
|
|
71
|
+
environmentId: inputs.environmentId,
|
|
72
|
+
mountPath: inputs.mountPath
|
|
73
|
+
} })).volumeCreate.id;
|
|
74
|
+
try {
|
|
75
|
+
await client.query(VOLUME_ATTACH_DEPLOY, {
|
|
76
|
+
serviceId: inputs.serviceId,
|
|
77
|
+
environmentId: inputs.environmentId
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
_pulumi_pulumi.log.warn(`Volume attached; redeploy skipped (service not deployable yet?): ${String(error)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
68
83
|
return {
|
|
69
84
|
id: volumeId,
|
|
70
85
|
outs: {
|
|
@@ -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 */\nexport class RailwayVolumeResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\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\tid: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tlet volumeId: string | undefined;\n\n\t\ttry {\n\t\t\tvolumeId = await findVolumeByService(\n\t\t\t\tclient,\n\t\t\t\tprops.projectId,\n\t\t\t\tprops.serviceId,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Railway volume refresh lookup failed; keeping existing state: ${String(error)}`,\n\t\t\t);\n\t\t}\n\n\t\t// A project-level service's volume can be momentarily unresolvable at\n\t\t// refresh (eventual consistency / environment-scoped volume instances), so\n\t\t// fall back to the stored id rather than throwing — that turned a healthy\n\t\t// volume into a refresh error. A genuinely-deleted volume is re-adopted or\n\t\t// recreated on the next `up` (create is adopt-or-create). `read` never\n\t\t// fabricates drift and never hard-fails.\n\t\tconst resolvedId = volumeId ?? props.volumeId ?? id;\n\n\t\treturn { id: resolvedId, props: { ...props, volumeId: resolvedId } };\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,IAAa,gCAAb,MAEA;CACC,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,IACA,OACqC;EACrC,MAAM,SAAS,IAAIA,qCAAc,MAAM,KAAK;EAE5C,IAAI;EAEJ,IAAI;GACH,WAAW,MAAM,oBAChB,QACA,MAAM,WACN,MAAM,SACP;EACD,SAAS,OAAO;GACf,eAAO,IAAI,KACV,iEAAiE,OAAO,KAAK,GAC9E;EACD;EAQA,MAAM,aAAa,YAAY,MAAM,YAAY;EAEjD,OAAO;GAAE,IAAI;GAAY,OAAO;IAAE,GAAG;IAAO,UAAU;GAAW;EAAE;CACpE;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"}
|
|
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_ATTACH_DEPLOY = `\n mutation($serviceId: String!, $environmentId: String!) {\n serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)\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 */\nexport class RailwayVolumeResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\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\n\t\t\t// A volume mounts into the container only on the NEXT deployment —\n\t\t\t// attaching alone changes nothing for a running service (the dashboard\n\t\t\t// redeploys after attach for the same reason). Best-effort: a service\n\t\t\t// with no deployable source yet (code service before its first\n\t\t\t// `railway up`) has nothing to redeploy, and its mount lands with that\n\t\t\t// first deploy anyway.\n\t\t\ttry {\n\t\t\t\tawait client.query(VOLUME_ATTACH_DEPLOY, {\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tpulumi.log.warn(\n\t\t\t\t\t`Volume attached; redeploy skipped (service not deployable yet?): ${String(error)}`,\n\t\t\t\t);\n\t\t\t}\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\tid: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tlet volumeId: string | undefined;\n\n\t\ttry {\n\t\t\tvolumeId = await findVolumeByService(\n\t\t\t\tclient,\n\t\t\t\tprops.projectId,\n\t\t\t\tprops.serviceId,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Railway volume refresh lookup failed; keeping existing state: ${String(error)}`,\n\t\t\t);\n\t\t}\n\n\t\t// A project-level service's volume can be momentarily unresolvable at\n\t\t// refresh (eventual consistency / environment-scoped volume instances), so\n\t\t// fall back to the stored id rather than throwing — that turned a healthy\n\t\t// volume into a refresh error. A genuinely-deleted volume is re-adopted or\n\t\t// recreated on the next `up` (create is adopt-or-create). `read` never\n\t\t// fabricates drift and never hard-fails.\n\t\tconst resolvedId = volumeId ?? props.volumeId ?? id;\n\n\t\treturn { id: resolvedId, props: { ...props, volumeId: resolvedId } };\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":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"volume.d.cts","names":[],"sources":["../../src/railway/volume.ts"],"mappings":";;;;;;;;;UAQiB,mBAAA;;EAEhB,KAAA;EAFmC;EAKnC,SAAA;EALmC;EAQnC,SAAA;EAHA;EAMA,aAAA;EAAA;EAGA,SAAA;AAAA;AAAS;AAAA,UAIA,oBAAA,SAA6B,mBAAmB;EAA3B;EAE9B,QAAQ;AAAA;AAAA;
|
|
1
|
+
{"version":3,"file":"volume.d.cts","names":[],"sources":["../../src/railway/volume.ts"],"mappings":";;;;;;;;;UAQiB,mBAAA;;EAEhB,KAAA;EAFmC;EAKnC,SAAA;EALmC;EAQnC,SAAA;EAHA;EAMA,aAAA;EAAA;EAGA,SAAA;AAAA;AAAS;AAAA,UAIA,oBAAA,SAA6B,mBAAmB;EAA3B;EAE9B,QAAQ;AAAA;AAAA;AAyFT;;;;;;AAzFS,cAyFI,6BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CACL,MAAA,EAAQ,mBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAiDpB,IAAA,CACL,EAAA,UACA,KAAA,EAAO,oBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EA4BpB,MAAA,CAAO,EAAA,UAAY,KAAA,EAAO,oBAAA,GAAuB,OAAA;EAYjD,IAAA,CACL,GAAA,UACA,IAAA,EAAM,oBAAA,EACN,IAAA,EAAM,mBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAkDtB,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAnDJ,sCAuDH,QAAA,EAAU,eAAA,EA3JgC;EA8J1C,OAAA,EAAS,cAAA,EA9JE;EAiKX,WAAA,EAAa,kBAAA,EAjKa;EAoK1B,OAAA,EAAS,cAAA;AAAA;;UAIO,iBAAA;EApKL;EAsKX,SAAA,EAAW,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;cAaX,aAAA,SAAsB,MAAA,CAAO,iBAAA;cAExC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"volume.d.mts","names":[],"sources":["../../src/railway/volume.ts"],"mappings":";;;;;;;;;UAQiB,mBAAA;;EAEhB,KAAA;EAFmC;EAKnC,SAAA;EALmC;EAQnC,SAAA;EAHA;EAMA,aAAA;EAAA;EAGA,SAAA;AAAA;AAAS;AAAA,UAIA,oBAAA,SAA6B,mBAAmB;EAA3B;EAE9B,QAAQ;AAAA;AAAA;
|
|
1
|
+
{"version":3,"file":"volume.d.mts","names":[],"sources":["../../src/railway/volume.ts"],"mappings":";;;;;;;;;UAQiB,mBAAA;;EAEhB,KAAA;EAFmC;EAKnC,SAAA;EALmC;EAQnC,SAAA;EAHA;EAMA,aAAA;EAAA;EAGA,SAAA;AAAA;AAAS;AAAA,UAIA,oBAAA,SAA6B,mBAAmB;EAA3B;EAE9B,QAAQ;AAAA;AAAA;AAyFT;;;;;;AAzFS,cAyFI,6BAAA,YACD,MAAA,CAAO,OAAA,CAAQ,gBAAA;EAEpB,MAAA,CACL,MAAA,EAAQ,mBAAA,GACN,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,YAAA;EAiDpB,IAAA,CACL,EAAA,UACA,KAAA,EAAO,oBAAA,GACL,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;EA4BpB,MAAA,CAAO,EAAA,UAAY,KAAA,EAAO,oBAAA,GAAuB,OAAA;EAYjD,IAAA,CACL,GAAA,UACA,IAAA,EAAM,oBAAA,EACN,IAAA,EAAM,mBAAA,GACJ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,UAAA;AAAA;;KAkDtB,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAnDJ,sCAuDH,QAAA,EAAU,eAAA,EA3JgC;EA8J1C,OAAA,EAAS,cAAA,EA9JE;EAiKX,WAAA,EAAa,kBAAA,EAjKa;EAoK1B,OAAA,EAAS,cAAA;AAAA;;UAIO,iBAAA;EApKL;EAsKX,SAAA,EAAW,MAAA,CAAO,KAAK;AAAA;;;;;;;;;;;cAaX,aAAA,SAAsB,MAAA,CAAO,iBAAA;cAExC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
|
package/dist/railway/volume.mjs
CHANGED
|
@@ -11,6 +11,11 @@ const VOLUME_CREATE = `
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
`;
|
|
14
|
+
const VOLUME_ATTACH_DEPLOY = `
|
|
15
|
+
mutation($serviceId: String!, $environmentId: String!) {
|
|
16
|
+
serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
14
19
|
const VOLUME_DELETE = `
|
|
15
20
|
mutation($volumeId: String!) {
|
|
16
21
|
volumeDelete(volumeId: $volumeId)
|
|
@@ -57,12 +62,22 @@ var RailwayVolumeResourceProvider = class {
|
|
|
57
62
|
const client = new RailwayClient(inputs.token);
|
|
58
63
|
let volumeId = await findVolumeByService(client, inputs.projectId, inputs.serviceId);
|
|
59
64
|
if (volumeId) pulumi.log.info(`Adopting existing volume for service (${volumeId})`);
|
|
60
|
-
else
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
else {
|
|
66
|
+
volumeId = (await client.query(VOLUME_CREATE, { input: {
|
|
67
|
+
projectId: inputs.projectId,
|
|
68
|
+
serviceId: inputs.serviceId,
|
|
69
|
+
environmentId: inputs.environmentId,
|
|
70
|
+
mountPath: inputs.mountPath
|
|
71
|
+
} })).volumeCreate.id;
|
|
72
|
+
try {
|
|
73
|
+
await client.query(VOLUME_ATTACH_DEPLOY, {
|
|
74
|
+
serviceId: inputs.serviceId,
|
|
75
|
+
environmentId: inputs.environmentId
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
pulumi.log.warn(`Volume attached; redeploy skipped (service not deployable yet?): ${String(error)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
66
81
|
return {
|
|
67
82
|
id: volumeId,
|
|
68
83
|
outs: {
|
|
@@ -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 */\nexport class RailwayVolumeResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\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\tid: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tlet volumeId: string | undefined;\n\n\t\ttry {\n\t\t\tvolumeId = await findVolumeByService(\n\t\t\t\tclient,\n\t\t\t\tprops.projectId,\n\t\t\t\tprops.serviceId,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Railway volume refresh lookup failed; keeping existing state: ${String(error)}`,\n\t\t\t);\n\t\t}\n\n\t\t// A project-level service's volume can be momentarily unresolvable at\n\t\t// refresh (eventual consistency / environment-scoped volume instances), so\n\t\t// fall back to the stored id rather than throwing — that turned a healthy\n\t\t// volume into a refresh error. A genuinely-deleted volume is re-adopted or\n\t\t// recreated on the next `up` (create is adopt-or-create). `read` never\n\t\t// fabricates drift and never hard-fails.\n\t\tconst resolvedId = volumeId ?? props.volumeId ?? id;\n\n\t\treturn { id: resolvedId, props: { ...props, volumeId: resolvedId } };\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,IAAa,gCAAb,MAEA;CACC,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,IACA,OACqC;EACrC,MAAM,SAAS,IAAI,cAAc,MAAM,KAAK;EAE5C,IAAI;EAEJ,IAAI;GACH,WAAW,MAAM,oBAChB,QACA,MAAM,WACN,MAAM,SACP;EACD,SAAS,OAAO;GACf,OAAO,IAAI,KACV,iEAAiE,OAAO,KAAK,GAC9E;EACD;EAQA,MAAM,aAAa,YAAY,MAAM,YAAY;EAEjD,OAAO;GAAE,IAAI;GAAY,OAAO;IAAE,GAAG;IAAO,UAAU;GAAW;EAAE;CACpE;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
|
+
{"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_ATTACH_DEPLOY = `\n mutation($serviceId: String!, $environmentId: String!) {\n serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)\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 */\nexport class RailwayVolumeResourceProvider\n\timplements pulumi.dynamic.ResourceProvider\n{\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\n\t\t\t// A volume mounts into the container only on the NEXT deployment —\n\t\t\t// attaching alone changes nothing for a running service (the dashboard\n\t\t\t// redeploys after attach for the same reason). Best-effort: a service\n\t\t\t// with no deployable source yet (code service before its first\n\t\t\t// `railway up`) has nothing to redeploy, and its mount lands with that\n\t\t\t// first deploy anyway.\n\t\t\ttry {\n\t\t\t\tawait client.query(VOLUME_ATTACH_DEPLOY, {\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tpulumi.log.warn(\n\t\t\t\t\t`Volume attached; redeploy skipped (service not deployable yet?): ${String(error)}`,\n\t\t\t\t);\n\t\t\t}\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\tid: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tlet volumeId: string | undefined;\n\n\t\ttry {\n\t\t\tvolumeId = await findVolumeByService(\n\t\t\t\tclient,\n\t\t\t\tprops.projectId,\n\t\t\t\tprops.serviceId,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tpulumi.log.warn(\n\t\t\t\t`Railway volume refresh lookup failed; keeping existing state: ${String(error)}`,\n\t\t\t);\n\t\t}\n\n\t\t// A project-level service's volume can be momentarily unresolvable at\n\t\t// refresh (eventual consistency / environment-scoped volume instances), so\n\t\t// fall back to the stored id rather than throwing — that turned a healthy\n\t\t// volume into a refresh error. A genuinely-deleted volume is re-adopted or\n\t\t// recreated on the next `up` (create is adopt-or-create). `read` never\n\t\t// fabricates drift and never hard-fails.\n\t\tconst resolvedId = volumeId ?? props.volumeId ?? id;\n\n\t\treturn { id: resolvedId, props: { ...props, volumeId: resolvedId } };\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":"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"}
|
|
@@ -13,17 +13,18 @@ function escapeAwkRegex(path) {
|
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Builds the shell filter applied to the `git ls-files` list before
|
|
16
|
-
* `rsync --files-from`.
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
16
|
+
* `rsync --files-from`. Every excluded entry drops the path and its subtree
|
|
17
|
+
* but keeps the entry's own `package.json`: an excluded directory may be a
|
|
18
|
+
* workspace member, and the package manager fails the whole install when the
|
|
19
|
+
* root manifest names a workspace whose manifest is missing from the copy
|
|
20
|
+
* (a kept manifest for a non-member is inert). Returns `cat` (passthrough)
|
|
19
21
|
* when nothing is excluded. Uses awk so it is portable across macOS and Linux.
|
|
20
22
|
*/
|
|
21
23
|
function buildSandboxFileFilter(excludePaths = []) {
|
|
22
24
|
if (excludePaths.length === 0) return "cat";
|
|
23
25
|
return `awk '${excludePaths.map((entry) => {
|
|
24
26
|
const escaped = escapeAwkRegex(entry);
|
|
25
|
-
|
|
26
|
-
return `!/^${escaped}(\\/|$)/`;
|
|
27
|
+
return `!(/^${escaped}(\\/|$)/ && !/^${escaped}\\/package\\.json$/)`;
|
|
27
28
|
}).join(" && ")}'`;
|
|
28
29
|
}
|
|
29
30
|
/** Root of the per-deploy sandbox tree. The DeploySandbox resource GCs this. */
|
|
@@ -156,4 +157,4 @@ Object.defineProperty(exports, 'isDeploySandbox', {
|
|
|
156
157
|
return isDeploySandbox;
|
|
157
158
|
}
|
|
158
159
|
});
|
|
159
|
-
//# sourceMappingURL=sandbox-
|
|
160
|
+
//# sourceMappingURL=sandbox-BBnhmyM_.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-BBnhmyM_.cjs","names":["fs","path","pulumi"],"sources":["../../sandbox/dist/index.mjs","../src/sandbox.ts"],"sourcesContent":["import { t as __name } from \"./chunk-OPjESj5l.mjs\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\n//#region src/index.ts\n/** Escapes a path literal for embedding inside an awk ERE. */\nfunction escapeAwkRegex(path) {\n\treturn path.replace(/[.[\\]{}()*+?^$|\\\\/]/g, \"\\\\$&\");\n}\n/**\n* Builds the shell filter applied to the `git ls-files` list before\n* `rsync --files-from`. Every excluded entry drops the path and its subtree\n* but keeps the entry's own `package.json`: an excluded directory may be a\n* workspace member, and the package manager fails the whole install when the\n* root manifest names a workspace whose manifest is missing from the copy\n* (a kept manifest for a non-member is inert). Returns `cat` (passthrough)\n* when nothing is excluded. Uses awk so it is portable across macOS and Linux.\n*/\nfunction buildSandboxFileFilter(excludePaths = []) {\n\tif (excludePaths.length === 0) return \"cat\";\n\treturn `awk '${excludePaths.map((entry) => {\n\t\tconst escaped = escapeAwkRegex(entry);\n\t\treturn `!(/^${escaped}(\\\\/|$)/ && !/^${escaped}\\\\/package\\\\.json$/)`;\n\t}).join(\" && \")}'`;\n}\n/** Root of the per-deploy sandbox tree. The DeploySandbox resource GCs this. */\nconst SANDBOX_ROOT = \"/tmp/infracraft\";\n/**\n* How a deploy's working copy is isolated. A closed set, so it is an enum rather\n* than a pair of booleans: the seam derives it from `dependsOn` presence\n* (DeploySandbox / GitGuard), which makes the impossible \"git-guard without a\n* sandbox\" state unrepresentable here.\n*/\nlet SandboxMode = /* @__PURE__ */ function(SandboxMode) {\n\t/** No isolation — run the CLI in the live working tree. */\n\tSandboxMode[\"NONE\"] = \"NONE\";\n\t/** Isolated copy carrying the repo's REAL `.git` (real commit/author exposed). */\n\tSandboxMode[\"ORIGINAL\"] = \"ORIGINAL\";\n\t/** Isolated copy with a metadata-free stub `.git` (no commit SHA / author). */\n\tSandboxMode[\"STUB\"] = \"STUB\";\n\treturn SandboxMode;\n}({});\n/**\n* Builds the shell for a deploy's `command.local.Command.create`. See\n* docs/superpowers/specs/2026-06-05-deploy-sandbox-design.md for the modes.\n*\n* Hardening: every script runs under POSIX `set -e` so any failed step aborts\n* instead of silently letting the platform CLI run against partial state.\n* `pipefail` is intentionally NOT used — the command runs under `/bin/sh`, which\n* may be `dash` (pipefail is a bash/ksh/zsh extension). To still catch a failing\n* `git ls-files` (a pipe would mask it, since an `rsync` of an empty list exits\n* 0), the copy is staged through intermediate list files rather than a pipe. The\n* `REPO` and `SANDBOX` command substitutions are explicitly guarded with clear\n* error messages.\n*/\nfunction buildSandboxScript(options) {\n\tconst { mode, appName, env, excludePaths, setup, cli } = options;\n\tconst head = `set -e; REPO=$(git rev-parse --show-toplevel) || { echo \"[infracraft] not inside a git repository\" >&2; exit 1; }`;\n\tconst runSetupAndCli = [setup, cli].filter(Boolean).join(\"; \");\n\tif (mode === \"NONE\") return `${head}; cd \"$REPO\"; ${runSetupAndCli}`;\n\tconst makeSandbox = [\n\t\t`PROJECT=$(basename \"$REPO\")`,\n\t\t`SANDBOX=$(mktemp -d \"${SANDBOX_ROOT}/$PROJECT-${env ? `${env}-${appName}` : appName}.XXXXXX\") || { echo \"[infracraft] mktemp failed\" >&2; exit 1; }`,\n\t\t`trap 'rm -rf \"$SANDBOX\"' EXIT`\n\t].join(\"; \");\n\tconst copy = [\n\t\t`git -C \"$REPO\" ls-files > \"$SANDBOX/.ic-ls\"`,\n\t\t`${mode === \"STUB\" ? buildSandboxFileFilter(excludePaths) : \"cat\"} \"$SANDBOX/.ic-ls\" > \"$SANDBOX/.ic-lsf\"`,\n\t\t`rsync -a --files-from=\"$SANDBOX/.ic-lsf\" \"$REPO\"/ \"$SANDBOX\"/`,\n\t\t`rm -f \"$SANDBOX/.ic-ls\" \"$SANDBOX/.ic-lsf\"`\n\t].join(\"; \");\n\tif (mode === \"STUB\") return `${head}; ${makeSandbox}; ${copy}; cd \"\\$SANDBOX\"; git init -q && git add -A; ${runSetupAndCli}`;\n\treturn `${head}; ${makeSandbox}; ${copy}; cp -c -R \"\\$REPO/.git\" \"\\$SANDBOX/.git\" 2>/dev/null || cp -R \"\\$REPO/.git\" \"\\$SANDBOX/.git\"; cd \"$SANDBOX\"; ${runSetupAndCli}`;\n}\n/** Sweep sandboxes orphaned by a hard-killed run (older than this). Kept well\n* above the worst-case deploy time (~30 min) but low enough that a SIGKILL\n* orphan does not linger for a day accumulating in /tmp. */\nconst STALE_SANDBOX_MS = 10800 * 1e3;\n/** mkdir the workspace root and GC stale sandboxes (best-effort). Sandboxes are\n* flat under the root (`/tmp/infracraft/<project>-<env>-<app>.XXXX`), so this\n* sweeps any entry older than the stale threshold. */\nfunction prepareSandboxWorkspace() {\n\tfs.mkdirSync(SANDBOX_ROOT, { recursive: true });\n\tlet entries = [];\n\ttry {\n\t\tentries = fs.readdirSync(SANDBOX_ROOT);\n\t} catch {\n\t\treturn;\n\t}\n\tconst now = Date.now();\n\tfor (const entry of entries) {\n\t\tconst full = path.join(SANDBOX_ROOT, entry);\n\t\ttry {\n\t\t\tif (now - fs.statSync(full).mtimeMs > STALE_SANDBOX_MS) fs.rmSync(full, {\n\t\t\t\trecursive: true,\n\t\t\t\tforce: true\n\t\t\t});\n\t\t} catch {}\n\t}\n}\n\n//#endregion\nexport { SandboxMode, buildSandboxFileFilter, buildSandboxScript, prepareSandboxWorkspace };\n//# sourceMappingURL=index.mjs.map","import { prepareSandboxWorkspace } from \"@infracraft/sandbox\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\nexport type { SandboxScriptOptions } from \"@infracraft/sandbox\";\nexport {\n\tbuildSandboxFileFilter,\n\tbuildSandboxScript,\n\tSandboxMode,\n} from \"@infracraft/sandbox\";\n\n/** Cross-bundle brand: `instanceof` is unreliable when the seam and the resource\n * come from different built entries, so the seam detects sandboxes by this. */\nconst DEPLOY_SANDBOX_BRAND = Symbol.for(\"@infracraft/pulumi/DeploySandbox\");\n\n/**\n * Isolation marker + workspace lifecycle. Listing it in a deploy's `dependsOn`\n * makes that deploy run in an isolated `/tmp/infracraft` copy. Carries no config;\n * the repo root is derived at runtime by the deploy command.\n */\nexport class DeploySandbox extends pulumi.ComponentResource {\n\tconstructor(name: string, opts?: pulumi.ComponentResourceOptions) {\n\t\tsuper(\"infracraft:sandbox:DeploySandbox\", name, {}, opts);\n\n\t\t(this as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] = true;\n\n\t\tif (!pulumi.runtime.isDryRun()) {\n\t\t\tprepareSandboxWorkspace();\n\t\t}\n\n\t\tthis.registerOutputs({});\n\t}\n}\n\n/** Bundle-safe check for a `DeploySandbox` in a `dependsOn` array. */\nexport function isDeploySandbox(value: unknown): value is DeploySandbox {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t(value as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] === true\n\t);\n}\n"],"mappings":";;;;;;;;;;AAMA,SAAS,eAAe,MAAM;CAC7B,OAAO,KAAK,QAAQ,wBAAwB,MAAM;AACnD;;;;;;;;;;AAUA,SAAS,uBAAuB,eAAe,CAAC,GAAG;CAClD,IAAI,aAAa,WAAW,GAAG,OAAO;CACtC,OAAO,QAAQ,aAAa,KAAK,UAAU;EAC1C,MAAM,UAAU,eAAe,KAAK;EACpC,OAAO,OAAO,QAAQ,iBAAiB,QAAQ;CAChD,CAAC,EAAE,KAAK,MAAM,EAAE;AACjB;;AAEA,MAAM,eAAe;;;;;;;AAOrB,IAAI,cAA8B,yBAAS,aAAa;;CAEvD,YAAY,UAAU;;CAEtB,YAAY,cAAc;;CAE1B,YAAY,UAAU;CACtB,OAAO;AACR,EAAE,CAAC,CAAC;;;;;;;;;;;;;;AAcJ,SAAS,mBAAmB,SAAS;CACpC,MAAM,EAAE,MAAM,SAAS,KAAK,cAAc,OAAO,QAAQ;CACzD,MAAM,OAAO;CACb,MAAM,iBAAiB,CAAC,OAAO,GAAG,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;CAC7D,IAAI,SAAS,QAAQ,OAAO,GAAG,KAAK,gBAAgB;CACpD,MAAM,cAAc;EACnB;EACA,wBAAwB,aAAa,YAAY,MAAM,GAAG,IAAI,GAAG,YAAY,QAAQ;EACrF;CACD,EAAE,KAAK,IAAI;CACX,MAAM,OAAO;EACZ;EACA,GAAG,SAAS,SAAS,uBAAuB,YAAY,IAAI,MAAM;EAClE;EACA;CACD,EAAE,KAAK,IAAI;CACX,IAAI,SAAS,QAAQ,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,KAAK,+CAA+C;CAC5G,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,KAAK,gHAAgH;AACzJ;;;;AAIA,MAAM,mBAAmB,QAAQ;;;;AAIjC,SAAS,0BAA0B;CAClC,QAAG,UAAU,cAAc,EAAE,WAAW,KAAK,CAAC;CAC9C,IAAI,UAAU,CAAC;CACf,IAAI;EACH,UAAUA,QAAG,YAAY,YAAY;CACtC,QAAQ;EACP;CACD;CACA,MAAM,MAAM,KAAK,IAAI;CACrB,KAAK,MAAM,SAAS,SAAS;EAC5B,MAAM,OAAOC,UAAK,KAAK,cAAc,KAAK;EAC1C,IAAI;GACH,IAAI,MAAMD,QAAG,SAAS,IAAI,EAAE,UAAU,kBAAkB,QAAG,OAAO,MAAM;IACvE,WAAW;IACX,OAAO;GACR,CAAC;EACF,QAAQ,CAAC;CACV;AACD;;;;;;ACvFA,MAAM,uBAAuB,OAAO,IAAI,kCAAkC;;;;;;AAO1E,IAAa,gBAAb,cAAmCE,eAAO,kBAAkB;CAC3D,YAAY,MAAc,MAAwC;EACjE,MAAM,oCAAoC,MAAM,CAAC,GAAG,IAAI;EAExD,AAAC,KAAiC,wBAAwB;EAE1D,IAAI,CAACA,eAAO,QAAQ,SAAS,GAC5B,wBAAwB;EAGzB,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD;;AAGA,SAAgB,gBAAgB,OAAwC;CACvE,OACC,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,0BAA0B;AAE/D"}
|
|
@@ -10,17 +10,18 @@ function escapeAwkRegex(path) {
|
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Builds the shell filter applied to the `git ls-files` list before
|
|
13
|
-
* `rsync --files-from`.
|
|
14
|
-
* `
|
|
15
|
-
*
|
|
13
|
+
* `rsync --files-from`. Every excluded entry drops the path and its subtree
|
|
14
|
+
* but keeps the entry's own `package.json`: an excluded directory may be a
|
|
15
|
+
* workspace member, and the package manager fails the whole install when the
|
|
16
|
+
* root manifest names a workspace whose manifest is missing from the copy
|
|
17
|
+
* (a kept manifest for a non-member is inert). Returns `cat` (passthrough)
|
|
16
18
|
* when nothing is excluded. Uses awk so it is portable across macOS and Linux.
|
|
17
19
|
*/
|
|
18
20
|
function buildSandboxFileFilter(excludePaths = []) {
|
|
19
21
|
if (excludePaths.length === 0) return "cat";
|
|
20
22
|
return `awk '${excludePaths.map((entry) => {
|
|
21
23
|
const escaped = escapeAwkRegex(entry);
|
|
22
|
-
|
|
23
|
-
return `!/^${escaped}(\\/|$)/`;
|
|
24
|
+
return `!(/^${escaped}(\\/|$)/ && !/^${escaped}\\/package\\.json$/)`;
|
|
24
25
|
}).join(" && ")}'`;
|
|
25
26
|
}
|
|
26
27
|
/** Root of the per-deploy sandbox tree. The DeploySandbox resource GCs this. */
|
|
@@ -124,4 +125,4 @@ function isDeploySandbox(value) {
|
|
|
124
125
|
|
|
125
126
|
//#endregion
|
|
126
127
|
export { buildSandboxScript as a, buildSandboxFileFilter as i, isDeploySandbox as n, SandboxMode as r, DeploySandbox as t };
|
|
127
|
-
//# sourceMappingURL=sandbox-
|
|
128
|
+
//# sourceMappingURL=sandbox-BQtqMu-5.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-BQtqMu-5.mjs","names":[],"sources":["../../sandbox/dist/index.mjs","../src/sandbox.ts"],"sourcesContent":["import { t as __name } from \"./chunk-OPjESj5l.mjs\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\n//#region src/index.ts\n/** Escapes a path literal for embedding inside an awk ERE. */\nfunction escapeAwkRegex(path) {\n\treturn path.replace(/[.[\\]{}()*+?^$|\\\\/]/g, \"\\\\$&\");\n}\n/**\n* Builds the shell filter applied to the `git ls-files` list before\n* `rsync --files-from`. Every excluded entry drops the path and its subtree\n* but keeps the entry's own `package.json`: an excluded directory may be a\n* workspace member, and the package manager fails the whole install when the\n* root manifest names a workspace whose manifest is missing from the copy\n* (a kept manifest for a non-member is inert). Returns `cat` (passthrough)\n* when nothing is excluded. Uses awk so it is portable across macOS and Linux.\n*/\nfunction buildSandboxFileFilter(excludePaths = []) {\n\tif (excludePaths.length === 0) return \"cat\";\n\treturn `awk '${excludePaths.map((entry) => {\n\t\tconst escaped = escapeAwkRegex(entry);\n\t\treturn `!(/^${escaped}(\\\\/|$)/ && !/^${escaped}\\\\/package\\\\.json$/)`;\n\t}).join(\" && \")}'`;\n}\n/** Root of the per-deploy sandbox tree. The DeploySandbox resource GCs this. */\nconst SANDBOX_ROOT = \"/tmp/infracraft\";\n/**\n* How a deploy's working copy is isolated. A closed set, so it is an enum rather\n* than a pair of booleans: the seam derives it from `dependsOn` presence\n* (DeploySandbox / GitGuard), which makes the impossible \"git-guard without a\n* sandbox\" state unrepresentable here.\n*/\nlet SandboxMode = /* @__PURE__ */ function(SandboxMode) {\n\t/** No isolation — run the CLI in the live working tree. */\n\tSandboxMode[\"NONE\"] = \"NONE\";\n\t/** Isolated copy carrying the repo's REAL `.git` (real commit/author exposed). */\n\tSandboxMode[\"ORIGINAL\"] = \"ORIGINAL\";\n\t/** Isolated copy with a metadata-free stub `.git` (no commit SHA / author). */\n\tSandboxMode[\"STUB\"] = \"STUB\";\n\treturn SandboxMode;\n}({});\n/**\n* Builds the shell for a deploy's `command.local.Command.create`. See\n* docs/superpowers/specs/2026-06-05-deploy-sandbox-design.md for the modes.\n*\n* Hardening: every script runs under POSIX `set -e` so any failed step aborts\n* instead of silently letting the platform CLI run against partial state.\n* `pipefail` is intentionally NOT used — the command runs under `/bin/sh`, which\n* may be `dash` (pipefail is a bash/ksh/zsh extension). To still catch a failing\n* `git ls-files` (a pipe would mask it, since an `rsync` of an empty list exits\n* 0), the copy is staged through intermediate list files rather than a pipe. The\n* `REPO` and `SANDBOX` command substitutions are explicitly guarded with clear\n* error messages.\n*/\nfunction buildSandboxScript(options) {\n\tconst { mode, appName, env, excludePaths, setup, cli } = options;\n\tconst head = `set -e; REPO=$(git rev-parse --show-toplevel) || { echo \"[infracraft] not inside a git repository\" >&2; exit 1; }`;\n\tconst runSetupAndCli = [setup, cli].filter(Boolean).join(\"; \");\n\tif (mode === \"NONE\") return `${head}; cd \"$REPO\"; ${runSetupAndCli}`;\n\tconst makeSandbox = [\n\t\t`PROJECT=$(basename \"$REPO\")`,\n\t\t`SANDBOX=$(mktemp -d \"${SANDBOX_ROOT}/$PROJECT-${env ? `${env}-${appName}` : appName}.XXXXXX\") || { echo \"[infracraft] mktemp failed\" >&2; exit 1; }`,\n\t\t`trap 'rm -rf \"$SANDBOX\"' EXIT`\n\t].join(\"; \");\n\tconst copy = [\n\t\t`git -C \"$REPO\" ls-files > \"$SANDBOX/.ic-ls\"`,\n\t\t`${mode === \"STUB\" ? buildSandboxFileFilter(excludePaths) : \"cat\"} \"$SANDBOX/.ic-ls\" > \"$SANDBOX/.ic-lsf\"`,\n\t\t`rsync -a --files-from=\"$SANDBOX/.ic-lsf\" \"$REPO\"/ \"$SANDBOX\"/`,\n\t\t`rm -f \"$SANDBOX/.ic-ls\" \"$SANDBOX/.ic-lsf\"`\n\t].join(\"; \");\n\tif (mode === \"STUB\") return `${head}; ${makeSandbox}; ${copy}; cd \"\\$SANDBOX\"; git init -q && git add -A; ${runSetupAndCli}`;\n\treturn `${head}; ${makeSandbox}; ${copy}; cp -c -R \"\\$REPO/.git\" \"\\$SANDBOX/.git\" 2>/dev/null || cp -R \"\\$REPO/.git\" \"\\$SANDBOX/.git\"; cd \"$SANDBOX\"; ${runSetupAndCli}`;\n}\n/** Sweep sandboxes orphaned by a hard-killed run (older than this). Kept well\n* above the worst-case deploy time (~30 min) but low enough that a SIGKILL\n* orphan does not linger for a day accumulating in /tmp. */\nconst STALE_SANDBOX_MS = 10800 * 1e3;\n/** mkdir the workspace root and GC stale sandboxes (best-effort). Sandboxes are\n* flat under the root (`/tmp/infracraft/<project>-<env>-<app>.XXXX`), so this\n* sweeps any entry older than the stale threshold. */\nfunction prepareSandboxWorkspace() {\n\tfs.mkdirSync(SANDBOX_ROOT, { recursive: true });\n\tlet entries = [];\n\ttry {\n\t\tentries = fs.readdirSync(SANDBOX_ROOT);\n\t} catch {\n\t\treturn;\n\t}\n\tconst now = Date.now();\n\tfor (const entry of entries) {\n\t\tconst full = path.join(SANDBOX_ROOT, entry);\n\t\ttry {\n\t\t\tif (now - fs.statSync(full).mtimeMs > STALE_SANDBOX_MS) fs.rmSync(full, {\n\t\t\t\trecursive: true,\n\t\t\t\tforce: true\n\t\t\t});\n\t\t} catch {}\n\t}\n}\n\n//#endregion\nexport { SandboxMode, buildSandboxFileFilter, buildSandboxScript, prepareSandboxWorkspace };\n//# sourceMappingURL=index.mjs.map","import { prepareSandboxWorkspace } from \"@infracraft/sandbox\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\nexport type { SandboxScriptOptions } from \"@infracraft/sandbox\";\nexport {\n\tbuildSandboxFileFilter,\n\tbuildSandboxScript,\n\tSandboxMode,\n} from \"@infracraft/sandbox\";\n\n/** Cross-bundle brand: `instanceof` is unreliable when the seam and the resource\n * come from different built entries, so the seam detects sandboxes by this. */\nconst DEPLOY_SANDBOX_BRAND = Symbol.for(\"@infracraft/pulumi/DeploySandbox\");\n\n/**\n * Isolation marker + workspace lifecycle. Listing it in a deploy's `dependsOn`\n * makes that deploy run in an isolated `/tmp/infracraft` copy. Carries no config;\n * the repo root is derived at runtime by the deploy command.\n */\nexport class DeploySandbox extends pulumi.ComponentResource {\n\tconstructor(name: string, opts?: pulumi.ComponentResourceOptions) {\n\t\tsuper(\"infracraft:sandbox:DeploySandbox\", name, {}, opts);\n\n\t\t(this as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] = true;\n\n\t\tif (!pulumi.runtime.isDryRun()) {\n\t\t\tprepareSandboxWorkspace();\n\t\t}\n\n\t\tthis.registerOutputs({});\n\t}\n}\n\n/** Bundle-safe check for a `DeploySandbox` in a `dependsOn` array. */\nexport function isDeploySandbox(value: unknown): value is DeploySandbox {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t(value as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] === true\n\t);\n}\n"],"mappings":";;;;;;;AAMA,SAAS,eAAe,MAAM;CAC7B,OAAO,KAAK,QAAQ,wBAAwB,MAAM;AACnD;;;;;;;;;;AAUA,SAAS,uBAAuB,eAAe,CAAC,GAAG;CAClD,IAAI,aAAa,WAAW,GAAG,OAAO;CACtC,OAAO,QAAQ,aAAa,KAAK,UAAU;EAC1C,MAAM,UAAU,eAAe,KAAK;EACpC,OAAO,OAAO,QAAQ,iBAAiB,QAAQ;CAChD,CAAC,EAAE,KAAK,MAAM,EAAE;AACjB;;AAEA,MAAM,eAAe;;;;;;;AAOrB,IAAI,cAA8B,yBAAS,aAAa;;CAEvD,YAAY,UAAU;;CAEtB,YAAY,cAAc;;CAE1B,YAAY,UAAU;CACtB,OAAO;AACR,EAAE,CAAC,CAAC;;;;;;;;;;;;;;AAcJ,SAAS,mBAAmB,SAAS;CACpC,MAAM,EAAE,MAAM,SAAS,KAAK,cAAc,OAAO,QAAQ;CACzD,MAAM,OAAO;CACb,MAAM,iBAAiB,CAAC,OAAO,GAAG,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;CAC7D,IAAI,SAAS,QAAQ,OAAO,GAAG,KAAK,gBAAgB;CACpD,MAAM,cAAc;EACnB;EACA,wBAAwB,aAAa,YAAY,MAAM,GAAG,IAAI,GAAG,YAAY,QAAQ;EACrF;CACD,EAAE,KAAK,IAAI;CACX,MAAM,OAAO;EACZ;EACA,GAAG,SAAS,SAAS,uBAAuB,YAAY,IAAI,MAAM;EAClE;EACA;CACD,EAAE,KAAK,IAAI;CACX,IAAI,SAAS,QAAQ,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,KAAK,+CAA+C;CAC5G,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,KAAK,gHAAgH;AACzJ;;;;AAIA,MAAM,mBAAmB,QAAQ;;;;AAIjC,SAAS,0BAA0B;CAClC,GAAG,UAAU,cAAc,EAAE,WAAW,KAAK,CAAC;CAC9C,IAAI,UAAU,CAAC;CACf,IAAI;EACH,UAAU,GAAG,YAAY,YAAY;CACtC,QAAQ;EACP;CACD;CACA,MAAM,MAAM,KAAK,IAAI;CACrB,KAAK,MAAM,SAAS,SAAS;EAC5B,MAAM,OAAO,KAAK,KAAK,cAAc,KAAK;EAC1C,IAAI;GACH,IAAI,MAAM,GAAG,SAAS,IAAI,EAAE,UAAU,kBAAkB,GAAG,OAAO,MAAM;IACvE,WAAW;IACX,OAAO;GACR,CAAC;EACF,QAAQ,CAAC;CACV;AACD;;;;;;ACvFA,MAAM,uBAAuB,OAAO,IAAI,kCAAkC;;;;;;AAO1E,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAC3D,YAAY,MAAc,MAAwC;EACjE,MAAM,oCAAoC,MAAM,CAAC,GAAG,IAAI;EAExD,AAAC,KAAiC,wBAAwB;EAE1D,IAAI,CAAC,OAAO,QAAQ,SAAS,GAC5B,wBAAwB;EAGzB,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD;;AAGA,SAAgB,gBAAgB,OAAwC;CACvE,OACC,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,0BAA0B;AAE/D"}
|
package/dist/sandbox.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_sandbox = require('./sandbox-
|
|
2
|
+
const require_sandbox = require('./sandbox-BBnhmyM_.cjs');
|
|
3
3
|
|
|
4
4
|
exports.DeploySandbox = require_sandbox.DeploySandbox;
|
|
5
5
|
exports.SandboxMode = require_sandbox.SandboxMode;
|
package/dist/sandbox.d.cts
CHANGED
|
@@ -5,9 +5,11 @@ import * as pulumi from "@pulumi/pulumi";
|
|
|
5
5
|
//#region src/index.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Builds the shell filter applied to the `git ls-files` list before
|
|
8
|
-
* `rsync --files-from`.
|
|
9
|
-
* `
|
|
10
|
-
*
|
|
8
|
+
* `rsync --files-from`. Every excluded entry drops the path and its subtree
|
|
9
|
+
* but keeps the entry's own `package.json`: an excluded directory may be a
|
|
10
|
+
* workspace member, and the package manager fails the whole install when the
|
|
11
|
+
* root manifest names a workspace whose manifest is missing from the copy
|
|
12
|
+
* (a kept manifest for a non-member is inert). Returns `cat` (passthrough)
|
|
11
13
|
* when nothing is excluded. Uses awk so it is portable across macOS and Linux.
|
|
12
14
|
*/
|
|
13
15
|
declare function buildSandboxFileFilter(excludePaths?: string[]): string;
|
package/dist/sandbox.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.d.cts","names":["t","__name","buildSandboxFileFilter","excludePaths","SandboxMode","NONE","ORIGINAL","STUB","SandboxScriptOptions","mode","appName","env","setup","cli","buildSandboxScript","options","prepareSandboxWorkspace"],"sources":["../../sandbox/dist/index.d.mts","../src/sandbox.ts"],"mappings":";;;;;;AAAmD;;;;
|
|
1
|
+
{"version":3,"file":"sandbox.d.cts","names":["t","__name","buildSandboxFileFilter","excludePaths","SandboxMode","NONE","ORIGINAL","STUB","SandboxScriptOptions","mode","appName","env","setup","cli","buildSandboxScript","options","prepareSandboxWorkspace"],"sources":["../../sandbox/dist/index.d.mts","../src/sandbox.ts"],"mappings":";;;;;;AAAmD;;;;AAYY;AAAA;;;iBAA9CE,sBAAAA,CAAuBC,YAAuB;;;;;AAazD;AAAA;aANOC,WAAAA;;EAEXC,IAAAA;EAQAI;EANAH,QAAAA;EAQAI;EANAH,IAAAA;AAAAA;AAAAA,UAEQC,oBAAAA;EAaRK;EAXAJ,IAAAA,EAAML,WAAW;EAWd;EATHM,OAAAA;EAwBiC;;EArBjCC,GAAAA;EAqB+D;EAnB/DR,YAAAA;;EAEAS,KAAAA;ECnBF;EDqBEC,GAAAA;AAAAA;;;;;;;;;;ACpB+D;AAcjE;;;iBDqBiBC,kBAAAA,CAAmBC,OAA6B,EAApBP,oBAAoB;;;;;;;AAvDd;;;;cCmBtC,aAAA,SAAsB,MAAA,CAAO,iBAAiB;cAC9C,IAAA,UAAc,IAAA,GAAO,MAAA,CAAO,wBAAA;AAAA;;iBAczB,eAAA,CAAgB,KAAA,YAAiB,KAAA,IAAS,aAAa"}
|
package/dist/sandbox.d.mts
CHANGED
|
@@ -5,9 +5,11 @@ import * as pulumi from "@pulumi/pulumi";
|
|
|
5
5
|
//#region src/index.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Builds the shell filter applied to the `git ls-files` list before
|
|
8
|
-
* `rsync --files-from`.
|
|
9
|
-
* `
|
|
10
|
-
*
|
|
8
|
+
* `rsync --files-from`. Every excluded entry drops the path and its subtree
|
|
9
|
+
* but keeps the entry's own `package.json`: an excluded directory may be a
|
|
10
|
+
* workspace member, and the package manager fails the whole install when the
|
|
11
|
+
* root manifest names a workspace whose manifest is missing from the copy
|
|
12
|
+
* (a kept manifest for a non-member is inert). Returns `cat` (passthrough)
|
|
11
13
|
* when nothing is excluded. Uses awk so it is portable across macOS and Linux.
|
|
12
14
|
*/
|
|
13
15
|
declare function buildSandboxFileFilter(excludePaths?: string[]): string;
|
package/dist/sandbox.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.d.mts","names":["t","__name","buildSandboxFileFilter","excludePaths","SandboxMode","NONE","ORIGINAL","STUB","SandboxScriptOptions","mode","appName","env","setup","cli","buildSandboxScript","options","prepareSandboxWorkspace"],"sources":["../../sandbox/dist/index.d.mts","../src/sandbox.ts"],"mappings":";;;;;;AAAmD;;;;
|
|
1
|
+
{"version":3,"file":"sandbox.d.mts","names":["t","__name","buildSandboxFileFilter","excludePaths","SandboxMode","NONE","ORIGINAL","STUB","SandboxScriptOptions","mode","appName","env","setup","cli","buildSandboxScript","options","prepareSandboxWorkspace"],"sources":["../../sandbox/dist/index.d.mts","../src/sandbox.ts"],"mappings":";;;;;;AAAmD;;;;AAYY;AAAA;;;iBAA9CE,sBAAAA,CAAuBC,YAAuB;;;;;AAazD;AAAA;aANOC,WAAAA;;EAEXC,IAAAA;EAQAI;EANAH,QAAAA;EAQAI;EANAH,IAAAA;AAAAA;AAAAA,UAEQC,oBAAAA;EAaRK;EAXAJ,IAAAA,EAAML,WAAW;EAWd;EATHM,OAAAA;EAwBiC;;EArBjCC,GAAAA;EAqB+D;EAnB/DR,YAAAA;;EAEAS,KAAAA;ECnBF;EDqBEC,GAAAA;AAAAA;;;;;;;;;;ACpB+D;AAcjE;;;iBDqBiBC,kBAAAA,CAAmBC,OAA6B,EAApBP,oBAAoB;;;;;;;AAvDd;;;;cCmBtC,aAAA,SAAsB,MAAA,CAAO,iBAAiB;cAC9C,IAAA,UAAc,IAAA,GAAO,MAAA,CAAO,wBAAA;AAAA;;iBAczB,eAAA,CAAgB,KAAA,YAAiB,KAAA,IAAS,aAAa"}
|
package/dist/sandbox.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as buildSandboxScript, i as buildSandboxFileFilter, n as isDeploySandbox, r as SandboxMode, t as DeploySandbox } from "./sandbox-
|
|
1
|
+
import { a as buildSandboxScript, i as buildSandboxFileFilter, n as isDeploySandbox, r as SandboxMode, t as DeploySandbox } from "./sandbox-BQtqMu-5.mjs";
|
|
2
2
|
|
|
3
3
|
export { DeploySandbox, SandboxMode, buildSandboxFileFilter, buildSandboxScript, isDeploySandbox };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@infracraft/pulumi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Native Pulumi providers for Railway, Neon, Vercel, and Fly.io with adopt-or-create semantics and deploy orchestration. No Terraform bridge.",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox-8xQPhxw7.mjs","names":[],"sources":["../../sandbox/dist/index.mjs","../src/sandbox.ts"],"sourcesContent":["import { t as __name } from \"./chunk-OPjESj5l.mjs\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\n//#region src/index.ts\n/** Escapes a path literal for embedding inside an awk ERE. */\nfunction escapeAwkRegex(path) {\n\treturn path.replace(/[.[\\]{}()*+?^$|\\\\/]/g, \"\\\\$&\");\n}\n/**\n* Builds the shell filter applied to the `git ls-files` list before\n* `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps\n* `apps/<x>/package.json` (the monorepo workspace graph needs it during build);\n* any other entry drops the path and its subtree. Returns `cat` (passthrough)\n* when nothing is excluded. Uses awk so it is portable across macOS and Linux.\n*/\nfunction buildSandboxFileFilter(excludePaths = []) {\n\tif (excludePaths.length === 0) return \"cat\";\n\treturn `awk '${excludePaths.map((entry) => {\n\t\tconst escaped = escapeAwkRegex(entry);\n\t\tif (entry.startsWith(\"apps/\")) return `!(/^${escaped}\\\\// && !/^${escaped}\\\\/package\\\\.json$/)`;\n\t\treturn `!/^${escaped}(\\\\/|$)/`;\n\t}).join(\" && \")}'`;\n}\n/** Root of the per-deploy sandbox tree. The DeploySandbox resource GCs this. */\nconst SANDBOX_ROOT = \"/tmp/infracraft\";\n/**\n* How a deploy's working copy is isolated. A closed set, so it is an enum rather\n* than a pair of booleans: the seam derives it from `dependsOn` presence\n* (DeploySandbox / GitGuard), which makes the impossible \"git-guard without a\n* sandbox\" state unrepresentable here.\n*/\nlet SandboxMode = /* @__PURE__ */ function(SandboxMode) {\n\t/** No isolation — run the CLI in the live working tree. */\n\tSandboxMode[\"NONE\"] = \"NONE\";\n\t/** Isolated copy carrying the repo's REAL `.git` (real commit/author exposed). */\n\tSandboxMode[\"ORIGINAL\"] = \"ORIGINAL\";\n\t/** Isolated copy with a metadata-free stub `.git` (no commit SHA / author). */\n\tSandboxMode[\"STUB\"] = \"STUB\";\n\treturn SandboxMode;\n}({});\n/**\n* Builds the shell for a deploy's `command.local.Command.create`. See\n* docs/superpowers/specs/2026-06-05-deploy-sandbox-design.md for the modes.\n*\n* Hardening: every script runs under POSIX `set -e` so any failed step aborts\n* instead of silently letting the platform CLI run against partial state.\n* `pipefail` is intentionally NOT used — the command runs under `/bin/sh`, which\n* may be `dash` (pipefail is a bash/ksh/zsh extension). To still catch a failing\n* `git ls-files` (a pipe would mask it, since an `rsync` of an empty list exits\n* 0), the copy is staged through intermediate list files rather than a pipe. The\n* `REPO` and `SANDBOX` command substitutions are explicitly guarded with clear\n* error messages.\n*/\nfunction buildSandboxScript(options) {\n\tconst { mode, appName, env, excludePaths, setup, cli } = options;\n\tconst head = `set -e; REPO=$(git rev-parse --show-toplevel) || { echo \"[infracraft] not inside a git repository\" >&2; exit 1; }`;\n\tconst runSetupAndCli = [setup, cli].filter(Boolean).join(\"; \");\n\tif (mode === \"NONE\") return `${head}; cd \"$REPO\"; ${runSetupAndCli}`;\n\tconst makeSandbox = [\n\t\t`PROJECT=$(basename \"$REPO\")`,\n\t\t`SANDBOX=$(mktemp -d \"${SANDBOX_ROOT}/$PROJECT-${env ? `${env}-${appName}` : appName}.XXXXXX\") || { echo \"[infracraft] mktemp failed\" >&2; exit 1; }`,\n\t\t`trap 'rm -rf \"$SANDBOX\"' EXIT`\n\t].join(\"; \");\n\tconst copy = [\n\t\t`git -C \"$REPO\" ls-files > \"$SANDBOX/.ic-ls\"`,\n\t\t`${mode === \"STUB\" ? buildSandboxFileFilter(excludePaths) : \"cat\"} \"$SANDBOX/.ic-ls\" > \"$SANDBOX/.ic-lsf\"`,\n\t\t`rsync -a --files-from=\"$SANDBOX/.ic-lsf\" \"$REPO\"/ \"$SANDBOX\"/`,\n\t\t`rm -f \"$SANDBOX/.ic-ls\" \"$SANDBOX/.ic-lsf\"`\n\t].join(\"; \");\n\tif (mode === \"STUB\") return `${head}; ${makeSandbox}; ${copy}; cd \"\\$SANDBOX\"; git init -q && git add -A; ${runSetupAndCli}`;\n\treturn `${head}; ${makeSandbox}; ${copy}; cp -c -R \"\\$REPO/.git\" \"\\$SANDBOX/.git\" 2>/dev/null || cp -R \"\\$REPO/.git\" \"\\$SANDBOX/.git\"; cd \"$SANDBOX\"; ${runSetupAndCli}`;\n}\n/** Sweep sandboxes orphaned by a hard-killed run (older than this). Kept well\n* above the worst-case deploy time (~30 min) but low enough that a SIGKILL\n* orphan does not linger for a day accumulating in /tmp. */\nconst STALE_SANDBOX_MS = 10800 * 1e3;\n/** mkdir the workspace root and GC stale sandboxes (best-effort). Sandboxes are\n* flat under the root (`/tmp/infracraft/<project>-<env>-<app>.XXXX`), so this\n* sweeps any entry older than the stale threshold. */\nfunction prepareSandboxWorkspace() {\n\tfs.mkdirSync(SANDBOX_ROOT, { recursive: true });\n\tlet entries = [];\n\ttry {\n\t\tentries = fs.readdirSync(SANDBOX_ROOT);\n\t} catch {\n\t\treturn;\n\t}\n\tconst now = Date.now();\n\tfor (const entry of entries) {\n\t\tconst full = path.join(SANDBOX_ROOT, entry);\n\t\ttry {\n\t\t\tif (now - fs.statSync(full).mtimeMs > STALE_SANDBOX_MS) fs.rmSync(full, {\n\t\t\t\trecursive: true,\n\t\t\t\tforce: true\n\t\t\t});\n\t\t} catch {}\n\t}\n}\n\n//#endregion\nexport { SandboxMode, buildSandboxFileFilter, buildSandboxScript, prepareSandboxWorkspace };\n//# sourceMappingURL=index.mjs.map","import { prepareSandboxWorkspace } from \"@infracraft/sandbox\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\nexport type { SandboxScriptOptions } from \"@infracraft/sandbox\";\nexport {\n\tbuildSandboxFileFilter,\n\tbuildSandboxScript,\n\tSandboxMode,\n} from \"@infracraft/sandbox\";\n\n/** Cross-bundle brand: `instanceof` is unreliable when the seam and the resource\n * come from different built entries, so the seam detects sandboxes by this. */\nconst DEPLOY_SANDBOX_BRAND = Symbol.for(\"@infracraft/pulumi/DeploySandbox\");\n\n/**\n * Isolation marker + workspace lifecycle. Listing it in a deploy's `dependsOn`\n * makes that deploy run in an isolated `/tmp/infracraft` copy. Carries no config;\n * the repo root is derived at runtime by the deploy command.\n */\nexport class DeploySandbox extends pulumi.ComponentResource {\n\tconstructor(name: string, opts?: pulumi.ComponentResourceOptions) {\n\t\tsuper(\"infracraft:sandbox:DeploySandbox\", name, {}, opts);\n\n\t\t(this as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] = true;\n\n\t\tif (!pulumi.runtime.isDryRun()) {\n\t\t\tprepareSandboxWorkspace();\n\t\t}\n\n\t\tthis.registerOutputs({});\n\t}\n}\n\n/** Bundle-safe check for a `DeploySandbox` in a `dependsOn` array. */\nexport function isDeploySandbox(value: unknown): value is DeploySandbox {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t(value as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] === true\n\t);\n}\n"],"mappings":";;;;;;;AAMA,SAAS,eAAe,MAAM;CAC7B,OAAO,KAAK,QAAQ,wBAAwB,MAAM;AACnD;;;;;;;;AAQA,SAAS,uBAAuB,eAAe,CAAC,GAAG;CAClD,IAAI,aAAa,WAAW,GAAG,OAAO;CACtC,OAAO,QAAQ,aAAa,KAAK,UAAU;EAC1C,MAAM,UAAU,eAAe,KAAK;EACpC,IAAI,MAAM,WAAW,OAAO,GAAG,OAAO,OAAO,QAAQ,aAAa,QAAQ;EAC1E,OAAO,MAAM,QAAQ;CACtB,CAAC,EAAE,KAAK,MAAM,EAAE;AACjB;;AAEA,MAAM,eAAe;;;;;;;AAOrB,IAAI,cAA8B,yBAAS,aAAa;;CAEvD,YAAY,UAAU;;CAEtB,YAAY,cAAc;;CAE1B,YAAY,UAAU;CACtB,OAAO;AACR,EAAE,CAAC,CAAC;;;;;;;;;;;;;;AAcJ,SAAS,mBAAmB,SAAS;CACpC,MAAM,EAAE,MAAM,SAAS,KAAK,cAAc,OAAO,QAAQ;CACzD,MAAM,OAAO;CACb,MAAM,iBAAiB,CAAC,OAAO,GAAG,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;CAC7D,IAAI,SAAS,QAAQ,OAAO,GAAG,KAAK,gBAAgB;CACpD,MAAM,cAAc;EACnB;EACA,wBAAwB,aAAa,YAAY,MAAM,GAAG,IAAI,GAAG,YAAY,QAAQ;EACrF;CACD,EAAE,KAAK,IAAI;CACX,MAAM,OAAO;EACZ;EACA,GAAG,SAAS,SAAS,uBAAuB,YAAY,IAAI,MAAM;EAClE;EACA;CACD,EAAE,KAAK,IAAI;CACX,IAAI,SAAS,QAAQ,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,KAAK,+CAA+C;CAC5G,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,KAAK,gHAAgH;AACzJ;;;;AAIA,MAAM,mBAAmB,QAAQ;;;;AAIjC,SAAS,0BAA0B;CAClC,GAAG,UAAU,cAAc,EAAE,WAAW,KAAK,CAAC;CAC9C,IAAI,UAAU,CAAC;CACf,IAAI;EACH,UAAU,GAAG,YAAY,YAAY;CACtC,QAAQ;EACP;CACD;CACA,MAAM,MAAM,KAAK,IAAI;CACrB,KAAK,MAAM,SAAS,SAAS;EAC5B,MAAM,OAAO,KAAK,KAAK,cAAc,KAAK;EAC1C,IAAI;GACH,IAAI,MAAM,GAAG,SAAS,IAAI,EAAE,UAAU,kBAAkB,GAAG,OAAO,MAAM;IACvE,WAAW;IACX,OAAO;GACR,CAAC;EACF,QAAQ,CAAC;CACV;AACD;;;;;;ACtFA,MAAM,uBAAuB,OAAO,IAAI,kCAAkC;;;;;;AAO1E,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAC3D,YAAY,MAAc,MAAwC;EACjE,MAAM,oCAAoC,MAAM,CAAC,GAAG,IAAI;EAExD,AAAC,KAAiC,wBAAwB;EAE1D,IAAI,CAAC,OAAO,QAAQ,SAAS,GAC5B,wBAAwB;EAGzB,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD;;AAGA,SAAgB,gBAAgB,OAAwC;CACvE,OACC,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,0BAA0B;AAE/D"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox-Bh7tQ1Jy.cjs","names":["fs","path","pulumi"],"sources":["../../sandbox/dist/index.mjs","../src/sandbox.ts"],"sourcesContent":["import { t as __name } from \"./chunk-OPjESj5l.mjs\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\n//#region src/index.ts\n/** Escapes a path literal for embedding inside an awk ERE. */\nfunction escapeAwkRegex(path) {\n\treturn path.replace(/[.[\\]{}()*+?^$|\\\\/]/g, \"\\\\$&\");\n}\n/**\n* Builds the shell filter applied to the `git ls-files` list before\n* `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps\n* `apps/<x>/package.json` (the monorepo workspace graph needs it during build);\n* any other entry drops the path and its subtree. Returns `cat` (passthrough)\n* when nothing is excluded. Uses awk so it is portable across macOS and Linux.\n*/\nfunction buildSandboxFileFilter(excludePaths = []) {\n\tif (excludePaths.length === 0) return \"cat\";\n\treturn `awk '${excludePaths.map((entry) => {\n\t\tconst escaped = escapeAwkRegex(entry);\n\t\tif (entry.startsWith(\"apps/\")) return `!(/^${escaped}\\\\// && !/^${escaped}\\\\/package\\\\.json$/)`;\n\t\treturn `!/^${escaped}(\\\\/|$)/`;\n\t}).join(\" && \")}'`;\n}\n/** Root of the per-deploy sandbox tree. The DeploySandbox resource GCs this. */\nconst SANDBOX_ROOT = \"/tmp/infracraft\";\n/**\n* How a deploy's working copy is isolated. A closed set, so it is an enum rather\n* than a pair of booleans: the seam derives it from `dependsOn` presence\n* (DeploySandbox / GitGuard), which makes the impossible \"git-guard without a\n* sandbox\" state unrepresentable here.\n*/\nlet SandboxMode = /* @__PURE__ */ function(SandboxMode) {\n\t/** No isolation — run the CLI in the live working tree. */\n\tSandboxMode[\"NONE\"] = \"NONE\";\n\t/** Isolated copy carrying the repo's REAL `.git` (real commit/author exposed). */\n\tSandboxMode[\"ORIGINAL\"] = \"ORIGINAL\";\n\t/** Isolated copy with a metadata-free stub `.git` (no commit SHA / author). */\n\tSandboxMode[\"STUB\"] = \"STUB\";\n\treturn SandboxMode;\n}({});\n/**\n* Builds the shell for a deploy's `command.local.Command.create`. See\n* docs/superpowers/specs/2026-06-05-deploy-sandbox-design.md for the modes.\n*\n* Hardening: every script runs under POSIX `set -e` so any failed step aborts\n* instead of silently letting the platform CLI run against partial state.\n* `pipefail` is intentionally NOT used — the command runs under `/bin/sh`, which\n* may be `dash` (pipefail is a bash/ksh/zsh extension). To still catch a failing\n* `git ls-files` (a pipe would mask it, since an `rsync` of an empty list exits\n* 0), the copy is staged through intermediate list files rather than a pipe. The\n* `REPO` and `SANDBOX` command substitutions are explicitly guarded with clear\n* error messages.\n*/\nfunction buildSandboxScript(options) {\n\tconst { mode, appName, env, excludePaths, setup, cli } = options;\n\tconst head = `set -e; REPO=$(git rev-parse --show-toplevel) || { echo \"[infracraft] not inside a git repository\" >&2; exit 1; }`;\n\tconst runSetupAndCli = [setup, cli].filter(Boolean).join(\"; \");\n\tif (mode === \"NONE\") return `${head}; cd \"$REPO\"; ${runSetupAndCli}`;\n\tconst makeSandbox = [\n\t\t`PROJECT=$(basename \"$REPO\")`,\n\t\t`SANDBOX=$(mktemp -d \"${SANDBOX_ROOT}/$PROJECT-${env ? `${env}-${appName}` : appName}.XXXXXX\") || { echo \"[infracraft] mktemp failed\" >&2; exit 1; }`,\n\t\t`trap 'rm -rf \"$SANDBOX\"' EXIT`\n\t].join(\"; \");\n\tconst copy = [\n\t\t`git -C \"$REPO\" ls-files > \"$SANDBOX/.ic-ls\"`,\n\t\t`${mode === \"STUB\" ? buildSandboxFileFilter(excludePaths) : \"cat\"} \"$SANDBOX/.ic-ls\" > \"$SANDBOX/.ic-lsf\"`,\n\t\t`rsync -a --files-from=\"$SANDBOX/.ic-lsf\" \"$REPO\"/ \"$SANDBOX\"/`,\n\t\t`rm -f \"$SANDBOX/.ic-ls\" \"$SANDBOX/.ic-lsf\"`\n\t].join(\"; \");\n\tif (mode === \"STUB\") return `${head}; ${makeSandbox}; ${copy}; cd \"\\$SANDBOX\"; git init -q && git add -A; ${runSetupAndCli}`;\n\treturn `${head}; ${makeSandbox}; ${copy}; cp -c -R \"\\$REPO/.git\" \"\\$SANDBOX/.git\" 2>/dev/null || cp -R \"\\$REPO/.git\" \"\\$SANDBOX/.git\"; cd \"$SANDBOX\"; ${runSetupAndCli}`;\n}\n/** Sweep sandboxes orphaned by a hard-killed run (older than this). Kept well\n* above the worst-case deploy time (~30 min) but low enough that a SIGKILL\n* orphan does not linger for a day accumulating in /tmp. */\nconst STALE_SANDBOX_MS = 10800 * 1e3;\n/** mkdir the workspace root and GC stale sandboxes (best-effort). Sandboxes are\n* flat under the root (`/tmp/infracraft/<project>-<env>-<app>.XXXX`), so this\n* sweeps any entry older than the stale threshold. */\nfunction prepareSandboxWorkspace() {\n\tfs.mkdirSync(SANDBOX_ROOT, { recursive: true });\n\tlet entries = [];\n\ttry {\n\t\tentries = fs.readdirSync(SANDBOX_ROOT);\n\t} catch {\n\t\treturn;\n\t}\n\tconst now = Date.now();\n\tfor (const entry of entries) {\n\t\tconst full = path.join(SANDBOX_ROOT, entry);\n\t\ttry {\n\t\t\tif (now - fs.statSync(full).mtimeMs > STALE_SANDBOX_MS) fs.rmSync(full, {\n\t\t\t\trecursive: true,\n\t\t\t\tforce: true\n\t\t\t});\n\t\t} catch {}\n\t}\n}\n\n//#endregion\nexport { SandboxMode, buildSandboxFileFilter, buildSandboxScript, prepareSandboxWorkspace };\n//# sourceMappingURL=index.mjs.map","import { prepareSandboxWorkspace } from \"@infracraft/sandbox\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\nexport type { SandboxScriptOptions } from \"@infracraft/sandbox\";\nexport {\n\tbuildSandboxFileFilter,\n\tbuildSandboxScript,\n\tSandboxMode,\n} from \"@infracraft/sandbox\";\n\n/** Cross-bundle brand: `instanceof` is unreliable when the seam and the resource\n * come from different built entries, so the seam detects sandboxes by this. */\nconst DEPLOY_SANDBOX_BRAND = Symbol.for(\"@infracraft/pulumi/DeploySandbox\");\n\n/**\n * Isolation marker + workspace lifecycle. Listing it in a deploy's `dependsOn`\n * makes that deploy run in an isolated `/tmp/infracraft` copy. Carries no config;\n * the repo root is derived at runtime by the deploy command.\n */\nexport class DeploySandbox extends pulumi.ComponentResource {\n\tconstructor(name: string, opts?: pulumi.ComponentResourceOptions) {\n\t\tsuper(\"infracraft:sandbox:DeploySandbox\", name, {}, opts);\n\n\t\t(this as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] = true;\n\n\t\tif (!pulumi.runtime.isDryRun()) {\n\t\t\tprepareSandboxWorkspace();\n\t\t}\n\n\t\tthis.registerOutputs({});\n\t}\n}\n\n/** Bundle-safe check for a `DeploySandbox` in a `dependsOn` array. */\nexport function isDeploySandbox(value: unknown): value is DeploySandbox {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t(value as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] === true\n\t);\n}\n"],"mappings":";;;;;;;;;;AAMA,SAAS,eAAe,MAAM;CAC7B,OAAO,KAAK,QAAQ,wBAAwB,MAAM;AACnD;;;;;;;;AAQA,SAAS,uBAAuB,eAAe,CAAC,GAAG;CAClD,IAAI,aAAa,WAAW,GAAG,OAAO;CACtC,OAAO,QAAQ,aAAa,KAAK,UAAU;EAC1C,MAAM,UAAU,eAAe,KAAK;EACpC,IAAI,MAAM,WAAW,OAAO,GAAG,OAAO,OAAO,QAAQ,aAAa,QAAQ;EAC1E,OAAO,MAAM,QAAQ;CACtB,CAAC,EAAE,KAAK,MAAM,EAAE;AACjB;;AAEA,MAAM,eAAe;;;;;;;AAOrB,IAAI,cAA8B,yBAAS,aAAa;;CAEvD,YAAY,UAAU;;CAEtB,YAAY,cAAc;;CAE1B,YAAY,UAAU;CACtB,OAAO;AACR,EAAE,CAAC,CAAC;;;;;;;;;;;;;;AAcJ,SAAS,mBAAmB,SAAS;CACpC,MAAM,EAAE,MAAM,SAAS,KAAK,cAAc,OAAO,QAAQ;CACzD,MAAM,OAAO;CACb,MAAM,iBAAiB,CAAC,OAAO,GAAG,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;CAC7D,IAAI,SAAS,QAAQ,OAAO,GAAG,KAAK,gBAAgB;CACpD,MAAM,cAAc;EACnB;EACA,wBAAwB,aAAa,YAAY,MAAM,GAAG,IAAI,GAAG,YAAY,QAAQ;EACrF;CACD,EAAE,KAAK,IAAI;CACX,MAAM,OAAO;EACZ;EACA,GAAG,SAAS,SAAS,uBAAuB,YAAY,IAAI,MAAM;EAClE;EACA;CACD,EAAE,KAAK,IAAI;CACX,IAAI,SAAS,QAAQ,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,KAAK,+CAA+C;CAC5G,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,KAAK,gHAAgH;AACzJ;;;;AAIA,MAAM,mBAAmB,QAAQ;;;;AAIjC,SAAS,0BAA0B;CAClC,QAAG,UAAU,cAAc,EAAE,WAAW,KAAK,CAAC;CAC9C,IAAI,UAAU,CAAC;CACf,IAAI;EACH,UAAUA,QAAG,YAAY,YAAY;CACtC,QAAQ;EACP;CACD;CACA,MAAM,MAAM,KAAK,IAAI;CACrB,KAAK,MAAM,SAAS,SAAS;EAC5B,MAAM,OAAOC,UAAK,KAAK,cAAc,KAAK;EAC1C,IAAI;GACH,IAAI,MAAMD,QAAG,SAAS,IAAI,EAAE,UAAU,kBAAkB,QAAG,OAAO,MAAM;IACvE,WAAW;IACX,OAAO;GACR,CAAC;EACF,QAAQ,CAAC;CACV;AACD;;;;;;ACtFA,MAAM,uBAAuB,OAAO,IAAI,kCAAkC;;;;;;AAO1E,IAAa,gBAAb,cAAmCE,eAAO,kBAAkB;CAC3D,YAAY,MAAc,MAAwC;EACjE,MAAM,oCAAoC,MAAM,CAAC,GAAG,IAAI;EAExD,AAAC,KAAiC,wBAAwB;EAE1D,IAAI,CAACA,eAAO,QAAQ,SAAS,GAC5B,wBAAwB;EAGzB,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD;;AAGA,SAAgB,gBAAgB,OAAwC;CACvE,OACC,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,0BAA0B;AAE/D"}
|