@infracraft/pulumi 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/railway/client.cjs +4 -1
- package/dist/railway/client.cjs.map +1 -1
- package/dist/railway/client.d.cts +5 -2
- package/dist/railway/client.d.cts.map +1 -1
- package/dist/railway/client.d.mts +5 -2
- package/dist/railway/client.d.mts.map +1 -1
- package/dist/railway/client.mjs +4 -1
- package/dist/railway/client.mjs.map +1 -1
- package/dist/railway/deploy.cjs +12 -3
- package/dist/railway/deploy.cjs.map +1 -1
- package/dist/railway/deploy.d.cts +11 -2
- package/dist/railway/deploy.d.cts.map +1 -1
- package/dist/railway/deploy.d.mts +11 -2
- package/dist/railway/deploy.d.mts.map +1 -1
- package/dist/railway/deploy.mjs +12 -3
- package/dist/railway/deploy.mjs.map +1 -1
- package/dist/vercel/deploy.cjs +3 -2
- package/dist/vercel/deploy.cjs.map +1 -1
- package/dist/vercel/deploy.d.cts +5 -0
- package/dist/vercel/deploy.d.cts.map +1 -1
- package/dist/vercel/deploy.d.mts +5 -0
- package/dist/vercel/deploy.d.mts.map +1 -1
- package/dist/vercel/deploy.mjs +3 -2
- package/dist/vercel/deploy.mjs.map +1 -1
- package/package.json +1 -1
package/dist/railway/client.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
19
|
+
/** Railway API token sent as `Authorization: Bearer` (account/team-scoped). */
|
|
20
20
|
private readonly token;
|
|
21
21
|
/**
|
|
22
|
-
* @param token Railway
|
|
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;
|
|
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
|
|
19
|
+
/** Railway API token sent as `Authorization: Bearer` (account/team-scoped). */
|
|
20
20
|
private readonly token;
|
|
21
21
|
/**
|
|
22
|
-
* @param token Railway
|
|
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;
|
|
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"}
|
package/dist/railway/client.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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"}
|
package/dist/railway/deploy.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"}
|
package/dist/railway/deploy.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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"}
|
package/dist/vercel/deploy.cjs
CHANGED
|
@@ -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.
|
|
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\
|
|
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"}
|
package/dist/vercel/deploy.d.cts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/vercel/deploy.d.mts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/vercel/deploy.mjs
CHANGED
|
@@ -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.
|
|
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\
|
|
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"}
|