@infracraft/pulumi 1.16.5 → 1.16.6

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.
@@ -24,10 +24,11 @@ function createDeployCommand(args, opts) {
24
24
  const sandbox = deps.some(require_sandbox.isDeploySandbox);
25
25
  const gitGuard = deps.some(require_git_guard.isGitGuard);
26
26
  if (gitGuard && !sandbox) throw new Error(`[infracraft] ${args.name}: GitGuard has no effect without a DeploySandbox in dependsOn`);
27
+ let mode = "NONE";
28
+ if (sandbox) mode = gitGuard ? "STUB" : "ORIGINAL";
27
29
  const env = _pulumi_pulumi.getStack();
28
30
  const create = _pulumi_pulumi.output(args.cli).apply((cli) => require_sandbox.buildSandboxScript({
29
- sandbox,
30
- gitGuard,
31
+ mode,
31
32
  appName: args.name,
32
33
  env,
33
34
  excludePaths: args.excludePaths,
@@ -47,4 +48,5 @@ function createDeployCommand(args, opts) {
47
48
 
48
49
  //#endregion
49
50
  exports.createDeployCommand = createDeployCommand;
51
+ exports.dependsOnList = dependsOnList;
50
52
  //# sourceMappingURL=deploy.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.cjs","names":["isDeploySandbox","isGitGuard","pulumi","buildSandboxScript","command"],"sources":["../../src/commands/deploy.ts"],"sourcesContent":["// src/commands/deploy.ts\nimport * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\nimport { isGitGuard } from \"../git-guard\";\nimport { buildSandboxScript, isDeploySandbox } from \"../sandbox\";\n\nexport interface CreateDeployCommandArgs {\n\t/** Resource name; the child command is `<name>` and the sandbox dir uses it. */\n\tname: string;\n\t/** Fully-formed platform deploy command (may be an Output, e.g. Railway). */\n\tcli: pulumi.Input<string>;\n\t/** Redeploy triggers (source/env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\t/** Upload-scoping excludes (applied only in stub mode). */\n\texcludePaths?: string[];\n\t/** Shell run in the working dir before `cli` (e.g. write railpack.json). */\n\tsetup?: string;\n\t/** Env passed to the command (secrets that survive preview; not the inlined ones). */\n\tenvironment?: Record<string, pulumi.Input<string>>;\n}\n\nexport interface CreateDeployCommandResult {\n\tcommand: command.local.Command;\n\t/** The deploy CLI's final stdout line (the production URL for Vercel/Fly). */\n\tdeploymentUrl: pulumi.Output<string>;\n}\n\n/** Reads a `dependsOn` opt into a flat array of resource instances. */\nfunction dependsOnList(opts: pulumi.ComponentResourceOptions): unknown[] {\n\tconst dep = opts.dependsOn;\n\n\tif (Array.isArray(dep)) {\n\t\treturn dep;\n\t}\n\n\treturn dep ? [dep] : [];\n}\n\n/**\n * Builds a sandboxed deploy command. Inspects `opts.dependsOn` by brand:\n * a DeploySandbox → isolate; a GitGuard → stub `.git`; GitGuard alone → throw.\n * The platform deploy resources call ONLY this; they never touch the sandbox.\n */\nexport function createDeployCommand(\n\targs: CreateDeployCommandArgs,\n\topts: pulumi.ComponentResourceOptions,\n): CreateDeployCommandResult {\n\tconst deps = dependsOnList(opts);\n\tconst sandbox = deps.some(isDeploySandbox);\n\tconst gitGuard = deps.some(isGitGuard);\n\n\tif (gitGuard && !sandbox) {\n\t\tthrow new Error(\n\t\t\t`[infracraft] ${args.name}: GitGuard has no effect without a DeploySandbox in dependsOn`,\n\t\t);\n\t}\n\n\tconst env = pulumi.getStack();\n\n\tconst create = pulumi.output(args.cli).apply((cli) =>\n\t\tbuildSandboxScript({\n\t\t\tsandbox,\n\t\t\tgitGuard,\n\t\t\tappName: args.name,\n\t\t\tenv,\n\t\t\texcludePaths: args.excludePaths,\n\t\t\tsetup: args.setup,\n\t\t\tcli,\n\t\t}),\n\t);\n\n\tconst cmd = new command.local.Command(\n\t\targs.name,\n\t\t{ create, triggers: args.triggers, environment: args.environment },\n\t\topts,\n\t);\n\n\tconst deploymentUrl = cmd.stdout.apply(\n\t\t(out) => out.trim().split(\"\\n\").pop() ?? \"\",\n\t);\n\n\treturn { command: cmd, deploymentUrl };\n}\n"],"mappings":";;;;;;;;;;;AA6BA,SAAS,cAAc,MAAkD;CACxE,MAAM,MAAM,KAAK;CAEjB,IAAI,MAAM,QAAQ,GAAG,GACpB,OAAO;CAGR,OAAO,MAAM,CAAC,GAAG,IAAI,CAAC;AACvB;;;;;;AAOA,SAAgB,oBACf,MACA,MAC4B;CAC5B,MAAM,OAAO,cAAc,IAAI;CAC/B,MAAM,UAAU,KAAK,KAAKA,+BAAe;CACzC,MAAM,WAAW,KAAK,KAAKC,4BAAU;CAErC,IAAI,YAAY,CAAC,SAChB,MAAM,IAAI,MACT,gBAAgB,KAAK,KAAK,8DAC3B;CAGD,MAAM,MAAMC,eAAO,SAAS;CAE5B,MAAM,SAASA,eAAO,OAAO,KAAK,GAAG,EAAE,OAAO,QAC7CC,mCAAmB;EAClB;EACA;EACA,SAAS,KAAK;EACd;EACA,cAAc,KAAK;EACnB,OAAO,KAAK;EACZ;CACD,CAAC,CACF;CAEA,MAAM,MAAM,IAAIC,gBAAQ,MAAM,QAC7B,KAAK,MACL;EAAE;EAAQ,UAAU,KAAK;EAAU,aAAa,KAAK;CAAY,GACjE,IACD;CAMA,OAAO;EAAE,SAAS;EAAK,eAJD,IAAI,OAAO,OAC/B,QAAQ,IAAI,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,KAAK,EAGP;CAAE;AACtC"}
1
+ {"version":3,"file":"deploy.cjs","names":["isDeploySandbox","isGitGuard","pulumi","buildSandboxScript","command"],"sources":["../../src/commands/deploy.ts"],"sourcesContent":["// src/commands/deploy.ts\nimport * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\nimport { isGitGuard } from \"../git-guard\";\nimport { buildSandboxScript, isDeploySandbox, SandboxMode } from \"../sandbox\";\n\nexport interface CreateDeployCommandArgs {\n\t/** Resource name; the child command is `<name>` and the sandbox dir uses it. */\n\tname: string;\n\t/** Fully-formed platform deploy command (may be an Output, e.g. Railway). */\n\tcli: pulumi.Input<string>;\n\t/** Redeploy triggers (source/env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\t/** Upload-scoping excludes (applied only in stub mode). */\n\texcludePaths?: string[];\n\t/** Shell run in the working dir before `cli` (e.g. write railpack.json). */\n\tsetup?: string;\n\t/** Env passed to the command (secrets that survive preview; not the inlined ones). */\n\tenvironment?: Record<string, pulumi.Input<string>>;\n}\n\nexport interface CreateDeployCommandResult {\n\tcommand: command.local.Command;\n\t/** The deploy CLI's final stdout line (the production URL for Vercel/Fly). */\n\tdeploymentUrl: pulumi.Output<string>;\n}\n\n/** Reads a `dependsOn` opt into a flat array of resource instances. */\nexport function dependsOnList(\n\topts: pulumi.ComponentResourceOptions,\n): unknown[] {\n\tconst dep = opts.dependsOn;\n\n\tif (Array.isArray(dep)) {\n\t\treturn dep;\n\t}\n\n\treturn dep ? [dep] : [];\n}\n\n/**\n * Builds a sandboxed deploy command. Inspects `opts.dependsOn` by brand:\n * a DeploySandbox → isolate; a GitGuard → stub `.git`; GitGuard alone → throw.\n * The platform deploy resources call ONLY this; they never touch the sandbox.\n */\nexport function createDeployCommand(\n\targs: CreateDeployCommandArgs,\n\topts: pulumi.ComponentResourceOptions,\n): CreateDeployCommandResult {\n\tconst deps = dependsOnList(opts);\n\tconst sandbox = deps.some(isDeploySandbox);\n\tconst gitGuard = deps.some(isGitGuard);\n\n\tif (gitGuard && !sandbox) {\n\t\tthrow new Error(\n\t\t\t`[infracraft] ${args.name}: GitGuard has no effect without a DeploySandbox in dependsOn`,\n\t\t);\n\t}\n\n\tlet mode = SandboxMode.NONE;\n\n\tif (sandbox) {\n\t\tmode = gitGuard ? SandboxMode.STUB : SandboxMode.ORIGINAL;\n\t}\n\n\tconst env = pulumi.getStack();\n\n\tconst create = pulumi.output(args.cli).apply((cli) =>\n\t\tbuildSandboxScript({\n\t\t\tmode,\n\t\t\tappName: args.name,\n\t\t\tenv,\n\t\t\texcludePaths: args.excludePaths,\n\t\t\tsetup: args.setup,\n\t\t\tcli,\n\t\t}),\n\t);\n\n\tconst cmd = new command.local.Command(\n\t\targs.name,\n\t\t{ create, triggers: args.triggers, environment: args.environment },\n\t\topts,\n\t);\n\n\tconst deploymentUrl = cmd.stdout.apply(\n\t\t(out) => out.trim().split(\"\\n\").pop() ?? \"\",\n\t);\n\n\treturn { command: cmd, deploymentUrl };\n}\n"],"mappings":";;;;;;;;;;;AA6BA,SAAgB,cACf,MACY;CACZ,MAAM,MAAM,KAAK;CAEjB,IAAI,MAAM,QAAQ,GAAG,GACpB,OAAO;CAGR,OAAO,MAAM,CAAC,GAAG,IAAI,CAAC;AACvB;;;;;;AAOA,SAAgB,oBACf,MACA,MAC4B;CAC5B,MAAM,OAAO,cAAc,IAAI;CAC/B,MAAM,UAAU,KAAK,KAAKA,+BAAe;CACzC,MAAM,WAAW,KAAK,KAAKC,4BAAU;CAErC,IAAI,YAAY,CAAC,SAChB,MAAM,IAAI,MACT,gBAAgB,KAAK,KAAK,8DAC3B;CAGD,IAAI;CAEJ,IAAI,SACH,OAAO;CAGR,MAAM,MAAMC,eAAO,SAAS;CAE5B,MAAM,SAASA,eAAO,OAAO,KAAK,GAAG,EAAE,OAAO,QAC7CC,mCAAmB;EAClB;EACA,SAAS,KAAK;EACd;EACA,cAAc,KAAK;EACnB,OAAO,KAAK;EACZ;CACD,CAAC,CACF;CAEA,MAAM,MAAM,IAAIC,gBAAQ,MAAM,QAC7B,KAAK,MACL;EAAE;EAAQ,UAAU,KAAK;EAAU,aAAa,KAAK;CAAY,GACjE,IACD;CAMA,OAAO;EAAE,SAAS;EAAK,eAJD,IAAI,OAAO,OAC/B,QAAQ,IAAI,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,KAAK,EAGP;CAAE;AACtC"}
@@ -22,6 +22,8 @@ interface CreateDeployCommandResult {
22
22
  /** The deploy CLI's final stdout line (the production URL for Vercel/Fly). */
23
23
  deploymentUrl: pulumi.Output<string>;
24
24
  }
25
+ /** Reads a `dependsOn` opt into a flat array of resource instances. */
26
+ declare function dependsOnList(opts: pulumi.ComponentResourceOptions): unknown[];
25
27
  /**
26
28
  * Builds a sandboxed deploy command. Inspects `opts.dependsOn` by brand:
27
29
  * a DeploySandbox → isolate; a GitGuard → stub `.git`; GitGuard alone → throw.
@@ -29,5 +31,5 @@ interface CreateDeployCommandResult {
29
31
  */
30
32
  declare function createDeployCommand(args: CreateDeployCommandArgs, opts: pulumi.ComponentResourceOptions): CreateDeployCommandResult;
31
33
  //#endregion
32
- export { CreateDeployCommandArgs, CreateDeployCommandResult, createDeployCommand };
34
+ export { CreateDeployCommandArgs, CreateDeployCommandResult, createDeployCommand, dependsOnList };
33
35
  //# sourceMappingURL=deploy.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/commands/deploy.ts"],"mappings":";;;;;UAOiB,uBAAA;;EAEhB,IAAA;EAFgB;EAIhB,GAAA,EAAK,MAAA,CAAO,KAAA;;EAEZ,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAAP;EAEvB,YAAA;EAI6B;EAF7B,KAAA;EAEoB;EAApB,WAAA,GAAc,MAAA,SAAe,MAAA,CAAO,KAAA;AAAA;AAAA,UAGpB,yBAAA;EAChB,OAAA,EAAS,OAAA,CAAQ,KAAA,CAAM,OAAA;EAZX;EAcZ,aAAA,EAAe,MAAA,CAAO,MAAM;AAAA;;;;;;iBAmBb,mBAAA,CACf,IAAA,EAAM,uBAAA,EACN,IAAA,EAAM,MAAA,CAAO,wBAAA,GACX,yBAAA"}
1
+ {"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/commands/deploy.ts"],"mappings":";;;;;UAOiB,uBAAA;;EAEhB,IAAA;EAFgB;EAIhB,GAAA,EAAK,MAAA,CAAO,KAAA;;EAEZ,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAAP;EAEvB,YAAA;EAI6B;EAF7B,KAAA;EAEoB;EAApB,WAAA,GAAc,MAAA,SAAe,MAAA,CAAO,KAAA;AAAA;AAAA,UAGpB,yBAAA;EAChB,OAAA,EAAS,OAAA,CAAQ,KAAA,CAAM,OAAA;EAZX;EAcZ,aAAA,EAAe,MAAA,CAAO,MAAM;AAAA;;iBAIb,aAAA,CACf,IAAA,EAAM,MAAA,CAAO,wBAAwB;;;;;;iBAgBtB,mBAAA,CACf,IAAA,EAAM,uBAAA,EACN,IAAA,EAAM,MAAA,CAAO,wBAAA,GACX,yBAAA"}
@@ -22,6 +22,8 @@ interface CreateDeployCommandResult {
22
22
  /** The deploy CLI's final stdout line (the production URL for Vercel/Fly). */
23
23
  deploymentUrl: pulumi.Output<string>;
24
24
  }
25
+ /** Reads a `dependsOn` opt into a flat array of resource instances. */
26
+ declare function dependsOnList(opts: pulumi.ComponentResourceOptions): unknown[];
25
27
  /**
26
28
  * Builds a sandboxed deploy command. Inspects `opts.dependsOn` by brand:
27
29
  * a DeploySandbox → isolate; a GitGuard → stub `.git`; GitGuard alone → throw.
@@ -29,5 +31,5 @@ interface CreateDeployCommandResult {
29
31
  */
30
32
  declare function createDeployCommand(args: CreateDeployCommandArgs, opts: pulumi.ComponentResourceOptions): CreateDeployCommandResult;
31
33
  //#endregion
32
- export { CreateDeployCommandArgs, CreateDeployCommandResult, createDeployCommand };
34
+ export { CreateDeployCommandArgs, CreateDeployCommandResult, createDeployCommand, dependsOnList };
33
35
  //# sourceMappingURL=deploy.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/commands/deploy.ts"],"mappings":";;;;;UAOiB,uBAAA;;EAEhB,IAAA;EAFgB;EAIhB,GAAA,EAAK,MAAA,CAAO,KAAA;;EAEZ,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAAP;EAEvB,YAAA;EAI6B;EAF7B,KAAA;EAEoB;EAApB,WAAA,GAAc,MAAA,SAAe,MAAA,CAAO,KAAA;AAAA;AAAA,UAGpB,yBAAA;EAChB,OAAA,EAAS,OAAA,CAAQ,KAAA,CAAM,OAAA;EAZX;EAcZ,aAAA,EAAe,MAAA,CAAO,MAAM;AAAA;;;;;;iBAmBb,mBAAA,CACf,IAAA,EAAM,uBAAA,EACN,IAAA,EAAM,MAAA,CAAO,wBAAA,GACX,yBAAA"}
1
+ {"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/commands/deploy.ts"],"mappings":";;;;;UAOiB,uBAAA;;EAEhB,IAAA;EAFgB;EAIhB,GAAA,EAAK,MAAA,CAAO,KAAA;;EAEZ,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAAP;EAEvB,YAAA;EAI6B;EAF7B,KAAA;EAEoB;EAApB,WAAA,GAAc,MAAA,SAAe,MAAA,CAAO,KAAA;AAAA;AAAA,UAGpB,yBAAA;EAChB,OAAA,EAAS,OAAA,CAAQ,KAAA,CAAM,OAAA;EAZX;EAcZ,aAAA,EAAe,MAAA,CAAO,MAAM;AAAA;;iBAIb,aAAA,CACf,IAAA,EAAM,MAAA,CAAO,wBAAwB;;;;;;iBAgBtB,mBAAA,CACf,IAAA,EAAM,uBAAA,EACN,IAAA,EAAM,MAAA,CAAO,wBAAA,GACX,yBAAA"}
@@ -1,6 +1,6 @@
1
1
  import { t as __name } from "../chunk-OPjESj5l.mjs";
2
2
  import { isGitGuard } from "../git-guard.mjs";
3
- import { buildSandboxScript, isDeploySandbox } from "../sandbox.mjs";
3
+ import { SandboxMode, buildSandboxScript, isDeploySandbox } from "../sandbox.mjs";
4
4
  import * as pulumi from "@pulumi/pulumi";
5
5
  import * as command from "@pulumi/command";
6
6
 
@@ -21,10 +21,11 @@ function createDeployCommand(args, opts) {
21
21
  const sandbox = deps.some(isDeploySandbox);
22
22
  const gitGuard = deps.some(isGitGuard);
23
23
  if (gitGuard && !sandbox) throw new Error(`[infracraft] ${args.name}: GitGuard has no effect without a DeploySandbox in dependsOn`);
24
+ let mode = "NONE";
25
+ if (sandbox) mode = gitGuard ? "STUB" : "ORIGINAL";
24
26
  const env = pulumi.getStack();
25
27
  const create = pulumi.output(args.cli).apply((cli) => buildSandboxScript({
26
- sandbox,
27
- gitGuard,
28
+ mode,
28
29
  appName: args.name,
29
30
  env,
30
31
  excludePaths: args.excludePaths,
@@ -43,5 +44,5 @@ function createDeployCommand(args, opts) {
43
44
  }
44
45
 
45
46
  //#endregion
46
- export { createDeployCommand };
47
+ export { createDeployCommand, dependsOnList };
47
48
  //# sourceMappingURL=deploy.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.mjs","names":[],"sources":["../../src/commands/deploy.ts"],"sourcesContent":["// src/commands/deploy.ts\nimport * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\nimport { isGitGuard } from \"../git-guard\";\nimport { buildSandboxScript, isDeploySandbox } from \"../sandbox\";\n\nexport interface CreateDeployCommandArgs {\n\t/** Resource name; the child command is `<name>` and the sandbox dir uses it. */\n\tname: string;\n\t/** Fully-formed platform deploy command (may be an Output, e.g. Railway). */\n\tcli: pulumi.Input<string>;\n\t/** Redeploy triggers (source/env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\t/** Upload-scoping excludes (applied only in stub mode). */\n\texcludePaths?: string[];\n\t/** Shell run in the working dir before `cli` (e.g. write railpack.json). */\n\tsetup?: string;\n\t/** Env passed to the command (secrets that survive preview; not the inlined ones). */\n\tenvironment?: Record<string, pulumi.Input<string>>;\n}\n\nexport interface CreateDeployCommandResult {\n\tcommand: command.local.Command;\n\t/** The deploy CLI's final stdout line (the production URL for Vercel/Fly). */\n\tdeploymentUrl: pulumi.Output<string>;\n}\n\n/** Reads a `dependsOn` opt into a flat array of resource instances. */\nfunction dependsOnList(opts: pulumi.ComponentResourceOptions): unknown[] {\n\tconst dep = opts.dependsOn;\n\n\tif (Array.isArray(dep)) {\n\t\treturn dep;\n\t}\n\n\treturn dep ? [dep] : [];\n}\n\n/**\n * Builds a sandboxed deploy command. Inspects `opts.dependsOn` by brand:\n * a DeploySandbox → isolate; a GitGuard → stub `.git`; GitGuard alone → throw.\n * The platform deploy resources call ONLY this; they never touch the sandbox.\n */\nexport function createDeployCommand(\n\targs: CreateDeployCommandArgs,\n\topts: pulumi.ComponentResourceOptions,\n): CreateDeployCommandResult {\n\tconst deps = dependsOnList(opts);\n\tconst sandbox = deps.some(isDeploySandbox);\n\tconst gitGuard = deps.some(isGitGuard);\n\n\tif (gitGuard && !sandbox) {\n\t\tthrow new Error(\n\t\t\t`[infracraft] ${args.name}: GitGuard has no effect without a DeploySandbox in dependsOn`,\n\t\t);\n\t}\n\n\tconst env = pulumi.getStack();\n\n\tconst create = pulumi.output(args.cli).apply((cli) =>\n\t\tbuildSandboxScript({\n\t\t\tsandbox,\n\t\t\tgitGuard,\n\t\t\tappName: args.name,\n\t\t\tenv,\n\t\t\texcludePaths: args.excludePaths,\n\t\t\tsetup: args.setup,\n\t\t\tcli,\n\t\t}),\n\t);\n\n\tconst cmd = new command.local.Command(\n\t\targs.name,\n\t\t{ create, triggers: args.triggers, environment: args.environment },\n\t\topts,\n\t);\n\n\tconst deploymentUrl = cmd.stdout.apply(\n\t\t(out) => out.trim().split(\"\\n\").pop() ?? \"\",\n\t);\n\n\treturn { command: cmd, deploymentUrl };\n}\n"],"mappings":";;;;;;;;AA6BA,SAAS,cAAc,MAAkD;CACxE,MAAM,MAAM,KAAK;CAEjB,IAAI,MAAM,QAAQ,GAAG,GACpB,OAAO;CAGR,OAAO,MAAM,CAAC,GAAG,IAAI,CAAC;AACvB;;;;;;AAOA,SAAgB,oBACf,MACA,MAC4B;CAC5B,MAAM,OAAO,cAAc,IAAI;CAC/B,MAAM,UAAU,KAAK,KAAK,eAAe;CACzC,MAAM,WAAW,KAAK,KAAK,UAAU;CAErC,IAAI,YAAY,CAAC,SAChB,MAAM,IAAI,MACT,gBAAgB,KAAK,KAAK,8DAC3B;CAGD,MAAM,MAAM,OAAO,SAAS;CAE5B,MAAM,SAAS,OAAO,OAAO,KAAK,GAAG,EAAE,OAAO,QAC7C,mBAAmB;EAClB;EACA;EACA,SAAS,KAAK;EACd;EACA,cAAc,KAAK;EACnB,OAAO,KAAK;EACZ;CACD,CAAC,CACF;CAEA,MAAM,MAAM,IAAI,QAAQ,MAAM,QAC7B,KAAK,MACL;EAAE;EAAQ,UAAU,KAAK;EAAU,aAAa,KAAK;CAAY,GACjE,IACD;CAMA,OAAO;EAAE,SAAS;EAAK,eAJD,IAAI,OAAO,OAC/B,QAAQ,IAAI,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,KAAK,EAGP;CAAE;AACtC"}
1
+ {"version":3,"file":"deploy.mjs","names":[],"sources":["../../src/commands/deploy.ts"],"sourcesContent":["// src/commands/deploy.ts\nimport * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\nimport { isGitGuard } from \"../git-guard\";\nimport { buildSandboxScript, isDeploySandbox, SandboxMode } from \"../sandbox\";\n\nexport interface CreateDeployCommandArgs {\n\t/** Resource name; the child command is `<name>` and the sandbox dir uses it. */\n\tname: string;\n\t/** Fully-formed platform deploy command (may be an Output, e.g. Railway). */\n\tcli: pulumi.Input<string>;\n\t/** Redeploy triggers (source/env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\t/** Upload-scoping excludes (applied only in stub mode). */\n\texcludePaths?: string[];\n\t/** Shell run in the working dir before `cli` (e.g. write railpack.json). */\n\tsetup?: string;\n\t/** Env passed to the command (secrets that survive preview; not the inlined ones). */\n\tenvironment?: Record<string, pulumi.Input<string>>;\n}\n\nexport interface CreateDeployCommandResult {\n\tcommand: command.local.Command;\n\t/** The deploy CLI's final stdout line (the production URL for Vercel/Fly). */\n\tdeploymentUrl: pulumi.Output<string>;\n}\n\n/** Reads a `dependsOn` opt into a flat array of resource instances. */\nexport function dependsOnList(\n\topts: pulumi.ComponentResourceOptions,\n): unknown[] {\n\tconst dep = opts.dependsOn;\n\n\tif (Array.isArray(dep)) {\n\t\treturn dep;\n\t}\n\n\treturn dep ? [dep] : [];\n}\n\n/**\n * Builds a sandboxed deploy command. Inspects `opts.dependsOn` by brand:\n * a DeploySandbox → isolate; a GitGuard → stub `.git`; GitGuard alone → throw.\n * The platform deploy resources call ONLY this; they never touch the sandbox.\n */\nexport function createDeployCommand(\n\targs: CreateDeployCommandArgs,\n\topts: pulumi.ComponentResourceOptions,\n): CreateDeployCommandResult {\n\tconst deps = dependsOnList(opts);\n\tconst sandbox = deps.some(isDeploySandbox);\n\tconst gitGuard = deps.some(isGitGuard);\n\n\tif (gitGuard && !sandbox) {\n\t\tthrow new Error(\n\t\t\t`[infracraft] ${args.name}: GitGuard has no effect without a DeploySandbox in dependsOn`,\n\t\t);\n\t}\n\n\tlet mode = SandboxMode.NONE;\n\n\tif (sandbox) {\n\t\tmode = gitGuard ? SandboxMode.STUB : SandboxMode.ORIGINAL;\n\t}\n\n\tconst env = pulumi.getStack();\n\n\tconst create = pulumi.output(args.cli).apply((cli) =>\n\t\tbuildSandboxScript({\n\t\t\tmode,\n\t\t\tappName: args.name,\n\t\t\tenv,\n\t\t\texcludePaths: args.excludePaths,\n\t\t\tsetup: args.setup,\n\t\t\tcli,\n\t\t}),\n\t);\n\n\tconst cmd = new command.local.Command(\n\t\targs.name,\n\t\t{ create, triggers: args.triggers, environment: args.environment },\n\t\topts,\n\t);\n\n\tconst deploymentUrl = cmd.stdout.apply(\n\t\t(out) => out.trim().split(\"\\n\").pop() ?? \"\",\n\t);\n\n\treturn { command: cmd, deploymentUrl };\n}\n"],"mappings":";;;;;;;;AA6BA,SAAgB,cACf,MACY;CACZ,MAAM,MAAM,KAAK;CAEjB,IAAI,MAAM,QAAQ,GAAG,GACpB,OAAO;CAGR,OAAO,MAAM,CAAC,GAAG,IAAI,CAAC;AACvB;;;;;;AAOA,SAAgB,oBACf,MACA,MAC4B;CAC5B,MAAM,OAAO,cAAc,IAAI;CAC/B,MAAM,UAAU,KAAK,KAAK,eAAe;CACzC,MAAM,WAAW,KAAK,KAAK,UAAU;CAErC,IAAI,YAAY,CAAC,SAChB,MAAM,IAAI,MACT,gBAAgB,KAAK,KAAK,8DAC3B;CAGD,IAAI;CAEJ,IAAI,SACH,OAAO;CAGR,MAAM,MAAM,OAAO,SAAS;CAE5B,MAAM,SAAS,OAAO,OAAO,KAAK,GAAG,EAAE,OAAO,QAC7C,mBAAmB;EAClB;EACA,SAAS,KAAK;EACd;EACA,cAAc,KAAK;EACnB,OAAO,KAAK;EACZ;CACD,CAAC,CACF;CAEA,MAAM,MAAM,IAAI,QAAQ,MAAM,QAC7B,KAAK,MACL;EAAE;EAAQ,UAAU,KAAK;EAAU,aAAa,KAAK;CAAY,GACjE,IACD;CAMA,OAAO;EAAE,SAAS;EAAK,eAJD,IAAI,OAAO,OAC/B,QAAQ,IAAI,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,KAAK,EAGP;CAAE;AACtC"}
@@ -22,11 +22,8 @@ var FlyDeploy = class extends _pulumi_pulumi.ComponentResource {
22
22
  const setup = `mkdir -p .fly && printf '%s' "$FLY_TOML_CONTENT" > ${configPath}`;
23
23
  const cli = `fly deploy --config ${configPath} --remote-only --ha=${highAvailability} --wait-timeout ${waitTimeout} --release-command-timeout ${releaseCommandTimeout}`;
24
24
  const triggers = _pulumi_pulumi.output(args.triggers).apply((values) => [...values, tomlContent]);
25
- let consumerDeps;
26
- if (Array.isArray(pulumiOpts.dependsOn)) consumerDeps = pulumiOpts.dependsOn;
27
- else if (pulumiOpts.dependsOn) consumerDeps = [pulumiOpts.dependsOn];
28
- else consumerDeps = [];
29
- require_commands_deploy.createDeployCommand({
25
+ const consumerDeps = require_commands_deploy.dependsOnList(pulumiOpts);
26
+ const { deploymentUrl } = require_commands_deploy.createDeployCommand({
30
27
  name,
31
28
  cli,
32
29
  triggers,
@@ -40,7 +37,8 @@ var FlyDeploy = class extends _pulumi_pulumi.ComponentResource {
40
37
  parent: this,
41
38
  dependsOn: [app, ...consumerDeps]
42
39
  });
43
- this.registerOutputs({});
40
+ this.deploymentUrl = deploymentUrl;
41
+ this.registerOutputs({ deploymentUrl: this.deploymentUrl });
44
42
  }
45
43
  };
46
44
 
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.cjs","names":["pulumi","generateFlyToml"],"sources":["../../src/fly/deploy.ts"],"sourcesContent":["// src/fly/deploy.ts (replace entire file)\nimport * as pulumi from \"@pulumi/pulumi\";\n\nimport { createDeployCommand } from \"../commands/deploy\";\nimport type { FlyApp } from \"./app\";\nimport type { FlyProvider } from \"./provider\";\nimport { type FlyTomlConfig, generateFlyToml } from \"./toml\";\n\ntype FlyDeployOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\tprovider: FlyProvider;\n\tapp: FlyApp;\n};\n\nexport interface FlyDeployArgs {\n\t/** fly.toml configuration. `config.app` must equal the FlyApp name. */\n\tconfig: FlyTomlConfig;\n\t/** Redeploy triggers; the generated toml content is appended automatically. */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\t/** `fly deploy --wait-timeout` seconds (default 300). */\n\twaitTimeout?: number;\n\t/** `fly deploy --release-command-timeout` seconds (default 600). */\n\treleaseCommandTimeout?: number;\n\t/** `fly deploy --ha` (default false). */\n\thighAvailability?: boolean;\n}\n\n/**\n * Deploys a Fly app via `fly deploy --remote-only` from a generated fly.toml.\n * Isolation/git are the seam's job (list a `DeploySandbox`, optionally a `GitGuard`).\n */\nexport class FlyDeploy extends pulumi.ComponentResource {\n\tconstructor(name: string, args: FlyDeployArgs, opts: FlyDeployOptions) {\n\t\tconst { provider, app, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:fly:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst tomlContent = generateFlyToml(args.config);\n\t\tconst configPath = `.fly/${args.config.app}.toml`;\n\n\t\tconst waitTimeout = args.waitTimeout ?? 300;\n\t\tconst releaseCommandTimeout = args.releaseCommandTimeout ?? 600;\n\t\tconst highAvailability = args.highAvailability ?? false;\n\n\t\t// The toml content arrives via FLY_TOML_CONTENT (avoids shell escaping).\n\t\tconst setup = `mkdir -p .fly && printf '%s' \"$FLY_TOML_CONTENT\" > ${configPath}`;\n\t\tconst cli = `fly deploy --config ${configPath} --remote-only --ha=${highAvailability} --wait-timeout ${waitTimeout} --release-command-timeout ${releaseCommandTimeout}`;\n\n\t\tconst triggers = pulumi\n\t\t\t.output(args.triggers)\n\t\t\t.apply((values) => [...values, tomlContent]);\n\n\t\tlet consumerDeps: pulumi.Resource[];\n\n\t\tif (Array.isArray(pulumiOpts.dependsOn)) {\n\t\t\tconsumerDeps = pulumiOpts.dependsOn as pulumi.Resource[];\n\t\t} else if (pulumiOpts.dependsOn) {\n\t\t\tconsumerDeps = [pulumiOpts.dependsOn as pulumi.Resource];\n\t\t} else {\n\t\t\tconsumerDeps = [];\n\t\t}\n\n\t\tcreateDeployCommand(\n\t\t\t{\n\t\t\t\tname,\n\t\t\t\tcli,\n\t\t\t\ttriggers,\n\t\t\t\tsetup,\n\t\t\t\tenvironment: {\n\t\t\t\t\tFLY_API_TOKEN: provider.token,\n\t\t\t\t\tFLY_TOML_CONTENT: tomlContent,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ ...pulumiOpts, parent: this, dependsOn: [app, ...consumerDeps] },\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;;;;;;;AA8BA,IAAa,YAAb,cAA+BA,eAAO,kBAAkB;CACvD,YAAY,MAAc,MAAqB,MAAwB;EACtE,MAAM,EAAE,UAAU,KAAK,GAAG,eAAe;EAEzC,MAAM,yBAAyB,MAAM,CAAC,GAAG,UAAU;EAEnD,MAAM,cAAcC,iCAAgB,KAAK,MAAM;EAC/C,MAAM,aAAa,QAAQ,KAAK,OAAO,IAAI;EAE3C,MAAM,cAAc,KAAK,eAAe;EACxC,MAAM,wBAAwB,KAAK,yBAAyB;EAC5D,MAAM,mBAAmB,KAAK,oBAAoB;EAGlD,MAAM,QAAQ,sDAAsD;EACpE,MAAM,MAAM,uBAAuB,WAAW,sBAAsB,iBAAiB,kBAAkB,YAAY,6BAA6B;EAEhJ,MAAM,WAAWD,eACf,OAAO,KAAK,QAAQ,EACpB,OAAO,WAAW,CAAC,GAAG,QAAQ,WAAW,CAAC;EAE5C,IAAI;EAEJ,IAAI,MAAM,QAAQ,WAAW,SAAS,GACrC,eAAe,WAAW;OACpB,IAAI,WAAW,WACrB,eAAe,CAAC,WAAW,SAA4B;OAEvD,eAAe,CAAC;EAGjB,4CACC;GACC;GACA;GACA;GACA;GACA,aAAa;IACZ,eAAe,SAAS;IACxB,kBAAkB;GACnB;EACD,GACA;GAAE,GAAG;GAAY,QAAQ;GAAM,WAAW,CAAC,KAAK,GAAG,YAAY;EAAE,CAClE;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
1
+ {"version":3,"file":"deploy.cjs","names":["pulumi","generateFlyToml","dependsOnList","createDeployCommand"],"sources":["../../src/fly/deploy.ts"],"sourcesContent":["// src/fly/deploy.ts (replace entire file)\nimport * as pulumi from \"@pulumi/pulumi\";\n\nimport { createDeployCommand, dependsOnList } from \"../commands/deploy\";\nimport type { FlyApp } from \"./app\";\nimport type { FlyProvider } from \"./provider\";\nimport { type FlyTomlConfig, generateFlyToml } from \"./toml\";\n\ntype FlyDeployOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\tprovider: FlyProvider;\n\tapp: FlyApp;\n};\n\nexport interface FlyDeployArgs {\n\t/** fly.toml configuration. `config.app` must equal the FlyApp name. */\n\tconfig: FlyTomlConfig;\n\t/** Redeploy triggers; the generated toml content is appended automatically. */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\t/** `fly deploy --wait-timeout` seconds (default 300). */\n\twaitTimeout?: number;\n\t/** `fly deploy --release-command-timeout` seconds (default 600). */\n\treleaseCommandTimeout?: number;\n\t/** `fly deploy --ha` (default false). */\n\thighAvailability?: boolean;\n}\n\n/**\n * Deploys a Fly app via `fly deploy --remote-only` from a generated fly.toml.\n * Isolation/git are the seam's job (list a `DeploySandbox`, optionally a `GitGuard`).\n */\nexport class FlyDeploy extends pulumi.ComponentResource {\n\t/** The deploy CLI's final stdout line (the Fly app URL when emitted). */\n\tpublic readonly deploymentUrl: pulumi.Output<string>;\n\n\tconstructor(name: string, args: FlyDeployArgs, opts: FlyDeployOptions) {\n\t\tconst { provider, app, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:fly:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst tomlContent = generateFlyToml(args.config);\n\t\tconst configPath = `.fly/${args.config.app}.toml`;\n\n\t\tconst waitTimeout = args.waitTimeout ?? 300;\n\t\tconst releaseCommandTimeout = args.releaseCommandTimeout ?? 600;\n\t\tconst highAvailability = args.highAvailability ?? false;\n\n\t\t// The toml content arrives via FLY_TOML_CONTENT (avoids shell escaping).\n\t\tconst setup = `mkdir -p .fly && printf '%s' \"$FLY_TOML_CONTENT\" > ${configPath}`;\n\t\tconst cli = `fly deploy --config ${configPath} --remote-only --ha=${highAvailability} --wait-timeout ${waitTimeout} --release-command-timeout ${releaseCommandTimeout}`;\n\n\t\tconst triggers = pulumi\n\t\t\t.output(args.triggers)\n\t\t\t.apply((values) => [...values, tomlContent]);\n\n\t\t// Keep the `app` ordering anchor first; append the consumer's deps\n\t\t// (DeploySandbox / GitGuard) using the seam's shared normaliser.\n\t\tconst consumerDeps = dependsOnList(pulumiOpts) as pulumi.Resource[];\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,\n\t\t\t\tsetup,\n\t\t\t\tenvironment: {\n\t\t\t\t\tFLY_API_TOKEN: provider.token,\n\t\t\t\t\tFLY_TOML_CONTENT: tomlContent,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ ...pulumiOpts, parent: this, dependsOn: [app, ...consumerDeps] },\n\t\t);\n\n\t\tthis.deploymentUrl = deploymentUrl;\n\n\t\tthis.registerOutputs({ deploymentUrl: this.deploymentUrl });\n\t}\n}\n"],"mappings":";;;;;;;;;;;;AA8BA,IAAa,YAAb,cAA+BA,eAAO,kBAAkB;CAIvD,YAAY,MAAc,MAAqB,MAAwB;EACtE,MAAM,EAAE,UAAU,KAAK,GAAG,eAAe;EAEzC,MAAM,yBAAyB,MAAM,CAAC,GAAG,UAAU;EAEnD,MAAM,cAAcC,iCAAgB,KAAK,MAAM;EAC/C,MAAM,aAAa,QAAQ,KAAK,OAAO,IAAI;EAE3C,MAAM,cAAc,KAAK,eAAe;EACxC,MAAM,wBAAwB,KAAK,yBAAyB;EAC5D,MAAM,mBAAmB,KAAK,oBAAoB;EAGlD,MAAM,QAAQ,sDAAsD;EACpE,MAAM,MAAM,uBAAuB,WAAW,sBAAsB,iBAAiB,kBAAkB,YAAY,6BAA6B;EAEhJ,MAAM,WAAWD,eACf,OAAO,KAAK,QAAQ,EACpB,OAAO,WAAW,CAAC,GAAG,QAAQ,WAAW,CAAC;EAI5C,MAAM,eAAeE,sCAAc,UAAU;EAE7C,MAAM,EAAE,kBAAkBC,4CACzB;GACC;GACA;GACA;GACA;GACA,aAAa;IACZ,eAAe,SAAS;IACxB,kBAAkB;GACnB;EACD,GACA;GAAE,GAAG;GAAY,QAAQ;GAAM,WAAW,CAAC,KAAK,GAAG,YAAY;EAAE,CAClE;EAEA,KAAK,gBAAgB;EAErB,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,CAAC;CAC3D;AACD"}
@@ -26,6 +26,8 @@ interface FlyDeployArgs {
26
26
  * Isolation/git are the seam's job (list a `DeploySandbox`, optionally a `GitGuard`).
27
27
  */
28
28
  declare class FlyDeploy extends pulumi.ComponentResource {
29
+ /** The deploy CLI's final stdout line (the Fly app URL when emitted). */
30
+ readonly deploymentUrl: pulumi.Output<string>;
29
31
  constructor(name: string, args: FlyDeployArgs, opts: FlyDeployOptions);
30
32
  }
31
33
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/fly/deploy.ts"],"mappings":";;;;;;;KAQK,gBAAA,GAAmB,IAAA,CAAK,MAAA,CAAO,wBAAA;EACnC,QAAA,EAAU,WAAA;EACV,GAAA,EAAK,MAAA;AAAA;AAAA,UAGW,aAAA;;EAEhB,MAAA,EAAQ,aAAA;EAPe;EASvB,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAPzB;EASL,WAAA;EATW;EAWX,qBAAA;EAb4B;EAe5B,gBAAA;AAAA;;;;;cAOY,SAAA,SAAkB,MAAA,CAAO,iBAAA;cACzB,IAAA,UAAc,IAAA,EAAM,aAAA,EAAe,IAAA,EAAM,gBAAA;AAAA"}
1
+ {"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/fly/deploy.ts"],"mappings":";;;;;;;KAQK,gBAAA,GAAmB,IAAA,CAAK,MAAA,CAAO,wBAAA;EACnC,QAAA,EAAU,WAAA;EACV,GAAA,EAAK,MAAA;AAAA;AAAA,UAGW,aAAA;;EAEhB,MAAA,EAAQ,aAAA;EAPe;EASvB,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAPzB;EASL,WAAA;EATW;EAWX,qBAAA;EAb4B;EAe5B,gBAAA;AAAA;;;;;cAOY,SAAA,SAAkB,MAAA,CAAO,iBAAA;EAjBrB;EAAA,SAmBA,aAAA,EAAe,MAAA,CAAO,MAAA;cAE1B,IAAA,UAAc,IAAA,EAAM,aAAA,EAAe,IAAA,EAAM,gBAAA;AAAA"}
@@ -26,6 +26,8 @@ interface FlyDeployArgs {
26
26
  * Isolation/git are the seam's job (list a `DeploySandbox`, optionally a `GitGuard`).
27
27
  */
28
28
  declare class FlyDeploy extends pulumi.ComponentResource {
29
+ /** The deploy CLI's final stdout line (the Fly app URL when emitted). */
30
+ readonly deploymentUrl: pulumi.Output<string>;
29
31
  constructor(name: string, args: FlyDeployArgs, opts: FlyDeployOptions);
30
32
  }
31
33
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/fly/deploy.ts"],"mappings":";;;;;;;KAQK,gBAAA,GAAmB,IAAA,CAAK,MAAA,CAAO,wBAAA;EACnC,QAAA,EAAU,WAAA;EACV,GAAA,EAAK,MAAA;AAAA;AAAA,UAGW,aAAA;;EAEhB,MAAA,EAAQ,aAAA;EAPe;EASvB,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAPzB;EASL,WAAA;EATW;EAWX,qBAAA;EAb4B;EAe5B,gBAAA;AAAA;;;;;cAOY,SAAA,SAAkB,MAAA,CAAO,iBAAA;cACzB,IAAA,UAAc,IAAA,EAAM,aAAA,EAAe,IAAA,EAAM,gBAAA;AAAA"}
1
+ {"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/fly/deploy.ts"],"mappings":";;;;;;;KAQK,gBAAA,GAAmB,IAAA,CAAK,MAAA,CAAO,wBAAA;EACnC,QAAA,EAAU,WAAA;EACV,GAAA,EAAK,MAAA;AAAA;AAAA,UAGW,aAAA;;EAEhB,MAAA,EAAQ,aAAA;EAPe;EASvB,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAPzB;EASL,WAAA;EATW;EAWX,qBAAA;EAb4B;EAe5B,gBAAA;AAAA;;;;;cAOY,SAAA,SAAkB,MAAA,CAAO,iBAAA;EAjBrB;EAAA,SAmBA,aAAA,EAAe,MAAA,CAAO,MAAA;cAE1B,IAAA,UAAc,IAAA,EAAM,aAAA,EAAe,IAAA,EAAM,gBAAA;AAAA"}
@@ -1,5 +1,5 @@
1
1
  import { t as __name } from "../chunk-OPjESj5l.mjs";
2
- import { createDeployCommand } from "../commands/deploy.mjs";
2
+ import { createDeployCommand, dependsOnList } from "../commands/deploy.mjs";
3
3
  import { generateFlyToml } from "./toml.mjs";
4
4
  import * as pulumi from "@pulumi/pulumi";
5
5
 
@@ -20,11 +20,8 @@ var FlyDeploy = class extends pulumi.ComponentResource {
20
20
  const setup = `mkdir -p .fly && printf '%s' "$FLY_TOML_CONTENT" > ${configPath}`;
21
21
  const cli = `fly deploy --config ${configPath} --remote-only --ha=${highAvailability} --wait-timeout ${waitTimeout} --release-command-timeout ${releaseCommandTimeout}`;
22
22
  const triggers = pulumi.output(args.triggers).apply((values) => [...values, tomlContent]);
23
- let consumerDeps;
24
- if (Array.isArray(pulumiOpts.dependsOn)) consumerDeps = pulumiOpts.dependsOn;
25
- else if (pulumiOpts.dependsOn) consumerDeps = [pulumiOpts.dependsOn];
26
- else consumerDeps = [];
27
- createDeployCommand({
23
+ const consumerDeps = dependsOnList(pulumiOpts);
24
+ const { deploymentUrl } = createDeployCommand({
28
25
  name,
29
26
  cli,
30
27
  triggers,
@@ -38,7 +35,8 @@ var FlyDeploy = class extends pulumi.ComponentResource {
38
35
  parent: this,
39
36
  dependsOn: [app, ...consumerDeps]
40
37
  });
41
- this.registerOutputs({});
38
+ this.deploymentUrl = deploymentUrl;
39
+ this.registerOutputs({ deploymentUrl: this.deploymentUrl });
42
40
  }
43
41
  };
44
42
 
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.mjs","names":[],"sources":["../../src/fly/deploy.ts"],"sourcesContent":["// src/fly/deploy.ts (replace entire file)\nimport * as pulumi from \"@pulumi/pulumi\";\n\nimport { createDeployCommand } from \"../commands/deploy\";\nimport type { FlyApp } from \"./app\";\nimport type { FlyProvider } from \"./provider\";\nimport { type FlyTomlConfig, generateFlyToml } from \"./toml\";\n\ntype FlyDeployOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\tprovider: FlyProvider;\n\tapp: FlyApp;\n};\n\nexport interface FlyDeployArgs {\n\t/** fly.toml configuration. `config.app` must equal the FlyApp name. */\n\tconfig: FlyTomlConfig;\n\t/** Redeploy triggers; the generated toml content is appended automatically. */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\t/** `fly deploy --wait-timeout` seconds (default 300). */\n\twaitTimeout?: number;\n\t/** `fly deploy --release-command-timeout` seconds (default 600). */\n\treleaseCommandTimeout?: number;\n\t/** `fly deploy --ha` (default false). */\n\thighAvailability?: boolean;\n}\n\n/**\n * Deploys a Fly app via `fly deploy --remote-only` from a generated fly.toml.\n * Isolation/git are the seam's job (list a `DeploySandbox`, optionally a `GitGuard`).\n */\nexport class FlyDeploy extends pulumi.ComponentResource {\n\tconstructor(name: string, args: FlyDeployArgs, opts: FlyDeployOptions) {\n\t\tconst { provider, app, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:fly:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst tomlContent = generateFlyToml(args.config);\n\t\tconst configPath = `.fly/${args.config.app}.toml`;\n\n\t\tconst waitTimeout = args.waitTimeout ?? 300;\n\t\tconst releaseCommandTimeout = args.releaseCommandTimeout ?? 600;\n\t\tconst highAvailability = args.highAvailability ?? false;\n\n\t\t// The toml content arrives via FLY_TOML_CONTENT (avoids shell escaping).\n\t\tconst setup = `mkdir -p .fly && printf '%s' \"$FLY_TOML_CONTENT\" > ${configPath}`;\n\t\tconst cli = `fly deploy --config ${configPath} --remote-only --ha=${highAvailability} --wait-timeout ${waitTimeout} --release-command-timeout ${releaseCommandTimeout}`;\n\n\t\tconst triggers = pulumi\n\t\t\t.output(args.triggers)\n\t\t\t.apply((values) => [...values, tomlContent]);\n\n\t\tlet consumerDeps: pulumi.Resource[];\n\n\t\tif (Array.isArray(pulumiOpts.dependsOn)) {\n\t\t\tconsumerDeps = pulumiOpts.dependsOn as pulumi.Resource[];\n\t\t} else if (pulumiOpts.dependsOn) {\n\t\t\tconsumerDeps = [pulumiOpts.dependsOn as pulumi.Resource];\n\t\t} else {\n\t\t\tconsumerDeps = [];\n\t\t}\n\n\t\tcreateDeployCommand(\n\t\t\t{\n\t\t\t\tname,\n\t\t\t\tcli,\n\t\t\t\ttriggers,\n\t\t\t\tsetup,\n\t\t\t\tenvironment: {\n\t\t\t\t\tFLY_API_TOKEN: provider.token,\n\t\t\t\t\tFLY_TOML_CONTENT: tomlContent,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ ...pulumiOpts, parent: this, dependsOn: [app, ...consumerDeps] },\n\t\t);\n\n\t\tthis.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;;;;;AA8BA,IAAa,YAAb,cAA+B,OAAO,kBAAkB;CACvD,YAAY,MAAc,MAAqB,MAAwB;EACtE,MAAM,EAAE,UAAU,KAAK,GAAG,eAAe;EAEzC,MAAM,yBAAyB,MAAM,CAAC,GAAG,UAAU;EAEnD,MAAM,cAAc,gBAAgB,KAAK,MAAM;EAC/C,MAAM,aAAa,QAAQ,KAAK,OAAO,IAAI;EAE3C,MAAM,cAAc,KAAK,eAAe;EACxC,MAAM,wBAAwB,KAAK,yBAAyB;EAC5D,MAAM,mBAAmB,KAAK,oBAAoB;EAGlD,MAAM,QAAQ,sDAAsD;EACpE,MAAM,MAAM,uBAAuB,WAAW,sBAAsB,iBAAiB,kBAAkB,YAAY,6BAA6B;EAEhJ,MAAM,WAAW,OACf,OAAO,KAAK,QAAQ,EACpB,OAAO,WAAW,CAAC,GAAG,QAAQ,WAAW,CAAC;EAE5C,IAAI;EAEJ,IAAI,MAAM,QAAQ,WAAW,SAAS,GACrC,eAAe,WAAW;OACpB,IAAI,WAAW,WACrB,eAAe,CAAC,WAAW,SAA4B;OAEvD,eAAe,CAAC;EAGjB,oBACC;GACC;GACA;GACA;GACA;GACA,aAAa;IACZ,eAAe,SAAS;IACxB,kBAAkB;GACnB;EACD,GACA;GAAE,GAAG;GAAY,QAAQ;GAAM,WAAW,CAAC,KAAK,GAAG,YAAY;EAAE,CAClE;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
1
+ {"version":3,"file":"deploy.mjs","names":[],"sources":["../../src/fly/deploy.ts"],"sourcesContent":["// src/fly/deploy.ts (replace entire file)\nimport * as pulumi from \"@pulumi/pulumi\";\n\nimport { createDeployCommand, dependsOnList } from \"../commands/deploy\";\nimport type { FlyApp } from \"./app\";\nimport type { FlyProvider } from \"./provider\";\nimport { type FlyTomlConfig, generateFlyToml } from \"./toml\";\n\ntype FlyDeployOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\tprovider: FlyProvider;\n\tapp: FlyApp;\n};\n\nexport interface FlyDeployArgs {\n\t/** fly.toml configuration. `config.app` must equal the FlyApp name. */\n\tconfig: FlyTomlConfig;\n\t/** Redeploy triggers; the generated toml content is appended automatically. */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\t/** `fly deploy --wait-timeout` seconds (default 300). */\n\twaitTimeout?: number;\n\t/** `fly deploy --release-command-timeout` seconds (default 600). */\n\treleaseCommandTimeout?: number;\n\t/** `fly deploy --ha` (default false). */\n\thighAvailability?: boolean;\n}\n\n/**\n * Deploys a Fly app via `fly deploy --remote-only` from a generated fly.toml.\n * Isolation/git are the seam's job (list a `DeploySandbox`, optionally a `GitGuard`).\n */\nexport class FlyDeploy extends pulumi.ComponentResource {\n\t/** The deploy CLI's final stdout line (the Fly app URL when emitted). */\n\tpublic readonly deploymentUrl: pulumi.Output<string>;\n\n\tconstructor(name: string, args: FlyDeployArgs, opts: FlyDeployOptions) {\n\t\tconst { provider, app, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:fly:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst tomlContent = generateFlyToml(args.config);\n\t\tconst configPath = `.fly/${args.config.app}.toml`;\n\n\t\tconst waitTimeout = args.waitTimeout ?? 300;\n\t\tconst releaseCommandTimeout = args.releaseCommandTimeout ?? 600;\n\t\tconst highAvailability = args.highAvailability ?? false;\n\n\t\t// The toml content arrives via FLY_TOML_CONTENT (avoids shell escaping).\n\t\tconst setup = `mkdir -p .fly && printf '%s' \"$FLY_TOML_CONTENT\" > ${configPath}`;\n\t\tconst cli = `fly deploy --config ${configPath} --remote-only --ha=${highAvailability} --wait-timeout ${waitTimeout} --release-command-timeout ${releaseCommandTimeout}`;\n\n\t\tconst triggers = pulumi\n\t\t\t.output(args.triggers)\n\t\t\t.apply((values) => [...values, tomlContent]);\n\n\t\t// Keep the `app` ordering anchor first; append the consumer's deps\n\t\t// (DeploySandbox / GitGuard) using the seam's shared normaliser.\n\t\tconst consumerDeps = dependsOnList(pulumiOpts) as pulumi.Resource[];\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,\n\t\t\t\tsetup,\n\t\t\t\tenvironment: {\n\t\t\t\t\tFLY_API_TOKEN: provider.token,\n\t\t\t\t\tFLY_TOML_CONTENT: tomlContent,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ ...pulumiOpts, parent: this, dependsOn: [app, ...consumerDeps] },\n\t\t);\n\n\t\tthis.deploymentUrl = deploymentUrl;\n\n\t\tthis.registerOutputs({ deploymentUrl: this.deploymentUrl });\n\t}\n}\n"],"mappings":";;;;;;;;;;AA8BA,IAAa,YAAb,cAA+B,OAAO,kBAAkB;CAIvD,YAAY,MAAc,MAAqB,MAAwB;EACtE,MAAM,EAAE,UAAU,KAAK,GAAG,eAAe;EAEzC,MAAM,yBAAyB,MAAM,CAAC,GAAG,UAAU;EAEnD,MAAM,cAAc,gBAAgB,KAAK,MAAM;EAC/C,MAAM,aAAa,QAAQ,KAAK,OAAO,IAAI;EAE3C,MAAM,cAAc,KAAK,eAAe;EACxC,MAAM,wBAAwB,KAAK,yBAAyB;EAC5D,MAAM,mBAAmB,KAAK,oBAAoB;EAGlD,MAAM,QAAQ,sDAAsD;EACpE,MAAM,MAAM,uBAAuB,WAAW,sBAAsB,iBAAiB,kBAAkB,YAAY,6BAA6B;EAEhJ,MAAM,WAAW,OACf,OAAO,KAAK,QAAQ,EACpB,OAAO,WAAW,CAAC,GAAG,QAAQ,WAAW,CAAC;EAI5C,MAAM,eAAe,cAAc,UAAU;EAE7C,MAAM,EAAE,kBAAkB,oBACzB;GACC;GACA;GACA;GACA;GACA,aAAa;IACZ,eAAe,SAAS;IACxB,kBAAkB;GACnB;EACD,GACA;GAAE,GAAG;GAAY,QAAQ;GAAM,WAAW,CAAC,KAAK,GAAG,YAAY;EAAE,CAClE;EAEA,KAAK,gBAAgB;EAErB,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,CAAC;CAC3D;AACD"}
@@ -10,8 +10,16 @@ _pulumi_pulumi = require_chunk.__toESM(_pulumi_pulumi, 1);
10
10
  * the Railway GraphQL API to a terminal status and fails the resource on
11
11
  * FAILED/CRASHED/REMOVED. Runs under `node -e`, single-quoted in the shell, so it
12
12
  * uses ONLY double quotes and reads inputs from IC_* env vars.
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.
13
21
  */
14
- const DEPLOY_WAIT_SCRIPT = `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);})();`;
22
+ const DEPLOY_WAIT_SCRIPT = `const t=process.env.IC_TOK,p=process.env.IC_PROJ,e=process.env.IC_ENV,s=process.env.IC_SVC;const since=Number(process.env.IC_SINCE||0)-120000;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));const ok={SUCCESS:1,SLEEPING:1};const bad={FAILED:1,CRASHED:1,REMOVED:1};(async()=>{let id;for(let i=0;i<12&&!id;i++){const d=await q("query($p:String!,$e:String!,$s:String!){deployments(first:10,input:{projectId:$p,environmentId:$e,serviceId:$s}){edges{node{id status createdAt}}}}",{p,e,s});const g=(d&&d.data&&d.data.deployments&&d.data.deployments.edges)||[];const fresh=g.map(x=>x.node).filter(n=>n&&n.createdAt&&Date.parse(n.createdAt)>=since).sort((a,b)=>Date.parse(b.createdAt)-Date.parse(a.createdAt));if(fresh[0])id=fresh[0].id;if(!id)await sl(5000);}if(!id){console.error("[infracraft] could not resolve a Railway deployment newer than this run; upload 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&&ok[st])process.exit(0);if(st==="SKIPPED"){console.error("[infracraft] railway deployment "+id+" SKIPPED (superseded) — not blocking");process.exit(0);}if(st&&bad[st]){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);})();`;
15
23
  /**
16
24
  * Deploys a Railway service and waits for a terminal status. Isolation/git are the
17
25
  * seam's job (list a `DeploySandbox` and optionally a `GitGuard` in `dependsOn`).
@@ -26,9 +34,9 @@ var RailwayDeploy = class extends _pulumi_pulumi.ComponentResource {
26
34
  constructor(name, args, opts) {
27
35
  const { provider, project, environment, service, projectToken, ...pulumiOpts } = opts;
28
36
  super("infracraft:railway:Deploy", name, {}, pulumiOpts);
29
- const cli = _pulumi_pulumi.interpolate`RAILWAY_TOKEN=${projectToken} railway up --ci --project ${project.id} --service ${service.id} --environment ${environment.id}; EXIT=$?; 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 '${DEPLOY_WAIT_SCRIPT}'`;
30
- const setup = args.railpackConfig ? `printf '${JSON.stringify(args.railpackConfig).replace(/'/g, "\\'")}' > railpack.json` : void 0;
31
- require_commands_deploy.createDeployCommand({
37
+ const cli = _pulumi_pulumi.interpolate`IC_SINCE=$(node -e "process.stdout.write(String(Date.now()))"); RAILWAY_TOKEN=${projectToken} railway up --ci --project ${project.id} --service ${service.id} --environment ${environment.id}; EXIT=$?; 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} IC_SINCE=$IC_SINCE node -e '${DEPLOY_WAIT_SCRIPT}'`;
38
+ const setup = args.railpackConfig ? `printf '%s' '${JSON.stringify(args.railpackConfig).replace(/'/g, "'\\''")}' > railpack.json` : void 0;
39
+ const { deploymentUrl } = require_commands_deploy.createDeployCommand({
32
40
  name,
33
41
  cli,
34
42
  triggers: args.triggers,
@@ -38,7 +46,8 @@ var RailwayDeploy = class extends _pulumi_pulumi.ComponentResource {
38
46
  parent: this,
39
47
  ...pulumiOpts
40
48
  });
41
- this.registerOutputs({});
49
+ this.deploymentUrl = deploymentUrl;
50
+ this.registerOutputs({ deploymentUrl: this.deploymentUrl });
42
51
  }
43
52
  };
44
53
 
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.cjs","names":["pulumi"],"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 * The deploy-wait poller. `railway up --ci` only blocks until upload; this polls\n * the Railway GraphQL API to a terminal status and fails the resource on\n * FAILED/CRASHED/REMOVED. Runs under `node -e`, single-quoted in the shell, so it\n * uses ONLY double quotes and reads inputs from IC_* env vars.\n */\nconst DEPLOY_WAIT_SCRIPT = `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/**\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\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// Token is inlined (not via the env map): it is an unknown secret at preview,\n\t\t// and an unknown secret in `environment` makes `pulumi preview` fail.\n\t\tconst cli = pulumi.interpolate`RAILWAY_TOKEN=${projectToken} railway up --ci --project ${project.id} --service ${service.id} --environment ${environment.id}; EXIT=$?; 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 '${DEPLOY_WAIT_SCRIPT}'`;\n\n\t\tconst setup = args.railpackConfig\n\t\t\t? `printf '${JSON.stringify(args.railpackConfig).replace(/'/g, \"\\\\'\")}' > railpack.json`\n\t\t\t: undefined;\n\n\t\tcreateDeployCommand(\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.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;AA0CA,MAAM,qBAAqB;;;;;;;;;;;AAY3B,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;EAIvD,MAAM,MAAM,eAAO,WAAW,iBAAiB,aAAa,6BAA6B,QAAQ,GAAG,aAAa,QAAQ,GAAG,iBAAiB,YAAY,GAAG,4HAA4H,aAAa,WAAW,QAAQ,GAAG,UAAU,YAAY,GAAG,UAAU,QAAQ,GAAG,YAAY,mBAAmB;EAExY,MAAM,QAAQ,KAAK,iBAChB,WAAW,KAAK,UAAU,KAAK,cAAc,EAAE,QAAQ,MAAM,KAAK,EAAE,qBACpE;EAEH,4CACC;GACC;GACA;GACA,UAAU,KAAK;GACf,cAAc,KAAK;GACnB;EACD,GACA;GAAE,QAAQ;GAAM,GAAG;EAAW,CAC/B;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
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 * The deploy-wait poller. `railway up --ci` only blocks until upload; this polls\n * the Railway GraphQL API to a terminal status and fails the resource on\n * FAILED/CRASHED/REMOVED. Runs under `node -e`, single-quoted in the shell, so it\n * uses ONLY double quotes and reads inputs from IC_* env vars.\n *\n * Deployment-id resolution filters by `createdAt >= IC_SINCE` (the epoch captured\n * just before `railway up`, minus a clock-skew buffer) and picks the newest such\n * deployment. This prevents latching onto the PREVIOUS deployment when Railway's\n * API has not yet registered the new one (a plain `first:1` race). SLEEPING (the\n * service deployed then scaled to zero) is treated as success; SKIPPED (the\n * deploy was superseded) is non-blocking; an unresolvable id (upload already\n * succeeded) does not block the release.\n */\nconst DEPLOY_WAIT_SCRIPT = `const t=process.env.IC_TOK,p=process.env.IC_PROJ,e=process.env.IC_ENV,s=process.env.IC_SVC;const since=Number(process.env.IC_SINCE||0)-120000;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));const ok={SUCCESS:1,SLEEPING:1};const bad={FAILED:1,CRASHED:1,REMOVED:1};(async()=>{let id;for(let i=0;i<12&&!id;i++){const d=await q(\"query($p:String!,$e:String!,$s:String!){deployments(first:10,input:{projectId:$p,environmentId:$e,serviceId:$s}){edges{node{id status createdAt}}}}\",{p,e,s});const g=(d&&d.data&&d.data.deployments&&d.data.deployments.edges)||[];const fresh=g.map(x=>x.node).filter(n=>n&&n.createdAt&&Date.parse(n.createdAt)>=since).sort((a,b)=>Date.parse(b.createdAt)-Date.parse(a.createdAt));if(fresh[0])id=fresh[0].id;if(!id)await sl(5000);}if(!id){console.error(\"[infracraft] could not resolve a Railway deployment newer than this run; upload 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&&ok[st])process.exit(0);if(st===\"SKIPPED\"){console.error(\"[infracraft] railway deployment \"+id+\" SKIPPED (superseded) — not blocking\");process.exit(0);}if(st&&bad[st]){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/**\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// Token is inlined (not via the env map): it is an unknown secret at preview,\n\t\t// and an unknown secret in `environment` makes `pulumi preview` fail.\n\t\t// IC_SINCE is captured just before `railway up` so the poller can tell the\n\t\t// new deployment apart from the previous one by createdAt.\n\t\tconst cli = pulumi.interpolate`IC_SINCE=$(node -e \"process.stdout.write(String(Date.now()))\"); RAILWAY_TOKEN=${projectToken} railway up --ci --project ${project.id} --service ${service.id} --environment ${environment.id}; EXIT=$?; 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} IC_SINCE=$IC_SINCE node -e '${DEPLOY_WAIT_SCRIPT}'`;\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":";;;;;;;;;;;;;;;;;;;;;AAkDA,MAAM,qBAAqB;;;;;;;;;;;AAY3B,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;EAMvD,MAAM,MAAM,eAAO,WAAW,iFAAiF,aAAa,6BAA6B,QAAQ,GAAG,aAAa,QAAQ,GAAG,iBAAiB,YAAY,GAAG,4HAA4H,aAAa,WAAW,QAAQ,GAAG,UAAU,YAAY,GAAG,UAAU,QAAQ,GAAG,+BAA+B,mBAAmB;EAI3d,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"}
@@ -37,6 +37,8 @@ type RailwayDeployOptions = Omit<pulumi.ComponentResourceOptions, "provider"> &
37
37
  * ```
38
38
  */
39
39
  declare class RailwayDeploy extends pulumi.ComponentResource {
40
+ /** The deploy CLI's final stdout line (Railway service URL when emitted). */
41
+ readonly deploymentUrl: pulumi.Output<string>;
40
42
  constructor(name: string, args: RailwayDeployArgs, opts: RailwayDeployOptions);
41
43
  }
42
44
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/railway/deploy.ts"],"mappings":";;;;;;;;UASiB,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;cAiCX,aAAA,SAAsB,MAAA,CAAO,iBAAA;cAExC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
1
+ {"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/railway/deploy.ts"],"mappings":";;;;;;;;UASiB,mBAAA;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;cAyCX,aAAA,SAAsB,MAAA,CAAO,iBAAA;EAtCjB;EAAA,SAwCR,aAAA,EAAe,MAAA,CAAO,MAAA;cAGrC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
@@ -37,6 +37,8 @@ type RailwayDeployOptions = Omit<pulumi.ComponentResourceOptions, "provider"> &
37
37
  * ```
38
38
  */
39
39
  declare class RailwayDeploy extends pulumi.ComponentResource {
40
+ /** The deploy CLI's final stdout line (Railway service URL when emitted). */
41
+ readonly deploymentUrl: pulumi.Output<string>;
40
42
  constructor(name: string, args: RailwayDeployArgs, opts: RailwayDeployOptions);
41
43
  }
42
44
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/railway/deploy.ts"],"mappings":";;;;;;;;UASiB,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;cAiCX,aAAA,SAAsB,MAAA,CAAO,iBAAA;cAExC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
1
+ {"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/railway/deploy.ts"],"mappings":";;;;;;;;UASiB,mBAAA;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;cAyCX,aAAA,SAAsB,MAAA,CAAO,iBAAA;EAtCjB;EAAA,SAwCR,aAAA,EAAe,MAAA,CAAO,MAAA;cAGrC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
@@ -8,8 +8,16 @@ import * as pulumi from "@pulumi/pulumi";
8
8
  * the Railway GraphQL API to a terminal status and fails the resource on
9
9
  * FAILED/CRASHED/REMOVED. Runs under `node -e`, single-quoted in the shell, so it
10
10
  * uses ONLY double quotes and reads inputs from IC_* env vars.
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.
11
19
  */
12
- const DEPLOY_WAIT_SCRIPT = `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);})();`;
20
+ const DEPLOY_WAIT_SCRIPT = `const t=process.env.IC_TOK,p=process.env.IC_PROJ,e=process.env.IC_ENV,s=process.env.IC_SVC;const since=Number(process.env.IC_SINCE||0)-120000;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));const ok={SUCCESS:1,SLEEPING:1};const bad={FAILED:1,CRASHED:1,REMOVED:1};(async()=>{let id;for(let i=0;i<12&&!id;i++){const d=await q("query($p:String!,$e:String!,$s:String!){deployments(first:10,input:{projectId:$p,environmentId:$e,serviceId:$s}){edges{node{id status createdAt}}}}",{p,e,s});const g=(d&&d.data&&d.data.deployments&&d.data.deployments.edges)||[];const fresh=g.map(x=>x.node).filter(n=>n&&n.createdAt&&Date.parse(n.createdAt)>=since).sort((a,b)=>Date.parse(b.createdAt)-Date.parse(a.createdAt));if(fresh[0])id=fresh[0].id;if(!id)await sl(5000);}if(!id){console.error("[infracraft] could not resolve a Railway deployment newer than this run; upload 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&&ok[st])process.exit(0);if(st==="SKIPPED"){console.error("[infracraft] railway deployment "+id+" SKIPPED (superseded) — not blocking");process.exit(0);}if(st&&bad[st]){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);})();`;
13
21
  /**
14
22
  * Deploys a Railway service and waits for a terminal status. Isolation/git are the
15
23
  * seam's job (list a `DeploySandbox` and optionally a `GitGuard` in `dependsOn`).
@@ -24,9 +32,9 @@ var RailwayDeploy = class extends pulumi.ComponentResource {
24
32
  constructor(name, args, opts) {
25
33
  const { provider, project, environment, service, projectToken, ...pulumiOpts } = opts;
26
34
  super("infracraft:railway:Deploy", name, {}, pulumiOpts);
27
- const cli = pulumi.interpolate`RAILWAY_TOKEN=${projectToken} railway up --ci --project ${project.id} --service ${service.id} --environment ${environment.id}; EXIT=$?; 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 '${DEPLOY_WAIT_SCRIPT}'`;
28
- const setup = args.railpackConfig ? `printf '${JSON.stringify(args.railpackConfig).replace(/'/g, "\\'")}' > railpack.json` : void 0;
29
- createDeployCommand({
35
+ const cli = pulumi.interpolate`IC_SINCE=$(node -e "process.stdout.write(String(Date.now()))"); RAILWAY_TOKEN=${projectToken} railway up --ci --project ${project.id} --service ${service.id} --environment ${environment.id}; EXIT=$?; 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} IC_SINCE=$IC_SINCE node -e '${DEPLOY_WAIT_SCRIPT}'`;
36
+ const setup = args.railpackConfig ? `printf '%s' '${JSON.stringify(args.railpackConfig).replace(/'/g, "'\\''")}' > railpack.json` : void 0;
37
+ const { deploymentUrl } = createDeployCommand({
30
38
  name,
31
39
  cli,
32
40
  triggers: args.triggers,
@@ -36,7 +44,8 @@ var RailwayDeploy = class extends pulumi.ComponentResource {
36
44
  parent: this,
37
45
  ...pulumiOpts
38
46
  });
39
- this.registerOutputs({});
47
+ this.deploymentUrl = deploymentUrl;
48
+ this.registerOutputs({ deploymentUrl: this.deploymentUrl });
40
49
  }
41
50
  };
42
51
 
@@ -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 * The deploy-wait poller. `railway up --ci` only blocks until upload; this polls\n * the Railway GraphQL API to a terminal status and fails the resource on\n * FAILED/CRASHED/REMOVED. Runs under `node -e`, single-quoted in the shell, so it\n * uses ONLY double quotes and reads inputs from IC_* env vars.\n */\nconst DEPLOY_WAIT_SCRIPT = `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/**\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\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// Token is inlined (not via the env map): it is an unknown secret at preview,\n\t\t// and an unknown secret in `environment` makes `pulumi preview` fail.\n\t\tconst cli = pulumi.interpolate`RAILWAY_TOKEN=${projectToken} railway up --ci --project ${project.id} --service ${service.id} --environment ${environment.id}; EXIT=$?; 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 '${DEPLOY_WAIT_SCRIPT}'`;\n\n\t\tconst setup = args.railpackConfig\n\t\t\t? `printf '${JSON.stringify(args.railpackConfig).replace(/'/g, \"\\\\'\")}' > railpack.json`\n\t\t\t: undefined;\n\n\t\tcreateDeployCommand(\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.registerOutputs({});\n\t}\n}\n"],"mappings":";;;;;;;;;;;AA0CA,MAAM,qBAAqB;;;;;;;;;;;AAY3B,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;EAIvD,MAAM,MAAM,OAAO,WAAW,iBAAiB,aAAa,6BAA6B,QAAQ,GAAG,aAAa,QAAQ,GAAG,iBAAiB,YAAY,GAAG,4HAA4H,aAAa,WAAW,QAAQ,GAAG,UAAU,YAAY,GAAG,UAAU,QAAQ,GAAG,YAAY,mBAAmB;EAExY,MAAM,QAAQ,KAAK,iBAChB,WAAW,KAAK,UAAU,KAAK,cAAc,EAAE,QAAQ,MAAM,KAAK,EAAE,qBACpE;EAEH,oBACC;GACC;GACA;GACA,UAAU,KAAK;GACf,cAAc,KAAK;GACnB;EACD,GACA;GAAE,QAAQ;GAAM,GAAG;EAAW,CAC/B;EAEA,KAAK,gBAAgB,CAAC,CAAC;CACxB;AACD"}
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 * The deploy-wait poller. `railway up --ci` only blocks until upload; this polls\n * the Railway GraphQL API to a terminal status and fails the resource on\n * FAILED/CRASHED/REMOVED. Runs under `node -e`, single-quoted in the shell, so it\n * uses ONLY double quotes and reads inputs from IC_* env vars.\n *\n * Deployment-id resolution filters by `createdAt >= IC_SINCE` (the epoch captured\n * just before `railway up`, minus a clock-skew buffer) and picks the newest such\n * deployment. This prevents latching onto the PREVIOUS deployment when Railway's\n * API has not yet registered the new one (a plain `first:1` race). SLEEPING (the\n * service deployed then scaled to zero) is treated as success; SKIPPED (the\n * deploy was superseded) is non-blocking; an unresolvable id (upload already\n * succeeded) does not block the release.\n */\nconst DEPLOY_WAIT_SCRIPT = `const t=process.env.IC_TOK,p=process.env.IC_PROJ,e=process.env.IC_ENV,s=process.env.IC_SVC;const since=Number(process.env.IC_SINCE||0)-120000;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));const ok={SUCCESS:1,SLEEPING:1};const bad={FAILED:1,CRASHED:1,REMOVED:1};(async()=>{let id;for(let i=0;i<12&&!id;i++){const d=await q(\"query($p:String!,$e:String!,$s:String!){deployments(first:10,input:{projectId:$p,environmentId:$e,serviceId:$s}){edges{node{id status createdAt}}}}\",{p,e,s});const g=(d&&d.data&&d.data.deployments&&d.data.deployments.edges)||[];const fresh=g.map(x=>x.node).filter(n=>n&&n.createdAt&&Date.parse(n.createdAt)>=since).sort((a,b)=>Date.parse(b.createdAt)-Date.parse(a.createdAt));if(fresh[0])id=fresh[0].id;if(!id)await sl(5000);}if(!id){console.error(\"[infracraft] could not resolve a Railway deployment newer than this run; upload 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&&ok[st])process.exit(0);if(st===\"SKIPPED\"){console.error(\"[infracraft] railway deployment \"+id+\" SKIPPED (superseded) — not blocking\");process.exit(0);}if(st&&bad[st]){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/**\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// Token is inlined (not via the env map): it is an unknown secret at preview,\n\t\t// and an unknown secret in `environment` makes `pulumi preview` fail.\n\t\t// IC_SINCE is captured just before `railway up` so the poller can tell the\n\t\t// new deployment apart from the previous one by createdAt.\n\t\tconst cli = pulumi.interpolate`IC_SINCE=$(node -e \"process.stdout.write(String(Date.now()))\"); RAILWAY_TOKEN=${projectToken} railway up --ci --project ${project.id} --service ${service.id} --environment ${environment.id}; EXIT=$?; 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} IC_SINCE=$IC_SINCE node -e '${DEPLOY_WAIT_SCRIPT}'`;\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":";;;;;;;;;;;;;;;;;;;AAkDA,MAAM,qBAAqB;;;;;;;;;;;AAY3B,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;EAMvD,MAAM,MAAM,OAAO,WAAW,iFAAiF,aAAa,6BAA6B,QAAQ,GAAG,aAAa,QAAQ,GAAG,iBAAiB,YAAY,GAAG,4HAA4H,aAAa,WAAW,QAAQ,GAAG,UAAU,YAAY,GAAG,UAAU,QAAQ,GAAG,+BAA+B,mBAAmB;EAI3d,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"}
package/dist/sandbox.cjs CHANGED
@@ -13,8 +13,8 @@ function escapeAwkRegex(path) {
13
13
  return path.replace(/[.[\]{}()*+?^$|\\/]/g, "\\$&");
14
14
  }
15
15
  /**
16
- * Builds the shell filter applied to newline-delimited `git ls-files` output
17
- * before `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps
16
+ * Builds the shell filter applied to the `git ls-files` list before
17
+ * `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps
18
18
  * `apps/<x>/package.json` (the monorepo workspace graph needs it during build);
19
19
  * any other entry drops the path and its subtree. Returns `cat` (passthrough)
20
20
  * when nothing is excluded. Uses awk so it is portable across macOS and Linux.
@@ -30,27 +30,59 @@ function buildSandboxFileFilter(excludePaths = []) {
30
30
  /** Root of the per-deploy sandbox tree. The DeploySandbox resource GCs this. */
31
31
  const SANDBOX_ROOT = "/tmp/infracraft";
32
32
  /**
33
+ * How a deploy's working copy is isolated. A closed set, so it is an enum rather
34
+ * than a pair of booleans: the seam derives it from `dependsOn` presence
35
+ * (DeploySandbox / GitGuard), which makes the impossible "git-guard without a
36
+ * sandbox" state unrepresentable here.
37
+ */
38
+ let SandboxMode = /* @__PURE__ */ function(SandboxMode) {
39
+ /** No isolation — run the CLI in the live working tree. */
40
+ SandboxMode["NONE"] = "NONE";
41
+ /** Isolated copy carrying the repo's REAL `.git` (real commit/author exposed). */
42
+ SandboxMode["ORIGINAL"] = "ORIGINAL";
43
+ /** Isolated copy with a metadata-free stub `.git` (no commit SHA / author). */
44
+ SandboxMode["STUB"] = "STUB";
45
+ return SandboxMode;
46
+ }({});
47
+ /**
33
48
  * Builds the shell for a deploy's `command.local.Command.create`. See
34
49
  * docs/superpowers/specs/2026-06-05-deploy-sandbox-design.md for the modes.
50
+ *
51
+ * Hardening: every script runs under POSIX `set -e` so any failed step aborts
52
+ * instead of silently letting the platform CLI run against partial state.
53
+ * `pipefail` is intentionally NOT used — the command runs under `/bin/sh`, which
54
+ * may be `dash` (pipefail is a bash/ksh/zsh extension). To still catch a failing
55
+ * `git ls-files` (a pipe would mask it, since an `rsync` of an empty list exits
56
+ * 0), the copy is staged through intermediate list files rather than a pipe. The
57
+ * `REPO` and `SANDBOX` command substitutions are explicitly guarded with clear
58
+ * error messages.
35
59
  */
36
60
  function buildSandboxScript(options) {
37
- const { sandbox, gitGuard, appName, env, excludePaths, setup, cli } = options;
38
- const head = `REPO=$(git rev-parse --show-toplevel)`;
61
+ const { mode, appName, env, excludePaths, setup, cli } = options;
62
+ const head = `set -e; REPO=$(git rev-parse --show-toplevel) || { echo "[infracraft] not inside a git repository" >&2; exit 1; }`;
39
63
  const runSetupAndCli = [setup, cli].filter(Boolean).join("; ");
40
- if (!sandbox) return `${head}; cd "$REPO"; ${runSetupAndCli}`;
64
+ if (mode === "NONE") return `${head}; cd "$REPO"; ${runSetupAndCli}`;
41
65
  const makeSandbox = [
42
66
  `PROJECT=$(basename "$REPO")`,
43
- `SANDBOX=$(mktemp -d "${SANDBOX_ROOT}/$PROJECT-${env ? `${env}-${appName}` : appName}.XXXXXX")`,
67
+ `SANDBOX=$(mktemp -d "${SANDBOX_ROOT}/$PROJECT-${env ? `${env}-${appName}` : appName}.XXXXXX") || { echo "[infracraft] mktemp failed" >&2; exit 1; }`,
44
68
  `trap 'rm -rf "$SANDBOX"' EXIT`
45
69
  ].join("; ");
46
- if (gitGuard) return `${head}; ${makeSandbox}; ${`git -C "$REPO" ls-files | ${buildSandboxFileFilter(excludePaths)} | rsync -a --files-from=- "$REPO"/ "$SANDBOX"/`}; cd "\$SANDBOX" && git init -q && git add -A; ${runSetupAndCli}`;
47
- return `${head}; ${makeSandbox}; git -C "\$REPO" ls-files | rsync -a --files-from=- "\$REPO"/ "\$SANDBOX"/; cp -c -R "\$REPO/.git" "\$SANDBOX/.git" 2>/dev/null || cp -R "\$REPO/.git" "\$SANDBOX/.git"; cd "$SANDBOX"; ${runSetupAndCli}`;
70
+ const copy = [
71
+ `git -C "$REPO" ls-files > "$SANDBOX/.ic-ls"`,
72
+ `${mode === "STUB" ? buildSandboxFileFilter(excludePaths) : "cat"} "$SANDBOX/.ic-ls" > "$SANDBOX/.ic-lsf"`,
73
+ `rsync -a --files-from="$SANDBOX/.ic-lsf" "$REPO"/ "$SANDBOX"/`,
74
+ `rm -f "$SANDBOX/.ic-ls" "$SANDBOX/.ic-lsf"`
75
+ ].join("; ");
76
+ if (mode === "STUB") return `${head}; ${makeSandbox}; ${copy}; cd "\$SANDBOX"; git init -q && git add -A; ${runSetupAndCli}`;
77
+ return `${head}; ${makeSandbox}; ${copy}; cp -c -R "\$REPO/.git" "\$SANDBOX/.git" 2>/dev/null || cp -R "\$REPO/.git" "\$SANDBOX/.git"; cd "$SANDBOX"; ${runSetupAndCli}`;
48
78
  }
49
79
  /** Cross-bundle brand: `instanceof` is unreliable when the seam and the resource
50
80
  * come from different built entries, so the seam detects sandboxes by this. */
51
81
  const DEPLOY_SANDBOX_BRAND = Symbol.for("@infracraft/pulumi/DeploySandbox");
52
- /** Sweep sandboxes orphaned by a hard-killed run (older than this). */
53
- const STALE_SANDBOX_MS = 1440 * 60 * 1e3;
82
+ /** Sweep sandboxes orphaned by a hard-killed run (older than this). Kept well
83
+ * above the worst-case deploy time (~30 min) but low enough that a SIGKILL
84
+ * orphan does not linger for a day accumulating in /tmp. */
85
+ const STALE_SANDBOX_MS = 10800 * 1e3;
54
86
  /**
55
87
  * Isolation marker + workspace lifecycle. Listing it in a deploy's `dependsOn`
56
88
  * makes that deploy run in an isolated `/tmp/infracraft` copy. Carries no config;
@@ -93,6 +125,7 @@ function isDeploySandbox(value) {
93
125
 
94
126
  //#endregion
95
127
  exports.DeploySandbox = DeploySandbox;
128
+ exports.SandboxMode = SandboxMode;
96
129
  exports.buildSandboxFileFilter = buildSandboxFileFilter;
97
130
  exports.buildSandboxScript = buildSandboxScript;
98
131
  exports.isDeploySandbox = isDeploySandbox;
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.cjs","names":["pulumi","fs","path"],"sources":["../src/sandbox.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\n/** Escapes a path literal for embedding inside an awk ERE. */\nfunction escapeAwkRegex(path: string): string {\n\treturn path.replace(/[.[\\]{}()*+?^$|\\\\/]/g, \"\\\\$&\");\n}\n\n/**\n * Builds the shell filter applied to newline-delimited `git ls-files` output\n * before `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps\n * `apps/<x>/package.json` (the monorepo workspace graph needs it during build);\n * any other entry drops the path and its subtree. Returns `cat` (passthrough)\n * when nothing is excluded. Uses awk so it is portable across macOS and Linux.\n */\nexport function buildSandboxFileFilter(excludePaths: string[] = []): string {\n\tif (excludePaths.length === 0) {\n\t\treturn \"cat\";\n\t}\n\n\tconst clauses = excludePaths.map((entry) => {\n\t\tconst escaped = escapeAwkRegex(entry);\n\n\t\tif (entry.startsWith(\"apps/\")) {\n\t\t\treturn `!(/^${escaped}\\\\// && !/^${escaped}\\\\/package\\\\.json$/)`;\n\t\t}\n\n\t\treturn `!/^${escaped}(\\\\/|$)/`;\n\t});\n\n\treturn `awk '${clauses.join(\" && \")}'`;\n}\n\n/** Root of the per-deploy sandbox tree. The DeploySandbox resource GCs this. */\nconst SANDBOX_ROOT = \"/tmp/infracraft\";\n\ninterface SandboxScriptOptions {\n\t/** Whether to isolate into a /tmp copy (false run in the live tree). */\n\tsandbox: boolean;\n\t/** Whether the sandbox `.git` is a metadata-free stub (true) or the real one. */\n\tgitGuard: boolean;\n\t/** Resource-derived name, used in the sandbox dir prefix. */\n\tappName: string;\n\t/** Stack/environment name, prefixed to the sandbox dir so leftovers and\n\t * concurrent deploys are identifiable (e.g. `staging-vercel-deploy-nexus.XXXX`). */\n\tenv?: string;\n\t/** Upload-scoping excludes; applied only in stub mode (see design spec). */\n\texcludePaths?: string[];\n\t/** Shell run in the working dir before `cli` (e.g. write railpack.json). */\n\tsetup?: string;\n\t/** Fully-formed platform deploy command (its exit code becomes the script's). */\n\tcli: string;\n}\n\n/**\n * Builds the shell for a deploy's `command.local.Command.create`. See\n * docs/superpowers/specs/2026-06-05-deploy-sandbox-design.md for the modes.\n */\nexport function buildSandboxScript(options: SandboxScriptOptions): string {\n\tconst { sandbox, gitGuard, appName, env, excludePaths, setup, cli } = options;\n\n\tconst head = `REPO=$(git rev-parse --show-toplevel)`;\n\tconst runSetupAndCli = [setup, cli].filter(Boolean).join(\"; \");\n\n\tif (!sandbox) {\n\t\treturn `${head}; cd \"$REPO\"; ${runSetupAndCli}`;\n\t}\n\n\t// Prefix the dir with the project (repo folder name) and env so leftovers and\n\t// concurrent deploys — even across repos and stacks — are identifiable at a\n\t// glance (e.g. `mlm-nvr-production-railway-deploy-mesh.XXXX`).\n\tconst dirPrefix = env ? `${env}-${appName}` : appName;\n\n\tconst makeSandbox = [\n\t\t`PROJECT=$(basename \"$REPO\")`,\n\t\t`SANDBOX=$(mktemp -d \"${SANDBOX_ROOT}/$PROJECT-${dirPrefix}.XXXXXX\")`,\n\t\t`trap 'rm -rf \"$SANDBOX\"' EXIT`,\n\t].join(\"; \");\n\n\tif (gitGuard) {\n\t\tconst filter = buildSandboxFileFilter(excludePaths);\n\n\t\tconst copy =\n\t\t\t`git -C \"$REPO\" ls-files | ${filter} | ` +\n\t\t\t`rsync -a --files-from=- \"$REPO\"/ \"$SANDBOX\"/`;\n\n\t\tconst stubGit = `cd \"$SANDBOX\" && git init -q && git add -A`;\n\n\t\treturn `${head}; ${makeSandbox}; ${copy}; ${stubGit}; ${runSetupAndCli}`;\n\t}\n\n\t// Original `.git`, full clean tree (no filter, no reconciliation needed).\n\tconst copy = `git -C \"$REPO\" ls-files | rsync -a --files-from=- \"$REPO\"/ \"$SANDBOX\"/`;\n\n\tconst copyGit =\n\t\t`cp -c -R \"$REPO/.git\" \"$SANDBOX/.git\" 2>/dev/null || ` +\n\t\t`cp -R \"$REPO/.git\" \"$SANDBOX/.git\"`;\n\n\treturn `${head}; ${makeSandbox}; ${copy}; ${copyGit}; cd \"$SANDBOX\"; ${runSetupAndCli}`;\n}\n\n/** Cross-bundle brand: `instanceof` is unreliable when the seam and the resource\n * come from different built entries, so the seam detects sandboxes by this. */\nconst DEPLOY_SANDBOX_BRAND = Symbol.for(\"@infracraft/pulumi/DeploySandbox\");\n\n/** Sweep sandboxes orphaned by a hard-killed run (older than this). */\nconst STALE_SANDBOX_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Isolation marker + workspace lifecycle. Listing it in a deploy's `dependsOn`\n * makes that deploy run in an isolated `/tmp/infracraft` copy. Carries no config;\n * the repo root is derived at runtime by the deploy command.\n */\nexport class DeploySandbox extends pulumi.ComponentResource {\n\tconstructor(name: string, opts?: pulumi.ComponentResourceOptions) {\n\t\tsuper(\"infracraft:sandbox:DeploySandbox\", name, {}, opts);\n\n\t\t(this as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] = true;\n\n\t\tif (!pulumi.runtime.isDryRun()) {\n\t\t\tthis.prepareWorkspace();\n\t\t}\n\n\t\tthis.registerOutputs({});\n\t}\n\n\t/** mkdir the workspace root and GC stale sandboxes (best-effort). Sandboxes are\n\t * flat under the root (`/tmp/infracraft/<project>-<env>-<app>.XXXX`), so this\n\t * sweeps any entry older than the stale threshold. */\n\tprivate prepareWorkspace(): void {\n\t\tfs.mkdirSync(SANDBOX_ROOT, { recursive: true });\n\n\t\tlet entries: string[] = [];\n\n\t\ttry {\n\t\t\tentries = fs.readdirSync(SANDBOX_ROOT) as string[];\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tconst now = Date.now();\n\n\t\tfor (const entry of entries) {\n\t\t\tconst full = path.join(SANDBOX_ROOT, entry);\n\n\t\t\ttry {\n\t\t\t\tif (now - fs.statSync(full).mtimeMs > STALE_SANDBOX_MS) {\n\t\t\t\t\tfs.rmSync(full, { recursive: true, force: true });\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Racing with an in-flight deploy's cleanup — ignore.\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Bundle-safe check for a `DeploySandbox` in a `dependsOn` array. */\nexport function isDeploySandbox(value: unknown): value is DeploySandbox {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t(value as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] === true\n\t);\n}\n"],"mappings":";;;;;;;;;;;AAKA,SAAS,eAAe,MAAsB;CAC7C,OAAO,KAAK,QAAQ,wBAAwB,MAAM;AACnD;;;;;;;;AASA,SAAgB,uBAAuB,eAAyB,CAAC,GAAW;CAC3E,IAAI,aAAa,WAAW,GAC3B,OAAO;CAaR,OAAO,QAVS,aAAa,KAAK,UAAU;EAC3C,MAAM,UAAU,eAAe,KAAK;EAEpC,IAAI,MAAM,WAAW,OAAO,GAC3B,OAAO,OAAO,QAAQ,aAAa,QAAQ;EAG5C,OAAO,MAAM,QAAQ;CACtB,CAEqB,EAAE,KAAK,MAAM,EAAE;AACrC;;AAGA,MAAM,eAAe;;;;;AAwBrB,SAAgB,mBAAmB,SAAuC;CACzE,MAAM,EAAE,SAAS,UAAU,SAAS,KAAK,cAAc,OAAO,QAAQ;CAEtE,MAAM,OAAO;CACb,MAAM,iBAAiB,CAAC,OAAO,GAAG,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;CAE7D,IAAI,CAAC,SACJ,OAAO,GAAG,KAAK,gBAAgB;CAQhC,MAAM,cAAc;EACnB;EACA,wBAAwB,aAAa,YAJpB,MAAM,GAAG,IAAI,GAAG,YAAY,QAIc;EAC3D;CACD,EAAE,KAAK,IAAI;CAEX,IAAI,UASH,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,6BARpB,uBAAuB,YAGH,EAAE,iDAKG,iDAAgB;CAUzD,OAAO,GAAG,KAAK,IAAI,YAAY,2LAAwC;AACxE;;;AAIA,MAAM,uBAAuB,OAAO,IAAI,kCAAkC;;AAG1E,MAAM,mBAAmB,OAAU,KAAK;;;;;;AAOxC,IAAa,gBAAb,cAAmCA,eAAO,kBAAkB;CAC3D,YAAY,MAAc,MAAwC;EACjE,MAAM,oCAAoC,MAAM,CAAC,GAAG,IAAI;EAExD,AAAC,KAAiC,wBAAwB;EAE1D,IAAI,CAACA,eAAO,QAAQ,SAAS,GAC5B,KAAK,iBAAiB;EAGvB,KAAK,gBAAgB,CAAC,CAAC;CACxB;;;;CAKA,AAAQ,mBAAyB;EAChC,QAAG,UAAU,cAAc,EAAE,WAAW,KAAK,CAAC;EAE9C,IAAI,UAAoB,CAAC;EAEzB,IAAI;GACH,UAAUC,QAAG,YAAY,YAAY;EACtC,QAAQ;GACP;EACD;EAEA,MAAM,MAAM,KAAK,IAAI;EAErB,KAAK,MAAM,SAAS,SAAS;GAC5B,MAAM,OAAOC,UAAK,KAAK,cAAc,KAAK;GAE1C,IAAI;IACH,IAAI,MAAMD,QAAG,SAAS,IAAI,EAAE,UAAU,kBACrC,QAAG,OAAO,MAAM;KAAE,WAAW;KAAM,OAAO;IAAK,CAAC;GAElD,QAAQ,CAER;EACD;CACD;AACD;;AAGA,SAAgB,gBAAgB,OAAwC;CACvE,OACC,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,0BAA0B;AAE/D"}
1
+ {"version":3,"file":"sandbox.cjs","names":["pulumi","fs","path"],"sources":["../src/sandbox.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\n/** Escapes a path literal for embedding inside an awk ERE. */\nfunction escapeAwkRegex(path: string): string {\n\treturn path.replace(/[.[\\]{}()*+?^$|\\\\/]/g, \"\\\\$&\");\n}\n\n/**\n * Builds the shell filter applied to the `git ls-files` list before\n * `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps\n * `apps/<x>/package.json` (the monorepo workspace graph needs it during build);\n * any other entry drops the path and its subtree. Returns `cat` (passthrough)\n * when nothing is excluded. Uses awk so it is portable across macOS and Linux.\n */\nexport function buildSandboxFileFilter(excludePaths: string[] = []): string {\n\tif (excludePaths.length === 0) {\n\t\treturn \"cat\";\n\t}\n\n\tconst clauses = excludePaths.map((entry) => {\n\t\tconst escaped = escapeAwkRegex(entry);\n\n\t\tif (entry.startsWith(\"apps/\")) {\n\t\t\treturn `!(/^${escaped}\\\\// && !/^${escaped}\\\\/package\\\\.json$/)`;\n\t\t}\n\n\t\treturn `!/^${escaped}(\\\\/|$)/`;\n\t});\n\n\treturn `awk '${clauses.join(\" && \")}'`;\n}\n\n/** Root of the per-deploy sandbox tree. The DeploySandbox resource GCs this. */\nconst SANDBOX_ROOT = \"/tmp/infracraft\";\n\n/**\n * How a deploy's working copy is isolated. A closed set, so it is an enum rather\n * than a pair of booleans: the seam derives it from `dependsOn` presence\n * (DeploySandbox / GitGuard), which makes the impossible \"git-guard without a\n * sandbox\" state unrepresentable here.\n */\nexport enum SandboxMode {\n\t/** No isolation — run the CLI in the live working tree. */\n\tNONE = \"NONE\",\n\t/** Isolated copy carrying the repo's REAL `.git` (real commit/author exposed). */\n\tORIGINAL = \"ORIGINAL\",\n\t/** Isolated copy with a metadata-free stub `.git` (no commit SHA / author). */\n\tSTUB = \"STUB\",\n}\n\ninterface SandboxScriptOptions {\n\t/** How to isolate the working copy (see {@link SandboxMode}). */\n\tmode: SandboxMode;\n\t/** Resource-derived name, used in the sandbox dir prefix. */\n\tappName: string;\n\t/** Stack/environment name, prefixed to the sandbox dir so leftovers and\n\t * concurrent deploys are identifiable (e.g. `mlm-nvr-staging-nexus.XXXX`). */\n\tenv?: string;\n\t/** Upload-scoping excludes; applied only in STUB mode (see design spec). */\n\texcludePaths?: string[];\n\t/** Shell run in the working dir before `cli` (e.g. write railpack.json). */\n\tsetup?: string;\n\t/** Fully-formed platform deploy command (its exit code becomes the script's). */\n\tcli: string;\n}\n\n/**\n * Builds the shell for a deploy's `command.local.Command.create`. See\n * docs/superpowers/specs/2026-06-05-deploy-sandbox-design.md for the modes.\n *\n * Hardening: every script runs under POSIX `set -e` so any failed step aborts\n * instead of silently letting the platform CLI run against partial state.\n * `pipefail` is intentionally NOT used — the command runs under `/bin/sh`, which\n * may be `dash` (pipefail is a bash/ksh/zsh extension). To still catch a failing\n * `git ls-files` (a pipe would mask it, since an `rsync` of an empty list exits\n * 0), the copy is staged through intermediate list files rather than a pipe. The\n * `REPO` and `SANDBOX` command substitutions are explicitly guarded with clear\n * error messages.\n */\nexport function buildSandboxScript(options: SandboxScriptOptions): string {\n\tconst { mode, appName, env, excludePaths, setup, cli } = options;\n\n\tconst head = `set -e; REPO=$(git rev-parse --show-toplevel) || { echo \"[infracraft] not inside a git repository\" >&2; exit 1; }`;\n\tconst runSetupAndCli = [setup, cli].filter(Boolean).join(\"; \");\n\n\tif (mode === SandboxMode.NONE) {\n\t\treturn `${head}; cd \"$REPO\"; ${runSetupAndCli}`;\n\t}\n\n\t// Prefix the dir with the project (repo folder name) and env so leftovers and\n\t// concurrent deploys — even across repos and stacks — are identifiable at a\n\t// glance (e.g. `mlm-nvr-production-railway-deploy-mesh.XXXX`).\n\tconst dirPrefix = env ? `${env}-${appName}` : appName;\n\n\tconst makeSandbox = [\n\t\t`PROJECT=$(basename \"$REPO\")`,\n\t\t`SANDBOX=$(mktemp -d \"${SANDBOX_ROOT}/$PROJECT-${dirPrefix}.XXXXXX\") || { echo \"[infracraft] mktemp failed\" >&2; exit 1; }`,\n\t\t`trap 'rm -rf \"$SANDBOX\"' EXIT`,\n\t].join(\"; \");\n\n\t// Copy tracked files via intermediate list files (no pipe — see hardening note).\n\t// STUB filters by excludePaths; ORIGINAL copies the full tracked tree (`cat`).\n\tconst filter =\n\t\tmode === SandboxMode.STUB ? buildSandboxFileFilter(excludePaths) : \"cat\";\n\n\tconst copy = [\n\t\t`git -C \"$REPO\" ls-files > \"$SANDBOX/.ic-ls\"`,\n\t\t`${filter} \"$SANDBOX/.ic-ls\" > \"$SANDBOX/.ic-lsf\"`,\n\t\t`rsync -a --files-from=\"$SANDBOX/.ic-lsf\" \"$REPO\"/ \"$SANDBOX\"/`,\n\t\t`rm -f \"$SANDBOX/.ic-ls\" \"$SANDBOX/.ic-lsf\"`,\n\t].join(\"; \");\n\n\tif (mode === SandboxMode.STUB) {\n\t\t// Metadata-free stub: unborn HEAD, no commit SHA / author reaches the platform.\n\t\tconst stubGit = `cd \"$SANDBOX\"; git init -q && git add -A`;\n\n\t\treturn `${head}; ${makeSandbox}; ${copy}; ${stubGit}; ${runSetupAndCli}`;\n\t}\n\n\t// ORIGINAL: real `.git`, CoW-copied (plain-copy fallback on non-CoW filesystems).\n\tconst copyGit =\n\t\t`cp -c -R \"$REPO/.git\" \"$SANDBOX/.git\" 2>/dev/null || ` +\n\t\t`cp -R \"$REPO/.git\" \"$SANDBOX/.git\"`;\n\n\treturn `${head}; ${makeSandbox}; ${copy}; ${copyGit}; cd \"$SANDBOX\"; ${runSetupAndCli}`;\n}\n\n/** Cross-bundle brand: `instanceof` is unreliable when the seam and the resource\n * come from different built entries, so the seam detects sandboxes by this. */\nconst DEPLOY_SANDBOX_BRAND = Symbol.for(\"@infracraft/pulumi/DeploySandbox\");\n\n/** Sweep sandboxes orphaned by a hard-killed run (older than this). Kept well\n * above the worst-case deploy time (~30 min) but low enough that a SIGKILL\n * orphan does not linger for a day accumulating in /tmp. */\nconst STALE_SANDBOX_MS = 3 * 60 * 60 * 1000;\n\n/**\n * Isolation marker + workspace lifecycle. Listing it in a deploy's `dependsOn`\n * makes that deploy run in an isolated `/tmp/infracraft` copy. Carries no config;\n * the repo root is derived at runtime by the deploy command.\n */\nexport class DeploySandbox extends pulumi.ComponentResource {\n\tconstructor(name: string, opts?: pulumi.ComponentResourceOptions) {\n\t\tsuper(\"infracraft:sandbox:DeploySandbox\", name, {}, opts);\n\n\t\t(this as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] = true;\n\n\t\tif (!pulumi.runtime.isDryRun()) {\n\t\t\tthis.prepareWorkspace();\n\t\t}\n\n\t\tthis.registerOutputs({});\n\t}\n\n\t/** mkdir the workspace root and GC stale sandboxes (best-effort). Sandboxes are\n\t * flat under the root (`/tmp/infracraft/<project>-<env>-<app>.XXXX`), so this\n\t * sweeps any entry older than the stale threshold. */\n\tprivate prepareWorkspace(): void {\n\t\tfs.mkdirSync(SANDBOX_ROOT, { recursive: true });\n\n\t\tlet entries: string[] = [];\n\n\t\ttry {\n\t\t\tentries = fs.readdirSync(SANDBOX_ROOT) as string[];\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tconst now = Date.now();\n\n\t\tfor (const entry of entries) {\n\t\t\tconst full = path.join(SANDBOX_ROOT, entry);\n\n\t\t\ttry {\n\t\t\t\tif (now - fs.statSync(full).mtimeMs > STALE_SANDBOX_MS) {\n\t\t\t\t\tfs.rmSync(full, { recursive: true, force: true });\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Racing with an in-flight deploy's cleanup — ignore.\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Bundle-safe check for a `DeploySandbox` in a `dependsOn` array. */\nexport function isDeploySandbox(value: unknown): value is DeploySandbox {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t(value as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] === true\n\t);\n}\n"],"mappings":";;;;;;;;;;;AAKA,SAAS,eAAe,MAAsB;CAC7C,OAAO,KAAK,QAAQ,wBAAwB,MAAM;AACnD;;;;;;;;AASA,SAAgB,uBAAuB,eAAyB,CAAC,GAAW;CAC3E,IAAI,aAAa,WAAW,GAC3B,OAAO;CAaR,OAAO,QAVS,aAAa,KAAK,UAAU;EAC3C,MAAM,UAAU,eAAe,KAAK;EAEpC,IAAI,MAAM,WAAW,OAAO,GAC3B,OAAO,OAAO,QAAQ,aAAa,QAAQ;EAG5C,OAAO,MAAM,QAAQ;CACtB,CAEqB,EAAE,KAAK,MAAM,EAAE;AACrC;;AAGA,MAAM,eAAe;;;;;;;AAQrB,IAAY,cAAL;;CAEN;;CAEA;;CAEA;;AACD;;;;;;;;;;;;;;AA+BA,SAAgB,mBAAmB,SAAuC;CACzE,MAAM,EAAE,MAAM,SAAS,KAAK,cAAc,OAAO,QAAQ;CAEzD,MAAM,OAAO;CACb,MAAM,iBAAiB,CAAC,OAAO,GAAG,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;CAE7D,IAAI,iBACH,OAAO,GAAG,KAAK,gBAAgB;CAQhC,MAAM,cAAc;EACnB;EACA,wBAAwB,aAAa,YAJpB,MAAM,GAAG,IAAI,GAAG,YAAY,QAIc;EAC3D;CACD,EAAE,KAAK,IAAI;CAOX,MAAM,OAAO;EACZ;EACA,GAJA,kBAA4B,uBAAuB,YAAY,IAAI,MAIzD;EACV;EACA;CACD,EAAE,KAAK,IAAI;CAEX,IAAI,iBAIH,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,KAAK,+CAAgB;CAQzD,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,KAAK,gHAA+B;AACxE;;;AAIA,MAAM,uBAAuB,OAAO,IAAI,kCAAkC;;;;AAK1E,MAAM,mBAAmB,QAAc;;;;;;AAOvC,IAAa,gBAAb,cAAmCA,eAAO,kBAAkB;CAC3D,YAAY,MAAc,MAAwC;EACjE,MAAM,oCAAoC,MAAM,CAAC,GAAG,IAAI;EAExD,AAAC,KAAiC,wBAAwB;EAE1D,IAAI,CAACA,eAAO,QAAQ,SAAS,GAC5B,KAAK,iBAAiB;EAGvB,KAAK,gBAAgB,CAAC,CAAC;CACxB;;;;CAKA,AAAQ,mBAAyB;EAChC,QAAG,UAAU,cAAc,EAAE,WAAW,KAAK,CAAC;EAE9C,IAAI,UAAoB,CAAC;EAEzB,IAAI;GACH,UAAUC,QAAG,YAAY,YAAY;EACtC,QAAQ;GACP;EACD;EAEA,MAAM,MAAM,KAAK,IAAI;EAErB,KAAK,MAAM,SAAS,SAAS;GAC5B,MAAM,OAAOC,UAAK,KAAK,cAAc,KAAK;GAE1C,IAAI;IACH,IAAI,MAAMD,QAAG,SAAS,IAAI,EAAE,UAAU,kBACrC,QAAG,OAAO,MAAM;KAAE,WAAW;KAAM,OAAO;IAAK,CAAC;GAElD,QAAQ,CAER;EACD;CACD;AACD;;AAGA,SAAgB,gBAAgB,OAAwC;CACvE,OACC,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,0BAA0B;AAE/D"}
@@ -3,24 +3,36 @@ import * as pulumi from "@pulumi/pulumi";
3
3
 
4
4
  //#region src/sandbox.d.ts
5
5
  /**
6
- * Builds the shell filter applied to newline-delimited `git ls-files` output
7
- * before `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps
6
+ * Builds the shell filter applied to the `git ls-files` list before
7
+ * `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps
8
8
  * `apps/<x>/package.json` (the monorepo workspace graph needs it during build);
9
9
  * any other entry drops the path and its subtree. Returns `cat` (passthrough)
10
10
  * when nothing is excluded. Uses awk so it is portable across macOS and Linux.
11
11
  */
12
12
  declare function buildSandboxFileFilter(excludePaths?: string[]): string;
13
+ /**
14
+ * How a deploy's working copy is isolated. A closed set, so it is an enum rather
15
+ * than a pair of booleans: the seam derives it from `dependsOn` presence
16
+ * (DeploySandbox / GitGuard), which makes the impossible "git-guard without a
17
+ * sandbox" state unrepresentable here.
18
+ */
19
+ declare enum SandboxMode {
20
+ /** No isolation — run the CLI in the live working tree. */
21
+ NONE = "NONE",
22
+ /** Isolated copy carrying the repo's REAL `.git` (real commit/author exposed). */
23
+ ORIGINAL = "ORIGINAL",
24
+ /** Isolated copy with a metadata-free stub `.git` (no commit SHA / author). */
25
+ STUB = "STUB"
26
+ }
13
27
  interface SandboxScriptOptions {
14
- /** Whether to isolate into a /tmp copy (false run in the live tree). */
15
- sandbox: boolean;
16
- /** Whether the sandbox `.git` is a metadata-free stub (true) or the real one. */
17
- gitGuard: boolean;
28
+ /** How to isolate the working copy (see {@link SandboxMode}). */
29
+ mode: SandboxMode;
18
30
  /** Resource-derived name, used in the sandbox dir prefix. */
19
31
  appName: string;
20
32
  /** Stack/environment name, prefixed to the sandbox dir so leftovers and
21
- * concurrent deploys are identifiable (e.g. `staging-vercel-deploy-nexus.XXXX`). */
33
+ * concurrent deploys are identifiable (e.g. `mlm-nvr-staging-nexus.XXXX`). */
22
34
  env?: string;
23
- /** Upload-scoping excludes; applied only in stub mode (see design spec). */
35
+ /** Upload-scoping excludes; applied only in STUB mode (see design spec). */
24
36
  excludePaths?: string[];
25
37
  /** Shell run in the working dir before `cli` (e.g. write railpack.json). */
26
38
  setup?: string;
@@ -30,6 +42,15 @@ interface SandboxScriptOptions {
30
42
  /**
31
43
  * Builds the shell for a deploy's `command.local.Command.create`. See
32
44
  * docs/superpowers/specs/2026-06-05-deploy-sandbox-design.md for the modes.
45
+ *
46
+ * Hardening: every script runs under POSIX `set -e` so any failed step aborts
47
+ * instead of silently letting the platform CLI run against partial state.
48
+ * `pipefail` is intentionally NOT used — the command runs under `/bin/sh`, which
49
+ * may be `dash` (pipefail is a bash/ksh/zsh extension). To still catch a failing
50
+ * `git ls-files` (a pipe would mask it, since an `rsync` of an empty list exits
51
+ * 0), the copy is staged through intermediate list files rather than a pipe. The
52
+ * `REPO` and `SANDBOX` command substitutions are explicitly guarded with clear
53
+ * error messages.
33
54
  */
34
55
  declare function buildSandboxScript(options: SandboxScriptOptions): string;
35
56
  /**
@@ -47,5 +68,5 @@ declare class DeploySandbox extends pulumi.ComponentResource {
47
68
  /** Bundle-safe check for a `DeploySandbox` in a `dependsOn` array. */
48
69
  declare function isDeploySandbox(value: unknown): value is DeploySandbox;
49
70
  //#endregion
50
- export { DeploySandbox, buildSandboxFileFilter, buildSandboxScript, isDeploySandbox };
71
+ export { DeploySandbox, SandboxMode, buildSandboxFileFilter, buildSandboxScript, isDeploySandbox };
51
72
  //# sourceMappingURL=sandbox.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.d.cts","names":[],"sources":["../src/sandbox.ts"],"mappings":";;;;;;;AAgBA;;;;iBAAgB,sBAAA,CAAuB,YAA2B;AAAA,UAqBxD,oBAAA;EAAoB;EAE7B,OAAA;EAF6B;EAI7B,QAAA;EAAA;EAEA,OAAA;EAGA;;EAAA,GAAA;EAMA;EAJA,YAAA;EAIG;EAFH,KAAA;EASiC;EAPjC,GAAA;AAAA;AAO+D;AAuDhE;;;AAvDgE,iBAAhD,kBAAA,CAAmB,OAA6B,EAApB,oBAAoB;;;;;;cAuDnD,aAAA,SAAsB,MAAA,CAAO,iBAAiB;cAC9C,IAAA,UAAc,IAAA,GAAO,MAAA,CAAO,wBAAA;EAehC;;AAAgB;EAAhB,QAAA,gBAAA;AAAA;;iBA4BO,eAAA,CAAgB,KAAA,YAAiB,KAAA,IAAS,aAAa"}
1
+ {"version":3,"file":"sandbox.d.cts","names":[],"sources":["../src/sandbox.ts"],"mappings":";;;;;;;AAgBA;;;;iBAAgB,sBAAA,CAAuB,YAA2B;AA2BlE;;;;;;AAAA,aAAY,WAAA;EAMP;EAJJ,IAAA;EAOS;EALT,QAAA;;EAEA,IAAA;AAAA;AAAA,UAGS,oBAAA;EAIT;EAFA,IAAA,EAAM,WAAW;EAOjB;EALA,OAAA;EASA;;EANA,GAAA;EAsBe;EApBf,YAAA;;EAEA,KAAA;EAkB+D;EAhB/D,GAAA;AAAA;;;;;;;;;;;;AA8FwB;AA4BzB;iBA1GgB,kBAAA,CAAmB,OAA6B,EAApB,oBAAoB;;;;;;cA8DnD,aAAA,SAAsB,MAAA,CAAO,iBAAiB;cAC9C,IAAA,UAAc,IAAA,GAAO,MAAA,CAAO,wBAAA;;;;UAehC,gBAAA;AAAA;;iBA4BO,eAAA,CAAgB,KAAA,YAAiB,KAAA,IAAS,aAAa"}
@@ -3,24 +3,36 @@ import * as pulumi from "@pulumi/pulumi";
3
3
 
4
4
  //#region src/sandbox.d.ts
5
5
  /**
6
- * Builds the shell filter applied to newline-delimited `git ls-files` output
7
- * before `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps
6
+ * Builds the shell filter applied to the `git ls-files` list before
7
+ * `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps
8
8
  * `apps/<x>/package.json` (the monorepo workspace graph needs it during build);
9
9
  * any other entry drops the path and its subtree. Returns `cat` (passthrough)
10
10
  * when nothing is excluded. Uses awk so it is portable across macOS and Linux.
11
11
  */
12
12
  declare function buildSandboxFileFilter(excludePaths?: string[]): string;
13
+ /**
14
+ * How a deploy's working copy is isolated. A closed set, so it is an enum rather
15
+ * than a pair of booleans: the seam derives it from `dependsOn` presence
16
+ * (DeploySandbox / GitGuard), which makes the impossible "git-guard without a
17
+ * sandbox" state unrepresentable here.
18
+ */
19
+ declare enum SandboxMode {
20
+ /** No isolation — run the CLI in the live working tree. */
21
+ NONE = "NONE",
22
+ /** Isolated copy carrying the repo's REAL `.git` (real commit/author exposed). */
23
+ ORIGINAL = "ORIGINAL",
24
+ /** Isolated copy with a metadata-free stub `.git` (no commit SHA / author). */
25
+ STUB = "STUB"
26
+ }
13
27
  interface SandboxScriptOptions {
14
- /** Whether to isolate into a /tmp copy (false run in the live tree). */
15
- sandbox: boolean;
16
- /** Whether the sandbox `.git` is a metadata-free stub (true) or the real one. */
17
- gitGuard: boolean;
28
+ /** How to isolate the working copy (see {@link SandboxMode}). */
29
+ mode: SandboxMode;
18
30
  /** Resource-derived name, used in the sandbox dir prefix. */
19
31
  appName: string;
20
32
  /** Stack/environment name, prefixed to the sandbox dir so leftovers and
21
- * concurrent deploys are identifiable (e.g. `staging-vercel-deploy-nexus.XXXX`). */
33
+ * concurrent deploys are identifiable (e.g. `mlm-nvr-staging-nexus.XXXX`). */
22
34
  env?: string;
23
- /** Upload-scoping excludes; applied only in stub mode (see design spec). */
35
+ /** Upload-scoping excludes; applied only in STUB mode (see design spec). */
24
36
  excludePaths?: string[];
25
37
  /** Shell run in the working dir before `cli` (e.g. write railpack.json). */
26
38
  setup?: string;
@@ -30,6 +42,15 @@ interface SandboxScriptOptions {
30
42
  /**
31
43
  * Builds the shell for a deploy's `command.local.Command.create`. See
32
44
  * docs/superpowers/specs/2026-06-05-deploy-sandbox-design.md for the modes.
45
+ *
46
+ * Hardening: every script runs under POSIX `set -e` so any failed step aborts
47
+ * instead of silently letting the platform CLI run against partial state.
48
+ * `pipefail` is intentionally NOT used — the command runs under `/bin/sh`, which
49
+ * may be `dash` (pipefail is a bash/ksh/zsh extension). To still catch a failing
50
+ * `git ls-files` (a pipe would mask it, since an `rsync` of an empty list exits
51
+ * 0), the copy is staged through intermediate list files rather than a pipe. The
52
+ * `REPO` and `SANDBOX` command substitutions are explicitly guarded with clear
53
+ * error messages.
33
54
  */
34
55
  declare function buildSandboxScript(options: SandboxScriptOptions): string;
35
56
  /**
@@ -47,5 +68,5 @@ declare class DeploySandbox extends pulumi.ComponentResource {
47
68
  /** Bundle-safe check for a `DeploySandbox` in a `dependsOn` array. */
48
69
  declare function isDeploySandbox(value: unknown): value is DeploySandbox;
49
70
  //#endregion
50
- export { DeploySandbox, buildSandboxFileFilter, buildSandboxScript, isDeploySandbox };
71
+ export { DeploySandbox, SandboxMode, buildSandboxFileFilter, buildSandboxScript, isDeploySandbox };
51
72
  //# sourceMappingURL=sandbox.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.d.mts","names":[],"sources":["../src/sandbox.ts"],"mappings":";;;;;;;AAgBA;;;;iBAAgB,sBAAA,CAAuB,YAA2B;AAAA,UAqBxD,oBAAA;EAAoB;EAE7B,OAAA;EAF6B;EAI7B,QAAA;EAAA;EAEA,OAAA;EAGA;;EAAA,GAAA;EAMA;EAJA,YAAA;EAIG;EAFH,KAAA;EASiC;EAPjC,GAAA;AAAA;AAO+D;AAuDhE;;;AAvDgE,iBAAhD,kBAAA,CAAmB,OAA6B,EAApB,oBAAoB;;;;;;cAuDnD,aAAA,SAAsB,MAAA,CAAO,iBAAiB;cAC9C,IAAA,UAAc,IAAA,GAAO,MAAA,CAAO,wBAAA;EAehC;;AAAgB;EAAhB,QAAA,gBAAA;AAAA;;iBA4BO,eAAA,CAAgB,KAAA,YAAiB,KAAA,IAAS,aAAa"}
1
+ {"version":3,"file":"sandbox.d.mts","names":[],"sources":["../src/sandbox.ts"],"mappings":";;;;;;;AAgBA;;;;iBAAgB,sBAAA,CAAuB,YAA2B;AA2BlE;;;;;;AAAA,aAAY,WAAA;EAMP;EAJJ,IAAA;EAOS;EALT,QAAA;;EAEA,IAAA;AAAA;AAAA,UAGS,oBAAA;EAIT;EAFA,IAAA,EAAM,WAAW;EAOjB;EALA,OAAA;EASA;;EANA,GAAA;EAsBe;EApBf,YAAA;;EAEA,KAAA;EAkB+D;EAhB/D,GAAA;AAAA;;;;;;;;;;;;AA8FwB;AA4BzB;iBA1GgB,kBAAA,CAAmB,OAA6B,EAApB,oBAAoB;;;;;;cA8DnD,aAAA,SAAsB,MAAA,CAAO,iBAAiB;cAC9C,IAAA,UAAc,IAAA,GAAO,MAAA,CAAO,wBAAA;;;;UAehC,gBAAA;AAAA;;iBA4BO,eAAA,CAAgB,KAAA,YAAiB,KAAA,IAAS,aAAa"}
package/dist/sandbox.mjs CHANGED
@@ -9,8 +9,8 @@ function escapeAwkRegex(path) {
9
9
  return path.replace(/[.[\]{}()*+?^$|\\/]/g, "\\$&");
10
10
  }
11
11
  /**
12
- * Builds the shell filter applied to newline-delimited `git ls-files` output
13
- * before `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps
12
+ * Builds the shell filter applied to the `git ls-files` list before
13
+ * `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps
14
14
  * `apps/<x>/package.json` (the monorepo workspace graph needs it during build);
15
15
  * any other entry drops the path and its subtree. Returns `cat` (passthrough)
16
16
  * when nothing is excluded. Uses awk so it is portable across macOS and Linux.
@@ -26,27 +26,59 @@ function buildSandboxFileFilter(excludePaths = []) {
26
26
  /** Root of the per-deploy sandbox tree. The DeploySandbox resource GCs this. */
27
27
  const SANDBOX_ROOT = "/tmp/infracraft";
28
28
  /**
29
+ * How a deploy's working copy is isolated. A closed set, so it is an enum rather
30
+ * than a pair of booleans: the seam derives it from `dependsOn` presence
31
+ * (DeploySandbox / GitGuard), which makes the impossible "git-guard without a
32
+ * sandbox" state unrepresentable here.
33
+ */
34
+ let SandboxMode = /* @__PURE__ */ function(SandboxMode) {
35
+ /** No isolation — run the CLI in the live working tree. */
36
+ SandboxMode["NONE"] = "NONE";
37
+ /** Isolated copy carrying the repo's REAL `.git` (real commit/author exposed). */
38
+ SandboxMode["ORIGINAL"] = "ORIGINAL";
39
+ /** Isolated copy with a metadata-free stub `.git` (no commit SHA / author). */
40
+ SandboxMode["STUB"] = "STUB";
41
+ return SandboxMode;
42
+ }({});
43
+ /**
29
44
  * Builds the shell for a deploy's `command.local.Command.create`. See
30
45
  * docs/superpowers/specs/2026-06-05-deploy-sandbox-design.md for the modes.
46
+ *
47
+ * Hardening: every script runs under POSIX `set -e` so any failed step aborts
48
+ * instead of silently letting the platform CLI run against partial state.
49
+ * `pipefail` is intentionally NOT used — the command runs under `/bin/sh`, which
50
+ * may be `dash` (pipefail is a bash/ksh/zsh extension). To still catch a failing
51
+ * `git ls-files` (a pipe would mask it, since an `rsync` of an empty list exits
52
+ * 0), the copy is staged through intermediate list files rather than a pipe. The
53
+ * `REPO` and `SANDBOX` command substitutions are explicitly guarded with clear
54
+ * error messages.
31
55
  */
32
56
  function buildSandboxScript(options) {
33
- const { sandbox, gitGuard, appName, env, excludePaths, setup, cli } = options;
34
- const head = `REPO=$(git rev-parse --show-toplevel)`;
57
+ const { mode, appName, env, excludePaths, setup, cli } = options;
58
+ const head = `set -e; REPO=$(git rev-parse --show-toplevel) || { echo "[infracraft] not inside a git repository" >&2; exit 1; }`;
35
59
  const runSetupAndCli = [setup, cli].filter(Boolean).join("; ");
36
- if (!sandbox) return `${head}; cd "$REPO"; ${runSetupAndCli}`;
60
+ if (mode === "NONE") return `${head}; cd "$REPO"; ${runSetupAndCli}`;
37
61
  const makeSandbox = [
38
62
  `PROJECT=$(basename "$REPO")`,
39
- `SANDBOX=$(mktemp -d "${SANDBOX_ROOT}/$PROJECT-${env ? `${env}-${appName}` : appName}.XXXXXX")`,
63
+ `SANDBOX=$(mktemp -d "${SANDBOX_ROOT}/$PROJECT-${env ? `${env}-${appName}` : appName}.XXXXXX") || { echo "[infracraft] mktemp failed" >&2; exit 1; }`,
40
64
  `trap 'rm -rf "$SANDBOX"' EXIT`
41
65
  ].join("; ");
42
- if (gitGuard) return `${head}; ${makeSandbox}; ${`git -C "$REPO" ls-files | ${buildSandboxFileFilter(excludePaths)} | rsync -a --files-from=- "$REPO"/ "$SANDBOX"/`}; cd "\$SANDBOX" && git init -q && git add -A; ${runSetupAndCli}`;
43
- return `${head}; ${makeSandbox}; git -C "\$REPO" ls-files | rsync -a --files-from=- "\$REPO"/ "\$SANDBOX"/; cp -c -R "\$REPO/.git" "\$SANDBOX/.git" 2>/dev/null || cp -R "\$REPO/.git" "\$SANDBOX/.git"; cd "$SANDBOX"; ${runSetupAndCli}`;
66
+ const copy = [
67
+ `git -C "$REPO" ls-files > "$SANDBOX/.ic-ls"`,
68
+ `${mode === "STUB" ? buildSandboxFileFilter(excludePaths) : "cat"} "$SANDBOX/.ic-ls" > "$SANDBOX/.ic-lsf"`,
69
+ `rsync -a --files-from="$SANDBOX/.ic-lsf" "$REPO"/ "$SANDBOX"/`,
70
+ `rm -f "$SANDBOX/.ic-ls" "$SANDBOX/.ic-lsf"`
71
+ ].join("; ");
72
+ if (mode === "STUB") return `${head}; ${makeSandbox}; ${copy}; cd "\$SANDBOX"; git init -q && git add -A; ${runSetupAndCli}`;
73
+ return `${head}; ${makeSandbox}; ${copy}; cp -c -R "\$REPO/.git" "\$SANDBOX/.git" 2>/dev/null || cp -R "\$REPO/.git" "\$SANDBOX/.git"; cd "$SANDBOX"; ${runSetupAndCli}`;
44
74
  }
45
75
  /** Cross-bundle brand: `instanceof` is unreliable when the seam and the resource
46
76
  * come from different built entries, so the seam detects sandboxes by this. */
47
77
  const DEPLOY_SANDBOX_BRAND = Symbol.for("@infracraft/pulumi/DeploySandbox");
48
- /** Sweep sandboxes orphaned by a hard-killed run (older than this). */
49
- const STALE_SANDBOX_MS = 1440 * 60 * 1e3;
78
+ /** Sweep sandboxes orphaned by a hard-killed run (older than this). Kept well
79
+ * above the worst-case deploy time (~30 min) but low enough that a SIGKILL
80
+ * orphan does not linger for a day accumulating in /tmp. */
81
+ const STALE_SANDBOX_MS = 10800 * 1e3;
50
82
  /**
51
83
  * Isolation marker + workspace lifecycle. Listing it in a deploy's `dependsOn`
52
84
  * makes that deploy run in an isolated `/tmp/infracraft` copy. Carries no config;
@@ -88,5 +120,5 @@ function isDeploySandbox(value) {
88
120
  }
89
121
 
90
122
  //#endregion
91
- export { DeploySandbox, buildSandboxFileFilter, buildSandboxScript, isDeploySandbox };
123
+ export { DeploySandbox, SandboxMode, buildSandboxFileFilter, buildSandboxScript, isDeploySandbox };
92
124
  //# sourceMappingURL=sandbox.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.mjs","names":[],"sources":["../src/sandbox.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\n/** Escapes a path literal for embedding inside an awk ERE. */\nfunction escapeAwkRegex(path: string): string {\n\treturn path.replace(/[.[\\]{}()*+?^$|\\\\/]/g, \"\\\\$&\");\n}\n\n/**\n * Builds the shell filter applied to newline-delimited `git ls-files` output\n * before `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps\n * `apps/<x>/package.json` (the monorepo workspace graph needs it during build);\n * any other entry drops the path and its subtree. Returns `cat` (passthrough)\n * when nothing is excluded. Uses awk so it is portable across macOS and Linux.\n */\nexport function buildSandboxFileFilter(excludePaths: string[] = []): string {\n\tif (excludePaths.length === 0) {\n\t\treturn \"cat\";\n\t}\n\n\tconst clauses = excludePaths.map((entry) => {\n\t\tconst escaped = escapeAwkRegex(entry);\n\n\t\tif (entry.startsWith(\"apps/\")) {\n\t\t\treturn `!(/^${escaped}\\\\// && !/^${escaped}\\\\/package\\\\.json$/)`;\n\t\t}\n\n\t\treturn `!/^${escaped}(\\\\/|$)/`;\n\t});\n\n\treturn `awk '${clauses.join(\" && \")}'`;\n}\n\n/** Root of the per-deploy sandbox tree. The DeploySandbox resource GCs this. */\nconst SANDBOX_ROOT = \"/tmp/infracraft\";\n\ninterface SandboxScriptOptions {\n\t/** Whether to isolate into a /tmp copy (false run in the live tree). */\n\tsandbox: boolean;\n\t/** Whether the sandbox `.git` is a metadata-free stub (true) or the real one. */\n\tgitGuard: boolean;\n\t/** Resource-derived name, used in the sandbox dir prefix. */\n\tappName: string;\n\t/** Stack/environment name, prefixed to the sandbox dir so leftovers and\n\t * concurrent deploys are identifiable (e.g. `staging-vercel-deploy-nexus.XXXX`). */\n\tenv?: string;\n\t/** Upload-scoping excludes; applied only in stub mode (see design spec). */\n\texcludePaths?: string[];\n\t/** Shell run in the working dir before `cli` (e.g. write railpack.json). */\n\tsetup?: string;\n\t/** Fully-formed platform deploy command (its exit code becomes the script's). */\n\tcli: string;\n}\n\n/**\n * Builds the shell for a deploy's `command.local.Command.create`. See\n * docs/superpowers/specs/2026-06-05-deploy-sandbox-design.md for the modes.\n */\nexport function buildSandboxScript(options: SandboxScriptOptions): string {\n\tconst { sandbox, gitGuard, appName, env, excludePaths, setup, cli } = options;\n\n\tconst head = `REPO=$(git rev-parse --show-toplevel)`;\n\tconst runSetupAndCli = [setup, cli].filter(Boolean).join(\"; \");\n\n\tif (!sandbox) {\n\t\treturn `${head}; cd \"$REPO\"; ${runSetupAndCli}`;\n\t}\n\n\t// Prefix the dir with the project (repo folder name) and env so leftovers and\n\t// concurrent deploys — even across repos and stacks — are identifiable at a\n\t// glance (e.g. `mlm-nvr-production-railway-deploy-mesh.XXXX`).\n\tconst dirPrefix = env ? `${env}-${appName}` : appName;\n\n\tconst makeSandbox = [\n\t\t`PROJECT=$(basename \"$REPO\")`,\n\t\t`SANDBOX=$(mktemp -d \"${SANDBOX_ROOT}/$PROJECT-${dirPrefix}.XXXXXX\")`,\n\t\t`trap 'rm -rf \"$SANDBOX\"' EXIT`,\n\t].join(\"; \");\n\n\tif (gitGuard) {\n\t\tconst filter = buildSandboxFileFilter(excludePaths);\n\n\t\tconst copy =\n\t\t\t`git -C \"$REPO\" ls-files | ${filter} | ` +\n\t\t\t`rsync -a --files-from=- \"$REPO\"/ \"$SANDBOX\"/`;\n\n\t\tconst stubGit = `cd \"$SANDBOX\" && git init -q && git add -A`;\n\n\t\treturn `${head}; ${makeSandbox}; ${copy}; ${stubGit}; ${runSetupAndCli}`;\n\t}\n\n\t// Original `.git`, full clean tree (no filter, no reconciliation needed).\n\tconst copy = `git -C \"$REPO\" ls-files | rsync -a --files-from=- \"$REPO\"/ \"$SANDBOX\"/`;\n\n\tconst copyGit =\n\t\t`cp -c -R \"$REPO/.git\" \"$SANDBOX/.git\" 2>/dev/null || ` +\n\t\t`cp -R \"$REPO/.git\" \"$SANDBOX/.git\"`;\n\n\treturn `${head}; ${makeSandbox}; ${copy}; ${copyGit}; cd \"$SANDBOX\"; ${runSetupAndCli}`;\n}\n\n/** Cross-bundle brand: `instanceof` is unreliable when the seam and the resource\n * come from different built entries, so the seam detects sandboxes by this. */\nconst DEPLOY_SANDBOX_BRAND = Symbol.for(\"@infracraft/pulumi/DeploySandbox\");\n\n/** Sweep sandboxes orphaned by a hard-killed run (older than this). */\nconst STALE_SANDBOX_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Isolation marker + workspace lifecycle. Listing it in a deploy's `dependsOn`\n * makes that deploy run in an isolated `/tmp/infracraft` copy. Carries no config;\n * the repo root is derived at runtime by the deploy command.\n */\nexport class DeploySandbox extends pulumi.ComponentResource {\n\tconstructor(name: string, opts?: pulumi.ComponentResourceOptions) {\n\t\tsuper(\"infracraft:sandbox:DeploySandbox\", name, {}, opts);\n\n\t\t(this as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] = true;\n\n\t\tif (!pulumi.runtime.isDryRun()) {\n\t\t\tthis.prepareWorkspace();\n\t\t}\n\n\t\tthis.registerOutputs({});\n\t}\n\n\t/** mkdir the workspace root and GC stale sandboxes (best-effort). Sandboxes are\n\t * flat under the root (`/tmp/infracraft/<project>-<env>-<app>.XXXX`), so this\n\t * sweeps any entry older than the stale threshold. */\n\tprivate prepareWorkspace(): void {\n\t\tfs.mkdirSync(SANDBOX_ROOT, { recursive: true });\n\n\t\tlet entries: string[] = [];\n\n\t\ttry {\n\t\t\tentries = fs.readdirSync(SANDBOX_ROOT) as string[];\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tconst now = Date.now();\n\n\t\tfor (const entry of entries) {\n\t\t\tconst full = path.join(SANDBOX_ROOT, entry);\n\n\t\t\ttry {\n\t\t\t\tif (now - fs.statSync(full).mtimeMs > STALE_SANDBOX_MS) {\n\t\t\t\t\tfs.rmSync(full, { recursive: true, force: true });\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Racing with an in-flight deploy's cleanup — ignore.\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Bundle-safe check for a `DeploySandbox` in a `dependsOn` array. */\nexport function isDeploySandbox(value: unknown): value is DeploySandbox {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t(value as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] === true\n\t);\n}\n"],"mappings":";;;;;;;AAKA,SAAS,eAAe,MAAsB;CAC7C,OAAO,KAAK,QAAQ,wBAAwB,MAAM;AACnD;;;;;;;;AASA,SAAgB,uBAAuB,eAAyB,CAAC,GAAW;CAC3E,IAAI,aAAa,WAAW,GAC3B,OAAO;CAaR,OAAO,QAVS,aAAa,KAAK,UAAU;EAC3C,MAAM,UAAU,eAAe,KAAK;EAEpC,IAAI,MAAM,WAAW,OAAO,GAC3B,OAAO,OAAO,QAAQ,aAAa,QAAQ;EAG5C,OAAO,MAAM,QAAQ;CACtB,CAEqB,EAAE,KAAK,MAAM,EAAE;AACrC;;AAGA,MAAM,eAAe;;;;;AAwBrB,SAAgB,mBAAmB,SAAuC;CACzE,MAAM,EAAE,SAAS,UAAU,SAAS,KAAK,cAAc,OAAO,QAAQ;CAEtE,MAAM,OAAO;CACb,MAAM,iBAAiB,CAAC,OAAO,GAAG,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;CAE7D,IAAI,CAAC,SACJ,OAAO,GAAG,KAAK,gBAAgB;CAQhC,MAAM,cAAc;EACnB;EACA,wBAAwB,aAAa,YAJpB,MAAM,GAAG,IAAI,GAAG,YAAY,QAIc;EAC3D;CACD,EAAE,KAAK,IAAI;CAEX,IAAI,UASH,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,6BARpB,uBAAuB,YAGH,EAAE,iDAKG,iDAAgB;CAUzD,OAAO,GAAG,KAAK,IAAI,YAAY,2LAAwC;AACxE;;;AAIA,MAAM,uBAAuB,OAAO,IAAI,kCAAkC;;AAG1E,MAAM,mBAAmB,OAAU,KAAK;;;;;;AAOxC,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAC3D,YAAY,MAAc,MAAwC;EACjE,MAAM,oCAAoC,MAAM,CAAC,GAAG,IAAI;EAExD,AAAC,KAAiC,wBAAwB;EAE1D,IAAI,CAAC,OAAO,QAAQ,SAAS,GAC5B,KAAK,iBAAiB;EAGvB,KAAK,gBAAgB,CAAC,CAAC;CACxB;;;;CAKA,AAAQ,mBAAyB;EAChC,GAAG,UAAU,cAAc,EAAE,WAAW,KAAK,CAAC;EAE9C,IAAI,UAAoB,CAAC;EAEzB,IAAI;GACH,UAAU,GAAG,YAAY,YAAY;EACtC,QAAQ;GACP;EACD;EAEA,MAAM,MAAM,KAAK,IAAI;EAErB,KAAK,MAAM,SAAS,SAAS;GAC5B,MAAM,OAAO,KAAK,KAAK,cAAc,KAAK;GAE1C,IAAI;IACH,IAAI,MAAM,GAAG,SAAS,IAAI,EAAE,UAAU,kBACrC,GAAG,OAAO,MAAM;KAAE,WAAW;KAAM,OAAO;IAAK,CAAC;GAElD,QAAQ,CAER;EACD;CACD;AACD;;AAGA,SAAgB,gBAAgB,OAAwC;CACvE,OACC,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,0BAA0B;AAE/D"}
1
+ {"version":3,"file":"sandbox.mjs","names":[],"sources":["../src/sandbox.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\n/** Escapes a path literal for embedding inside an awk ERE. */\nfunction escapeAwkRegex(path: string): string {\n\treturn path.replace(/[.[\\]{}()*+?^$|\\\\/]/g, \"\\\\$&\");\n}\n\n/**\n * Builds the shell filter applied to the `git ls-files` list before\n * `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps\n * `apps/<x>/package.json` (the monorepo workspace graph needs it during build);\n * any other entry drops the path and its subtree. Returns `cat` (passthrough)\n * when nothing is excluded. Uses awk so it is portable across macOS and Linux.\n */\nexport function buildSandboxFileFilter(excludePaths: string[] = []): string {\n\tif (excludePaths.length === 0) {\n\t\treturn \"cat\";\n\t}\n\n\tconst clauses = excludePaths.map((entry) => {\n\t\tconst escaped = escapeAwkRegex(entry);\n\n\t\tif (entry.startsWith(\"apps/\")) {\n\t\t\treturn `!(/^${escaped}\\\\// && !/^${escaped}\\\\/package\\\\.json$/)`;\n\t\t}\n\n\t\treturn `!/^${escaped}(\\\\/|$)/`;\n\t});\n\n\treturn `awk '${clauses.join(\" && \")}'`;\n}\n\n/** Root of the per-deploy sandbox tree. The DeploySandbox resource GCs this. */\nconst SANDBOX_ROOT = \"/tmp/infracraft\";\n\n/**\n * How a deploy's working copy is isolated. A closed set, so it is an enum rather\n * than a pair of booleans: the seam derives it from `dependsOn` presence\n * (DeploySandbox / GitGuard), which makes the impossible \"git-guard without a\n * sandbox\" state unrepresentable here.\n */\nexport enum SandboxMode {\n\t/** No isolation — run the CLI in the live working tree. */\n\tNONE = \"NONE\",\n\t/** Isolated copy carrying the repo's REAL `.git` (real commit/author exposed). */\n\tORIGINAL = \"ORIGINAL\",\n\t/** Isolated copy with a metadata-free stub `.git` (no commit SHA / author). */\n\tSTUB = \"STUB\",\n}\n\ninterface SandboxScriptOptions {\n\t/** How to isolate the working copy (see {@link SandboxMode}). */\n\tmode: SandboxMode;\n\t/** Resource-derived name, used in the sandbox dir prefix. */\n\tappName: string;\n\t/** Stack/environment name, prefixed to the sandbox dir so leftovers and\n\t * concurrent deploys are identifiable (e.g. `mlm-nvr-staging-nexus.XXXX`). */\n\tenv?: string;\n\t/** Upload-scoping excludes; applied only in STUB mode (see design spec). */\n\texcludePaths?: string[];\n\t/** Shell run in the working dir before `cli` (e.g. write railpack.json). */\n\tsetup?: string;\n\t/** Fully-formed platform deploy command (its exit code becomes the script's). */\n\tcli: string;\n}\n\n/**\n * Builds the shell for a deploy's `command.local.Command.create`. See\n * docs/superpowers/specs/2026-06-05-deploy-sandbox-design.md for the modes.\n *\n * Hardening: every script runs under POSIX `set -e` so any failed step aborts\n * instead of silently letting the platform CLI run against partial state.\n * `pipefail` is intentionally NOT used — the command runs under `/bin/sh`, which\n * may be `dash` (pipefail is a bash/ksh/zsh extension). To still catch a failing\n * `git ls-files` (a pipe would mask it, since an `rsync` of an empty list exits\n * 0), the copy is staged through intermediate list files rather than a pipe. The\n * `REPO` and `SANDBOX` command substitutions are explicitly guarded with clear\n * error messages.\n */\nexport function buildSandboxScript(options: SandboxScriptOptions): string {\n\tconst { mode, appName, env, excludePaths, setup, cli } = options;\n\n\tconst head = `set -e; REPO=$(git rev-parse --show-toplevel) || { echo \"[infracraft] not inside a git repository\" >&2; exit 1; }`;\n\tconst runSetupAndCli = [setup, cli].filter(Boolean).join(\"; \");\n\n\tif (mode === SandboxMode.NONE) {\n\t\treturn `${head}; cd \"$REPO\"; ${runSetupAndCli}`;\n\t}\n\n\t// Prefix the dir with the project (repo folder name) and env so leftovers and\n\t// concurrent deploys — even across repos and stacks — are identifiable at a\n\t// glance (e.g. `mlm-nvr-production-railway-deploy-mesh.XXXX`).\n\tconst dirPrefix = env ? `${env}-${appName}` : appName;\n\n\tconst makeSandbox = [\n\t\t`PROJECT=$(basename \"$REPO\")`,\n\t\t`SANDBOX=$(mktemp -d \"${SANDBOX_ROOT}/$PROJECT-${dirPrefix}.XXXXXX\") || { echo \"[infracraft] mktemp failed\" >&2; exit 1; }`,\n\t\t`trap 'rm -rf \"$SANDBOX\"' EXIT`,\n\t].join(\"; \");\n\n\t// Copy tracked files via intermediate list files (no pipe — see hardening note).\n\t// STUB filters by excludePaths; ORIGINAL copies the full tracked tree (`cat`).\n\tconst filter =\n\t\tmode === SandboxMode.STUB ? buildSandboxFileFilter(excludePaths) : \"cat\";\n\n\tconst copy = [\n\t\t`git -C \"$REPO\" ls-files > \"$SANDBOX/.ic-ls\"`,\n\t\t`${filter} \"$SANDBOX/.ic-ls\" > \"$SANDBOX/.ic-lsf\"`,\n\t\t`rsync -a --files-from=\"$SANDBOX/.ic-lsf\" \"$REPO\"/ \"$SANDBOX\"/`,\n\t\t`rm -f \"$SANDBOX/.ic-ls\" \"$SANDBOX/.ic-lsf\"`,\n\t].join(\"; \");\n\n\tif (mode === SandboxMode.STUB) {\n\t\t// Metadata-free stub: unborn HEAD, no commit SHA / author reaches the platform.\n\t\tconst stubGit = `cd \"$SANDBOX\"; git init -q && git add -A`;\n\n\t\treturn `${head}; ${makeSandbox}; ${copy}; ${stubGit}; ${runSetupAndCli}`;\n\t}\n\n\t// ORIGINAL: real `.git`, CoW-copied (plain-copy fallback on non-CoW filesystems).\n\tconst copyGit =\n\t\t`cp -c -R \"$REPO/.git\" \"$SANDBOX/.git\" 2>/dev/null || ` +\n\t\t`cp -R \"$REPO/.git\" \"$SANDBOX/.git\"`;\n\n\treturn `${head}; ${makeSandbox}; ${copy}; ${copyGit}; cd \"$SANDBOX\"; ${runSetupAndCli}`;\n}\n\n/** Cross-bundle brand: `instanceof` is unreliable when the seam and the resource\n * come from different built entries, so the seam detects sandboxes by this. */\nconst DEPLOY_SANDBOX_BRAND = Symbol.for(\"@infracraft/pulumi/DeploySandbox\");\n\n/** Sweep sandboxes orphaned by a hard-killed run (older than this). Kept well\n * above the worst-case deploy time (~30 min) but low enough that a SIGKILL\n * orphan does not linger for a day accumulating in /tmp. */\nconst STALE_SANDBOX_MS = 3 * 60 * 60 * 1000;\n\n/**\n * Isolation marker + workspace lifecycle. Listing it in a deploy's `dependsOn`\n * makes that deploy run in an isolated `/tmp/infracraft` copy. Carries no config;\n * the repo root is derived at runtime by the deploy command.\n */\nexport class DeploySandbox extends pulumi.ComponentResource {\n\tconstructor(name: string, opts?: pulumi.ComponentResourceOptions) {\n\t\tsuper(\"infracraft:sandbox:DeploySandbox\", name, {}, opts);\n\n\t\t(this as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] = true;\n\n\t\tif (!pulumi.runtime.isDryRun()) {\n\t\t\tthis.prepareWorkspace();\n\t\t}\n\n\t\tthis.registerOutputs({});\n\t}\n\n\t/** mkdir the workspace root and GC stale sandboxes (best-effort). Sandboxes are\n\t * flat under the root (`/tmp/infracraft/<project>-<env>-<app>.XXXX`), so this\n\t * sweeps any entry older than the stale threshold. */\n\tprivate prepareWorkspace(): void {\n\t\tfs.mkdirSync(SANDBOX_ROOT, { recursive: true });\n\n\t\tlet entries: string[] = [];\n\n\t\ttry {\n\t\t\tentries = fs.readdirSync(SANDBOX_ROOT) as string[];\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tconst now = Date.now();\n\n\t\tfor (const entry of entries) {\n\t\t\tconst full = path.join(SANDBOX_ROOT, entry);\n\n\t\t\ttry {\n\t\t\t\tif (now - fs.statSync(full).mtimeMs > STALE_SANDBOX_MS) {\n\t\t\t\t\tfs.rmSync(full, { recursive: true, force: true });\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Racing with an in-flight deploy's cleanup — ignore.\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Bundle-safe check for a `DeploySandbox` in a `dependsOn` array. */\nexport function isDeploySandbox(value: unknown): value is DeploySandbox {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t(value as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] === true\n\t);\n}\n"],"mappings":";;;;;;;AAKA,SAAS,eAAe,MAAsB;CAC7C,OAAO,KAAK,QAAQ,wBAAwB,MAAM;AACnD;;;;;;;;AASA,SAAgB,uBAAuB,eAAyB,CAAC,GAAW;CAC3E,IAAI,aAAa,WAAW,GAC3B,OAAO;CAaR,OAAO,QAVS,aAAa,KAAK,UAAU;EAC3C,MAAM,UAAU,eAAe,KAAK;EAEpC,IAAI,MAAM,WAAW,OAAO,GAC3B,OAAO,OAAO,QAAQ,aAAa,QAAQ;EAG5C,OAAO,MAAM,QAAQ;CACtB,CAEqB,EAAE,KAAK,MAAM,EAAE;AACrC;;AAGA,MAAM,eAAe;;;;;;;AAQrB,IAAY,cAAL;;CAEN;;CAEA;;CAEA;;AACD;;;;;;;;;;;;;;AA+BA,SAAgB,mBAAmB,SAAuC;CACzE,MAAM,EAAE,MAAM,SAAS,KAAK,cAAc,OAAO,QAAQ;CAEzD,MAAM,OAAO;CACb,MAAM,iBAAiB,CAAC,OAAO,GAAG,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;CAE7D,IAAI,iBACH,OAAO,GAAG,KAAK,gBAAgB;CAQhC,MAAM,cAAc;EACnB;EACA,wBAAwB,aAAa,YAJpB,MAAM,GAAG,IAAI,GAAG,YAAY,QAIc;EAC3D;CACD,EAAE,KAAK,IAAI;CAOX,MAAM,OAAO;EACZ;EACA,GAJA,kBAA4B,uBAAuB,YAAY,IAAI,MAIzD;EACV;EACA;CACD,EAAE,KAAK,IAAI;CAEX,IAAI,iBAIH,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,KAAK,+CAAgB;CAQzD,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,KAAK,gHAA+B;AACxE;;;AAIA,MAAM,uBAAuB,OAAO,IAAI,kCAAkC;;;;AAK1E,MAAM,mBAAmB,QAAc;;;;;;AAOvC,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAC3D,YAAY,MAAc,MAAwC;EACjE,MAAM,oCAAoC,MAAM,CAAC,GAAG,IAAI;EAExD,AAAC,KAAiC,wBAAwB;EAE1D,IAAI,CAAC,OAAO,QAAQ,SAAS,GAC5B,KAAK,iBAAiB;EAGvB,KAAK,gBAAgB,CAAC,CAAC;CACxB;;;;CAKA,AAAQ,mBAAyB;EAChC,GAAG,UAAU,cAAc,EAAE,WAAW,KAAK,CAAC;EAE9C,IAAI,UAAoB,CAAC;EAEzB,IAAI;GACH,UAAU,GAAG,YAAY,YAAY;EACtC,QAAQ;GACP;EACD;EAEA,MAAM,MAAM,KAAK,IAAI;EAErB,KAAK,MAAM,SAAS,SAAS;GAC5B,MAAM,OAAO,KAAK,KAAK,cAAc,KAAK;GAE1C,IAAI;IACH,IAAI,MAAM,GAAG,SAAS,IAAI,EAAE,UAAU,kBACrC,GAAG,OAAO,MAAM;KAAE,WAAW;KAAM,OAAO;IAAK,CAAC;GAElD,QAAQ,CAER;EACD;CACD;AACD;;AAGA,SAAgB,gBAAgB,OAAwC;CACvE,OACC,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,0BAA0B;AAE/D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infracraft/pulumi",
3
- "version": "1.16.5",
3
+ "version": "1.16.6",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {