@infracraft/pulumi 1.16.7 → 1.17.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/bin/monitor-deployment.cjs +38 -0
- package/dist/railway/bin/monitor-deployment.cjs.map +1 -0
- package/dist/railway/bin/monitor-deployment.d.cts +1 -0
- package/dist/railway/bin/monitor-deployment.d.mts +1 -0
- package/dist/railway/bin/monitor-deployment.mjs +39 -0
- package/dist/railway/bin/monitor-deployment.mjs.map +1 -0
- package/dist/railway/deploy.cjs +7 -14
- package/dist/railway/deploy.cjs.map +1 -1
- package/dist/railway/deploy.d.cts.map +1 -1
- package/dist/railway/deploy.d.mts.map +1 -1
- package/dist/railway/deploy.mjs +7 -14
- package/dist/railway/deploy.mjs.map +1 -1
- package/dist/railway/deployment-monitor.cjs +240 -0
- package/dist/railway/deployment-monitor.cjs.map +1 -0
- package/dist/railway/deployment-monitor.d.cts +75 -0
- package/dist/railway/deployment-monitor.d.cts.map +1 -0
- package/dist/railway/deployment-monitor.d.mts +75 -0
- package/dist/railway/deployment-monitor.d.mts.map +1 -0
- package/dist/railway/deployment-monitor.mjs +237 -0
- package/dist/railway/deployment-monitor.mjs.map +1 -0
- package/dist/railway/volume.cjs +12 -5
- package/dist/railway/volume.cjs.map +1 -1
- package/dist/railway/volume.d.cts +19 -1
- package/dist/railway/volume.d.cts.map +1 -1
- package/dist/railway/volume.d.mts +19 -1
- package/dist/railway/volume.d.mts.map +1 -1
- package/dist/railway/volume.mjs +12 -6
- package/dist/railway/volume.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const require_chunk = require('../../chunk-BVYJZCqc.cjs');
|
|
2
|
+
const require_railway_deployment_monitor = require('../deployment-monitor.cjs');
|
|
3
|
+
|
|
4
|
+
//#region src/railway/bin/monitor-deployment.ts
|
|
5
|
+
/**
|
|
6
|
+
* Runnable entry for the Railway deploy monitor — invoked by `RailwayDeploy` as
|
|
7
|
+
* `node <dist>/railway/bin/monitor-deployment.mjs` after `railway up --detach`.
|
|
8
|
+
*
|
|
9
|
+
* Reads the deploy context from `IC_*` env vars, wires the real `fetch`/`setTimeout`/stderr
|
|
10
|
+
* collaborators, and exits non-zero iff the Railway API reports a failed (or never-resolved-
|
|
11
|
+
* after-failed-upload) deployment. All decision logic lives in the unit-tested
|
|
12
|
+
* `deployment-monitor` module; this file is only IO + process glue.
|
|
13
|
+
*/
|
|
14
|
+
async function main() {
|
|
15
|
+
const result = await require_railway_deployment_monitor.monitorRailwayDeployment({
|
|
16
|
+
apiUrl: process.env.IC_API_URL || void 0,
|
|
17
|
+
projectToken: process.env.IC_TOK ?? "",
|
|
18
|
+
projectId: process.env.IC_PROJ ?? "",
|
|
19
|
+
environmentId: process.env.IC_ENV ?? "",
|
|
20
|
+
serviceId: process.env.IC_SVC ?? "",
|
|
21
|
+
deploymentId: process.env.IC_DEPLOY_ID || void 0,
|
|
22
|
+
uploadOutput: process.env.IC_UP_OUT,
|
|
23
|
+
uploadExitCode: Number(process.env.IC_UP_EXIT ?? "0"),
|
|
24
|
+
since: Number(process.env.IC_SINCE ?? "0")
|
|
25
|
+
}, {
|
|
26
|
+
fetch: globalThis.fetch,
|
|
27
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
28
|
+
log: (line) => process.stderr.write(`${line}\n`)
|
|
29
|
+
});
|
|
30
|
+
process.exit(result.failed ? 1 : 0);
|
|
31
|
+
}
|
|
32
|
+
main().catch((error) => {
|
|
33
|
+
process.stderr.write(`[infracraft] deploy monitor crashed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
//# sourceMappingURL=monitor-deployment.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"monitor-deployment.cjs","names":["monitorRailwayDeployment"],"sources":["../../../src/railway/bin/monitor-deployment.ts"],"sourcesContent":["/**\n * Runnable entry for the Railway deploy monitor — invoked by `RailwayDeploy` as\n * `node <dist>/railway/bin/monitor-deployment.mjs` after `railway up --detach`.\n *\n * Reads the deploy context from `IC_*` env vars, wires the real `fetch`/`setTimeout`/stderr\n * collaborators, and exits non-zero iff the Railway API reports a failed (or never-resolved-\n * after-failed-upload) deployment. All decision logic lives in the unit-tested\n * `deployment-monitor` module; this file is only IO + process glue.\n */\nimport { monitorRailwayDeployment } from \"../deployment-monitor\";\n\nasync function main(): Promise<void> {\n\tconst result = await monitorRailwayDeployment(\n\t\t{\n\t\t\t// Defaults to Railway's public API; override only to point at a proxy or a test server.\n\t\t\tapiUrl: process.env.IC_API_URL || undefined,\n\t\t\tprojectToken: process.env.IC_TOK ?? \"\",\n\t\t\tprojectId: process.env.IC_PROJ ?? \"\",\n\t\t\tenvironmentId: process.env.IC_ENV ?? \"\",\n\t\t\tserviceId: process.env.IC_SVC ?? \"\",\n\t\t\tdeploymentId: process.env.IC_DEPLOY_ID || undefined,\n\t\t\tuploadOutput: process.env.IC_UP_OUT,\n\t\t\tuploadExitCode: Number(process.env.IC_UP_EXIT ?? \"0\"),\n\t\t\tsince: Number(process.env.IC_SINCE ?? \"0\"),\n\t\t},\n\t\t{\n\t\t\tfetch: globalThis.fetch,\n\t\t\tsleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n\t\t\tlog: (line) => process.stderr.write(`${line}\\n`),\n\t\t},\n\t);\n\n\tprocess.exit(result.failed ? 1 : 0);\n}\n\nmain().catch((error) => {\n\tprocess.stderr.write(\n\t\t`[infracraft] deploy monitor crashed: ${error instanceof Error ? error.message : String(error)}\\n`,\n\t);\n\n\tprocess.exit(1);\n});\n"],"mappings":";;;;;;;;;;;;;AAWA,eAAe,OAAsB;CACpC,MAAM,SAAS,MAAMA,4DACpB;EAEC,QAAQ,QAAQ,IAAI,cAAc;EAClC,cAAc,QAAQ,IAAI,UAAU;EACpC,WAAW,QAAQ,IAAI,WAAW;EAClC,eAAe,QAAQ,IAAI,UAAU;EACrC,WAAW,QAAQ,IAAI,UAAU;EACjC,cAAc,QAAQ,IAAI,gBAAgB;EAC1C,cAAc,QAAQ,IAAI;EAC1B,gBAAgB,OAAO,QAAQ,IAAI,cAAc,GAAG;EACpD,OAAO,OAAO,QAAQ,IAAI,YAAY,GAAG;CAC1C,GACA;EACC,OAAO,WAAW;EAClB,QAAQ,OAAO,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;EAC/D,MAAM,SAAS,QAAQ,OAAO,MAAM,GAAG,KAAK,GAAG;CAChD,CACD;CAEA,QAAQ,KAAK,OAAO,SAAS,IAAI,CAAC;AACnC;AAEA,KAAK,EAAE,OAAO,UAAU;CACvB,QAAQ,OAAO,MACd,wCAAwC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAChG;CAEA,QAAQ,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { t as __name } from "../../chunk-OPjESj5l.mjs";
|
|
2
|
+
import { monitorRailwayDeployment } from "../deployment-monitor.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/railway/bin/monitor-deployment.ts
|
|
5
|
+
/**
|
|
6
|
+
* Runnable entry for the Railway deploy monitor — invoked by `RailwayDeploy` as
|
|
7
|
+
* `node <dist>/railway/bin/monitor-deployment.mjs` after `railway up --detach`.
|
|
8
|
+
*
|
|
9
|
+
* Reads the deploy context from `IC_*` env vars, wires the real `fetch`/`setTimeout`/stderr
|
|
10
|
+
* collaborators, and exits non-zero iff the Railway API reports a failed (or never-resolved-
|
|
11
|
+
* after-failed-upload) deployment. All decision logic lives in the unit-tested
|
|
12
|
+
* `deployment-monitor` module; this file is only IO + process glue.
|
|
13
|
+
*/
|
|
14
|
+
async function main() {
|
|
15
|
+
const result = await monitorRailwayDeployment({
|
|
16
|
+
apiUrl: process.env.IC_API_URL || void 0,
|
|
17
|
+
projectToken: process.env.IC_TOK ?? "",
|
|
18
|
+
projectId: process.env.IC_PROJ ?? "",
|
|
19
|
+
environmentId: process.env.IC_ENV ?? "",
|
|
20
|
+
serviceId: process.env.IC_SVC ?? "",
|
|
21
|
+
deploymentId: process.env.IC_DEPLOY_ID || void 0,
|
|
22
|
+
uploadOutput: process.env.IC_UP_OUT,
|
|
23
|
+
uploadExitCode: Number(process.env.IC_UP_EXIT ?? "0"),
|
|
24
|
+
since: Number(process.env.IC_SINCE ?? "0")
|
|
25
|
+
}, {
|
|
26
|
+
fetch: globalThis.fetch,
|
|
27
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
28
|
+
log: (line) => process.stderr.write(`${line}\n`)
|
|
29
|
+
});
|
|
30
|
+
process.exit(result.failed ? 1 : 0);
|
|
31
|
+
}
|
|
32
|
+
main().catch((error) => {
|
|
33
|
+
process.stderr.write(`[infracraft] deploy monitor crashed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
export { };
|
|
39
|
+
//# sourceMappingURL=monitor-deployment.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"monitor-deployment.mjs","names":[],"sources":["../../../src/railway/bin/monitor-deployment.ts"],"sourcesContent":["/**\n * Runnable entry for the Railway deploy monitor — invoked by `RailwayDeploy` as\n * `node <dist>/railway/bin/monitor-deployment.mjs` after `railway up --detach`.\n *\n * Reads the deploy context from `IC_*` env vars, wires the real `fetch`/`setTimeout`/stderr\n * collaborators, and exits non-zero iff the Railway API reports a failed (or never-resolved-\n * after-failed-upload) deployment. All decision logic lives in the unit-tested\n * `deployment-monitor` module; this file is only IO + process glue.\n */\nimport { monitorRailwayDeployment } from \"../deployment-monitor\";\n\nasync function main(): Promise<void> {\n\tconst result = await monitorRailwayDeployment(\n\t\t{\n\t\t\t// Defaults to Railway's public API; override only to point at a proxy or a test server.\n\t\t\tapiUrl: process.env.IC_API_URL || undefined,\n\t\t\tprojectToken: process.env.IC_TOK ?? \"\",\n\t\t\tprojectId: process.env.IC_PROJ ?? \"\",\n\t\t\tenvironmentId: process.env.IC_ENV ?? \"\",\n\t\t\tserviceId: process.env.IC_SVC ?? \"\",\n\t\t\tdeploymentId: process.env.IC_DEPLOY_ID || undefined,\n\t\t\tuploadOutput: process.env.IC_UP_OUT,\n\t\t\tuploadExitCode: Number(process.env.IC_UP_EXIT ?? \"0\"),\n\t\t\tsince: Number(process.env.IC_SINCE ?? \"0\"),\n\t\t},\n\t\t{\n\t\t\tfetch: globalThis.fetch,\n\t\t\tsleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n\t\t\tlog: (line) => process.stderr.write(`${line}\\n`),\n\t\t},\n\t);\n\n\tprocess.exit(result.failed ? 1 : 0);\n}\n\nmain().catch((error) => {\n\tprocess.stderr.write(\n\t\t`[infracraft] deploy monitor crashed: ${error instanceof Error ? error.message : String(error)}\\n`,\n\t);\n\n\tprocess.exit(1);\n});\n"],"mappings":";;;;;;;;;;;;;AAWA,eAAe,OAAsB;CACpC,MAAM,SAAS,MAAM,yBACpB;EAEC,QAAQ,QAAQ,IAAI,cAAc;EAClC,cAAc,QAAQ,IAAI,UAAU;EACpC,WAAW,QAAQ,IAAI,WAAW;EAClC,eAAe,QAAQ,IAAI,UAAU;EACrC,WAAW,QAAQ,IAAI,UAAU;EACjC,cAAc,QAAQ,IAAI,gBAAgB;EAC1C,cAAc,QAAQ,IAAI;EAC1B,gBAAgB,OAAO,QAAQ,IAAI,cAAc,GAAG;EACpD,OAAO,OAAO,QAAQ,IAAI,YAAY,GAAG;CAC1C,GACA;EACC,OAAO,WAAW;EAClB,QAAQ,OAAO,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;EAC/D,MAAM,SAAS,QAAQ,OAAO,MAAM,GAAG,KAAK,GAAG;CAChD,CACD;CAEA,QAAQ,KAAK,OAAO,SAAS,IAAI,CAAC;AACnC;AAEA,KAAK,EAAE,OAAO,UAAU;CACvB,QAAQ,OAAO,MACd,wCAAwC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAChG;CAEA,QAAQ,KAAK,CAAC;AACf,CAAC"}
|
package/dist/railway/deploy.cjs
CHANGED
|
@@ -3,23 +3,16 @@ const require_chunk = require('../chunk-BVYJZCqc.cjs');
|
|
|
3
3
|
const require_commands_deploy = require('../commands/deploy.cjs');
|
|
4
4
|
let _pulumi_pulumi = require("@pulumi/pulumi");
|
|
5
5
|
_pulumi_pulumi = require_chunk.__toESM(_pulumi_pulumi, 1);
|
|
6
|
+
let node_url = require("node:url");
|
|
6
7
|
|
|
7
8
|
//#region src/railway/deploy.ts
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* Deployment-id resolution filters by `createdAt >= IC_SINCE` (the epoch captured
|
|
15
|
-
* just before `railway up`, minus a clock-skew buffer) and picks the newest such
|
|
16
|
-
* deployment. This prevents latching onto the PREVIOUS deployment when Railway's
|
|
17
|
-
* API has not yet registered the new one (a plain `first:1` race). SLEEPING (the
|
|
18
|
-
* service deployed then scaled to zero) is treated as success; SKIPPED (the
|
|
19
|
-
* deploy was superseded) is non-blocking; an unresolvable id (upload already
|
|
20
|
-
* succeeded) does not block the release.
|
|
10
|
+
* Absolute path to the runnable deploy monitor, resolved next to this module in `dist`.
|
|
11
|
+
* `railway up` only uploads + triggers; this bin makes the Railway GraphQL API — not the
|
|
12
|
+
* CLI's exit code — the source of truth for pass/fail, and dumps build + deploy logs on
|
|
13
|
+
* failure. Its logic lives in the unit-tested `deployment-monitor` module.
|
|
21
14
|
*/
|
|
22
|
-
const
|
|
15
|
+
const MONITOR_BIN = (0, node_url.fileURLToPath)(new URL("./bin/monitor-deployment.mjs", require("url").pathToFileURL(__filename).href));
|
|
23
16
|
/**
|
|
24
17
|
* Deploys a Railway service and waits for a terminal status. Isolation/git are the
|
|
25
18
|
* seam's job (list a `DeploySandbox` and optionally a `GitGuard` in `dependsOn`).
|
|
@@ -34,7 +27,7 @@ var RailwayDeploy = class extends _pulumi_pulumi.ComponentResource {
|
|
|
34
27
|
constructor(name, args, opts) {
|
|
35
28
|
const { provider, project, environment, service, projectToken, ...pulumiOpts } = opts;
|
|
36
29
|
super("infracraft:railway:Deploy", name, {}, pulumiOpts);
|
|
37
|
-
const cli = _pulumi_pulumi.interpolate`IC_SINCE=$(node -e "process.stdout.write(String(Date.now()))"); RAILWAY_TOKEN=${projectToken} railway up --
|
|
30
|
+
const cli = _pulumi_pulumi.interpolate`IC_SINCE=$(node -e "process.stdout.write(String(Date.now()))"); IC_UP_OUT=$(RAILWAY_TOKEN=${projectToken} railway up --detach --json --project ${project.id} --service ${service.id} --environment ${environment.id} 2>&1); IC_UP_EXIT=$?; printf '%s\\n' "$IC_UP_OUT"; if [ -n "$INFRACRAFT_SKIP_DEPLOY_WAIT" ]; then exit "$IC_UP_EXIT"; fi; IC_UP_OUT="$IC_UP_OUT" IC_UP_EXIT=$IC_UP_EXIT IC_TOK=${projectToken} IC_PROJ=${project.id} IC_ENV=${environment.id} IC_SVC=${service.id} IC_SINCE=$IC_SINCE node "${MONITOR_BIN}"`;
|
|
38
31
|
const setup = args.railpackConfig ? `printf '%s' '${JSON.stringify(args.railpackConfig).replace(/'/g, "'\\''")}' > railpack.json` : void 0;
|
|
39
32
|
const { deploymentUrl } = require_commands_deploy.createDeployCommand({
|
|
40
33
|
name,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.cjs","names":["pulumi","createDeployCommand"],"sources":["../../src/railway/deploy.ts"],"sourcesContent":["// src/railway/deploy.ts (replace entire file)\nimport * as pulumi from \"@pulumi/pulumi\";\n\nimport { createDeployCommand } from \"../commands/deploy\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport { RailwayBuilder, type RailwayService } from \"./service\";\n\nexport interface RailwayDeployConfig {\n\tbuilder?: RailwayBuilder;\n\tstartCommand?: string;\n\tpreDeployCommand?: string;\n}\n\nexport interface RailwayDeployArgs {\n\t/** Redeploy triggers (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\t/** Paths excluded from the upload when running with `DeploySandbox` + `GitGuard`. */\n\texcludePaths?: string[];\n\t/** Railpack configuration written to `railpack.json` before deploy. */\n\trailpackConfig?: Record<string, unknown>;\n}\n\ntype RailwayDeployOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\tprovider: RailwayProvider;\n\tproject: RailwayProject;\n\tenvironment: RailwayEnvironment;\n\tservice: RailwayService;\n\t/** Environment-scoped Railway deploy token (provision via RailwayProjectToken). */\n\tprojectToken: pulumi.Input<string>;\n};\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"deploy.cjs","names":["pulumi","createDeployCommand"],"sources":["../../src/railway/deploy.ts"],"sourcesContent":["// src/railway/deploy.ts (replace entire file)\nimport { fileURLToPath } from \"node:url\";\n\nimport * as pulumi from \"@pulumi/pulumi\";\n\nimport { createDeployCommand } from \"../commands/deploy\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport { RailwayBuilder, type RailwayService } from \"./service\";\n\nexport interface RailwayDeployConfig {\n\tbuilder?: RailwayBuilder;\n\tstartCommand?: string;\n\tpreDeployCommand?: string;\n}\n\nexport interface RailwayDeployArgs {\n\t/** Redeploy triggers (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\t/** Paths excluded from the upload when running with `DeploySandbox` + `GitGuard`. */\n\texcludePaths?: string[];\n\t/** Railpack configuration written to `railpack.json` before deploy. */\n\trailpackConfig?: Record<string, unknown>;\n}\n\ntype RailwayDeployOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\tprovider: RailwayProvider;\n\tproject: RailwayProject;\n\tenvironment: RailwayEnvironment;\n\tservice: RailwayService;\n\t/** Environment-scoped Railway deploy token (provision via RailwayProjectToken). */\n\tprojectToken: pulumi.Input<string>;\n};\n\n/**\n * Absolute path to the runnable deploy monitor, resolved next to this module in `dist`.\n * `railway up` only uploads + triggers; this bin makes the Railway GraphQL API — not the\n * CLI's exit code — the source of truth for pass/fail, and dumps build + deploy logs on\n * failure. Its logic lives in the unit-tested `deployment-monitor` module.\n */\nconst MONITOR_BIN = fileURLToPath(\n\tnew URL(\"./bin/monitor-deployment.mjs\", import.meta.url),\n);\n\n/**\n * Deploys a Railway service and waits for a terminal status. Isolation/git are the\n * seam's job (list a `DeploySandbox` and optionally a `GitGuard` in `dependsOn`).\n *\n * @example\n * ```typescript\n * new RailwayDeploy(\"mesh\", { triggers: [sourceHash], railpackConfig: { apt: [\"libatomic1\"] } },\n * { provider, project, environment, service, projectToken: token.token, dependsOn: [sandbox, gitGuard] });\n * ```\n */\nexport class RailwayDeploy extends pulumi.ComponentResource {\n\t/** The deploy CLI's final stdout line (Railway service URL when emitted). */\n\tpublic readonly deploymentUrl: pulumi.Output<string>;\n\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\t// `railway up --detach` uploads + triggers WITHOUT attaching to the build-log\n\t\t// stream — that long-lived stream is what intermittently times out and makes the\n\t\t// CLI exit non-zero even when the deploy actually succeeds. We capture its `--json`\n\t\t// output (for the exact deployment id) and exit code, re-emit it for visibility,\n\t\t// then hand off to the monitor bin which polls the Railway API to a terminal status.\n\t\t// The API — not the CLI exit code — decides pass/fail. Token is inlined (not via the\n\t\t// env map): an unknown secret in `environment` makes `pulumi preview` fail. IC_SINCE\n\t\t// is captured just before `railway up` as a createdAt fallback for id resolution.\n\t\tconst cli = pulumi.interpolate`IC_SINCE=$(node -e \"process.stdout.write(String(Date.now()))\"); IC_UP_OUT=$(RAILWAY_TOKEN=${projectToken} railway up --detach --json --project ${project.id} --service ${service.id} --environment ${environment.id} 2>&1); IC_UP_EXIT=$?; printf '%s\\\\n' \"$IC_UP_OUT\"; if [ -n \"$INFRACRAFT_SKIP_DEPLOY_WAIT\" ]; then exit \"$IC_UP_EXIT\"; fi; IC_UP_OUT=\"$IC_UP_OUT\" IC_UP_EXIT=$IC_UP_EXIT IC_TOK=${projectToken} IC_PROJ=${project.id} IC_ENV=${environment.id} IC_SVC=${service.id} IC_SINCE=$IC_SINCE node \"${MONITOR_BIN}\"`;\n\n\t\t// `printf '%s'` (not a bare format string) so railpack values containing %\n\t\t// are literal; the JSON is single-quote-escaped the POSIX way (' -> '\\'').\n\t\tconst setup = args.railpackConfig\n\t\t\t? `printf '%s' '${JSON.stringify(args.railpackConfig).replace(/'/g, \"'\\\\''\")}' > railpack.json`\n\t\t\t: undefined;\n\n\t\tconst { deploymentUrl } = createDeployCommand(\n\t\t\t{\n\t\t\t\tname,\n\t\t\t\tcli,\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\texcludePaths: args.excludePaths,\n\t\t\t\tsetup,\n\t\t\t},\n\t\t\t{ parent: this, ...pulumiOpts },\n\t\t);\n\n\t\tthis.deploymentUrl = deploymentUrl;\n\n\t\tthis.registerOutputs({ deploymentUrl: this.deploymentUrl });\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;AA4CA,MAAM,0CACL,IAAI,IAAI,6EAA+C,CACxD;;;;;;;;;;;AAYA,IAAa,gBAAb,cAAmCA,eAAO,kBAAkB;CAI3D,YACC,MACA,MACA,MACC;EACD,MAAM,EACL,UACA,SACA,aACA,SACA,cACA,GAAG,eACA;EAEJ,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAUvD,MAAM,MAAM,eAAO,WAAW,6FAA6F,aAAa,wCAAwC,QAAQ,GAAG,aAAa,QAAQ,GAAG,iBAAiB,YAAY,GAAG,kLAAkL,aAAa,WAAW,QAAQ,GAAG,UAAU,YAAY,GAAG,UAAU,QAAQ,GAAG,4BAA4B,YAAY;EAI9hB,MAAM,QAAQ,KAAK,iBAChB,gBAAgB,KAAK,UAAU,KAAK,cAAc,EAAE,QAAQ,MAAM,OAAO,EAAE,qBAC3E;EAEH,MAAM,EAAE,kBAAkBC,4CACzB;GACC;GACA;GACA,UAAU,KAAK;GACf,cAAc,KAAK;GACnB;EACD,GACA;GAAE,QAAQ;GAAM,GAAG;EAAW,CAC/B;EAEA,KAAK,gBAAgB;EAErB,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,CAAC;CAC3D;AACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/railway/deploy.ts"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/railway/deploy.ts"],"mappings":";;;;;;;;UAWiB,mBAAA;EAChB,OAAA,GAAU,cAAc;EACxB,YAAA;EACA,gBAAA;AAAA;AAAA,UAGgB,iBAAA;EALQ;EAOxB,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAPpB;EASV,YAAA;EAPA;EASA,cAAA,GAAiB,MAAA;AAAA;AAAA,KAGb,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAGP,QAAA,EAAU,eAAA;EACV,OAAA,EAAS,cAAA;EACT,WAAA,EAAa,kBAAA;EACb,OAAA,EAAS,cAAA,EAVQ;EAYjB,YAAA,EAAc,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;AAZE;cAmCX,aAAA,SAAsB,MAAA,CAAO,iBAAA;EAhCjB;EAAA,SAkCR,aAAA,EAAe,MAAA,CAAO,MAAA;cAGrC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/railway/deploy.ts"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/railway/deploy.ts"],"mappings":";;;;;;;;UAWiB,mBAAA;EAChB,OAAA,GAAU,cAAc;EACxB,YAAA;EACA,gBAAA;AAAA;AAAA,UAGgB,iBAAA;EALQ;EAOxB,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAPpB;EASV,YAAA;EAPA;EASA,cAAA,GAAiB,MAAA;AAAA;AAAA,KAGb,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAGP,QAAA,EAAU,eAAA;EACV,OAAA,EAAS,cAAA;EACT,WAAA,EAAa,kBAAA;EACb,OAAA,EAAS,cAAA,EAVQ;EAYjB,YAAA,EAAc,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;AAZE;cAmCX,aAAA,SAAsB,MAAA,CAAO,iBAAA;EAhCjB;EAAA,SAkCR,aAAA,EAAe,MAAA,CAAO,MAAA;cAGrC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
|
package/dist/railway/deploy.mjs
CHANGED
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
import { t as __name } from "../chunk-OPjESj5l.mjs";
|
|
2
2
|
import { createDeployCommand } from "../commands/deploy.mjs";
|
|
3
3
|
import * as pulumi from "@pulumi/pulumi";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
|
|
5
6
|
//#region src/railway/deploy.ts
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Deployment-id resolution filters by `createdAt >= IC_SINCE` (the epoch captured
|
|
13
|
-
* just before `railway up`, minus a clock-skew buffer) and picks the newest such
|
|
14
|
-
* deployment. This prevents latching onto the PREVIOUS deployment when Railway's
|
|
15
|
-
* API has not yet registered the new one (a plain `first:1` race). SLEEPING (the
|
|
16
|
-
* service deployed then scaled to zero) is treated as success; SKIPPED (the
|
|
17
|
-
* deploy was superseded) is non-blocking; an unresolvable id (upload already
|
|
18
|
-
* succeeded) does not block the release.
|
|
8
|
+
* Absolute path to the runnable deploy monitor, resolved next to this module in `dist`.
|
|
9
|
+
* `railway up` only uploads + triggers; this bin makes the Railway GraphQL API — not the
|
|
10
|
+
* CLI's exit code — the source of truth for pass/fail, and dumps build + deploy logs on
|
|
11
|
+
* failure. Its logic lives in the unit-tested `deployment-monitor` module.
|
|
19
12
|
*/
|
|
20
|
-
const
|
|
13
|
+
const MONITOR_BIN = fileURLToPath(new URL("./bin/monitor-deployment.mjs", import.meta.url));
|
|
21
14
|
/**
|
|
22
15
|
* Deploys a Railway service and waits for a terminal status. Isolation/git are the
|
|
23
16
|
* seam's job (list a `DeploySandbox` and optionally a `GitGuard` in `dependsOn`).
|
|
@@ -32,7 +25,7 @@ var RailwayDeploy = class extends pulumi.ComponentResource {
|
|
|
32
25
|
constructor(name, args, opts) {
|
|
33
26
|
const { provider, project, environment, service, projectToken, ...pulumiOpts } = opts;
|
|
34
27
|
super("infracraft:railway:Deploy", name, {}, pulumiOpts);
|
|
35
|
-
const cli = pulumi.interpolate`IC_SINCE=$(node -e "process.stdout.write(String(Date.now()))"); RAILWAY_TOKEN=${projectToken} railway up --
|
|
28
|
+
const cli = pulumi.interpolate`IC_SINCE=$(node -e "process.stdout.write(String(Date.now()))"); IC_UP_OUT=$(RAILWAY_TOKEN=${projectToken} railway up --detach --json --project ${project.id} --service ${service.id} --environment ${environment.id} 2>&1); IC_UP_EXIT=$?; printf '%s\\n' "$IC_UP_OUT"; if [ -n "$INFRACRAFT_SKIP_DEPLOY_WAIT" ]; then exit "$IC_UP_EXIT"; fi; IC_UP_OUT="$IC_UP_OUT" IC_UP_EXIT=$IC_UP_EXIT IC_TOK=${projectToken} IC_PROJ=${project.id} IC_ENV=${environment.id} IC_SVC=${service.id} IC_SINCE=$IC_SINCE node "${MONITOR_BIN}"`;
|
|
36
29
|
const setup = args.railpackConfig ? `printf '%s' '${JSON.stringify(args.railpackConfig).replace(/'/g, "'\\''")}' > railpack.json` : void 0;
|
|
37
30
|
const { deploymentUrl } = createDeployCommand({
|
|
38
31
|
name,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.mjs","names":[],"sources":["../../src/railway/deploy.ts"],"sourcesContent":["// src/railway/deploy.ts (replace entire file)\nimport * as pulumi from \"@pulumi/pulumi\";\n\nimport { createDeployCommand } from \"../commands/deploy\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport { RailwayBuilder, type RailwayService } from \"./service\";\n\nexport interface RailwayDeployConfig {\n\tbuilder?: RailwayBuilder;\n\tstartCommand?: string;\n\tpreDeployCommand?: string;\n}\n\nexport interface RailwayDeployArgs {\n\t/** Redeploy triggers (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\t/** Paths excluded from the upload when running with `DeploySandbox` + `GitGuard`. */\n\texcludePaths?: string[];\n\t/** Railpack configuration written to `railpack.json` before deploy. */\n\trailpackConfig?: Record<string, unknown>;\n}\n\ntype RailwayDeployOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\tprovider: RailwayProvider;\n\tproject: RailwayProject;\n\tenvironment: RailwayEnvironment;\n\tservice: RailwayService;\n\t/** Environment-scoped Railway deploy token (provision via RailwayProjectToken). */\n\tprojectToken: pulumi.Input<string>;\n};\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"deploy.mjs","names":[],"sources":["../../src/railway/deploy.ts"],"sourcesContent":["// src/railway/deploy.ts (replace entire file)\nimport { fileURLToPath } from \"node:url\";\n\nimport * as pulumi from \"@pulumi/pulumi\";\n\nimport { createDeployCommand } from \"../commands/deploy\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport { RailwayBuilder, type RailwayService } from \"./service\";\n\nexport interface RailwayDeployConfig {\n\tbuilder?: RailwayBuilder;\n\tstartCommand?: string;\n\tpreDeployCommand?: string;\n}\n\nexport interface RailwayDeployArgs {\n\t/** Redeploy triggers (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\t/** Paths excluded from the upload when running with `DeploySandbox` + `GitGuard`. */\n\texcludePaths?: string[];\n\t/** Railpack configuration written to `railpack.json` before deploy. */\n\trailpackConfig?: Record<string, unknown>;\n}\n\ntype RailwayDeployOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\tprovider: RailwayProvider;\n\tproject: RailwayProject;\n\tenvironment: RailwayEnvironment;\n\tservice: RailwayService;\n\t/** Environment-scoped Railway deploy token (provision via RailwayProjectToken). */\n\tprojectToken: pulumi.Input<string>;\n};\n\n/**\n * Absolute path to the runnable deploy monitor, resolved next to this module in `dist`.\n * `railway up` only uploads + triggers; this bin makes the Railway GraphQL API — not the\n * CLI's exit code — the source of truth for pass/fail, and dumps build + deploy logs on\n * failure. Its logic lives in the unit-tested `deployment-monitor` module.\n */\nconst MONITOR_BIN = fileURLToPath(\n\tnew URL(\"./bin/monitor-deployment.mjs\", import.meta.url),\n);\n\n/**\n * Deploys a Railway service and waits for a terminal status. Isolation/git are the\n * seam's job (list a `DeploySandbox` and optionally a `GitGuard` in `dependsOn`).\n *\n * @example\n * ```typescript\n * new RailwayDeploy(\"mesh\", { triggers: [sourceHash], railpackConfig: { apt: [\"libatomic1\"] } },\n * { provider, project, environment, service, projectToken: token.token, dependsOn: [sandbox, gitGuard] });\n * ```\n */\nexport class RailwayDeploy extends pulumi.ComponentResource {\n\t/** The deploy CLI's final stdout line (Railway service URL when emitted). */\n\tpublic readonly deploymentUrl: pulumi.Output<string>;\n\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\t// `railway up --detach` uploads + triggers WITHOUT attaching to the build-log\n\t\t// stream — that long-lived stream is what intermittently times out and makes the\n\t\t// CLI exit non-zero even when the deploy actually succeeds. We capture its `--json`\n\t\t// output (for the exact deployment id) and exit code, re-emit it for visibility,\n\t\t// then hand off to the monitor bin which polls the Railway API to a terminal status.\n\t\t// The API — not the CLI exit code — decides pass/fail. Token is inlined (not via the\n\t\t// env map): an unknown secret in `environment` makes `pulumi preview` fail. IC_SINCE\n\t\t// is captured just before `railway up` as a createdAt fallback for id resolution.\n\t\tconst cli = pulumi.interpolate`IC_SINCE=$(node -e \"process.stdout.write(String(Date.now()))\"); IC_UP_OUT=$(RAILWAY_TOKEN=${projectToken} railway up --detach --json --project ${project.id} --service ${service.id} --environment ${environment.id} 2>&1); IC_UP_EXIT=$?; printf '%s\\\\n' \"$IC_UP_OUT\"; if [ -n \"$INFRACRAFT_SKIP_DEPLOY_WAIT\" ]; then exit \"$IC_UP_EXIT\"; fi; IC_UP_OUT=\"$IC_UP_OUT\" IC_UP_EXIT=$IC_UP_EXIT IC_TOK=${projectToken} IC_PROJ=${project.id} IC_ENV=${environment.id} IC_SVC=${service.id} IC_SINCE=$IC_SINCE node \"${MONITOR_BIN}\"`;\n\n\t\t// `printf '%s'` (not a bare format string) so railpack values containing %\n\t\t// are literal; the JSON is single-quote-escaped the POSIX way (' -> '\\'').\n\t\tconst setup = args.railpackConfig\n\t\t\t? `printf '%s' '${JSON.stringify(args.railpackConfig).replace(/'/g, \"'\\\\''\")}' > railpack.json`\n\t\t\t: undefined;\n\n\t\tconst { deploymentUrl } = createDeployCommand(\n\t\t\t{\n\t\t\t\tname,\n\t\t\t\tcli,\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\texcludePaths: args.excludePaths,\n\t\t\t\tsetup,\n\t\t\t},\n\t\t\t{ parent: this, ...pulumiOpts },\n\t\t);\n\n\t\tthis.deploymentUrl = deploymentUrl;\n\n\t\tthis.registerOutputs({ deploymentUrl: this.deploymentUrl });\n\t}\n}\n"],"mappings":";;;;;;;;;;;;AA4CA,MAAM,cAAc,cACnB,IAAI,IAAI,gCAAgC,OAAO,KAAK,GAAG,CACxD;;;;;;;;;;;AAYA,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAI3D,YACC,MACA,MACA,MACC;EACD,MAAM,EACL,UACA,SACA,aACA,SACA,cACA,GAAG,eACA;EAEJ,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAUvD,MAAM,MAAM,OAAO,WAAW,6FAA6F,aAAa,wCAAwC,QAAQ,GAAG,aAAa,QAAQ,GAAG,iBAAiB,YAAY,GAAG,kLAAkL,aAAa,WAAW,QAAQ,GAAG,UAAU,YAAY,GAAG,UAAU,QAAQ,GAAG,4BAA4B,YAAY;EAI9hB,MAAM,QAAQ,KAAK,iBAChB,gBAAgB,KAAK,UAAU,KAAK,cAAc,EAAE,QAAQ,MAAM,OAAO,EAAE,qBAC3E;EAEH,MAAM,EAAE,kBAAkB,oBACzB;GACC;GACA;GACA,UAAU,KAAK;GACf,cAAc,KAAK;GACnB;EACD,GACA;GAAE,QAAQ;GAAM,GAAG;EAAW,CAC/B;EAEA,KAAK,gBAAgB;EAErB,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,CAAC;CAC3D;AACD"}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_chunk = require('../chunk-BVYJZCqc.cjs');
|
|
3
|
+
|
|
4
|
+
//#region src/railway/deployment-monitor.ts
|
|
5
|
+
/**
|
|
6
|
+
* API-authoritative Railway deploy monitor.
|
|
7
|
+
*
|
|
8
|
+
* `railway up` is used ONLY to upload + trigger (run it `--detach` so it never holds the
|
|
9
|
+
* flaky build-log stream). This module then treats the Railway GraphQL API — not the CLI's
|
|
10
|
+
* exit code — as the source of truth: it polls the deployment to a terminal status, fails on
|
|
11
|
+
* FAILED/CRASHED/REMOVED, and on failure pulls the full build + deploy logs so the operator
|
|
12
|
+
* sees exactly why it broke.
|
|
13
|
+
*
|
|
14
|
+
* Pure and dependency-injected (`fetch`/`sleep`/`log`) so every decision is unit-tested. The
|
|
15
|
+
* runnable wrapper that wires real IO lives in `./bin/monitor-deployment.ts`.
|
|
16
|
+
*/
|
|
17
|
+
/** Tunable knobs, named here so the timing/limits are discoverable in one place. */
|
|
18
|
+
const DEFAULTS = {
|
|
19
|
+
apiUrl: "https://backboard.railway.app/graphql/v2",
|
|
20
|
+
/** Attempts × interval to resolve the deployment id before giving up. */
|
|
21
|
+
resolveAttempts: 12,
|
|
22
|
+
resolveIntervalMs: 5e3,
|
|
23
|
+
/** Attempts × interval to poll a resolved deployment to a terminal status. */
|
|
24
|
+
pollAttempts: 120,
|
|
25
|
+
pollIntervalMs: 1e4,
|
|
26
|
+
/** Per-request abort timeout so a hung connection can't stall the loop. */
|
|
27
|
+
requestTimeoutMs: 15e3,
|
|
28
|
+
/** Build + deploy log lines surfaced on failure. */
|
|
29
|
+
failureLogLimit: 250,
|
|
30
|
+
/** Clock-skew buffer so the createdAt filter never drops the just-created deployment. */
|
|
31
|
+
skewMs: 12e4
|
|
32
|
+
};
|
|
33
|
+
/** Railway GraphQL operations, named so each query is readable at its call site. */
|
|
34
|
+
const QUERIES = {
|
|
35
|
+
resolveDeployments: "query($p:String!,$e:String!,$s:String!){deployments(first:10,input:{projectId:$p,environmentId:$e,serviceId:$s}){edges{node{id status createdAt}}}}",
|
|
36
|
+
deploymentStatus: "query($d:String!){deployment(id:$d){status}}",
|
|
37
|
+
buildLogs: "query($d:String!,$n:Int!){buildLogs(deploymentId:$d,limit:$n){message severity timestamp}}",
|
|
38
|
+
deploymentLogs: "query($d:String!,$n:Int!){deploymentLogs(deploymentId:$d,limit:$n){message severity timestamp}}"
|
|
39
|
+
};
|
|
40
|
+
const UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
41
|
+
/** Status → disposition; index directly, defaulting unknown/in-flight statuses to PENDING. */
|
|
42
|
+
const DISPOSITION_BY_STATUS = {
|
|
43
|
+
SUCCESS: "LIVE",
|
|
44
|
+
SLEEPING: "LIVE",
|
|
45
|
+
FAILED: "FAILED",
|
|
46
|
+
CRASHED: "FAILED",
|
|
47
|
+
REMOVED: "FAILED",
|
|
48
|
+
SKIPPED: "SUPERSEDED"
|
|
49
|
+
};
|
|
50
|
+
/** The terminal classification of a monitor run. */
|
|
51
|
+
let MonitorOutcome = /* @__PURE__ */ function(MonitorOutcome) {
|
|
52
|
+
MonitorOutcome["SUCCESS"] = "SUCCESS";
|
|
53
|
+
MonitorOutcome["SKIPPED"] = "SKIPPED";
|
|
54
|
+
MonitorOutcome["FAILED"] = "FAILED";
|
|
55
|
+
MonitorOutcome["TIMED_OUT"] = "TIMED_OUT";
|
|
56
|
+
MonitorOutcome["NO_DEPLOYMENT"] = "NO_DEPLOYMENT";
|
|
57
|
+
return MonitorOutcome;
|
|
58
|
+
}({});
|
|
59
|
+
/**
|
|
60
|
+
* Best-effort extraction of a deployment id from `railway up`'s output. Tries, in order:
|
|
61
|
+
* the JSON trigger payload (whole or per-line), a `?id=<uuid>` Build-Logs URL, then any bare
|
|
62
|
+
* UUID. Returns undefined so the caller can fall back to createdAt resolution.
|
|
63
|
+
*/
|
|
64
|
+
function parseDeploymentId(output) {
|
|
65
|
+
if (!output) return;
|
|
66
|
+
const fromObject = (value) => {
|
|
67
|
+
if (!value || typeof value !== "object") return;
|
|
68
|
+
const record = value;
|
|
69
|
+
if (typeof record.deploymentId === "string") return record.deploymentId;
|
|
70
|
+
if (typeof record.id === "string" && UUID.test(record.id)) return record.id;
|
|
71
|
+
const nested = record.deployment;
|
|
72
|
+
return nested && typeof nested.id === "string" ? nested.id : void 0;
|
|
73
|
+
};
|
|
74
|
+
const tryJson = (text) => {
|
|
75
|
+
try {
|
|
76
|
+
return fromObject(JSON.parse(text));
|
|
77
|
+
} catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const whole = tryJson(output);
|
|
82
|
+
if (whole) return whole;
|
|
83
|
+
for (const line of output.split("\n")) {
|
|
84
|
+
const fromLine = tryJson(line.trim());
|
|
85
|
+
if (fromLine) return fromLine;
|
|
86
|
+
}
|
|
87
|
+
const fromUrl = output.match(new RegExp(`[?&]id=(${UUID.source})`, "i"));
|
|
88
|
+
if (fromUrl) return fromUrl[1];
|
|
89
|
+
const bare = output.match(UUID);
|
|
90
|
+
return bare ? bare[0] : void 0;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A fault-tolerant Railway GraphQL caller bound to one deployment context. Every method
|
|
94
|
+
* swallows transient transport/timeout errors (returning undefined/empty) so the monitor's
|
|
95
|
+
* loops own the retry cadence — the network blips that broke the old CLI path are absorbed here.
|
|
96
|
+
*/
|
|
97
|
+
function createRailwayApi(deps, config) {
|
|
98
|
+
const call = async (query, variables) => {
|
|
99
|
+
try {
|
|
100
|
+
const signal = config.requestTimeoutMs > 0 && typeof AbortSignal?.timeout === "function" ? AbortSignal.timeout(config.requestTimeoutMs) : void 0;
|
|
101
|
+
const response = await deps.fetch(config.apiUrl, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: {
|
|
104
|
+
"Content-Type": "application/json",
|
|
105
|
+
"Project-Access-Token": config.projectToken
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
query,
|
|
109
|
+
variables
|
|
110
|
+
}),
|
|
111
|
+
signal
|
|
112
|
+
});
|
|
113
|
+
if (!response.ok) return;
|
|
114
|
+
return (await response.json()).data;
|
|
115
|
+
} catch {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
/** Newest deployment created at/after `since` (skew-buffered), or undefined. */
|
|
121
|
+
async resolveLatestDeployment(since) {
|
|
122
|
+
const [newest] = ((await call(QUERIES.resolveDeployments, {
|
|
123
|
+
p: config.projectId,
|
|
124
|
+
e: config.environmentId,
|
|
125
|
+
s: config.serviceId
|
|
126
|
+
}))?.deployments?.edges ?? []).map((edge) => edge.node).filter((node) => node?.createdAt && Date.parse(node.createdAt) >= since).sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
|
|
127
|
+
return newest?.id;
|
|
128
|
+
},
|
|
129
|
+
async deploymentStatus(id) {
|
|
130
|
+
return (await call(QUERIES.deploymentStatus, { d: id }))?.deployment?.status;
|
|
131
|
+
},
|
|
132
|
+
async failureLogs(id, limit) {
|
|
133
|
+
const read = async (query, field) => {
|
|
134
|
+
return ((await call(query, {
|
|
135
|
+
d: id,
|
|
136
|
+
n: limit
|
|
137
|
+
}))?.[field] ?? []).map((line) => line.message ?? "");
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
build: await read(QUERIES.buildLogs, "buildLogs"),
|
|
141
|
+
deploy: await read(QUERIES.deploymentLogs, "deploymentLogs")
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Watches a Railway deployment to a terminal state. The Railway API is authoritative: the
|
|
148
|
+
* CLI's exit code only matters when NO deployment was ever created.
|
|
149
|
+
*/
|
|
150
|
+
async function monitorRailwayDeployment(input, deps) {
|
|
151
|
+
const resolveAttempts = input.resolveAttempts ?? DEFAULTS.resolveAttempts;
|
|
152
|
+
const resolveIntervalMs = input.resolveIntervalMs ?? DEFAULTS.resolveIntervalMs;
|
|
153
|
+
const pollAttempts = input.pollAttempts ?? DEFAULTS.pollAttempts;
|
|
154
|
+
const pollIntervalMs = input.pollIntervalMs ?? DEFAULTS.pollIntervalMs;
|
|
155
|
+
const failureLogLimit = input.failureLogLimit ?? DEFAULTS.failureLogLimit;
|
|
156
|
+
const api = createRailwayApi(deps, {
|
|
157
|
+
apiUrl: input.apiUrl ?? DEFAULTS.apiUrl,
|
|
158
|
+
projectToken: input.projectToken,
|
|
159
|
+
projectId: input.projectId,
|
|
160
|
+
environmentId: input.environmentId,
|
|
161
|
+
serviceId: input.serviceId,
|
|
162
|
+
requestTimeoutMs: input.requestTimeoutMs ?? DEFAULTS.requestTimeoutMs
|
|
163
|
+
});
|
|
164
|
+
const dumpFailureLogs = async (id) => {
|
|
165
|
+
const logs = await api.failureLogs(id, failureLogLimit);
|
|
166
|
+
for (const [label, lines] of [["build logs", logs.build], ["deploy logs", logs.deploy]]) {
|
|
167
|
+
deps.log(`[infracraft] ───── railway ${label} (deployment ${id}) ─────`);
|
|
168
|
+
if (lines.length === 0) {
|
|
169
|
+
deps.log(" (no log lines returned)");
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
for (const line of lines) deps.log(` ${line}`);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
let deploymentId = input.deploymentId ?? parseDeploymentId(input.uploadOutput);
|
|
176
|
+
if (!deploymentId) {
|
|
177
|
+
const threshold = input.since - DEFAULTS.skewMs;
|
|
178
|
+
for (let attempt = 0; attempt < resolveAttempts && !deploymentId; attempt++) {
|
|
179
|
+
deploymentId = await api.resolveLatestDeployment(threshold);
|
|
180
|
+
if (!deploymentId) await deps.sleep(resolveIntervalMs);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!deploymentId) {
|
|
184
|
+
const failed = input.uploadExitCode !== 0;
|
|
185
|
+
deps.log(failed ? "[infracraft] railway up failed and no deployment was created — failing the resource" : "[infracraft] no deployment resolved but railway up succeeded — not blocking the release");
|
|
186
|
+
return {
|
|
187
|
+
outcome: "NO_DEPLOYMENT",
|
|
188
|
+
failed
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
deps.log(`[infracraft] monitoring railway deployment ${deploymentId}`);
|
|
192
|
+
let lastStatus;
|
|
193
|
+
for (let attempt = 0; attempt < pollAttempts; attempt++) {
|
|
194
|
+
const status = await api.deploymentStatus(deploymentId);
|
|
195
|
+
if (status) {
|
|
196
|
+
lastStatus = status;
|
|
197
|
+
deps.log(`[infracraft] railway deployment ${deploymentId} status=${status}`);
|
|
198
|
+
const disposition = DISPOSITION_BY_STATUS[status] ?? "PENDING";
|
|
199
|
+
if (disposition === "LIVE") return {
|
|
200
|
+
outcome: "SUCCESS",
|
|
201
|
+
failed: false,
|
|
202
|
+
deploymentId,
|
|
203
|
+
status
|
|
204
|
+
};
|
|
205
|
+
if (disposition === "SUPERSEDED") {
|
|
206
|
+
deps.log(`[infracraft] railway deployment ${deploymentId} SKIPPED (superseded) — not blocking`);
|
|
207
|
+
return {
|
|
208
|
+
outcome: "SKIPPED",
|
|
209
|
+
failed: false,
|
|
210
|
+
deploymentId,
|
|
211
|
+
status
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (disposition === "FAILED") {
|
|
215
|
+
await dumpFailureLogs(deploymentId);
|
|
216
|
+
return {
|
|
217
|
+
outcome: "FAILED",
|
|
218
|
+
failed: true,
|
|
219
|
+
deploymentId,
|
|
220
|
+
status
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
await deps.sleep(pollIntervalMs);
|
|
225
|
+
}
|
|
226
|
+
deps.log(`[infracraft] timed out waiting for railway deployment ${deploymentId} (last status=${lastStatus ?? "unknown"})`);
|
|
227
|
+
await dumpFailureLogs(deploymentId);
|
|
228
|
+
return {
|
|
229
|
+
outcome: "TIMED_OUT",
|
|
230
|
+
failed: true,
|
|
231
|
+
deploymentId,
|
|
232
|
+
status: lastStatus
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
//#endregion
|
|
237
|
+
exports.MonitorOutcome = MonitorOutcome;
|
|
238
|
+
exports.monitorRailwayDeployment = monitorRailwayDeployment;
|
|
239
|
+
exports.parseDeploymentId = parseDeploymentId;
|
|
240
|
+
//# sourceMappingURL=deployment-monitor.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deployment-monitor.cjs","names":[],"sources":["../../src/railway/deployment-monitor.ts"],"sourcesContent":["/**\n * API-authoritative Railway deploy monitor.\n *\n * `railway up` is used ONLY to upload + trigger (run it `--detach` so it never holds the\n * flaky build-log stream). This module then treats the Railway GraphQL API — not the CLI's\n * exit code — as the source of truth: it polls the deployment to a terminal status, fails on\n * FAILED/CRASHED/REMOVED, and on failure pulls the full build + deploy logs so the operator\n * sees exactly why it broke.\n *\n * Pure and dependency-injected (`fetch`/`sleep`/`log`) so every decision is unit-tested. The\n * runnable wrapper that wires real IO lives in `./bin/monitor-deployment.ts`.\n */\n\n/** Tunable knobs, named here so the timing/limits are discoverable in one place. */\nconst DEFAULTS = {\n\tapiUrl: \"https://backboard.railway.app/graphql/v2\",\n\t/** Attempts × interval to resolve the deployment id before giving up. */\n\tresolveAttempts: 12,\n\tresolveIntervalMs: 5_000,\n\t/** Attempts × interval to poll a resolved deployment to a terminal status. */\n\tpollAttempts: 120,\n\tpollIntervalMs: 10_000,\n\t/** Per-request abort timeout so a hung connection can't stall the loop. */\n\trequestTimeoutMs: 15_000,\n\t/** Build + deploy log lines surfaced on failure. */\n\tfailureLogLimit: 250,\n\t/** Clock-skew buffer so the createdAt filter never drops the just-created deployment. */\n\tskewMs: 120_000,\n} as const;\n\n/** Railway GraphQL operations, named so each query is readable at its call site. */\nconst QUERIES = {\n\tresolveDeployments:\n\t\t\"query($p:String!,$e:String!,$s:String!){deployments(first:10,input:{projectId:$p,environmentId:$e,serviceId:$s}){edges{node{id status createdAt}}}}\",\n\tdeploymentStatus: \"query($d:String!){deployment(id:$d){status}}\",\n\tbuildLogs:\n\t\t\"query($d:String!,$n:Int!){buildLogs(deploymentId:$d,limit:$n){message severity timestamp}}\",\n\tdeploymentLogs:\n\t\t\"query($d:String!,$n:Int!){deploymentLogs(deploymentId:$d,limit:$n){message severity timestamp}}\",\n} as const;\n\nconst UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;\n\n/** What a Railway deployment status means for the monitor's control flow. */\nenum DeploymentDisposition {\n\t/** Live release — SUCCESS, or SLEEPING (deployed then scaled to zero). */\n\tLIVE = \"LIVE\",\n\t/** Terminal failure that must fail the Pulumi resource. */\n\tFAILED = \"FAILED\",\n\t/** Superseded by a newer deployment — non-blocking. */\n\tSUPERSEDED = \"SUPERSEDED\",\n\t/** Still in flight (BUILDING, DEPLOYING, …) — keep polling. */\n\tPENDING = \"PENDING\",\n}\n\n/** Status → disposition; index directly, defaulting unknown/in-flight statuses to PENDING. */\nconst DISPOSITION_BY_STATUS: Readonly<Record<string, DeploymentDisposition>> = {\n\tSUCCESS: DeploymentDisposition.LIVE,\n\tSLEEPING: DeploymentDisposition.LIVE,\n\tFAILED: DeploymentDisposition.FAILED,\n\tCRASHED: DeploymentDisposition.FAILED,\n\tREMOVED: DeploymentDisposition.FAILED,\n\tSKIPPED: DeploymentDisposition.SUPERSEDED,\n};\n\n/** The terminal classification of a monitor run. */\nexport enum MonitorOutcome {\n\tSUCCESS = \"SUCCESS\",\n\tSKIPPED = \"SKIPPED\",\n\tFAILED = \"FAILED\",\n\tTIMED_OUT = \"TIMED_OUT\",\n\tNO_DEPLOYMENT = \"NO_DEPLOYMENT\",\n}\n\n/** What the monitor observed and whether the Pulumi resource should fail. */\nexport interface MonitorResult {\n\toutcome: MonitorOutcome;\n\t/** True iff the deploy command should exit non-zero (fail the Pulumi resource). */\n\tfailed: boolean;\n\tdeploymentId?: string;\n\tstatus?: string;\n}\n\n/** Everything the monitor needs to identify and watch one deployment. */\nexport interface MonitorInput {\n\t/** Railway GraphQL endpoint (override in tests). */\n\tapiUrl?: string;\n\t/** Project-scoped access token (sent as the `Project-Access-Token` header). */\n\tprojectToken: string;\n\tprojectId: string;\n\tenvironmentId: string;\n\tserviceId: string;\n\t/** Deployment id captured from `railway up --detach --json` (preferred, exact). */\n\tdeploymentId?: string;\n\t/** Raw `railway up` output, parsed for the id when `deploymentId` is absent. */\n\tuploadOutput?: string;\n\t/** `railway up` exit code: decides pass/fail only when no deployment can be resolved. */\n\tuploadExitCode: number;\n\t/** Epoch ms captured just before `railway up` (createdAt fallback for id resolution). */\n\tsince: number;\n\tresolveAttempts?: number;\n\tresolveIntervalMs?: number;\n\tpollAttempts?: number;\n\tpollIntervalMs?: number;\n\trequestTimeoutMs?: number;\n\tfailureLogLimit?: number;\n}\n\n/** Injected side-effecting collaborators (real impls in the bin, fakes in tests). */\nexport interface MonitorDeps {\n\tfetch: typeof fetch;\n\tsleep: (ms: number) => Promise<void>;\n\tlog: (line: string) => void;\n}\n\n/**\n * Best-effort extraction of a deployment id from `railway up`'s output. Tries, in order:\n * the JSON trigger payload (whole or per-line), a `?id=<uuid>` Build-Logs URL, then any bare\n * UUID. Returns undefined so the caller can fall back to createdAt resolution.\n */\nexport function parseDeploymentId(\n\toutput: string | undefined,\n): string | undefined {\n\tif (!output) {\n\t\treturn undefined;\n\t}\n\n\tconst fromObject = (value: unknown): string | undefined => {\n\t\tif (!value || typeof value !== \"object\") {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst record = value as Record<string, unknown>;\n\n\t\tif (typeof record.deploymentId === \"string\") {\n\t\t\treturn record.deploymentId;\n\t\t}\n\n\t\tif (typeof record.id === \"string\" && UUID.test(record.id)) {\n\t\t\treturn record.id;\n\t\t}\n\n\t\tconst nested = record.deployment as Record<string, unknown> | undefined;\n\n\t\treturn nested && typeof nested.id === \"string\" ? nested.id : undefined;\n\t};\n\n\tconst tryJson = (text: string): string | undefined => {\n\t\ttry {\n\t\t\treturn fromObject(JSON.parse(text));\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t};\n\n\tconst whole = tryJson(output);\n\n\tif (whole) {\n\t\treturn whole;\n\t}\n\n\tfor (const line of output.split(\"\\n\")) {\n\t\tconst fromLine = tryJson(line.trim());\n\n\t\tif (fromLine) {\n\t\t\treturn fromLine;\n\t\t}\n\t}\n\n\tconst fromUrl = output.match(new RegExp(`[?&]id=(${UUID.source})`, \"i\"));\n\n\tif (fromUrl) {\n\t\treturn fromUrl[1];\n\t}\n\n\tconst bare = output.match(UUID);\n\n\treturn bare ? bare[0] : undefined;\n}\n\ninterface DeploymentNode {\n\tid: string;\n\tstatus: string;\n\tcreatedAt: string;\n}\n\ninterface LogLine {\n\tmessage: string;\n\tseverity?: string;\n}\n\n/**\n * A fault-tolerant Railway GraphQL caller bound to one deployment context. Every method\n * swallows transient transport/timeout errors (returning undefined/empty) so the monitor's\n * loops own the retry cadence — the network blips that broke the old CLI path are absorbed here.\n */\nfunction createRailwayApi(\n\tdeps: MonitorDeps,\n\tconfig: {\n\t\tapiUrl: string;\n\t\tprojectToken: string;\n\t\tprojectId: string;\n\t\tenvironmentId: string;\n\t\tserviceId: string;\n\t\trequestTimeoutMs: number;\n\t},\n) {\n\tconst call = async <T>(\n\t\tquery: string,\n\t\tvariables: Record<string, unknown>,\n\t): Promise<T | undefined> => {\n\t\ttry {\n\t\t\tconst signal =\n\t\t\t\tconfig.requestTimeoutMs > 0 &&\n\t\t\t\ttypeof AbortSignal?.timeout === \"function\"\n\t\t\t\t\t? AbortSignal.timeout(config.requestTimeoutMs)\n\t\t\t\t\t: undefined;\n\n\t\t\tconst response = await deps.fetch(config.apiUrl, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\"Project-Access-Token\": config.projectToken,\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({ query, variables }),\n\t\t\t\tsignal,\n\t\t\t});\n\n\t\t\tif (!response.ok) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\treturn ((await response.json()) as { data?: T }).data;\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t};\n\n\treturn {\n\t\t/** Newest deployment created at/after `since` (skew-buffered), or undefined. */\n\t\tasync resolveLatestDeployment(since: number): Promise<string | undefined> {\n\t\t\tconst data = await call<{\n\t\t\t\tdeployments: { edges: { node: DeploymentNode }[] };\n\t\t\t}>(QUERIES.resolveDeployments, {\n\t\t\t\tp: config.projectId,\n\t\t\t\te: config.environmentId,\n\t\t\t\ts: config.serviceId,\n\t\t\t});\n\n\t\t\tconst [newest] = (data?.deployments?.edges ?? [])\n\t\t\t\t.map((edge) => edge.node)\n\t\t\t\t.filter(\n\t\t\t\t\t(node) => node?.createdAt && Date.parse(node.createdAt) >= since,\n\t\t\t\t)\n\t\t\t\t.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));\n\n\t\t\treturn newest?.id;\n\t\t},\n\n\t\tasync deploymentStatus(id: string): Promise<string | undefined> {\n\t\t\tconst data = await call<{ deployment: { status: string } | null }>(\n\t\t\t\tQUERIES.deploymentStatus,\n\t\t\t\t{ d: id },\n\t\t\t);\n\n\t\t\treturn data?.deployment?.status;\n\t\t},\n\n\t\tasync failureLogs(\n\t\t\tid: string,\n\t\t\tlimit: number,\n\t\t): Promise<{ build: string[]; deploy: string[] }> {\n\t\t\tconst read = async (query: string, field: string): Promise<string[]> => {\n\t\t\t\tconst data = await call<Record<string, LogLine[]>>(query, {\n\t\t\t\t\td: id,\n\t\t\t\t\tn: limit,\n\t\t\t\t});\n\n\t\t\t\treturn (data?.[field] ?? []).map((line) => line.message ?? \"\");\n\t\t\t};\n\n\t\t\treturn {\n\t\t\t\tbuild: await read(QUERIES.buildLogs, \"buildLogs\"),\n\t\t\t\tdeploy: await read(QUERIES.deploymentLogs, \"deploymentLogs\"),\n\t\t\t};\n\t\t},\n\t};\n}\n\n/**\n * Watches a Railway deployment to a terminal state. The Railway API is authoritative: the\n * CLI's exit code only matters when NO deployment was ever created.\n */\nexport async function monitorRailwayDeployment(\n\tinput: MonitorInput,\n\tdeps: MonitorDeps,\n): Promise<MonitorResult> {\n\tconst resolveAttempts = input.resolveAttempts ?? DEFAULTS.resolveAttempts;\n\tconst resolveIntervalMs =\n\t\tinput.resolveIntervalMs ?? DEFAULTS.resolveIntervalMs;\n\tconst pollAttempts = input.pollAttempts ?? DEFAULTS.pollAttempts;\n\tconst pollIntervalMs = input.pollIntervalMs ?? DEFAULTS.pollIntervalMs;\n\tconst failureLogLimit = input.failureLogLimit ?? DEFAULTS.failureLogLimit;\n\n\tconst api = createRailwayApi(deps, {\n\t\tapiUrl: input.apiUrl ?? DEFAULTS.apiUrl,\n\t\tprojectToken: input.projectToken,\n\t\tprojectId: input.projectId,\n\t\tenvironmentId: input.environmentId,\n\t\tserviceId: input.serviceId,\n\t\trequestTimeoutMs: input.requestTimeoutMs ?? DEFAULTS.requestTimeoutMs,\n\t});\n\n\tconst dumpFailureLogs = async (id: string): Promise<void> => {\n\t\tconst logs = await api.failureLogs(id, failureLogLimit);\n\n\t\tfor (const [label, lines] of [\n\t\t\t[\"build logs\", logs.build],\n\t\t\t[\"deploy logs\", logs.deploy],\n\t\t] as const) {\n\t\t\tdeps.log(`[infracraft] ───── railway ${label} (deployment ${id}) ─────`);\n\n\t\t\tif (lines.length === 0) {\n\t\t\t\tdeps.log(\" (no log lines returned)\");\n\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor (const line of lines) {\n\t\t\t\tdeps.log(` ${line}`);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Identify the deployment: prefer the captured id, then parse the upload output,\n\t// then resolve the newest deployment created after this run started.\n\tlet deploymentId =\n\t\tinput.deploymentId ?? parseDeploymentId(input.uploadOutput);\n\n\tif (!deploymentId) {\n\t\tconst threshold = input.since - DEFAULTS.skewMs;\n\n\t\tfor (\n\t\t\tlet attempt = 0;\n\t\t\tattempt < resolveAttempts && !deploymentId;\n\t\t\tattempt++\n\t\t) {\n\t\t\tdeploymentId = await api.resolveLatestDeployment(threshold);\n\n\t\t\tif (!deploymentId) {\n\t\t\t\tawait deps.sleep(resolveIntervalMs);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!deploymentId) {\n\t\tconst failed = input.uploadExitCode !== 0;\n\n\t\tdeps.log(\n\t\t\tfailed\n\t\t\t\t? \"[infracraft] railway up failed and no deployment was created — failing the resource\"\n\t\t\t\t: \"[infracraft] no deployment resolved but railway up succeeded — not blocking the release\",\n\t\t);\n\n\t\treturn { outcome: MonitorOutcome.NO_DEPLOYMENT, failed };\n\t}\n\n\tdeps.log(`[infracraft] monitoring railway deployment ${deploymentId}`);\n\n\t// 2. Poll the API to a terminal status — this, not the CLI, decides pass/fail.\n\tlet lastStatus: string | undefined;\n\n\tfor (let attempt = 0; attempt < pollAttempts; attempt++) {\n\t\tconst status = await api.deploymentStatus(deploymentId);\n\n\t\tif (status) {\n\t\t\tlastStatus = status;\n\n\t\t\tdeps.log(\n\t\t\t\t`[infracraft] railway deployment ${deploymentId} status=${status}`,\n\t\t\t);\n\n\t\t\tconst disposition =\n\t\t\t\tDISPOSITION_BY_STATUS[status] ?? DeploymentDisposition.PENDING;\n\n\t\t\tif (disposition === DeploymentDisposition.LIVE) {\n\t\t\t\treturn {\n\t\t\t\t\toutcome: MonitorOutcome.SUCCESS,\n\t\t\t\t\tfailed: false,\n\t\t\t\t\tdeploymentId,\n\t\t\t\t\tstatus,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (disposition === DeploymentDisposition.SUPERSEDED) {\n\t\t\t\tdeps.log(\n\t\t\t\t\t`[infracraft] railway deployment ${deploymentId} SKIPPED (superseded) — not blocking`,\n\t\t\t\t);\n\n\t\t\t\treturn {\n\t\t\t\t\toutcome: MonitorOutcome.SKIPPED,\n\t\t\t\t\tfailed: false,\n\t\t\t\t\tdeploymentId,\n\t\t\t\t\tstatus,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (disposition === DeploymentDisposition.FAILED) {\n\t\t\t\tawait dumpFailureLogs(deploymentId);\n\n\t\t\t\treturn {\n\t\t\t\t\toutcome: MonitorOutcome.FAILED,\n\t\t\t\t\tfailed: true,\n\t\t\t\t\tdeploymentId,\n\t\t\t\t\tstatus,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tawait deps.sleep(pollIntervalMs);\n\t}\n\n\tdeps.log(\n\t\t`[infracraft] timed out waiting for railway deployment ${deploymentId} (last status=${lastStatus ?? \"unknown\"})`,\n\t);\n\n\tawait dumpFailureLogs(deploymentId);\n\n\treturn {\n\t\toutcome: MonitorOutcome.TIMED_OUT,\n\t\tfailed: true,\n\t\tdeploymentId,\n\t\tstatus: lastStatus,\n\t};\n}\n"],"mappings":"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"}
|