@infracraft/pulumi 1.12.0 → 1.13.1

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.
@@ -19,7 +19,10 @@ const RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2";
19
19
  */
20
20
  var RailwayClient = class {
21
21
  /**
22
- * @param token Railway API bearer token (project-scoped or account-scoped)
22
+ * @param token Railway **account- or team-scoped** API token. This client sends
23
+ * `Authorization: Bearer <token>`, which Railway's GraphQL v2 API accepts for
24
+ * account/team tokens. Railway **project** tokens are NOT accepted as Bearer here —
25
+ * they require the `Project-Access-Token` header — so do not pass a project token.
23
26
  */
24
27
  constructor(token) {
25
28
  this.token = token;
@@ -1 +1 @@
1
- {"version":3,"file":"client.cjs","names":[],"sources":["../../src/railway/client.ts"],"sourcesContent":["const RAILWAY_API_URL = \"https://backboard.railway.app/graphql/v2\";\n\n/** Standard GraphQL response envelope from Railway's API. */\ninterface GraphQLResponse<T> {\n\t/** The resolved data payload, present on success. */\n\tdata?: T;\n\n\t/** Array of GraphQL-level errors, present on partial or full failure. */\n\terrors?: Array<{ message: string }>;\n}\n\n/**\n * Typed GraphQL client for Railway's API.\n *\n * Wraps `fetch` with auth headers, JSON serialization, and error\n * extraction so callers only deal with typed response data.\n *\n * @example\n * ```typescript\n * const client = new RailwayClient(token);\n * const result = await client.query<{ project: { id: string } }>(\n * `query { project(id: \"abc\") { id name } }`\n * );\n * ```\n */\nexport class RailwayClient {\n\t/** Railway API bearer token. */\n\tprivate readonly token: string;\n\n\t/**\n\t * @param token Railway API bearer token (project-scoped or account-scoped)\n\t */\n\tconstructor(token: string) {\n\t\tthis.token = token;\n\t}\n\n\t/**\n\t * Executes a GraphQL query or mutation against Railway's API.\n\t *\n\t * @param query The GraphQL query or mutation string\n\t * @param variables Optional variables for parameterized queries\n\t * @returns The typed data payload from the response\n\t * @throws {Error} On HTTP transport errors (non-2xx status)\n\t * @throws {Error} On GraphQL-level errors (errors array in response)\n\t * @throws {Error} When the response contains no data payload\n\t */\n\tasync query<T>(\n\t\tquery: string,\n\t\tvariables?: Record<string, unknown>,\n\t): Promise<T> {\n\t\tconst response = await fetch(RAILWAY_API_URL, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\tAuthorization: `Bearer ${this.token}`,\n\t\t\t},\n\t\t\tbody: JSON.stringify({ query, variables }),\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst body = await response.text();\n\n\t\t\tthrow new Error(\n\t\t\t\t`Railway API HTTP error: ${response.status} ${response.statusText}\\n${body}`,\n\t\t\t);\n\t\t}\n\n\t\tconst json = (await response.json()) as GraphQLResponse<T>;\n\n\t\tif (json.errors && json.errors.length > 0) {\n\t\t\tthrow new Error(`Railway API error: ${json.errors[0].message}`);\n\t\t}\n\n\t\tif (!json.data) {\n\t\t\tthrow new Error(\"Railway API returned no data\");\n\t\t}\n\n\t\treturn json.data;\n\t}\n}\n"],"mappings":";;;;AAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;AAyBxB,IAAa,gBAAb,MAA2B;;;;CAO1B,YAAY,OAAe;EAC1B,KAAK,QAAQ;CACd;;;;;;;;;;;CAYA,MAAM,MACL,OACA,WACa;EACb,MAAM,WAAW,MAAM,MAAM,iBAAiB;GAC7C,QAAQ;GACR,SAAS;IACR,gBAAgB;IAChB,eAAe,UAAU,KAAK;GAC/B;GACA,MAAM,KAAK,UAAU;IAAE;IAAO;GAAU,CAAC;EAC1C,CAAC;EAED,IAAI,CAAC,SAAS,IAAI;GACjB,MAAM,OAAO,MAAM,SAAS,KAAK;GAEjC,MAAM,IAAI,MACT,2BAA2B,SAAS,OAAO,GAAG,SAAS,WAAW,IAAI,MACvE;EACD;EAEA,MAAM,OAAQ,MAAM,SAAS,KAAK;EAElC,IAAI,KAAK,UAAU,KAAK,OAAO,SAAS,GACvC,MAAM,IAAI,MAAM,sBAAsB,KAAK,OAAO,GAAG,SAAS;EAG/D,IAAI,CAAC,KAAK,MACT,MAAM,IAAI,MAAM,8BAA8B;EAG/C,OAAO,KAAK;CACb;AACD"}
1
+ {"version":3,"file":"client.cjs","names":[],"sources":["../../src/railway/client.ts"],"sourcesContent":["const RAILWAY_API_URL = \"https://backboard.railway.app/graphql/v2\";\n\n/** Standard GraphQL response envelope from Railway's API. */\ninterface GraphQLResponse<T> {\n\t/** The resolved data payload, present on success. */\n\tdata?: T;\n\n\t/** Array of GraphQL-level errors, present on partial or full failure. */\n\terrors?: Array<{ message: string }>;\n}\n\n/**\n * Typed GraphQL client for Railway's API.\n *\n * Wraps `fetch` with auth headers, JSON serialization, and error\n * extraction so callers only deal with typed response data.\n *\n * @example\n * ```typescript\n * const client = new RailwayClient(token);\n * const result = await client.query<{ project: { id: string } }>(\n * `query { project(id: \"abc\") { id name } }`\n * );\n * ```\n */\nexport class RailwayClient {\n\t/** Railway API token sent as `Authorization: Bearer` (account/team-scoped). */\n\tprivate readonly token: string;\n\n\t/**\n\t * @param token Railway **account- or team-scoped** API token. This client sends\n\t * `Authorization: Bearer <token>`, which Railway's GraphQL v2 API accepts for\n\t * account/team tokens. Railway **project** tokens are NOT accepted as Bearer here —\n\t * they require the `Project-Access-Token` header — so do not pass a project token.\n\t */\n\tconstructor(token: string) {\n\t\tthis.token = token;\n\t}\n\n\t/**\n\t * Executes a GraphQL query or mutation against Railway's API.\n\t *\n\t * @param query The GraphQL query or mutation string\n\t * @param variables Optional variables for parameterized queries\n\t * @returns The typed data payload from the response\n\t * @throws {Error} On HTTP transport errors (non-2xx status)\n\t * @throws {Error} On GraphQL-level errors (errors array in response)\n\t * @throws {Error} When the response contains no data payload\n\t */\n\tasync query<T>(\n\t\tquery: string,\n\t\tvariables?: Record<string, unknown>,\n\t): Promise<T> {\n\t\tconst response = await fetch(RAILWAY_API_URL, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\tAuthorization: `Bearer ${this.token}`,\n\t\t\t},\n\t\t\tbody: JSON.stringify({ query, variables }),\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst body = await response.text();\n\n\t\t\tthrow new Error(\n\t\t\t\t`Railway API HTTP error: ${response.status} ${response.statusText}\\n${body}`,\n\t\t\t);\n\t\t}\n\n\t\tconst json = (await response.json()) as GraphQLResponse<T>;\n\n\t\tif (json.errors && json.errors.length > 0) {\n\t\t\tthrow new Error(`Railway API error: ${json.errors[0].message}`);\n\t\t}\n\n\t\tif (!json.data) {\n\t\t\tthrow new Error(\"Railway API returned no data\");\n\t\t}\n\n\t\treturn json.data;\n\t}\n}\n"],"mappings":";;;;AAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;AAyBxB,IAAa,gBAAb,MAA2B;;;;;;;CAU1B,YAAY,OAAe;EAC1B,KAAK,QAAQ;CACd;;;;;;;;;;;CAYA,MAAM,MACL,OACA,WACa;EACb,MAAM,WAAW,MAAM,MAAM,iBAAiB;GAC7C,QAAQ;GACR,SAAS;IACR,gBAAgB;IAChB,eAAe,UAAU,KAAK;GAC/B;GACA,MAAM,KAAK,UAAU;IAAE;IAAO;GAAU,CAAC;EAC1C,CAAC;EAED,IAAI,CAAC,SAAS,IAAI;GACjB,MAAM,OAAO,MAAM,SAAS,KAAK;GAEjC,MAAM,IAAI,MACT,2BAA2B,SAAS,OAAO,GAAG,SAAS,WAAW,IAAI,MACvE;EACD;EAEA,MAAM,OAAQ,MAAM,SAAS,KAAK;EAElC,IAAI,KAAK,UAAU,KAAK,OAAO,SAAS,GACvC,MAAM,IAAI,MAAM,sBAAsB,KAAK,OAAO,GAAG,SAAS;EAG/D,IAAI,CAAC,KAAK,MACT,MAAM,IAAI,MAAM,8BAA8B;EAG/C,OAAO,KAAK;CACb;AACD"}
@@ -16,10 +16,13 @@ import { t as __name } from "../chunk-OPjESj5l.cjs";
16
16
  * ```
17
17
  */
18
18
  declare class RailwayClient {
19
- /** Railway API bearer token. */
19
+ /** Railway API token sent as `Authorization: Bearer` (account/team-scoped). */
20
20
  private readonly token;
21
21
  /**
22
- * @param token Railway API bearer token (project-scoped or account-scoped)
22
+ * @param token Railway **account- or team-scoped** API token. This client sends
23
+ * `Authorization: Bearer <token>`, which Railway's GraphQL v2 API accepts for
24
+ * account/team tokens. Railway **project** tokens are NOT accepted as Bearer here —
25
+ * they require the `Project-Access-Token` header — so do not pass a project token.
23
26
  */
24
27
  constructor(token: string);
25
28
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.cts","names":[],"sources":["../../src/railway/client.ts"],"mappings":";;;;;;AAyBA;;;;;;;;;;;cAAa,aAAA;EAqBA;EAAA,iBAnBK,KAAA;EAqBJ;;;cAhBD,KAAA;EAiBA;AAAA;;;;;;;;;EAHN,KAAA,GAAA,CACL,KAAA,UACA,SAAA,GAAY,MAAA,oBACV,OAAA,CAAQ,CAAA;AAAA"}
1
+ {"version":3,"file":"client.d.cts","names":[],"sources":["../../src/railway/client.ts"],"mappings":";;;;;;AAyBA;;;;;;;;;;;cAAa,aAAA;EAwBA;EAAA,iBAtBK,KAAA;EAwBJ;;;;;AACD;cAjBA,KAAA;;;;;;;;;;;EAcN,KAAA,GAAA,CACL,KAAA,UACA,SAAA,GAAY,MAAA,oBACV,OAAA,CAAQ,CAAA;AAAA"}
@@ -16,10 +16,13 @@ import { t as __name } from "../chunk-OPjESj5l.mjs";
16
16
  * ```
17
17
  */
18
18
  declare class RailwayClient {
19
- /** Railway API bearer token. */
19
+ /** Railway API token sent as `Authorization: Bearer` (account/team-scoped). */
20
20
  private readonly token;
21
21
  /**
22
- * @param token Railway API bearer token (project-scoped or account-scoped)
22
+ * @param token Railway **account- or team-scoped** API token. This client sends
23
+ * `Authorization: Bearer <token>`, which Railway's GraphQL v2 API accepts for
24
+ * account/team tokens. Railway **project** tokens are NOT accepted as Bearer here —
25
+ * they require the `Project-Access-Token` header — so do not pass a project token.
23
26
  */
24
27
  constructor(token: string);
25
28
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.mts","names":[],"sources":["../../src/railway/client.ts"],"mappings":";;;;;;AAyBA;;;;;;;;;;;cAAa,aAAA;EAqBA;EAAA,iBAnBK,KAAA;EAqBJ;;;cAhBD,KAAA;EAiBA;AAAA;;;;;;;;;EAHN,KAAA,GAAA,CACL,KAAA,UACA,SAAA,GAAY,MAAA,oBACV,OAAA,CAAQ,CAAA;AAAA"}
1
+ {"version":3,"file":"client.d.mts","names":[],"sources":["../../src/railway/client.ts"],"mappings":";;;;;;AAyBA;;;;;;;;;;;cAAa,aAAA;EAwBA;EAAA,iBAtBK,KAAA;EAwBJ;;;;;AACD;cAjBA,KAAA;;;;;;;;;;;EAcN,KAAA,GAAA,CACL,KAAA,UACA,SAAA,GAAY,MAAA,oBACV,OAAA,CAAQ,CAAA;AAAA"}
@@ -18,7 +18,10 @@ const RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2";
18
18
  */
19
19
  var RailwayClient = class {
20
20
  /**
21
- * @param token Railway API bearer token (project-scoped or account-scoped)
21
+ * @param token Railway **account- or team-scoped** API token. This client sends
22
+ * `Authorization: Bearer <token>`, which Railway's GraphQL v2 API accepts for
23
+ * account/team tokens. Railway **project** tokens are NOT accepted as Bearer here —
24
+ * they require the `Project-Access-Token` header — so do not pass a project token.
22
25
  */
23
26
  constructor(token) {
24
27
  this.token = token;
@@ -1 +1 @@
1
- {"version":3,"file":"client.mjs","names":[],"sources":["../../src/railway/client.ts"],"sourcesContent":["const RAILWAY_API_URL = \"https://backboard.railway.app/graphql/v2\";\n\n/** Standard GraphQL response envelope from Railway's API. */\ninterface GraphQLResponse<T> {\n\t/** The resolved data payload, present on success. */\n\tdata?: T;\n\n\t/** Array of GraphQL-level errors, present on partial or full failure. */\n\terrors?: Array<{ message: string }>;\n}\n\n/**\n * Typed GraphQL client for Railway's API.\n *\n * Wraps `fetch` with auth headers, JSON serialization, and error\n * extraction so callers only deal with typed response data.\n *\n * @example\n * ```typescript\n * const client = new RailwayClient(token);\n * const result = await client.query<{ project: { id: string } }>(\n * `query { project(id: \"abc\") { id name } }`\n * );\n * ```\n */\nexport class RailwayClient {\n\t/** Railway API bearer token. */\n\tprivate readonly token: string;\n\n\t/**\n\t * @param token Railway API bearer token (project-scoped or account-scoped)\n\t */\n\tconstructor(token: string) {\n\t\tthis.token = token;\n\t}\n\n\t/**\n\t * Executes a GraphQL query or mutation against Railway's API.\n\t *\n\t * @param query The GraphQL query or mutation string\n\t * @param variables Optional variables for parameterized queries\n\t * @returns The typed data payload from the response\n\t * @throws {Error} On HTTP transport errors (non-2xx status)\n\t * @throws {Error} On GraphQL-level errors (errors array in response)\n\t * @throws {Error} When the response contains no data payload\n\t */\n\tasync query<T>(\n\t\tquery: string,\n\t\tvariables?: Record<string, unknown>,\n\t): Promise<T> {\n\t\tconst response = await fetch(RAILWAY_API_URL, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\tAuthorization: `Bearer ${this.token}`,\n\t\t\t},\n\t\t\tbody: JSON.stringify({ query, variables }),\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst body = await response.text();\n\n\t\t\tthrow new Error(\n\t\t\t\t`Railway API HTTP error: ${response.status} ${response.statusText}\\n${body}`,\n\t\t\t);\n\t\t}\n\n\t\tconst json = (await response.json()) as GraphQLResponse<T>;\n\n\t\tif (json.errors && json.errors.length > 0) {\n\t\t\tthrow new Error(`Railway API error: ${json.errors[0].message}`);\n\t\t}\n\n\t\tif (!json.data) {\n\t\t\tthrow new Error(\"Railway API returned no data\");\n\t\t}\n\n\t\treturn json.data;\n\t}\n}\n"],"mappings":";;;AAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;AAyBxB,IAAa,gBAAb,MAA2B;;;;CAO1B,YAAY,OAAe;EAC1B,KAAK,QAAQ;CACd;;;;;;;;;;;CAYA,MAAM,MACL,OACA,WACa;EACb,MAAM,WAAW,MAAM,MAAM,iBAAiB;GAC7C,QAAQ;GACR,SAAS;IACR,gBAAgB;IAChB,eAAe,UAAU,KAAK;GAC/B;GACA,MAAM,KAAK,UAAU;IAAE;IAAO;GAAU,CAAC;EAC1C,CAAC;EAED,IAAI,CAAC,SAAS,IAAI;GACjB,MAAM,OAAO,MAAM,SAAS,KAAK;GAEjC,MAAM,IAAI,MACT,2BAA2B,SAAS,OAAO,GAAG,SAAS,WAAW,IAAI,MACvE;EACD;EAEA,MAAM,OAAQ,MAAM,SAAS,KAAK;EAElC,IAAI,KAAK,UAAU,KAAK,OAAO,SAAS,GACvC,MAAM,IAAI,MAAM,sBAAsB,KAAK,OAAO,GAAG,SAAS;EAG/D,IAAI,CAAC,KAAK,MACT,MAAM,IAAI,MAAM,8BAA8B;EAG/C,OAAO,KAAK;CACb;AACD"}
1
+ {"version":3,"file":"client.mjs","names":[],"sources":["../../src/railway/client.ts"],"sourcesContent":["const RAILWAY_API_URL = \"https://backboard.railway.app/graphql/v2\";\n\n/** Standard GraphQL response envelope from Railway's API. */\ninterface GraphQLResponse<T> {\n\t/** The resolved data payload, present on success. */\n\tdata?: T;\n\n\t/** Array of GraphQL-level errors, present on partial or full failure. */\n\terrors?: Array<{ message: string }>;\n}\n\n/**\n * Typed GraphQL client for Railway's API.\n *\n * Wraps `fetch` with auth headers, JSON serialization, and error\n * extraction so callers only deal with typed response data.\n *\n * @example\n * ```typescript\n * const client = new RailwayClient(token);\n * const result = await client.query<{ project: { id: string } }>(\n * `query { project(id: \"abc\") { id name } }`\n * );\n * ```\n */\nexport class RailwayClient {\n\t/** Railway API token sent as `Authorization: Bearer` (account/team-scoped). */\n\tprivate readonly token: string;\n\n\t/**\n\t * @param token Railway **account- or team-scoped** API token. This client sends\n\t * `Authorization: Bearer <token>`, which Railway's GraphQL v2 API accepts for\n\t * account/team tokens. Railway **project** tokens are NOT accepted as Bearer here —\n\t * they require the `Project-Access-Token` header — so do not pass a project token.\n\t */\n\tconstructor(token: string) {\n\t\tthis.token = token;\n\t}\n\n\t/**\n\t * Executes a GraphQL query or mutation against Railway's API.\n\t *\n\t * @param query The GraphQL query or mutation string\n\t * @param variables Optional variables for parameterized queries\n\t * @returns The typed data payload from the response\n\t * @throws {Error} On HTTP transport errors (non-2xx status)\n\t * @throws {Error} On GraphQL-level errors (errors array in response)\n\t * @throws {Error} When the response contains no data payload\n\t */\n\tasync query<T>(\n\t\tquery: string,\n\t\tvariables?: Record<string, unknown>,\n\t): Promise<T> {\n\t\tconst response = await fetch(RAILWAY_API_URL, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\tAuthorization: `Bearer ${this.token}`,\n\t\t\t},\n\t\t\tbody: JSON.stringify({ query, variables }),\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst body = await response.text();\n\n\t\t\tthrow new Error(\n\t\t\t\t`Railway API HTTP error: ${response.status} ${response.statusText}\\n${body}`,\n\t\t\t);\n\t\t}\n\n\t\tconst json = (await response.json()) as GraphQLResponse<T>;\n\n\t\tif (json.errors && json.errors.length > 0) {\n\t\t\tthrow new Error(`Railway API error: ${json.errors[0].message}`);\n\t\t}\n\n\t\tif (!json.data) {\n\t\t\tthrow new Error(\"Railway API returned no data\");\n\t\t}\n\n\t\treturn json.data;\n\t}\n}\n"],"mappings":";;;AAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;AAyBxB,IAAa,gBAAb,MAA2B;;;;;;;CAU1B,YAAY,OAAe;EAC1B,KAAK,QAAQ;CACd;;;;;;;;;;;CAYA,MAAM,MACL,OACA,WACa;EACb,MAAM,WAAW,MAAM,MAAM,iBAAiB;GAC7C,QAAQ;GACR,SAAS;IACR,gBAAgB;IAChB,eAAe,UAAU,KAAK;GAC/B;GACA,MAAM,KAAK,UAAU;IAAE;IAAO;GAAU,CAAC;EAC1C,CAAC;EAED,IAAI,CAAC,SAAS,IAAI;GACjB,MAAM,OAAO,MAAM,SAAS,KAAK;GAEjC,MAAM,IAAI,MACT,2BAA2B,SAAS,OAAO,GAAG,SAAS,WAAW,IAAI,MACvE;EACD;EAEA,MAAM,OAAQ,MAAM,SAAS,KAAK;EAElC,IAAI,KAAK,UAAU,KAAK,OAAO,SAAS,GACvC,MAAM,IAAI,MAAM,sBAAsB,KAAK,OAAO,GAAG,SAAS;EAG/D,IAAI,CAAC,KAAK,MACT,MAAM,IAAI,MAAM,8BAA8B;EAG/C,OAAO,KAAK;CACb;AACD"}
@@ -9,9 +9,18 @@ _pulumi_pulumi = require_chunk.__toESM(_pulumi_pulumi, 1);
9
9
  //#region src/railway/deploy.ts
10
10
  const LOCK_DIR = "/tmp/.railway-upload-lock";
11
11
  /**
12
- * Deploys a Railway service and waits for the build to complete.
12
+ * Deploys a Railway service and waits for the deployment to reach a terminal status.
13
+ *
14
+ * `railway up --ci` only blocks until the build UPLOAD completes; Railway's image
15
+ * finalization, the pre-deploy hook (e.g. migrations), the health check, and promotion
16
+ * all run AFTER the CLI returns. So after `up` succeeds, this polls the Railway GraphQL API
17
+ * for the deployment's status and **fails the resource (exit 1) on FAILED / CRASHED /
18
+ * REMOVED** — without it, a deployment that fails post-build is reported as a successful
19
+ * `pulumi up` and the previous version silently stays live. Set `INFRACRAFT_SKIP_DEPLOY_WAIT`
20
+ * to bypass the wait. Status reads use the env-scoped project token via the
21
+ * `Project-Access-Token` header (Railway rejects project tokens on `Authorization: Bearer`);
22
+ * the poll runs under `node` (Pulumi's nodejs runtime) and uses `fetch` (Node 18+).
13
23
  *
14
- * Uses `railway up --ci` which blocks until the build finishes.
15
24
  * Multiple deploys run in parallel — a mkdir lock serializes only the
16
25
  * brief upload phase (~5s) when `.railwayignore` must be consistent,
17
26
  * then releases so builds stream concurrently.
@@ -33,7 +42,7 @@ var RailwayDeploy = class extends _pulumi_pulumi.ComponentResource {
33
42
  return dir;
34
43
  }).join("\\n");
35
44
  const setupLines = [ignorePatterns ? `printf '${ignorePatterns}\\n' > .railwayignore` : "", args.railpackConfig ? `printf '${JSON.stringify(args.railpackConfig).replace(/'/g, "\\'")}' > railpack.json` : ""].filter(Boolean).join("; ");
36
- const deployCmd = _pulumi_pulumi.interpolate`while ! mkdir ${LOCK_DIR} 2>/dev/null; do sleep 1; done; ${setupLines}; { sleep 5; rm -f .railwayignore railpack.json; rmdir ${LOCK_DIR} 2>/dev/null; } & RAILWAY_TOKEN=${projectToken} railway up --ci --project ${project.id} --service ${service.id} --environment ${environment.id}; EXIT=$?; rm -f .railwayignore railpack.json; rmdir ${LOCK_DIR} 2>/dev/null; wait; exit $EXIT`;
45
+ const deployCmd = _pulumi_pulumi.interpolate`while ! mkdir ${LOCK_DIR} 2>/dev/null; do sleep 1; done; ${setupLines}; { sleep 5; rm -f .railwayignore railpack.json; rmdir ${LOCK_DIR} 2>/dev/null; } & RAILWAY_TOKEN=${projectToken} railway up --ci --project ${project.id} --service ${service.id} --environment ${environment.id}; EXIT=$?; rm -f .railwayignore railpack.json; rmdir ${LOCK_DIR} 2>/dev/null; wait; if [ "$EXIT" -ne 0 ]; then exit "$EXIT"; fi; if [ -n "$INFRACRAFT_SKIP_DEPLOY_WAIT" ]; then exit 0; fi; IC_TOK=${projectToken} IC_PROJ=${project.id} IC_ENV=${environment.id} IC_SVC=${service.id} node -e '${`const t=process.env.IC_TOK,p=process.env.IC_PROJ,e=process.env.IC_ENV,s=process.env.IC_SVC;const u="https://backboard.railway.app/graphql/v2";const q=(query,variables)=>fetch(u,{method:"POST",headers:{"Project-Access-Token":t,"Content-Type":"application/json"},body:JSON.stringify({query,variables})}).then(r=>r.json()).catch(()=>({}));const sl=ms=>new Promise(r=>setTimeout(r,ms));(async()=>{let id;for(let i=0;i<6&&!id;i++){const d=await q("query($p:String!,$e:String!,$s:String!){deployments(first:1,input:{projectId:$p,environmentId:$e,serviceId:$s}){edges{node{id status}}}}",{p,e,s});const g=d&&d.data&&d.data.deployments&&d.data.deployments.edges;if(g&&g[0])id=g[0].node.id;if(!id)await sl(5000);}if(!id){console.error("[infracraft] could not resolve Railway deployment id; build succeeded, not blocking");process.exit(0);}for(let i=0;i<120;i++){const r=await q("query($d:String!){deployment(id:$d){status}}",{d:id});const st=r&&r.data&&r.data.deployment&&r.data.deployment.status;if(st)console.error("[infracraft] railway deployment "+id+" status="+st);if(st==="SUCCESS")process.exit(0);if(st==="FAILED"||st==="CRASHED"||st==="REMOVED"){const l=await q("query($d:String!){deploymentLogs(deploymentId:$d,limit:60){message}}",{d:id});const lg=(l&&l.data&&l.data.deploymentLogs)||[];for(const x of lg)console.error(" "+(x.message||""));console.error("[infracraft] railway deployment "+id+" "+st+" — failing the Pulumi resource");process.exit(1);}await sl(10000);}console.error("[infracraft] timed out waiting for Railway deployment "+id);process.exit(1);})();`}'`;
37
46
  new _pulumi_command.local.Command(`${name}-deploy`, {
38
47
  create: deployCmd,
39
48
  triggers: args.triggers,
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.cjs","names":["pulumi","command","stableDir"],"sources":["../../src/railway/deploy.ts"],"sourcesContent":["import * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { stableDir } from \"../stable-dir\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport { RailwayBuilder, type RailwayService } from \"./service\";\n\n/** Build and deploy configuration for a Railway service. */\nexport interface RailwayDeployConfig {\n\t/** Build system to use when building the service. */\n\tbuilder?: RailwayBuilder;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: string;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: string;\n}\n\n/** Args for RailwayDeploy. */\nexport interface RailwayDeployArgs {\n\t/**\n\t * Absolute path to the monorepo root (working directory for `railway up`).\n\t * Stored relative to the Pulumi program directory so the command stays stable\n\t * across machines and CI (see {@link stableDir}).\n\t */\n\tdirectory: string;\n\n\t/** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\n\t/** Directories to exclude via `.railwayignore`. */\n\texcludePaths?: string[];\n\n\t/** Railpack configuration written to `railpack.json` before deploy. */\n\trailpackConfig?: Record<string, unknown>;\n}\n\n/** Options type for RailwayDeploy — replaces Pulumi's native `provider` field. */\ntype RailwayDeployOptions = 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\t/** Environment-scoped Railway deploy token. Provision via {@link RailwayProjectToken}. */\n\tprojectToken: pulumi.Input<string>;\n};\n\nconst LOCK_DIR = \"/tmp/.railway-upload-lock\";\n\n/**\n * Deploys a Railway service and waits for the build to complete.\n *\n * Uses `railway up --ci` which blocks until the build finishes.\n * Multiple deploys run in parallel — a mkdir lock serializes only the\n * brief upload phase (~5s) when `.railwayignore` must be consistent,\n * then releases so builds stream concurrently.\n *\n * @example\n * ```typescript\n * new RailwayDeploy(\"api-deploy\", {\n * directory: monorepoRoot,\n * triggers: [sourceHash],\n * }, { provider, project, environment, service, projectToken: stagingToken.token });\n * ```\n */\nexport class RailwayDeploy extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayDeployArgs,\n\t\topts: RailwayDeployOptions,\n\t) {\n\t\tconst {\n\t\t\tprovider,\n\t\t\tproject,\n\t\t\tenvironment,\n\t\t\tservice,\n\t\t\tprojectToken,\n\t\t\t...pulumiOpts\n\t\t} = opts;\n\n\t\tsuper(\"infracraft:railway:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst ignorePatterns = (args.excludePaths ?? [])\n\t\t\t.map((dir) => {\n\t\t\t\tif (dir.startsWith(\"apps/\")) {\n\t\t\t\t\treturn `${dir}/**\\\\n!${dir}/package.json`;\n\t\t\t\t}\n\n\t\t\t\treturn dir;\n\t\t\t})\n\t\t\t.join(\"\\\\n\");\n\n\t\tconst writeIgnore = ignorePatterns\n\t\t\t? `printf '${ignorePatterns}\\\\n' > .railwayignore`\n\t\t\t: \"\";\n\n\t\tconst writeRailpack = args.railpackConfig\n\t\t\t? `printf '${JSON.stringify(args.railpackConfig).replace(/'/g, \"\\\\'\")}' > railpack.json`\n\t\t\t: \"\";\n\n\t\tconst setupLines = [writeIgnore, writeRailpack].filter(Boolean).join(\"; \");\n\n\t\t// The deploy token is inlined into the create command (RAILWAY_TOKEN=… railway up) rather\n\t\t// than passed via command.local.Command's `environment` map. The token is auto-minted (a\n\t\t// RailwayProjectToken), so it is an UNKNOWN secret at preview time, and an unknown secret in\n\t\t// the `environment` map makes `pulumi preview` fail (\"malformed RPC secret: missing value\").\n\t\t// Inlining keeps the token secret in state while letting preview serialize cleanly.\n\t\tconst deployCmd = pulumi.interpolate`while ! mkdir ${LOCK_DIR} 2>/dev/null; do sleep 1; done; ${setupLines}; { sleep 5; rm -f .railwayignore railpack.json; rmdir ${LOCK_DIR} 2>/dev/null; } & RAILWAY_TOKEN=${projectToken} railway up --ci --project ${project.id} --service ${service.id} --environment ${environment.id}; EXIT=$?; rm -f .railwayignore railpack.json; rmdir ${LOCK_DIR} 2>/dev/null; wait; exit $EXIT`;\n\n\t\tnew command.local.Command(\n\t\t\t`${name}-deploy`,\n\t\t\t{\n\t\t\t\tcreate: deployCmd,\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\tdir: stableDir(args.directory),\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;;;;AA4DA,MAAM,WAAW;;;;;;;;;;;;;;;;;AAkBjB,IAAa,gBAAb,cAAmCA,eAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EACL,UACA,SACA,aACA,SACA,cACA,GAAG,eACA;EAEJ,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,MAAM,kBAAkB,KAAK,gBAAgB,CAAC,GAC5C,KAAK,QAAQ;GACb,IAAI,IAAI,WAAW,OAAO,GACzB,OAAO,GAAG,IAAI,SAAS,IAAI;GAG5B,OAAO;EACR,CAAC,EACA,KAAK,KAAK;EAUZ,MAAM,aAAa,CARC,iBACjB,WAAW,eAAe,yBAC1B,IAEmB,KAAK,iBACxB,WAAW,KAAK,UAAU,KAAK,cAAc,EAAE,QAAQ,MAAM,KAAK,EAAE,qBACpE,EAE2C,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;EAOzE,MAAM,YAAY,eAAO,WAAW,iBAAiB,SAAS,kCAAkC,WAAW,yDAAyD,SAAS,kCAAkC,aAAa,6BAA6B,QAAQ,GAAG,aAAa,QAAQ,GAAG,iBAAiB,YAAY,GAAG,uDAAuD,SAAS;EAE5X,IAAIC,gBAAQ,MAAM,QACjB,GAAG,KAAK,UACR;GACC,QAAQ;GACR,UAAU,KAAK;GACf,KAAKC,6BAAU,KAAK,SAAS;EAC9B,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
1
+ {"version":3,"file":"deploy.cjs","names":["pulumi","command","stableDir"],"sources":["../../src/railway/deploy.ts"],"sourcesContent":["import * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { stableDir } from \"../stable-dir\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport { RailwayBuilder, type RailwayService } from \"./service\";\n\n/** Build and deploy configuration for a Railway service. */\nexport interface RailwayDeployConfig {\n\t/** Build system to use when building the service. */\n\tbuilder?: RailwayBuilder;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: string;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: string;\n}\n\n/** Args for RailwayDeploy. */\nexport interface RailwayDeployArgs {\n\t/**\n\t * Absolute path to the monorepo root (working directory for `railway up`).\n\t * Stored relative to the Pulumi program directory so the command stays stable\n\t * across machines and CI (see {@link stableDir}).\n\t */\n\tdirectory: string;\n\n\t/** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\n\t/** Directories to exclude via `.railwayignore`. */\n\texcludePaths?: string[];\n\n\t/** Railpack configuration written to `railpack.json` before deploy. */\n\trailpackConfig?: Record<string, unknown>;\n}\n\n/** Options type for RailwayDeploy — replaces Pulumi's native `provider` field. */\ntype RailwayDeployOptions = 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\t/** Environment-scoped Railway deploy token. Provision via {@link RailwayProjectToken}. */\n\tprojectToken: pulumi.Input<string>;\n};\n\nconst LOCK_DIR = \"/tmp/.railway-upload-lock\";\n\n/**\n * Deploys a Railway service and waits for the deployment to reach a terminal status.\n *\n * `railway up --ci` only blocks until the build UPLOAD completes; Railway's image\n * finalization, the pre-deploy hook (e.g. migrations), the health check, and promotion\n * all run AFTER the CLI returns. So after `up` succeeds, this polls the Railway GraphQL API\n * for the deployment's status and **fails the resource (exit 1) on FAILED / CRASHED /\n * REMOVED** — without it, a deployment that fails post-build is reported as a successful\n * `pulumi up` and the previous version silently stays live. Set `INFRACRAFT_SKIP_DEPLOY_WAIT`\n * to bypass the wait. Status reads use the env-scoped project token via the\n * `Project-Access-Token` header (Railway rejects project tokens on `Authorization: Bearer`);\n * the poll runs under `node` (Pulumi's nodejs runtime) and uses `fetch` (Node 18+).\n *\n * Multiple deploys run in parallel — a mkdir lock serializes only the\n * brief upload phase (~5s) when `.railwayignore` must be consistent,\n * then releases so builds stream concurrently.\n *\n * @example\n * ```typescript\n * new RailwayDeploy(\"api-deploy\", {\n * directory: monorepoRoot,\n * triggers: [sourceHash],\n * }, { provider, project, environment, service, projectToken: stagingToken.token });\n * ```\n */\nexport class RailwayDeploy extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayDeployArgs,\n\t\topts: RailwayDeployOptions,\n\t) {\n\t\tconst {\n\t\t\tprovider,\n\t\t\tproject,\n\t\t\tenvironment,\n\t\t\tservice,\n\t\t\tprojectToken,\n\t\t\t...pulumiOpts\n\t\t} = opts;\n\n\t\tsuper(\"infracraft:railway:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst ignorePatterns = (args.excludePaths ?? [])\n\t\t\t.map((dir) => {\n\t\t\t\tif (dir.startsWith(\"apps/\")) {\n\t\t\t\t\treturn `${dir}/**\\\\n!${dir}/package.json`;\n\t\t\t\t}\n\n\t\t\t\treturn dir;\n\t\t\t})\n\t\t\t.join(\"\\\\n\");\n\n\t\tconst writeIgnore = ignorePatterns\n\t\t\t? `printf '${ignorePatterns}\\\\n' > .railwayignore`\n\t\t\t: \"\";\n\n\t\tconst writeRailpack = args.railpackConfig\n\t\t\t? `printf '${JSON.stringify(args.railpackConfig).replace(/'/g, \"\\\\'\")}' > railpack.json`\n\t\t\t: \"\";\n\n\t\tconst setupLines = [writeIgnore, writeRailpack].filter(Boolean).join(\"; \");\n\n\t\t// The deploy token is inlined into the create command (RAILWAY_TOKEN=… railway up) rather\n\t\t// than passed via command.local.Command's `environment` map. The token is auto-minted (a\n\t\t// RailwayProjectToken), so it is an UNKNOWN secret at preview time, and an unknown secret in\n\t\t// the `environment` map makes `pulumi preview` fail (\"malformed RPC secret: missing value\").\n\t\t// Inlining keeps the token secret in state while letting preview serialize cleanly.\n\t\t// Polls the Railway deployment to a terminal status after `railway up` returns. Runs via\n\t\t// `node -e` and is single-quoted in the shell, so the script uses ONLY double quotes — no\n\t\t// single quotes, no backticks, no `${…}` — and reads its inputs from IC_* env vars. The\n\t\t// `$p`/`$e`/`$s`/`$d` tokens are GraphQL variables (the shell single-quoting leaves them\n\t\t// literal). Exits 0 on SUCCESS (or if the deployment id can't be resolved — build already\n\t\t// succeeded), and exits 1 (failing the Pulumi resource) on FAILED/CRASHED/REMOVED or timeout.\n\t\tconst deployWaitScript = `const t=process.env.IC_TOK,p=process.env.IC_PROJ,e=process.env.IC_ENV,s=process.env.IC_SVC;const u=\"https://backboard.railway.app/graphql/v2\";const q=(query,variables)=>fetch(u,{method:\"POST\",headers:{\"Project-Access-Token\":t,\"Content-Type\":\"application/json\"},body:JSON.stringify({query,variables})}).then(r=>r.json()).catch(()=>({}));const sl=ms=>new Promise(r=>setTimeout(r,ms));(async()=>{let id;for(let i=0;i<6&&!id;i++){const d=await q(\"query($p:String!,$e:String!,$s:String!){deployments(first:1,input:{projectId:$p,environmentId:$e,serviceId:$s}){edges{node{id status}}}}\",{p,e,s});const g=d&&d.data&&d.data.deployments&&d.data.deployments.edges;if(g&&g[0])id=g[0].node.id;if(!id)await sl(5000);}if(!id){console.error(\"[infracraft] could not resolve Railway deployment id; build succeeded, not blocking\");process.exit(0);}for(let i=0;i<120;i++){const r=await q(\"query($d:String!){deployment(id:$d){status}}\",{d:id});const st=r&&r.data&&r.data.deployment&&r.data.deployment.status;if(st)console.error(\"[infracraft] railway deployment \"+id+\" status=\"+st);if(st===\"SUCCESS\")process.exit(0);if(st===\"FAILED\"||st===\"CRASHED\"||st===\"REMOVED\"){const l=await q(\"query($d:String!){deploymentLogs(deploymentId:$d,limit:60){message}}\",{d:id});const lg=(l&&l.data&&l.data.deploymentLogs)||[];for(const x of lg)console.error(\" \"+(x.message||\"\"));console.error(\"[infracraft] railway deployment \"+id+\" \"+st+\" — failing the Pulumi resource\");process.exit(1);}await sl(10000);}console.error(\"[infracraft] timed out waiting for Railway deployment \"+id);process.exit(1);})();`;\n\n\t\tconst deployCmd = pulumi.interpolate`while ! mkdir ${LOCK_DIR} 2>/dev/null; do sleep 1; done; ${setupLines}; { sleep 5; rm -f .railwayignore railpack.json; rmdir ${LOCK_DIR} 2>/dev/null; } & RAILWAY_TOKEN=${projectToken} railway up --ci --project ${project.id} --service ${service.id} --environment ${environment.id}; EXIT=$?; rm -f .railwayignore railpack.json; rmdir ${LOCK_DIR} 2>/dev/null; wait; if [ \"$EXIT\" -ne 0 ]; then exit \"$EXIT\"; fi; if [ -n \"$INFRACRAFT_SKIP_DEPLOY_WAIT\" ]; then exit 0; fi; IC_TOK=${projectToken} IC_PROJ=${project.id} IC_ENV=${environment.id} IC_SVC=${service.id} node -e '${deployWaitScript}'`;\n\n\t\tnew command.local.Command(\n\t\t\t`${name}-deploy`,\n\t\t\t{\n\t\t\t\tcreate: deployCmd,\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\tdir: stableDir(args.directory),\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;;;;AA4DA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BjB,IAAa,gBAAb,cAAmCA,eAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EACL,UACA,SACA,aACA,SACA,cACA,GAAG,eACA;EAEJ,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,MAAM,kBAAkB,KAAK,gBAAgB,CAAC,GAC5C,KAAK,QAAQ;GACb,IAAI,IAAI,WAAW,OAAO,GACzB,OAAO,GAAG,IAAI,SAAS,IAAI;GAG5B,OAAO;EACR,CAAC,EACA,KAAK,KAAK;EAUZ,MAAM,aAAa,CARC,iBACjB,WAAW,eAAe,yBAC1B,IAEmB,KAAK,iBACxB,WAAW,KAAK,UAAU,KAAK,cAAc,EAAE,QAAQ,MAAM,KAAK,EAAE,qBACpE,EAE2C,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;EAezE,MAAM,YAAY,eAAO,WAAW,iBAAiB,SAAS,kCAAkC,WAAW,yDAAyD,SAAS,kCAAkC,aAAa,6BAA6B,QAAQ,GAAG,aAAa,QAAQ,GAAG,iBAAiB,YAAY,GAAG,uDAAuD,SAAS,qIAAqI,aAAa,WAAW,QAAQ,GAAG,UAAU,YAAY,GAAG,UAAU,QAAQ,GAAG,YAAY,kiDAAiB;EAE/mB,IAAIC,gBAAQ,MAAM,QACjB,GAAG,KAAK,UACR;GACC,QAAQ;GACR,UAAU,KAAK;GACf,KAAKC,6BAAU,KAAK,SAAS;EAC9B,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
@@ -39,9 +39,18 @@ type RailwayDeployOptions = Omit<pulumi.ComponentResourceOptions, "provider"> &
39
39
  projectToken: pulumi.Input<string>;
40
40
  };
41
41
  /**
42
- * Deploys a Railway service and waits for the build to complete.
42
+ * Deploys a Railway service and waits for the deployment to reach a terminal status.
43
+ *
44
+ * `railway up --ci` only blocks until the build UPLOAD completes; Railway's image
45
+ * finalization, the pre-deploy hook (e.g. migrations), the health check, and promotion
46
+ * all run AFTER the CLI returns. So after `up` succeeds, this polls the Railway GraphQL API
47
+ * for the deployment's status and **fails the resource (exit 1) on FAILED / CRASHED /
48
+ * REMOVED** — without it, a deployment that fails post-build is reported as a successful
49
+ * `pulumi up` and the previous version silently stays live. Set `INFRACRAFT_SKIP_DEPLOY_WAIT`
50
+ * to bypass the wait. Status reads use the env-scoped project token via the
51
+ * `Project-Access-Token` header (Railway rejects project tokens on `Authorization: Bearer`);
52
+ * the poll runs under `node` (Pulumi's nodejs runtime) and uses `fetch` (Node 18+).
43
53
  *
44
- * Uses `railway up --ci` which blocks until the build finishes.
45
54
  * Multiple deploys run in parallel — a mkdir lock serializes only the
46
55
  * brief upload phase (~5s) when `.railwayignore` must be consistent,
47
56
  * then releases so builds stream concurrently.
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/railway/deploy.ts"],"mappings":";;;;;;;;;UASiB,mBAAA;;EAEhB,OAAA,GAAU,cAAc;EAFW;EAKnC,YAAA;EAHwB;EAMxB,gBAAA;AAAA;;UAIgB,iBAAA;EAJA;AAAA;AAIjB;;;EAMC,SAAA;EAGU;EAAV,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAMP;EAHvB,YAAA;EANA;EASA,cAAA,GAAiB,MAAA;AAAA;;KAIb,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAXuB,sCAe9B,QAAA,EAAU,eAAA,EATV;EAYA,OAAA,EAAS,cAAA,EAZc;EAevB,WAAA,EAAa,kBAAA,EAXT;EAcJ,OAAA,EAAS,cAAA;EAGT,YAAA,EAAc,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;;;;;;cAqBT,aAAA,SAAsB,MAAA,CAAO,iBAAA;cAExC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
1
+ {"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/railway/deploy.ts"],"mappings":";;;;;;;;;UASiB,mBAAA;;EAEhB,OAAA,GAAU,cAAc;EAFW;EAKnC,YAAA;EAHwB;EAMxB,gBAAA;AAAA;;UAIgB,iBAAA;EAJA;AAAA;AAIjB;;;EAMC,SAAA;EAGU;EAAV,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAMP;EAHvB,YAAA;EANA;EASA,cAAA,GAAiB,MAAA;AAAA;;KAIb,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAXuB,sCAe9B,QAAA,EAAU,eAAA,EATV;EAYA,OAAA,EAAS,cAAA,EAZc;EAevB,WAAA,EAAa,kBAAA,EAXT;EAcJ,OAAA,EAAS,cAAA;EAGT,YAAA,EAAc,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;AAAK;AA8B3B;;cAAa,aAAA,SAAsB,MAAA,CAAO,iBAAA;cAExC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
@@ -39,9 +39,18 @@ type RailwayDeployOptions = Omit<pulumi.ComponentResourceOptions, "provider"> &
39
39
  projectToken: pulumi.Input<string>;
40
40
  };
41
41
  /**
42
- * Deploys a Railway service and waits for the build to complete.
42
+ * Deploys a Railway service and waits for the deployment to reach a terminal status.
43
+ *
44
+ * `railway up --ci` only blocks until the build UPLOAD completes; Railway's image
45
+ * finalization, the pre-deploy hook (e.g. migrations), the health check, and promotion
46
+ * all run AFTER the CLI returns. So after `up` succeeds, this polls the Railway GraphQL API
47
+ * for the deployment's status and **fails the resource (exit 1) on FAILED / CRASHED /
48
+ * REMOVED** — without it, a deployment that fails post-build is reported as a successful
49
+ * `pulumi up` and the previous version silently stays live. Set `INFRACRAFT_SKIP_DEPLOY_WAIT`
50
+ * to bypass the wait. Status reads use the env-scoped project token via the
51
+ * `Project-Access-Token` header (Railway rejects project tokens on `Authorization: Bearer`);
52
+ * the poll runs under `node` (Pulumi's nodejs runtime) and uses `fetch` (Node 18+).
43
53
  *
44
- * Uses `railway up --ci` which blocks until the build finishes.
45
54
  * Multiple deploys run in parallel — a mkdir lock serializes only the
46
55
  * brief upload phase (~5s) when `.railwayignore` must be consistent,
47
56
  * then releases so builds stream concurrently.
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/railway/deploy.ts"],"mappings":";;;;;;;;;UASiB,mBAAA;;EAEhB,OAAA,GAAU,cAAc;EAFW;EAKnC,YAAA;EAHwB;EAMxB,gBAAA;AAAA;;UAIgB,iBAAA;EAJA;AAAA;AAIjB;;;EAMC,SAAA;EAGU;EAAV,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAMP;EAHvB,YAAA;EANA;EASA,cAAA,GAAiB,MAAA;AAAA;;KAIb,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAXuB,sCAe9B,QAAA,EAAU,eAAA,EATV;EAYA,OAAA,EAAS,cAAA,EAZc;EAevB,WAAA,EAAa,kBAAA,EAXT;EAcJ,OAAA,EAAS,cAAA;EAGT,YAAA,EAAc,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;;;;;;cAqBT,aAAA,SAAsB,MAAA,CAAO,iBAAA;cAExC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
1
+ {"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/railway/deploy.ts"],"mappings":";;;;;;;;;UASiB,mBAAA;;EAEhB,OAAA,GAAU,cAAc;EAFW;EAKnC,YAAA;EAHwB;EAMxB,gBAAA;AAAA;;UAIgB,iBAAA;EAJA;AAAA;AAIjB;;;EAMC,SAAA;EAGU;EAAV,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAMP;EAHvB,YAAA;EANA;EASA,cAAA,GAAiB,MAAA;AAAA;;KAIb,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAXuB,sCAe9B,QAAA,EAAU,eAAA,EATV;EAYA,OAAA,EAAS,cAAA,EAZc;EAevB,WAAA,EAAa,kBAAA,EAXT;EAcJ,OAAA,EAAS,cAAA;EAGT,YAAA,EAAc,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;AAAK;AA8B3B;;cAAa,aAAA,SAAsB,MAAA,CAAO,iBAAA;cAExC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
@@ -6,9 +6,18 @@ import * as pulumi from "@pulumi/pulumi";
6
6
  //#region src/railway/deploy.ts
7
7
  const LOCK_DIR = "/tmp/.railway-upload-lock";
8
8
  /**
9
- * Deploys a Railway service and waits for the build to complete.
9
+ * Deploys a Railway service and waits for the deployment to reach a terminal status.
10
+ *
11
+ * `railway up --ci` only blocks until the build UPLOAD completes; Railway's image
12
+ * finalization, the pre-deploy hook (e.g. migrations), the health check, and promotion
13
+ * all run AFTER the CLI returns. So after `up` succeeds, this polls the Railway GraphQL API
14
+ * for the deployment's status and **fails the resource (exit 1) on FAILED / CRASHED /
15
+ * REMOVED** — without it, a deployment that fails post-build is reported as a successful
16
+ * `pulumi up` and the previous version silently stays live. Set `INFRACRAFT_SKIP_DEPLOY_WAIT`
17
+ * to bypass the wait. Status reads use the env-scoped project token via the
18
+ * `Project-Access-Token` header (Railway rejects project tokens on `Authorization: Bearer`);
19
+ * the poll runs under `node` (Pulumi's nodejs runtime) and uses `fetch` (Node 18+).
10
20
  *
11
- * Uses `railway up --ci` which blocks until the build finishes.
12
21
  * Multiple deploys run in parallel — a mkdir lock serializes only the
13
22
  * brief upload phase (~5s) when `.railwayignore` must be consistent,
14
23
  * then releases so builds stream concurrently.
@@ -30,7 +39,7 @@ var RailwayDeploy = class extends pulumi.ComponentResource {
30
39
  return dir;
31
40
  }).join("\\n");
32
41
  const setupLines = [ignorePatterns ? `printf '${ignorePatterns}\\n' > .railwayignore` : "", args.railpackConfig ? `printf '${JSON.stringify(args.railpackConfig).replace(/'/g, "\\'")}' > railpack.json` : ""].filter(Boolean).join("; ");
33
- const deployCmd = pulumi.interpolate`while ! mkdir ${LOCK_DIR} 2>/dev/null; do sleep 1; done; ${setupLines}; { sleep 5; rm -f .railwayignore railpack.json; rmdir ${LOCK_DIR} 2>/dev/null; } & RAILWAY_TOKEN=${projectToken} railway up --ci --project ${project.id} --service ${service.id} --environment ${environment.id}; EXIT=$?; rm -f .railwayignore railpack.json; rmdir ${LOCK_DIR} 2>/dev/null; wait; exit $EXIT`;
42
+ const deployCmd = pulumi.interpolate`while ! mkdir ${LOCK_DIR} 2>/dev/null; do sleep 1; done; ${setupLines}; { sleep 5; rm -f .railwayignore railpack.json; rmdir ${LOCK_DIR} 2>/dev/null; } & RAILWAY_TOKEN=${projectToken} railway up --ci --project ${project.id} --service ${service.id} --environment ${environment.id}; EXIT=$?; rm -f .railwayignore railpack.json; rmdir ${LOCK_DIR} 2>/dev/null; wait; if [ "$EXIT" -ne 0 ]; then exit "$EXIT"; fi; if [ -n "$INFRACRAFT_SKIP_DEPLOY_WAIT" ]; then exit 0; fi; IC_TOK=${projectToken} IC_PROJ=${project.id} IC_ENV=${environment.id} IC_SVC=${service.id} node -e '${`const t=process.env.IC_TOK,p=process.env.IC_PROJ,e=process.env.IC_ENV,s=process.env.IC_SVC;const u="https://backboard.railway.app/graphql/v2";const q=(query,variables)=>fetch(u,{method:"POST",headers:{"Project-Access-Token":t,"Content-Type":"application/json"},body:JSON.stringify({query,variables})}).then(r=>r.json()).catch(()=>({}));const sl=ms=>new Promise(r=>setTimeout(r,ms));(async()=>{let id;for(let i=0;i<6&&!id;i++){const d=await q("query($p:String!,$e:String!,$s:String!){deployments(first:1,input:{projectId:$p,environmentId:$e,serviceId:$s}){edges{node{id status}}}}",{p,e,s});const g=d&&d.data&&d.data.deployments&&d.data.deployments.edges;if(g&&g[0])id=g[0].node.id;if(!id)await sl(5000);}if(!id){console.error("[infracraft] could not resolve Railway deployment id; build succeeded, not blocking");process.exit(0);}for(let i=0;i<120;i++){const r=await q("query($d:String!){deployment(id:$d){status}}",{d:id});const st=r&&r.data&&r.data.deployment&&r.data.deployment.status;if(st)console.error("[infracraft] railway deployment "+id+" status="+st);if(st==="SUCCESS")process.exit(0);if(st==="FAILED"||st==="CRASHED"||st==="REMOVED"){const l=await q("query($d:String!){deploymentLogs(deploymentId:$d,limit:60){message}}",{d:id});const lg=(l&&l.data&&l.data.deploymentLogs)||[];for(const x of lg)console.error(" "+(x.message||""));console.error("[infracraft] railway deployment "+id+" "+st+" — failing the Pulumi resource");process.exit(1);}await sl(10000);}console.error("[infracraft] timed out waiting for Railway deployment "+id);process.exit(1);})();`}'`;
34
43
  new command.local.Command(`${name}-deploy`, {
35
44
  create: deployCmd,
36
45
  triggers: args.triggers,
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.mjs","names":[],"sources":["../../src/railway/deploy.ts"],"sourcesContent":["import * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { stableDir } from \"../stable-dir\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport { RailwayBuilder, type RailwayService } from \"./service\";\n\n/** Build and deploy configuration for a Railway service. */\nexport interface RailwayDeployConfig {\n\t/** Build system to use when building the service. */\n\tbuilder?: RailwayBuilder;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: string;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: string;\n}\n\n/** Args for RailwayDeploy. */\nexport interface RailwayDeployArgs {\n\t/**\n\t * Absolute path to the monorepo root (working directory for `railway up`).\n\t * Stored relative to the Pulumi program directory so the command stays stable\n\t * across machines and CI (see {@link stableDir}).\n\t */\n\tdirectory: string;\n\n\t/** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\n\t/** Directories to exclude via `.railwayignore`. */\n\texcludePaths?: string[];\n\n\t/** Railpack configuration written to `railpack.json` before deploy. */\n\trailpackConfig?: Record<string, unknown>;\n}\n\n/** Options type for RailwayDeploy — replaces Pulumi's native `provider` field. */\ntype RailwayDeployOptions = 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\t/** Environment-scoped Railway deploy token. Provision via {@link RailwayProjectToken}. */\n\tprojectToken: pulumi.Input<string>;\n};\n\nconst LOCK_DIR = \"/tmp/.railway-upload-lock\";\n\n/**\n * Deploys a Railway service and waits for the build to complete.\n *\n * Uses `railway up --ci` which blocks until the build finishes.\n * Multiple deploys run in parallel — a mkdir lock serializes only the\n * brief upload phase (~5s) when `.railwayignore` must be consistent,\n * then releases so builds stream concurrently.\n *\n * @example\n * ```typescript\n * new RailwayDeploy(\"api-deploy\", {\n * directory: monorepoRoot,\n * triggers: [sourceHash],\n * }, { provider, project, environment, service, projectToken: stagingToken.token });\n * ```\n */\nexport class RailwayDeploy extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayDeployArgs,\n\t\topts: RailwayDeployOptions,\n\t) {\n\t\tconst {\n\t\t\tprovider,\n\t\t\tproject,\n\t\t\tenvironment,\n\t\t\tservice,\n\t\t\tprojectToken,\n\t\t\t...pulumiOpts\n\t\t} = opts;\n\n\t\tsuper(\"infracraft:railway:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst ignorePatterns = (args.excludePaths ?? [])\n\t\t\t.map((dir) => {\n\t\t\t\tif (dir.startsWith(\"apps/\")) {\n\t\t\t\t\treturn `${dir}/**\\\\n!${dir}/package.json`;\n\t\t\t\t}\n\n\t\t\t\treturn dir;\n\t\t\t})\n\t\t\t.join(\"\\\\n\");\n\n\t\tconst writeIgnore = ignorePatterns\n\t\t\t? `printf '${ignorePatterns}\\\\n' > .railwayignore`\n\t\t\t: \"\";\n\n\t\tconst writeRailpack = args.railpackConfig\n\t\t\t? `printf '${JSON.stringify(args.railpackConfig).replace(/'/g, \"\\\\'\")}' > railpack.json`\n\t\t\t: \"\";\n\n\t\tconst setupLines = [writeIgnore, writeRailpack].filter(Boolean).join(\"; \");\n\n\t\t// The deploy token is inlined into the create command (RAILWAY_TOKEN=… railway up) rather\n\t\t// than passed via command.local.Command's `environment` map. The token is auto-minted (a\n\t\t// RailwayProjectToken), so it is an UNKNOWN secret at preview time, and an unknown secret in\n\t\t// the `environment` map makes `pulumi preview` fail (\"malformed RPC secret: missing value\").\n\t\t// Inlining keeps the token secret in state while letting preview serialize cleanly.\n\t\tconst deployCmd = pulumi.interpolate`while ! mkdir ${LOCK_DIR} 2>/dev/null; do sleep 1; done; ${setupLines}; { sleep 5; rm -f .railwayignore railpack.json; rmdir ${LOCK_DIR} 2>/dev/null; } & RAILWAY_TOKEN=${projectToken} railway up --ci --project ${project.id} --service ${service.id} --environment ${environment.id}; EXIT=$?; rm -f .railwayignore railpack.json; rmdir ${LOCK_DIR} 2>/dev/null; wait; exit $EXIT`;\n\n\t\tnew command.local.Command(\n\t\t\t`${name}-deploy`,\n\t\t\t{\n\t\t\t\tcreate: deployCmd,\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\tdir: stableDir(args.directory),\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;AA4DA,MAAM,WAAW;;;;;;;;;;;;;;;;;AAkBjB,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EACL,UACA,SACA,aACA,SACA,cACA,GAAG,eACA;EAEJ,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,MAAM,kBAAkB,KAAK,gBAAgB,CAAC,GAC5C,KAAK,QAAQ;GACb,IAAI,IAAI,WAAW,OAAO,GACzB,OAAO,GAAG,IAAI,SAAS,IAAI;GAG5B,OAAO;EACR,CAAC,EACA,KAAK,KAAK;EAUZ,MAAM,aAAa,CARC,iBACjB,WAAW,eAAe,yBAC1B,IAEmB,KAAK,iBACxB,WAAW,KAAK,UAAU,KAAK,cAAc,EAAE,QAAQ,MAAM,KAAK,EAAE,qBACpE,EAE2C,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;EAOzE,MAAM,YAAY,OAAO,WAAW,iBAAiB,SAAS,kCAAkC,WAAW,yDAAyD,SAAS,kCAAkC,aAAa,6BAA6B,QAAQ,GAAG,aAAa,QAAQ,GAAG,iBAAiB,YAAY,GAAG,uDAAuD,SAAS;EAE5X,IAAI,QAAQ,MAAM,QACjB,GAAG,KAAK,UACR;GACC,QAAQ;GACR,UAAU,KAAK;GACf,KAAK,UAAU,KAAK,SAAS;EAC9B,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
1
+ {"version":3,"file":"deploy.mjs","names":[],"sources":["../../src/railway/deploy.ts"],"sourcesContent":["import * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { stableDir } from \"../stable-dir\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport { RailwayBuilder, type RailwayService } from \"./service\";\n\n/** Build and deploy configuration for a Railway service. */\nexport interface RailwayDeployConfig {\n\t/** Build system to use when building the service. */\n\tbuilder?: RailwayBuilder;\n\n\t/** Shell command executed to start the service at runtime. */\n\tstartCommand?: string;\n\n\t/** Shell command executed before the main deploy (e.g. migrations). */\n\tpreDeployCommand?: string;\n}\n\n/** Args for RailwayDeploy. */\nexport interface RailwayDeployArgs {\n\t/**\n\t * Absolute path to the monorepo root (working directory for `railway up`).\n\t * Stored relative to the Pulumi program directory so the command stays stable\n\t * across machines and CI (see {@link stableDir}).\n\t */\n\tdirectory: string;\n\n\t/** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\n\t/** Directories to exclude via `.railwayignore`. */\n\texcludePaths?: string[];\n\n\t/** Railpack configuration written to `railpack.json` before deploy. */\n\trailpackConfig?: Record<string, unknown>;\n}\n\n/** Options type for RailwayDeploy — replaces Pulumi's native `provider` field. */\ntype RailwayDeployOptions = 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\t/** Environment-scoped Railway deploy token. Provision via {@link RailwayProjectToken}. */\n\tprojectToken: pulumi.Input<string>;\n};\n\nconst LOCK_DIR = \"/tmp/.railway-upload-lock\";\n\n/**\n * Deploys a Railway service and waits for the deployment to reach a terminal status.\n *\n * `railway up --ci` only blocks until the build UPLOAD completes; Railway's image\n * finalization, the pre-deploy hook (e.g. migrations), the health check, and promotion\n * all run AFTER the CLI returns. So after `up` succeeds, this polls the Railway GraphQL API\n * for the deployment's status and **fails the resource (exit 1) on FAILED / CRASHED /\n * REMOVED** — without it, a deployment that fails post-build is reported as a successful\n * `pulumi up` and the previous version silently stays live. Set `INFRACRAFT_SKIP_DEPLOY_WAIT`\n * to bypass the wait. Status reads use the env-scoped project token via the\n * `Project-Access-Token` header (Railway rejects project tokens on `Authorization: Bearer`);\n * the poll runs under `node` (Pulumi's nodejs runtime) and uses `fetch` (Node 18+).\n *\n * Multiple deploys run in parallel — a mkdir lock serializes only the\n * brief upload phase (~5s) when `.railwayignore` must be consistent,\n * then releases so builds stream concurrently.\n *\n * @example\n * ```typescript\n * new RailwayDeploy(\"api-deploy\", {\n * directory: monorepoRoot,\n * triggers: [sourceHash],\n * }, { provider, project, environment, service, projectToken: stagingToken.token });\n * ```\n */\nexport class RailwayDeploy extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayDeployArgs,\n\t\topts: RailwayDeployOptions,\n\t) {\n\t\tconst {\n\t\t\tprovider,\n\t\t\tproject,\n\t\t\tenvironment,\n\t\t\tservice,\n\t\t\tprojectToken,\n\t\t\t...pulumiOpts\n\t\t} = opts;\n\n\t\tsuper(\"infracraft:railway:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst ignorePatterns = (args.excludePaths ?? [])\n\t\t\t.map((dir) => {\n\t\t\t\tif (dir.startsWith(\"apps/\")) {\n\t\t\t\t\treturn `${dir}/**\\\\n!${dir}/package.json`;\n\t\t\t\t}\n\n\t\t\t\treturn dir;\n\t\t\t})\n\t\t\t.join(\"\\\\n\");\n\n\t\tconst writeIgnore = ignorePatterns\n\t\t\t? `printf '${ignorePatterns}\\\\n' > .railwayignore`\n\t\t\t: \"\";\n\n\t\tconst writeRailpack = args.railpackConfig\n\t\t\t? `printf '${JSON.stringify(args.railpackConfig).replace(/'/g, \"\\\\'\")}' > railpack.json`\n\t\t\t: \"\";\n\n\t\tconst setupLines = [writeIgnore, writeRailpack].filter(Boolean).join(\"; \");\n\n\t\t// The deploy token is inlined into the create command (RAILWAY_TOKEN=… railway up) rather\n\t\t// than passed via command.local.Command's `environment` map. The token is auto-minted (a\n\t\t// RailwayProjectToken), so it is an UNKNOWN secret at preview time, and an unknown secret in\n\t\t// the `environment` map makes `pulumi preview` fail (\"malformed RPC secret: missing value\").\n\t\t// Inlining keeps the token secret in state while letting preview serialize cleanly.\n\t\t// Polls the Railway deployment to a terminal status after `railway up` returns. Runs via\n\t\t// `node -e` and is single-quoted in the shell, so the script uses ONLY double quotes — no\n\t\t// single quotes, no backticks, no `${…}` — and reads its inputs from IC_* env vars. The\n\t\t// `$p`/`$e`/`$s`/`$d` tokens are GraphQL variables (the shell single-quoting leaves them\n\t\t// literal). Exits 0 on SUCCESS (or if the deployment id can't be resolved — build already\n\t\t// succeeded), and exits 1 (failing the Pulumi resource) on FAILED/CRASHED/REMOVED or timeout.\n\t\tconst deployWaitScript = `const t=process.env.IC_TOK,p=process.env.IC_PROJ,e=process.env.IC_ENV,s=process.env.IC_SVC;const u=\"https://backboard.railway.app/graphql/v2\";const q=(query,variables)=>fetch(u,{method:\"POST\",headers:{\"Project-Access-Token\":t,\"Content-Type\":\"application/json\"},body:JSON.stringify({query,variables})}).then(r=>r.json()).catch(()=>({}));const sl=ms=>new Promise(r=>setTimeout(r,ms));(async()=>{let id;for(let i=0;i<6&&!id;i++){const d=await q(\"query($p:String!,$e:String!,$s:String!){deployments(first:1,input:{projectId:$p,environmentId:$e,serviceId:$s}){edges{node{id status}}}}\",{p,e,s});const g=d&&d.data&&d.data.deployments&&d.data.deployments.edges;if(g&&g[0])id=g[0].node.id;if(!id)await sl(5000);}if(!id){console.error(\"[infracraft] could not resolve Railway deployment id; build succeeded, not blocking\");process.exit(0);}for(let i=0;i<120;i++){const r=await q(\"query($d:String!){deployment(id:$d){status}}\",{d:id});const st=r&&r.data&&r.data.deployment&&r.data.deployment.status;if(st)console.error(\"[infracraft] railway deployment \"+id+\" status=\"+st);if(st===\"SUCCESS\")process.exit(0);if(st===\"FAILED\"||st===\"CRASHED\"||st===\"REMOVED\"){const l=await q(\"query($d:String!){deploymentLogs(deploymentId:$d,limit:60){message}}\",{d:id});const lg=(l&&l.data&&l.data.deploymentLogs)||[];for(const x of lg)console.error(\" \"+(x.message||\"\"));console.error(\"[infracraft] railway deployment \"+id+\" \"+st+\" — failing the Pulumi resource\");process.exit(1);}await sl(10000);}console.error(\"[infracraft] timed out waiting for Railway deployment \"+id);process.exit(1);})();`;\n\n\t\tconst deployCmd = pulumi.interpolate`while ! mkdir ${LOCK_DIR} 2>/dev/null; do sleep 1; done; ${setupLines}; { sleep 5; rm -f .railwayignore railpack.json; rmdir ${LOCK_DIR} 2>/dev/null; } & RAILWAY_TOKEN=${projectToken} railway up --ci --project ${project.id} --service ${service.id} --environment ${environment.id}; EXIT=$?; rm -f .railwayignore railpack.json; rmdir ${LOCK_DIR} 2>/dev/null; wait; if [ \"$EXIT\" -ne 0 ]; then exit \"$EXIT\"; fi; if [ -n \"$INFRACRAFT_SKIP_DEPLOY_WAIT\" ]; then exit 0; fi; IC_TOK=${projectToken} IC_PROJ=${project.id} IC_ENV=${environment.id} IC_SVC=${service.id} node -e '${deployWaitScript}'`;\n\n\t\tnew command.local.Command(\n\t\t\t`${name}-deploy`,\n\t\t\t{\n\t\t\t\tcreate: deployCmd,\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\tdir: stableDir(args.directory),\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;AA4DA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BjB,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EACL,UACA,SACA,aACA,SACA,cACA,GAAG,eACA;EAEJ,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,MAAM,kBAAkB,KAAK,gBAAgB,CAAC,GAC5C,KAAK,QAAQ;GACb,IAAI,IAAI,WAAW,OAAO,GACzB,OAAO,GAAG,IAAI,SAAS,IAAI;GAG5B,OAAO;EACR,CAAC,EACA,KAAK,KAAK;EAUZ,MAAM,aAAa,CARC,iBACjB,WAAW,eAAe,yBAC1B,IAEmB,KAAK,iBACxB,WAAW,KAAK,UAAU,KAAK,cAAc,EAAE,QAAQ,MAAM,KAAK,EAAE,qBACpE,EAE2C,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;EAezE,MAAM,YAAY,OAAO,WAAW,iBAAiB,SAAS,kCAAkC,WAAW,yDAAyD,SAAS,kCAAkC,aAAa,6BAA6B,QAAQ,GAAG,aAAa,QAAQ,GAAG,iBAAiB,YAAY,GAAG,uDAAuD,SAAS,qIAAqI,aAAa,WAAW,QAAQ,GAAG,UAAU,YAAY,GAAG,UAAU,QAAQ,GAAG,YAAY,kiDAAiB;EAE/mB,IAAI,QAAQ,MAAM,QACjB,GAAG,KAAK,UACR;GACC,QAAQ;GACR,UAAU,KAAK;GACf,KAAK,UAAU,KAAK,SAAS;EAC9B,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
@@ -134,7 +134,7 @@ var RailwayVolume = class extends _pulumi_pulumi.ComponentResource {
134
134
  serviceId: service.id,
135
135
  environmentId: environment.id,
136
136
  mountPath: args.mountPath
137
- }, { parent: this });
137
+ }, _pulumi_pulumi.mergeOptions(pulumiOpts, { parent: this }));
138
138
  this.registerOutputs({});
139
139
  }
140
140
  };
@@ -1 +1 @@
1
- {"version":3,"file":"volume.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/volume.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway volume dynamic provider. */\nexport interface RailwayVolumeInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the volume to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Absolute path inside the container where the volume is mounted (e.g. `\"/data\"`). */\n\tmountPath: string;\n}\n\n/** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */\ninterface RailwayVolumeOutputs extends RailwayVolumeInputs {\n\t/** Railway-assigned volume UUID (set after create or adopt). */\n\tvolumeId: string;\n}\n\nconst VOLUME_CREATE = `\n mutation($input: VolumeCreateInput!) {\n volumeCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst VOLUME_DELETE = `\n mutation($volumeId: String!) {\n volumeDelete(volumeId: $volumeId)\n }\n`;\n\nconst VOLUMES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n volumes {\n edges {\n node {\n id\n name\n volumeInstances {\n edges {\n node {\n id\n mountPath\n serviceId\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Finds an existing volume attached to a specific service within a project.\n */\nasync function findVolumeByService(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n): Promise<string | undefined> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tvolumes: {\n\t\t\t\tedges: Array<{\n\t\t\t\t\tnode: {\n\t\t\t\t\t\tid: string;\n\t\t\t\t\t\tvolumeInstances: {\n\t\t\t\t\t\t\tedges: Array<{\n\t\t\t\t\t\t\t\tnode: { serviceId: string };\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t};\n\t}>(VOLUMES_QUERY, { projectId });\n\n\tconst match = result.project.volumes.edges.find((edge) =>\n\t\tedge.node.volumeInstances.edges.some(\n\t\t\t(vi) => vi.node.serviceId === serviceId,\n\t\t),\n\t);\n\n\treturn match?.node.id;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway persistent volumes.\n *\n * Uses adopt-or-create on `create()`: finds an existing volume by service\n * before creating a new one. Volumes are immutable after creation — changing\n * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.\n */\nclass RailwayVolumeResourceProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tlet volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t);\n\n\t\tif (volumeId) {\n\t\t\tpulumi.log.info(`Adopting existing volume for service (${volumeId})`);\n\t\t} else {\n\t\t\tconst result = await client.query<{\n\t\t\t\tvolumeCreate: { id: string };\n\t\t\t}>(VOLUME_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tmountPath: inputs.mountPath,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tvolumeId = result.volumeCreate.id;\n\t\t}\n\n\t\treturn {\n\t\t\tid: volumeId,\n\t\t\touts: { ...inputs, volumeId },\n\t\t};\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.serviceId,\n\t\t);\n\n\t\tif (!volumeId) {\n\t\t\tthrow new Error(\"Railway volume not found during refresh\");\n\t\t}\n\n\t\treturn { id: volumeId, props: { ...props, volumeId } };\n\t}\n\n\tasync delete(id: string, props: RailwayVolumeOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(VOLUME_DELETE, { volumeId: id });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway volume (may already be deleted)\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayVolumeOutputs,\n\t\tnews: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.mountPath !== news.mountPath) {\n\t\t\treplaces.push(\"mountPath\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayVolumeResource extends pulumi.dynamic.Resource {\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tmountPath: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayVolumeResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, volumeId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */\ntype RailwayVolumeOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayVolume. */\nexport interface RailwayVolumeArgs {\n\t/** Absolute path inside the container where the volume is mounted. */\n\tmountPath: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway persistent volume with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new RailwayVolume(\"api-data\", {\n * mountPath: \"/data\",\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayVolume extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayVolumeArgs,\n\t\topts: RailwayVolumeOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Volume\", name, {}, pulumiOpts);\n\n\t\tnew RailwayVolumeResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tmountPath: args.mountPath,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;;AA+BA,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,gBAAgB;;;;;AAMtB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,eAAe,oBACd,QACA,WACA,WAC8B;CAwB9B,QANc,MAjBO,OAAO,MAezB,eAAe,EAAE,UAAU,CAAC,GAEV,QAAQ,QAAQ,MAAM,MAAM,SAChD,KAAK,KAAK,gBAAgB,MAAM,MAC9B,OAAO,GAAG,KAAK,cAAc,SAC/B,CAGU,GAAG,KAAK;AACpB;;;;;;;;AASA,IAAM,gCAAN,MAA+E;CAC9E,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAIA,qCAAc,OAAO,KAAK;EAE7C,IAAI,WAAW,MAAM,oBACpB,QACA,OAAO,WACP,OAAO,SACR;EAEA,IAAI,UACH,eAAO,IAAI,KAAK,yCAAyC,SAAS,EAAE;OAapE,YAAW,MAXU,OAAO,MAEzB,eAAe,EACjB,OAAO;GACN,WAAW,OAAO;GAClB,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,WAAW,OAAO;EACnB,EACD,CAAC,GAEiB,aAAa;EAGhC,OAAO;GACN,IAAI;GACJ,MAAM;IAAE,GAAG;IAAQ;GAAS;EAC7B;CACD;CAEA,MAAM,KACL,KACA,OACqC;EAGrC,MAAM,WAAW,MAAM,oBACtB,IAHkBA,qCAAc,MAAM,KAGjC,GACL,MAAM,WACN,MAAM,SACP;EAEA,IAAI,CAAC,UACJ,MAAM,IAAI,MAAM,yCAAyC;EAG1D,OAAO;GAAE,IAAI;GAAU,OAAO;IAAE,GAAG;IAAO;GAAS;EAAE;CACtD;CAEA,MAAM,OAAO,IAAY,OAA4C;EACpE,MAAM,SAAS,IAAIA,qCAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MAAM,eAAe,EAAE,UAAU,GAAG,CAAC;EACnD,QAAQ;GACP,eAAO,IAAI,KACV,0DACD;EACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,wBAAN,cAAoCC,eAAO,QAAQ,SAAS;CAC3D,YACC,MACA,MAOA,MACC;EACD,MACC,IAAI,8BAA8B,GAClC,MACA;GAAE,GAAG;GAAM,UAAU;EAAU,GAC/B,IACD;CACD;AACD;;;;;;;;;;;AAoCA,IAAa,gBAAb,cAAmCA,eAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,SAAS,GAAG,eAAe;EAEnE,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,IAAI,sBACH,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,WAAW,KAAK;EACjB,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
1
+ {"version":3,"file":"volume.cjs","names":["RailwayClient","pulumi"],"sources":["../../src/railway/volume.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway volume dynamic provider. */\nexport interface RailwayVolumeInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the volume to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Absolute path inside the container where the volume is mounted (e.g. `\"/data\"`). */\n\tmountPath: string;\n}\n\n/** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */\ninterface RailwayVolumeOutputs extends RailwayVolumeInputs {\n\t/** Railway-assigned volume UUID (set after create or adopt). */\n\tvolumeId: string;\n}\n\nconst VOLUME_CREATE = `\n mutation($input: VolumeCreateInput!) {\n volumeCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst VOLUME_DELETE = `\n mutation($volumeId: String!) {\n volumeDelete(volumeId: $volumeId)\n }\n`;\n\nconst VOLUMES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n volumes {\n edges {\n node {\n id\n name\n volumeInstances {\n edges {\n node {\n id\n mountPath\n serviceId\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Finds an existing volume attached to a specific service within a project.\n */\nasync function findVolumeByService(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n): Promise<string | undefined> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tvolumes: {\n\t\t\t\tedges: Array<{\n\t\t\t\t\tnode: {\n\t\t\t\t\t\tid: string;\n\t\t\t\t\t\tvolumeInstances: {\n\t\t\t\t\t\t\tedges: Array<{\n\t\t\t\t\t\t\t\tnode: { serviceId: string };\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t};\n\t}>(VOLUMES_QUERY, { projectId });\n\n\tconst match = result.project.volumes.edges.find((edge) =>\n\t\tedge.node.volumeInstances.edges.some(\n\t\t\t(vi) => vi.node.serviceId === serviceId,\n\t\t),\n\t);\n\n\treturn match?.node.id;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway persistent volumes.\n *\n * Uses adopt-or-create on `create()`: finds an existing volume by service\n * before creating a new one. Volumes are immutable after creation — changing\n * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.\n */\nclass RailwayVolumeResourceProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tlet volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t);\n\n\t\tif (volumeId) {\n\t\t\tpulumi.log.info(`Adopting existing volume for service (${volumeId})`);\n\t\t} else {\n\t\t\tconst result = await client.query<{\n\t\t\t\tvolumeCreate: { id: string };\n\t\t\t}>(VOLUME_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tmountPath: inputs.mountPath,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tvolumeId = result.volumeCreate.id;\n\t\t}\n\n\t\treturn {\n\t\t\tid: volumeId,\n\t\t\touts: { ...inputs, volumeId },\n\t\t};\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.serviceId,\n\t\t);\n\n\t\tif (!volumeId) {\n\t\t\tthrow new Error(\"Railway volume not found during refresh\");\n\t\t}\n\n\t\treturn { id: volumeId, props: { ...props, volumeId } };\n\t}\n\n\tasync delete(id: string, props: RailwayVolumeOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(VOLUME_DELETE, { volumeId: id });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway volume (may already be deleted)\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayVolumeOutputs,\n\t\tnews: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.mountPath !== news.mountPath) {\n\t\t\treplaces.push(\"mountPath\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayVolumeResource extends pulumi.dynamic.Resource {\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tmountPath: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayVolumeResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, volumeId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */\ntype RailwayVolumeOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayVolume. */\nexport interface RailwayVolumeArgs {\n\t/** Absolute path inside the container where the volume is mounted. */\n\tmountPath: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway persistent volume with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new RailwayVolume(\"api-data\", {\n * mountPath: \"/data\",\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayVolume extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayVolumeArgs,\n\t\topts: RailwayVolumeOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Volume\", name, {}, pulumiOpts);\n\n\t\tnew RailwayVolumeResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tmountPath: args.mountPath,\n\t\t\t},\n\t\t\t// Forward the consumer's resource options to the underlying resource. Pulumi\n\t\t\t// auto-inherits `provider`/`protect` from the parent component, but NOT options\n\t\t\t// like `retainOnDelete` — without this pass-through, setting `retainOnDelete` on a\n\t\t\t// RailwayVolume would silently never reach the actual cloud volume.\n\t\t\tpulumi.mergeOptions(pulumiOpts, { parent: this }),\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;;AA+BA,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,gBAAgB;;;;;AAMtB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,eAAe,oBACd,QACA,WACA,WAC8B;CAwB9B,QANc,MAjBO,OAAO,MAezB,eAAe,EAAE,UAAU,CAAC,GAEV,QAAQ,QAAQ,MAAM,MAAM,SAChD,KAAK,KAAK,gBAAgB,MAAM,MAC9B,OAAO,GAAG,KAAK,cAAc,SAC/B,CAGU,GAAG,KAAK;AACpB;;;;;;;;AASA,IAAM,gCAAN,MAA+E;CAC9E,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAIA,qCAAc,OAAO,KAAK;EAE7C,IAAI,WAAW,MAAM,oBACpB,QACA,OAAO,WACP,OAAO,SACR;EAEA,IAAI,UACH,eAAO,IAAI,KAAK,yCAAyC,SAAS,EAAE;OAapE,YAAW,MAXU,OAAO,MAEzB,eAAe,EACjB,OAAO;GACN,WAAW,OAAO;GAClB,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,WAAW,OAAO;EACnB,EACD,CAAC,GAEiB,aAAa;EAGhC,OAAO;GACN,IAAI;GACJ,MAAM;IAAE,GAAG;IAAQ;GAAS;EAC7B;CACD;CAEA,MAAM,KACL,KACA,OACqC;EAGrC,MAAM,WAAW,MAAM,oBACtB,IAHkBA,qCAAc,MAAM,KAGjC,GACL,MAAM,WACN,MAAM,SACP;EAEA,IAAI,CAAC,UACJ,MAAM,IAAI,MAAM,yCAAyC;EAG1D,OAAO;GAAE,IAAI;GAAU,OAAO;IAAE,GAAG;IAAO;GAAS;EAAE;CACtD;CAEA,MAAM,OAAO,IAAY,OAA4C;EACpE,MAAM,SAAS,IAAIA,qCAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MAAM,eAAe,EAAE,UAAU,GAAG,CAAC;EACnD,QAAQ;GACP,eAAO,IAAI,KACV,0DACD;EACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,wBAAN,cAAoCC,eAAO,QAAQ,SAAS;CAC3D,YACC,MACA,MAOA,MACC;EACD,MACC,IAAI,8BAA8B,GAClC,MACA;GAAE,GAAG;GAAM,UAAU;EAAU,GAC/B,IACD;CACD;AACD;;;;;;;;;;;AAoCA,IAAa,gBAAb,cAAmCA,eAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,SAAS,GAAG,eAAe;EAEnE,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,IAAI,sBACH,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,WAAW,KAAK;EACjB,GAKAA,eAAO,aAAa,YAAY,EAAE,QAAQ,KAAK,CAAC,CACjD;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
@@ -132,7 +132,7 @@ var RailwayVolume = class extends pulumi.ComponentResource {
132
132
  serviceId: service.id,
133
133
  environmentId: environment.id,
134
134
  mountPath: args.mountPath
135
- }, { parent: this });
135
+ }, pulumi.mergeOptions(pulumiOpts, { parent: this }));
136
136
  this.registerOutputs({});
137
137
  }
138
138
  };
@@ -1 +1 @@
1
- {"version":3,"file":"volume.mjs","names":[],"sources":["../../src/railway/volume.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway volume dynamic provider. */\nexport interface RailwayVolumeInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the volume to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Absolute path inside the container where the volume is mounted (e.g. `\"/data\"`). */\n\tmountPath: string;\n}\n\n/** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */\ninterface RailwayVolumeOutputs extends RailwayVolumeInputs {\n\t/** Railway-assigned volume UUID (set after create or adopt). */\n\tvolumeId: string;\n}\n\nconst VOLUME_CREATE = `\n mutation($input: VolumeCreateInput!) {\n volumeCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst VOLUME_DELETE = `\n mutation($volumeId: String!) {\n volumeDelete(volumeId: $volumeId)\n }\n`;\n\nconst VOLUMES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n volumes {\n edges {\n node {\n id\n name\n volumeInstances {\n edges {\n node {\n id\n mountPath\n serviceId\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Finds an existing volume attached to a specific service within a project.\n */\nasync function findVolumeByService(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n): Promise<string | undefined> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tvolumes: {\n\t\t\t\tedges: Array<{\n\t\t\t\t\tnode: {\n\t\t\t\t\t\tid: string;\n\t\t\t\t\t\tvolumeInstances: {\n\t\t\t\t\t\t\tedges: Array<{\n\t\t\t\t\t\t\t\tnode: { serviceId: string };\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t};\n\t}>(VOLUMES_QUERY, { projectId });\n\n\tconst match = result.project.volumes.edges.find((edge) =>\n\t\tedge.node.volumeInstances.edges.some(\n\t\t\t(vi) => vi.node.serviceId === serviceId,\n\t\t),\n\t);\n\n\treturn match?.node.id;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway persistent volumes.\n *\n * Uses adopt-or-create on `create()`: finds an existing volume by service\n * before creating a new one. Volumes are immutable after creation — changing\n * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.\n */\nclass RailwayVolumeResourceProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tlet volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t);\n\n\t\tif (volumeId) {\n\t\t\tpulumi.log.info(`Adopting existing volume for service (${volumeId})`);\n\t\t} else {\n\t\t\tconst result = await client.query<{\n\t\t\t\tvolumeCreate: { id: string };\n\t\t\t}>(VOLUME_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tmountPath: inputs.mountPath,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tvolumeId = result.volumeCreate.id;\n\t\t}\n\n\t\treturn {\n\t\t\tid: volumeId,\n\t\t\touts: { ...inputs, volumeId },\n\t\t};\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.serviceId,\n\t\t);\n\n\t\tif (!volumeId) {\n\t\t\tthrow new Error(\"Railway volume not found during refresh\");\n\t\t}\n\n\t\treturn { id: volumeId, props: { ...props, volumeId } };\n\t}\n\n\tasync delete(id: string, props: RailwayVolumeOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(VOLUME_DELETE, { volumeId: id });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway volume (may already be deleted)\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayVolumeOutputs,\n\t\tnews: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.mountPath !== news.mountPath) {\n\t\t\treplaces.push(\"mountPath\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayVolumeResource extends pulumi.dynamic.Resource {\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tmountPath: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayVolumeResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, volumeId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */\ntype RailwayVolumeOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayVolume. */\nexport interface RailwayVolumeArgs {\n\t/** Absolute path inside the container where the volume is mounted. */\n\tmountPath: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway persistent volume with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new RailwayVolume(\"api-data\", {\n * mountPath: \"/data\",\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayVolume extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayVolumeArgs,\n\t\topts: RailwayVolumeOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Volume\", name, {}, pulumiOpts);\n\n\t\tnew RailwayVolumeResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tmountPath: args.mountPath,\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;AA+BA,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,gBAAgB;;;;;AAMtB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,eAAe,oBACd,QACA,WACA,WAC8B;CAwB9B,QANc,MAjBO,OAAO,MAezB,eAAe,EAAE,UAAU,CAAC,GAEV,QAAQ,QAAQ,MAAM,MAAM,SAChD,KAAK,KAAK,gBAAgB,MAAM,MAC9B,OAAO,GAAG,KAAK,cAAc,SAC/B,CAGU,GAAG,KAAK;AACpB;;;;;;;;AASA,IAAM,gCAAN,MAA+E;CAC9E,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAI,cAAc,OAAO,KAAK;EAE7C,IAAI,WAAW,MAAM,oBACpB,QACA,OAAO,WACP,OAAO,SACR;EAEA,IAAI,UACH,OAAO,IAAI,KAAK,yCAAyC,SAAS,EAAE;OAapE,YAAW,MAXU,OAAO,MAEzB,eAAe,EACjB,OAAO;GACN,WAAW,OAAO;GAClB,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,WAAW,OAAO;EACnB,EACD,CAAC,GAEiB,aAAa;EAGhC,OAAO;GACN,IAAI;GACJ,MAAM;IAAE,GAAG;IAAQ;GAAS;EAC7B;CACD;CAEA,MAAM,KACL,KACA,OACqC;EAGrC,MAAM,WAAW,MAAM,oBACtB,IAHkB,cAAc,MAAM,KAGjC,GACL,MAAM,WACN,MAAM,SACP;EAEA,IAAI,CAAC,UACJ,MAAM,IAAI,MAAM,yCAAyC;EAG1D,OAAO;GAAE,IAAI;GAAU,OAAO;IAAE,GAAG;IAAO;GAAS;EAAE;CACtD;CAEA,MAAM,OAAO,IAAY,OAA4C;EACpE,MAAM,SAAS,IAAI,cAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MAAM,eAAe,EAAE,UAAU,GAAG,CAAC;EACnD,QAAQ;GACP,OAAO,IAAI,KACV,0DACD;EACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,wBAAN,cAAoC,OAAO,QAAQ,SAAS;CAC3D,YACC,MACA,MAOA,MACC;EACD,MACC,IAAI,8BAA8B,GAClC,MACA;GAAE,GAAG;GAAM,UAAU;EAAU,GAC/B,IACD;CACD;AACD;;;;;;;;;;;AAoCA,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,SAAS,GAAG,eAAe;EAEnE,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,IAAI,sBACH,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,WAAW,KAAK;EACjB,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
1
+ {"version":3,"file":"volume.mjs","names":[],"sources":["../../src/railway/volume.ts"],"sourcesContent":["import * as pulumi from \"@pulumi/pulumi\";\nimport { RailwayClient } from \"./client\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport type { RailwayService } from \"./service\";\n\n/** Resolved inputs for the Railway volume dynamic provider. */\nexport interface RailwayVolumeInputs {\n\t/** Railway API bearer token. */\n\ttoken: string;\n\n\t/** Railway project UUID. */\n\tprojectId: string;\n\n\t/** Railway service UUID to attach the volume to. */\n\tserviceId: string;\n\n\t/** Railway environment UUID (e.g. production). */\n\tenvironmentId: string;\n\n\t/** Absolute path inside the container where the volume is mounted (e.g. `\"/data\"`). */\n\tmountPath: string;\n}\n\n/** Persisted state for the Railway volume, extending inputs with the Railway-assigned ID. */\ninterface RailwayVolumeOutputs extends RailwayVolumeInputs {\n\t/** Railway-assigned volume UUID (set after create or adopt). */\n\tvolumeId: string;\n}\n\nconst VOLUME_CREATE = `\n mutation($input: VolumeCreateInput!) {\n volumeCreate(input: $input) {\n id\n name\n }\n }\n`;\n\nconst VOLUME_DELETE = `\n mutation($volumeId: String!) {\n volumeDelete(volumeId: $volumeId)\n }\n`;\n\nconst VOLUMES_QUERY = `\n query($projectId: String!) {\n project(id: $projectId) {\n volumes {\n edges {\n node {\n id\n name\n volumeInstances {\n edges {\n node {\n id\n mountPath\n serviceId\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Finds an existing volume attached to a specific service within a project.\n */\nasync function findVolumeByService(\n\tclient: RailwayClient,\n\tprojectId: string,\n\tserviceId: string,\n): Promise<string | undefined> {\n\tconst result = await client.query<{\n\t\tproject: {\n\t\t\tvolumes: {\n\t\t\t\tedges: Array<{\n\t\t\t\t\tnode: {\n\t\t\t\t\t\tid: string;\n\t\t\t\t\t\tvolumeInstances: {\n\t\t\t\t\t\t\tedges: Array<{\n\t\t\t\t\t\t\t\tnode: { serviceId: string };\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t}>;\n\t\t\t};\n\t\t};\n\t}>(VOLUMES_QUERY, { projectId });\n\n\tconst match = result.project.volumes.edges.find((edge) =>\n\t\tedge.node.volumeInstances.edges.some(\n\t\t\t(vi) => vi.node.serviceId === serviceId,\n\t\t),\n\t);\n\n\treturn match?.node.id;\n}\n\n/**\n * Dynamic provider implementing CRUD for Railway persistent volumes.\n *\n * Uses adopt-or-create on `create()`: finds an existing volume by service\n * before creating a new one. Volumes are immutable after creation — changing\n * `serviceId`, `mountPath`, `environmentId`, or `projectId` triggers replacement.\n */\nclass RailwayVolumeResourceProvider implements pulumi.dynamic.ResourceProvider {\n\tasync create(\n\t\tinputs: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.CreateResult> {\n\t\tconst client = new RailwayClient(inputs.token);\n\n\t\tlet volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tinputs.projectId,\n\t\t\tinputs.serviceId,\n\t\t);\n\n\t\tif (volumeId) {\n\t\t\tpulumi.log.info(`Adopting existing volume for service (${volumeId})`);\n\t\t} else {\n\t\t\tconst result = await client.query<{\n\t\t\t\tvolumeCreate: { id: string };\n\t\t\t}>(VOLUME_CREATE, {\n\t\t\t\tinput: {\n\t\t\t\t\tprojectId: inputs.projectId,\n\t\t\t\t\tserviceId: inputs.serviceId,\n\t\t\t\t\tenvironmentId: inputs.environmentId,\n\t\t\t\t\tmountPath: inputs.mountPath,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tvolumeId = result.volumeCreate.id;\n\t\t}\n\n\t\treturn {\n\t\t\tid: volumeId,\n\t\t\touts: { ...inputs, volumeId },\n\t\t};\n\t}\n\n\tasync read(\n\t\t_id: string,\n\t\tprops: RailwayVolumeOutputs,\n\t): Promise<pulumi.dynamic.ReadResult> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\tconst volumeId = await findVolumeByService(\n\t\t\tclient,\n\t\t\tprops.projectId,\n\t\t\tprops.serviceId,\n\t\t);\n\n\t\tif (!volumeId) {\n\t\t\tthrow new Error(\"Railway volume not found during refresh\");\n\t\t}\n\n\t\treturn { id: volumeId, props: { ...props, volumeId } };\n\t}\n\n\tasync delete(id: string, props: RailwayVolumeOutputs): Promise<void> {\n\t\tconst client = new RailwayClient(props.token);\n\n\t\ttry {\n\t\t\tawait client.query(VOLUME_DELETE, { volumeId: id });\n\t\t} catch {\n\t\t\tpulumi.log.warn(\n\t\t\t\t\"Failed to delete Railway volume (may already be deleted)\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync diff(\n\t\t_id: string,\n\t\tolds: RailwayVolumeOutputs,\n\t\tnews: RailwayVolumeInputs,\n\t): Promise<pulumi.dynamic.DiffResult> {\n\t\tconst replaces: string[] = [];\n\n\t\tif (olds.serviceId !== news.serviceId) {\n\t\t\treplaces.push(\"serviceId\");\n\t\t}\n\n\t\tif (olds.mountPath !== news.mountPath) {\n\t\t\treplaces.push(\"mountPath\");\n\t\t}\n\n\t\tif (olds.environmentId !== news.environmentId) {\n\t\t\treplaces.push(\"environmentId\");\n\t\t}\n\n\t\tif (olds.projectId !== news.projectId) {\n\t\t\treplaces.push(\"projectId\");\n\t\t}\n\n\t\treturn {\n\t\t\tchanges: replaces.length > 0,\n\t\t\treplaces,\n\t\t\tdeleteBeforeReplace: true,\n\t\t};\n\t}\n}\n\n/** Internal dynamic resource — not part of the public API. */\nclass RailwayVolumeResource extends pulumi.dynamic.Resource {\n\tconstructor(\n\t\tname: string,\n\t\targs: {\n\t\t\ttoken: pulumi.Input<string>;\n\t\t\tprojectId: pulumi.Input<string>;\n\t\t\tserviceId: pulumi.Input<string>;\n\t\t\tenvironmentId: pulumi.Input<string>;\n\t\t\tmountPath: pulumi.Input<string>;\n\t\t},\n\t\topts?: pulumi.CustomResourceOptions,\n\t) {\n\t\tsuper(\n\t\t\tnew RailwayVolumeResourceProvider(),\n\t\t\tname,\n\t\t\t{ ...args, volumeId: undefined },\n\t\t\topts,\n\t\t);\n\t}\n}\n\n/** Options type for RailwayVolume — replaces Pulumi's native `provider` field. */\ntype RailwayVolumeOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\t/** Railway authentication context. */\n\tprovider: RailwayProvider;\n\n\t/** Railway project context. */\n\tproject: RailwayProject;\n\n\t/** Railway environment context. */\n\tenvironment: RailwayEnvironment;\n\n\t/** Railway service context. */\n\tservice: RailwayService;\n};\n\n/** Args for RailwayVolume. */\nexport interface RailwayVolumeArgs {\n\t/** Absolute path inside the container where the volume is mounted. */\n\tmountPath: pulumi.Input<string>;\n}\n\n/**\n * Manages a Railway persistent volume with adopt-or-create semantics.\n *\n * @example\n * ```typescript\n * new RailwayVolume(\"api-data\", {\n * mountPath: \"/data\",\n * }, { provider, project, environment, service });\n * ```\n */\nexport class RailwayVolume extends pulumi.ComponentResource {\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayVolumeArgs,\n\t\topts: RailwayVolumeOptions,\n\t) {\n\t\tconst { provider, project, environment, service, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:railway:Volume\", name, {}, pulumiOpts);\n\n\t\tnew RailwayVolumeResource(\n\t\t\t`${name}-resource`,\n\t\t\t{\n\t\t\t\ttoken: provider.token,\n\t\t\t\tprojectId: project.id,\n\t\t\t\tserviceId: service.id,\n\t\t\t\tenvironmentId: environment.id,\n\t\t\t\tmountPath: args.mountPath,\n\t\t\t},\n\t\t\t// Forward the consumer's resource options to the underlying resource. Pulumi\n\t\t\t// auto-inherits `provider`/`protect` from the parent component, but NOT options\n\t\t\t// like `retainOnDelete` — without this pass-through, setting `retainOnDelete` on a\n\t\t\t// RailwayVolume would silently never reach the actual cloud volume.\n\t\t\tpulumi.mergeOptions(pulumiOpts, { parent: this }),\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;AA+BA,MAAM,gBAAgB;;;;;;;;AAStB,MAAM,gBAAgB;;;;;AAMtB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,eAAe,oBACd,QACA,WACA,WAC8B;CAwB9B,QANc,MAjBO,OAAO,MAezB,eAAe,EAAE,UAAU,CAAC,GAEV,QAAQ,QAAQ,MAAM,MAAM,SAChD,KAAK,KAAK,gBAAgB,MAAM,MAC9B,OAAO,GAAG,KAAK,cAAc,SAC/B,CAGU,GAAG,KAAK;AACpB;;;;;;;;AASA,IAAM,gCAAN,MAA+E;CAC9E,MAAM,OACL,QACuC;EACvC,MAAM,SAAS,IAAI,cAAc,OAAO,KAAK;EAE7C,IAAI,WAAW,MAAM,oBACpB,QACA,OAAO,WACP,OAAO,SACR;EAEA,IAAI,UACH,OAAO,IAAI,KAAK,yCAAyC,SAAS,EAAE;OAapE,YAAW,MAXU,OAAO,MAEzB,eAAe,EACjB,OAAO;GACN,WAAW,OAAO;GAClB,WAAW,OAAO;GAClB,eAAe,OAAO;GACtB,WAAW,OAAO;EACnB,EACD,CAAC,GAEiB,aAAa;EAGhC,OAAO;GACN,IAAI;GACJ,MAAM;IAAE,GAAG;IAAQ;GAAS;EAC7B;CACD;CAEA,MAAM,KACL,KACA,OACqC;EAGrC,MAAM,WAAW,MAAM,oBACtB,IAHkB,cAAc,MAAM,KAGjC,GACL,MAAM,WACN,MAAM,SACP;EAEA,IAAI,CAAC,UACJ,MAAM,IAAI,MAAM,yCAAyC;EAG1D,OAAO;GAAE,IAAI;GAAU,OAAO;IAAE,GAAG;IAAO;GAAS;EAAE;CACtD;CAEA,MAAM,OAAO,IAAY,OAA4C;EACpE,MAAM,SAAS,IAAI,cAAc,MAAM,KAAK;EAE5C,IAAI;GACH,MAAM,OAAO,MAAM,eAAe,EAAE,UAAU,GAAG,CAAC;EACnD,QAAQ;GACP,OAAO,IAAI,KACV,0DACD;EACD;CACD;CAEA,MAAM,KACL,KACA,MACA,MACqC;EACrC,MAAM,WAAqB,CAAC;EAE5B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,IAAI,KAAK,kBAAkB,KAAK,eAC/B,SAAS,KAAK,eAAe;EAG9B,IAAI,KAAK,cAAc,KAAK,WAC3B,SAAS,KAAK,WAAW;EAG1B,OAAO;GACN,SAAS,SAAS,SAAS;GAC3B;GACA,qBAAqB;EACtB;CACD;AACD;;AAGA,IAAM,wBAAN,cAAoC,OAAO,QAAQ,SAAS;CAC3D,YACC,MACA,MAOA,MACC;EACD,MACC,IAAI,8BAA8B,GAClC,MACA;GAAE,GAAG;GAAM,UAAU;EAAU,GAC/B,IACD;CACD;AACD;;;;;;;;;;;AAoCA,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAC3D,YACC,MACA,MACA,MACC;EACD,MAAM,EAAE,UAAU,SAAS,aAAa,SAAS,GAAG,eAAe;EAEnE,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAEvD,IAAI,sBACH,GAAG,KAAK,YACR;GACC,OAAO,SAAS;GAChB,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,eAAe,YAAY;GAC3B,WAAW,KAAK;EACjB,GAKA,OAAO,aAAa,YAAY,EAAE,QAAQ,KAAK,CAAC,CACjD;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
@@ -31,7 +31,7 @@ var VercelDeploy = class extends _pulumi_pulumi.ComponentResource {
31
31
  super("infracraft:vercel:Deploy", name, {}, pulumiOpts);
32
32
  const projectId = project ? project.id : args.projectId;
33
33
  if (!projectId) throw new Error("VercelDeploy: either `args.projectId` or `opts.project` must be provided");
34
- new _pulumi_command.local.Command(`${name}-deploy`, {
34
+ const deployCmd = new _pulumi_command.local.Command(`${name}-deploy`, {
35
35
  create: "vercel deploy --prod --yes",
36
36
  triggers: args.triggers,
37
37
  dir: require_stable_dir.stableDir(args.monorepoRoot),
@@ -41,7 +41,8 @@ var VercelDeploy = class extends _pulumi_pulumi.ComponentResource {
41
41
  VERCEL_PROJECT_ID: projectId
42
42
  }
43
43
  }, { parent: this });
44
- this.registerOutputs({});
44
+ this.deploymentUrl = deployCmd.stdout.apply((out) => out.trim().split("\n").pop() ?? "");
45
+ this.registerOutputs({ deploymentUrl: this.deploymentUrl });
45
46
  }
46
47
  };
47
48
 
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.cjs","names":["pulumi","command","stableDir"],"sources":["../../src/vercel/deploy.ts"],"sourcesContent":["import * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { stableDir } from \"../stable-dir\";\nimport type { VercelProject } from \"./project\";\nimport type { VercelProvider } from \"./provider\";\n\n/** Options type for VercelDeploy — replaces Pulumi's native `provider` field. */\ntype VercelDeployOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n\n\t/**\n\t * VercelProject resource to source the project ID from.\n\t * When provided, `args.projectId` is optional and ignored if both are given.\n\t */\n\tproject?: VercelProject;\n};\n\n/** Args for VercelDeploy. */\nexport interface VercelDeployArgs {\n\t/**\n\t * Vercel project ID.\n\t * Required when `opts.project` is not provided.\n\t */\n\tprojectId?: pulumi.Input<string>;\n\n\t/**\n\t * Absolute path to the monorepo root (working directory for `vercel deploy`).\n\t * Stored relative to the Pulumi program directory so the command stays stable\n\t * across machines and CI (see {@link stableDir}).\n\t */\n\tmonorepoRoot: string;\n\n\t/** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n}\n\n/**\n * Deploys a Vercel project via `vercel deploy --prod --yes` CLI.\n *\n * Triggers on source hash (computed from the app directory) and env content hash\n * (from `VercelVariable.contentHash`). When an env value changes — whether from\n * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes\n * and a redeploy is triggered.\n *\n * @example\n * ```typescript\n * new VercelDeploy(\"nexus-deploy\", {\n * projectId: vercelProject.id,\n * rootDirectory: \"apps/nexus\",\n * monorepoRoot,\n * env: { NEXT_PUBLIC_API_URL: meshUrl },\n * }, { provider });\n * ```\n */\nexport class VercelDeploy extends pulumi.ComponentResource {\n\tconstructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions) {\n\t\tconst { provider, project, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst projectId = project\n\t\t\t? project.id\n\t\t\t: (args.projectId as pulumi.Input<string>);\n\n\t\tif (!projectId) {\n\t\t\tthrow new Error(\n\t\t\t\t\"VercelDeploy: either `args.projectId` or `opts.project` must be provided\",\n\t\t\t);\n\t\t}\n\n\t\tnew command.local.Command(\n\t\t\t`${name}-deploy`,\n\t\t\t{\n\t\t\t\tcreate: \"vercel deploy --prod --yes\",\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\tdir: stableDir(args.monorepoRoot),\n\t\t\t\tenvironment: {\n\t\t\t\t\tVERCEL_TOKEN: provider.token,\n\t\t\t\t\tVERCEL_ORG_ID: provider.teamId,\n\t\t\t\t\tVERCEL_PROJECT_ID: projectId,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,IAAa,eAAb,cAAkCA,eAAO,kBAAkB;CAC1D,YAAY,MAAc,MAAwB,MAA2B;EAC5E,MAAM,EAAE,UAAU,SAAS,GAAG,eAAe;EAE7C,MAAM,4BAA4B,MAAM,CAAC,GAAG,UAAU;EAEtD,MAAM,YAAY,UACf,QAAQ,KACP,KAAK;EAET,IAAI,CAAC,WACJ,MAAM,IAAI,MACT,0EACD;EAGD,IAAIC,gBAAQ,MAAM,QACjB,GAAG,KAAK,UACR;GACC,QAAQ;GACR,UAAU,KAAK;GACf,KAAKC,6BAAU,KAAK,YAAY;GAChC,aAAa;IACZ,cAAc,SAAS;IACvB,eAAe,SAAS;IACxB,mBAAmB;GACpB;EACD,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
1
+ {"version":3,"file":"deploy.cjs","names":["pulumi","command","stableDir"],"sources":["../../src/vercel/deploy.ts"],"sourcesContent":["import * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { stableDir } from \"../stable-dir\";\nimport type { VercelProject } from \"./project\";\nimport type { VercelProvider } from \"./provider\";\n\n/** Options type for VercelDeploy — replaces Pulumi's native `provider` field. */\ntype VercelDeployOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n\n\t/**\n\t * VercelProject resource to source the project ID from.\n\t * When provided, `args.projectId` is optional and ignored if both are given.\n\t */\n\tproject?: VercelProject;\n};\n\n/** Args for VercelDeploy. */\nexport interface VercelDeployArgs {\n\t/**\n\t * Vercel project ID.\n\t * Required when `opts.project` is not provided.\n\t */\n\tprojectId?: pulumi.Input<string>;\n\n\t/**\n\t * Absolute path to the monorepo root (working directory for `vercel deploy`).\n\t * Stored relative to the Pulumi program directory so the command stays stable\n\t * across machines and CI (see {@link stableDir}).\n\t */\n\tmonorepoRoot: string;\n\n\t/** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n}\n\n/**\n * Deploys a Vercel project via `vercel deploy --prod --yes` CLI.\n *\n * Triggers on source hash (computed from the app directory) and env content hash\n * (from `VercelVariable.contentHash`). When an env value changes — whether from\n * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes\n * and a redeploy is triggered.\n *\n * @example\n * ```typescript\n * new VercelDeploy(\"nexus-deploy\", {\n * projectId: vercelProject.id,\n * rootDirectory: \"apps/nexus\",\n * monorepoRoot,\n * env: { NEXT_PUBLIC_API_URL: meshUrl },\n * }, { provider });\n * ```\n */\nexport class VercelDeploy extends pulumi.ComponentResource {\n\t/**\n\t * The production deployment URL printed by `vercel deploy` (its final stdout line).\n\t * Surfaces the deployed link in stack outputs after `pulumi up` without visiting the dashboard.\n\t */\n\tpublic readonly deploymentUrl: pulumi.Output<string>;\n\n\tconstructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions) {\n\t\tconst { provider, project, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst projectId = project\n\t\t\t? project.id\n\t\t\t: (args.projectId as pulumi.Input<string>);\n\n\t\tif (!projectId) {\n\t\t\tthrow new Error(\n\t\t\t\t\"VercelDeploy: either `args.projectId` or `opts.project` must be provided\",\n\t\t\t);\n\t\t}\n\n\t\tconst deployCmd = new command.local.Command(\n\t\t\t`${name}-deploy`,\n\t\t\t{\n\t\t\t\tcreate: \"vercel deploy --prod --yes\",\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\tdir: stableDir(args.monorepoRoot),\n\t\t\t\tenvironment: {\n\t\t\t\t\tVERCEL_TOKEN: provider.token,\n\t\t\t\t\tVERCEL_ORG_ID: provider.teamId,\n\t\t\t\t\tVERCEL_PROJECT_ID: projectId,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.deploymentUrl = deployCmd.stdout.apply(\n\t\t\t(out) => out.trim().split(\"\\n\").pop() ?? \"\",\n\t\t);\n\n\t\tthis.registerOutputs({ deploymentUrl: this.deploymentUrl });\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,IAAa,eAAb,cAAkCA,eAAO,kBAAkB;CAO1D,YAAY,MAAc,MAAwB,MAA2B;EAC5E,MAAM,EAAE,UAAU,SAAS,GAAG,eAAe;EAE7C,MAAM,4BAA4B,MAAM,CAAC,GAAG,UAAU;EAEtD,MAAM,YAAY,UACf,QAAQ,KACP,KAAK;EAET,IAAI,CAAC,WACJ,MAAM,IAAI,MACT,0EACD;EAGD,MAAM,YAAY,IAAIC,gBAAQ,MAAM,QACnC,GAAG,KAAK,UACR;GACC,QAAQ;GACR,UAAU,KAAK;GACf,KAAKC,6BAAU,KAAK,YAAY;GAChC,aAAa;IACZ,cAAc,SAAS;IACvB,eAAe,SAAS;IACxB,mBAAmB;GACpB;EACD,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,UAAU,OAAO,OACpC,QAAQ,IAAI,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,KAAK,EAC1C;EAEA,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,CAAC;CAC3D;AACD"}
@@ -48,6 +48,11 @@ interface VercelDeployArgs {
48
48
  * ```
49
49
  */
50
50
  declare class VercelDeploy extends pulumi.ComponentResource {
51
+ /**
52
+ * The production deployment URL printed by `vercel deploy` (its final stdout line).
53
+ * Surfaces the deployed link in stack outputs after `pulumi up` without visiting the dashboard.
54
+ */
55
+ readonly deploymentUrl: pulumi.Output<string>;
51
56
  constructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions);
52
57
  }
53
58
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/vercel/deploy.ts"],"mappings":";;;;;;;KAOK,mBAAA,GAAsB,IAAA,CAAK,MAAA,CAAO,wBAAA;uCAEtC,QAAA,EAAU,cAAA;EAFa;;;;EAQvB,OAAA,GAAU,aAAA;AAAA;;UAIM,gBAAA;EAZU;;;;EAiB1B,SAAA,GAAY,MAAA,CAAO,KAAA;EATnB;;;AAAuB;AAIxB;EAYC,YAAA;;EAGA,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;;;AAAK;AAqBpC;;;;cAAa,YAAA,SAAqB,MAAA,CAAO,iBAAA;cAC5B,IAAA,UAAc,IAAA,EAAM,gBAAA,EAAkB,IAAA,EAAM,mBAAA;AAAA"}
1
+ {"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/vercel/deploy.ts"],"mappings":";;;;;;;KAOK,mBAAA,GAAsB,IAAA,CAAK,MAAA,CAAO,wBAAA;uCAEtC,QAAA,EAAU,cAAA;EAFa;;;;EAQvB,OAAA,GAAU,aAAA;AAAA;;UAIM,gBAAA;EAZU;;;;EAiB1B,SAAA,GAAY,MAAA,CAAO,KAAA;EATnB;;;AAAuB;AAIxB;EAYC,YAAA;;EAGA,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;;;AAAK;AAqBpC;;;;cAAa,YAAA,SAAqB,MAAA,CAAO,iBAAA;EAOgB;;;;EAAA,SAFxC,aAAA,EAAe,MAAA,CAAO,MAAA;cAE1B,IAAA,UAAc,IAAA,EAAM,gBAAA,EAAkB,IAAA,EAAM,mBAAA;AAAA"}
@@ -48,6 +48,11 @@ interface VercelDeployArgs {
48
48
  * ```
49
49
  */
50
50
  declare class VercelDeploy extends pulumi.ComponentResource {
51
+ /**
52
+ * The production deployment URL printed by `vercel deploy` (its final stdout line).
53
+ * Surfaces the deployed link in stack outputs after `pulumi up` without visiting the dashboard.
54
+ */
55
+ readonly deploymentUrl: pulumi.Output<string>;
51
56
  constructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions);
52
57
  }
53
58
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/vercel/deploy.ts"],"mappings":";;;;;;;KAOK,mBAAA,GAAsB,IAAA,CAAK,MAAA,CAAO,wBAAA;uCAEtC,QAAA,EAAU,cAAA;EAFa;;;;EAQvB,OAAA,GAAU,aAAA;AAAA;;UAIM,gBAAA;EAZU;;;;EAiB1B,SAAA,GAAY,MAAA,CAAO,KAAA;EATnB;;;AAAuB;AAIxB;EAYC,YAAA;;EAGA,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;;;AAAK;AAqBpC;;;;cAAa,YAAA,SAAqB,MAAA,CAAO,iBAAA;cAC5B,IAAA,UAAc,IAAA,EAAM,gBAAA,EAAkB,IAAA,EAAM,mBAAA;AAAA"}
1
+ {"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/vercel/deploy.ts"],"mappings":";;;;;;;KAOK,mBAAA,GAAsB,IAAA,CAAK,MAAA,CAAO,wBAAA;uCAEtC,QAAA,EAAU,cAAA;EAFa;;;;EAQvB,OAAA,GAAU,aAAA;AAAA;;UAIM,gBAAA;EAZU;;;;EAiB1B,SAAA,GAAY,MAAA,CAAO,KAAA;EATnB;;;AAAuB;AAIxB;EAYC,YAAA;;EAGA,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;;;AAAK;AAqBpC;;;;cAAa,YAAA,SAAqB,MAAA,CAAO,iBAAA;EAOgB;;;;EAAA,SAFxC,aAAA,EAAe,MAAA,CAAO,MAAA;cAE1B,IAAA,UAAc,IAAA,EAAM,gBAAA,EAAkB,IAAA,EAAM,mBAAA;AAAA"}
@@ -28,7 +28,7 @@ var VercelDeploy = class extends pulumi.ComponentResource {
28
28
  super("infracraft:vercel:Deploy", name, {}, pulumiOpts);
29
29
  const projectId = project ? project.id : args.projectId;
30
30
  if (!projectId) throw new Error("VercelDeploy: either `args.projectId` or `opts.project` must be provided");
31
- new command.local.Command(`${name}-deploy`, {
31
+ const deployCmd = new command.local.Command(`${name}-deploy`, {
32
32
  create: "vercel deploy --prod --yes",
33
33
  triggers: args.triggers,
34
34
  dir: stableDir(args.monorepoRoot),
@@ -38,7 +38,8 @@ var VercelDeploy = class extends pulumi.ComponentResource {
38
38
  VERCEL_PROJECT_ID: projectId
39
39
  }
40
40
  }, { parent: this });
41
- this.registerOutputs({});
41
+ this.deploymentUrl = deployCmd.stdout.apply((out) => out.trim().split("\n").pop() ?? "");
42
+ this.registerOutputs({ deploymentUrl: this.deploymentUrl });
42
43
  }
43
44
  };
44
45
 
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.mjs","names":[],"sources":["../../src/vercel/deploy.ts"],"sourcesContent":["import * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { stableDir } from \"../stable-dir\";\nimport type { VercelProject } from \"./project\";\nimport type { VercelProvider } from \"./provider\";\n\n/** Options type for VercelDeploy — replaces Pulumi's native `provider` field. */\ntype VercelDeployOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n\n\t/**\n\t * VercelProject resource to source the project ID from.\n\t * When provided, `args.projectId` is optional and ignored if both are given.\n\t */\n\tproject?: VercelProject;\n};\n\n/** Args for VercelDeploy. */\nexport interface VercelDeployArgs {\n\t/**\n\t * Vercel project ID.\n\t * Required when `opts.project` is not provided.\n\t */\n\tprojectId?: pulumi.Input<string>;\n\n\t/**\n\t * Absolute path to the monorepo root (working directory for `vercel deploy`).\n\t * Stored relative to the Pulumi program directory so the command stays stable\n\t * across machines and CI (see {@link stableDir}).\n\t */\n\tmonorepoRoot: string;\n\n\t/** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n}\n\n/**\n * Deploys a Vercel project via `vercel deploy --prod --yes` CLI.\n *\n * Triggers on source hash (computed from the app directory) and env content hash\n * (from `VercelVariable.contentHash`). When an env value changes — whether from\n * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes\n * and a redeploy is triggered.\n *\n * @example\n * ```typescript\n * new VercelDeploy(\"nexus-deploy\", {\n * projectId: vercelProject.id,\n * rootDirectory: \"apps/nexus\",\n * monorepoRoot,\n * env: { NEXT_PUBLIC_API_URL: meshUrl },\n * }, { provider });\n * ```\n */\nexport class VercelDeploy extends pulumi.ComponentResource {\n\tconstructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions) {\n\t\tconst { provider, project, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst projectId = project\n\t\t\t? project.id\n\t\t\t: (args.projectId as pulumi.Input<string>);\n\n\t\tif (!projectId) {\n\t\t\tthrow new Error(\n\t\t\t\t\"VercelDeploy: either `args.projectId` or `opts.project` must be provided\",\n\t\t\t);\n\t\t}\n\n\t\tnew command.local.Command(\n\t\t\t`${name}-deploy`,\n\t\t\t{\n\t\t\t\tcreate: \"vercel deploy --prod --yes\",\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\tdir: stableDir(args.monorepoRoot),\n\t\t\t\tenvironment: {\n\t\t\t\t\tVERCEL_TOKEN: provider.token,\n\t\t\t\t\tVERCEL_ORG_ID: provider.teamId,\n\t\t\t\t\tVERCEL_PROJECT_ID: projectId,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAuDA,IAAa,eAAb,cAAkC,OAAO,kBAAkB;CAC1D,YAAY,MAAc,MAAwB,MAA2B;EAC5E,MAAM,EAAE,UAAU,SAAS,GAAG,eAAe;EAE7C,MAAM,4BAA4B,MAAM,CAAC,GAAG,UAAU;EAEtD,MAAM,YAAY,UACf,QAAQ,KACP,KAAK;EAET,IAAI,CAAC,WACJ,MAAM,IAAI,MACT,0EACD;EAGD,IAAI,QAAQ,MAAM,QACjB,GAAG,KAAK,UACR;GACC,QAAQ;GACR,UAAU,KAAK;GACf,KAAK,UAAU,KAAK,YAAY;GAChC,aAAa;IACZ,cAAc,SAAS;IACvB,eAAe,SAAS;IACxB,mBAAmB;GACpB;EACD,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
1
+ {"version":3,"file":"deploy.mjs","names":[],"sources":["../../src/vercel/deploy.ts"],"sourcesContent":["import * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { stableDir } from \"../stable-dir\";\nimport type { VercelProject } from \"./project\";\nimport type { VercelProvider } from \"./provider\";\n\n/** Options type for VercelDeploy — replaces Pulumi's native `provider` field. */\ntype VercelDeployOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n\n\t/**\n\t * VercelProject resource to source the project ID from.\n\t * When provided, `args.projectId` is optional and ignored if both are given.\n\t */\n\tproject?: VercelProject;\n};\n\n/** Args for VercelDeploy. */\nexport interface VercelDeployArgs {\n\t/**\n\t * Vercel project ID.\n\t * Required when `opts.project` is not provided.\n\t */\n\tprojectId?: pulumi.Input<string>;\n\n\t/**\n\t * Absolute path to the monorepo root (working directory for `vercel deploy`).\n\t * Stored relative to the Pulumi program directory so the command stays stable\n\t * across machines and CI (see {@link stableDir}).\n\t */\n\tmonorepoRoot: string;\n\n\t/** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n}\n\n/**\n * Deploys a Vercel project via `vercel deploy --prod --yes` CLI.\n *\n * Triggers on source hash (computed from the app directory) and env content hash\n * (from `VercelVariable.contentHash`). When an env value changes — whether from\n * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes\n * and a redeploy is triggered.\n *\n * @example\n * ```typescript\n * new VercelDeploy(\"nexus-deploy\", {\n * projectId: vercelProject.id,\n * rootDirectory: \"apps/nexus\",\n * monorepoRoot,\n * env: { NEXT_PUBLIC_API_URL: meshUrl },\n * }, { provider });\n * ```\n */\nexport class VercelDeploy extends pulumi.ComponentResource {\n\t/**\n\t * The production deployment URL printed by `vercel deploy` (its final stdout line).\n\t * Surfaces the deployed link in stack outputs after `pulumi up` without visiting the dashboard.\n\t */\n\tpublic readonly deploymentUrl: pulumi.Output<string>;\n\n\tconstructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions) {\n\t\tconst { provider, project, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst projectId = project\n\t\t\t? project.id\n\t\t\t: (args.projectId as pulumi.Input<string>);\n\n\t\tif (!projectId) {\n\t\t\tthrow new Error(\n\t\t\t\t\"VercelDeploy: either `args.projectId` or `opts.project` must be provided\",\n\t\t\t);\n\t\t}\n\n\t\tconst deployCmd = new command.local.Command(\n\t\t\t`${name}-deploy`,\n\t\t\t{\n\t\t\t\tcreate: \"vercel deploy --prod --yes\",\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\tdir: stableDir(args.monorepoRoot),\n\t\t\t\tenvironment: {\n\t\t\t\t\tVERCEL_TOKEN: provider.token,\n\t\t\t\t\tVERCEL_ORG_ID: provider.teamId,\n\t\t\t\t\tVERCEL_PROJECT_ID: projectId,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.deploymentUrl = deployCmd.stdout.apply(\n\t\t\t(out) => out.trim().split(\"\\n\").pop() ?? \"\",\n\t\t);\n\n\t\tthis.registerOutputs({ deploymentUrl: this.deploymentUrl });\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAuDA,IAAa,eAAb,cAAkC,OAAO,kBAAkB;CAO1D,YAAY,MAAc,MAAwB,MAA2B;EAC5E,MAAM,EAAE,UAAU,SAAS,GAAG,eAAe;EAE7C,MAAM,4BAA4B,MAAM,CAAC,GAAG,UAAU;EAEtD,MAAM,YAAY,UACf,QAAQ,KACP,KAAK;EAET,IAAI,CAAC,WACJ,MAAM,IAAI,MACT,0EACD;EAGD,MAAM,YAAY,IAAI,QAAQ,MAAM,QACnC,GAAG,KAAK,UACR;GACC,QAAQ;GACR,UAAU,KAAK;GACf,KAAK,UAAU,KAAK,YAAY;GAChC,aAAa;IACZ,cAAc,SAAS;IACvB,eAAe,SAAS;IACxB,mBAAmB;GACpB;EACD,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,UAAU,OAAO,OACpC,QAAQ,IAAI,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,KAAK,EAC1C;EAEA,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,CAAC;CAC3D;AACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infracraft/pulumi",
3
- "version": "1.12.0",
3
+ "version": "1.13.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {