@infracraft/pulumi 1.13.1 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_chunk = require('../chunk-BVYJZCqc.cjs');
3
+ const require_git_guard = require('../git-guard.cjs');
3
4
  const require_stable_dir = require('../stable-dir.cjs');
4
5
  let _pulumi_command = require("@pulumi/command");
5
6
  _pulumi_command = require_chunk.__toESM(_pulumi_command, 1);
@@ -7,21 +8,87 @@ let _pulumi_pulumi = require("@pulumi/pulumi");
7
8
  _pulumi_pulumi = require_chunk.__toESM(_pulumi_pulumi, 1);
8
9
 
9
10
  //#region src/vercel/deploy.ts
11
+ /** mkdir-based lock serializing the brief window when `.vercelignore` is written. */
12
+ const LOCK_DIR = "/tmp/.vercel-upload-lock";
13
+ /** Where a committed `.vercelignore` is parked while the engine owns the file. */
14
+ const IGNORE_BACKUP = ".vercelignore.infracraft-bak";
10
15
  /**
11
- * Deploys a Vercel project via `vercel deploy --prod --yes` CLI.
16
+ * Patterns the engine ALWAYS writes to `.vercelignore`, independent of `excludePaths`.
17
+ *
18
+ * `gitGuard` hides the real `.git` by renaming it to {@link GUARD_DIR} (or a
19
+ * {@link LEGACY_GUARD_DIRS} name) for the duration of a deploy, leaving a stub `.git`.
20
+ * Vercel default-ignores `.git` but NOT the renamed guard dir, and — unlike the Railway
21
+ * CLI — Vercel never reads `.gitignore` (where gitGuard also records the guard dir).
22
+ * Without this exclusion the renamed real `.git`, with its 100MB+ pack files, is
23
+ * uploaded and trips Vercel's 100MB-per-file limit. The transient `.vercelignore*` (the
24
+ * generated file and its {@link IGNORE_BACKUP}) is excluded too, so the engine's own
25
+ * scratch files never ship.
26
+ */
27
+ const ALWAYS_IGNORE = [
28
+ require_git_guard.GUARD_DIR,
29
+ ...require_git_guard.LEGACY_GUARD_DIRS,
30
+ ".vercelignore*"
31
+ ];
32
+ /**
33
+ * Builds the newline-joined `.vercelignore` body for the deploy.
34
+ *
35
+ * Prepends {@link ALWAYS_IGNORE} (the guard dir and scratch files) to the consumer's
36
+ * `excludePaths`, applying the workspace-preserving `apps/<name>` rule so other apps'
37
+ * code is dropped while their `package.json` stays for the build's dependency graph.
38
+ *
39
+ * @param excludePaths Consumer-supplied paths to exclude (optional)
40
+ * @returns The ignore body, one pattern per line
41
+ * @example
42
+ * ```typescript
43
+ * buildVercelIgnore(["apps/mesh", "docs"]);
44
+ * // ".git-infracraft-pulumi-guard\n…\napps/mesh/**\n!apps/mesh/package.json\ndocs"
45
+ * ```
46
+ */
47
+ function buildVercelIgnore(excludePaths) {
48
+ const excludeLines = (excludePaths ?? []).map((entry) => entry.startsWith("apps/") ? `${entry}/**\n!${entry}/package.json` : entry);
49
+ return [...ALWAYS_IGNORE, ...excludeLines].join("\n");
50
+ }
51
+ /**
52
+ * Builds the `create` shell command that wraps `vercel deploy` in the ignore engine.
53
+ *
54
+ * The engine acquires a mkdir lock, parks any committed `.vercelignore` at
55
+ * {@link IGNORE_BACKUP}, writes the generated body (`printf` expands the encoded `\n`
56
+ * sequences), runs the deploy, and — in a background timer so a slow remote build does
57
+ * not block sibling deploys — restores the parked file and releases the lock. The
58
+ * deploy's exit status is captured into `EXIT` before `wait`, so the Pulumi resource
59
+ * fails when the deploy fails.
60
+ *
61
+ * @param excludePaths Consumer-supplied paths to exclude (optional)
62
+ * @returns A single-line shell command (no raw newlines)
63
+ */
64
+ function buildVercelDeployCommand(excludePaths) {
65
+ return `while ! mkdir ${LOCK_DIR} 2>/dev/null; do sleep 1; done; if [ -f .vercelignore ]; then mv .vercelignore ${IGNORE_BACKUP}; fi; printf '${buildVercelIgnore(excludePaths).replace(/\n/g, "\\n")}\\n' > .vercelignore; { sleep 8; ${`rm -f .vercelignore; [ -f ${IGNORE_BACKUP} ] && mv ${IGNORE_BACKUP} .vercelignore`}; rmdir ${LOCK_DIR} 2>/dev/null; } & vercel deploy --prod --yes; EXIT=$?; wait; exit $EXIT`;
66
+ }
67
+ /**
68
+ * Deploys a Vercel project via `vercel deploy --prod --yes`, generating a
69
+ * `.vercelignore` around the upload — the same exclude engine as `RailwayDeploy`.
12
70
  *
13
71
  * Triggers on source hash (computed from the app directory) and env content hash
14
72
  * (from `VercelVariable.contentHash`). When an env value changes — whether from
15
73
  * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes
16
74
  * and a redeploy is triggered.
17
75
  *
76
+ * The upload is wrapped in an ignore engine mirroring Railway's: a mkdir lock
77
+ * serializes the brief window in which `.vercelignore` must be consistent, the file is
78
+ * written (gitGuard guard dir + {@link VercelDeployArgs.excludePaths}), then a
79
+ * background timer restores the repository and releases the lock so concurrent deploys
80
+ * stream. Any committed `.vercelignore` is parked at {@link IGNORE_BACKUP} and restored
81
+ * afterward, so this never destroys a consumer's file — and because a committed
82
+ * `.vercelignore` is git-tracked, a hard-killed run is recoverable with
83
+ * `git checkout .vercelignore`.
84
+ *
18
85
  * @example
19
86
  * ```typescript
20
87
  * new VercelDeploy("nexus-deploy", {
21
88
  * projectId: vercelProject.id,
22
- * rootDirectory: "apps/nexus",
23
89
  * monorepoRoot,
24
- * env: { NEXT_PUBLIC_API_URL: meshUrl },
90
+ * triggers: [sourceHash, envHash],
91
+ * excludePaths: ["apps/mesh", "docs"],
25
92
  * }, { provider });
26
93
  * ```
27
94
  */
@@ -32,7 +99,7 @@ var VercelDeploy = class extends _pulumi_pulumi.ComponentResource {
32
99
  const projectId = project ? project.id : args.projectId;
33
100
  if (!projectId) throw new Error("VercelDeploy: either `args.projectId` or `opts.project` must be provided");
34
101
  const deployCmd = new _pulumi_command.local.Command(`${name}-deploy`, {
35
- create: "vercel deploy --prod --yes",
102
+ create: buildVercelDeployCommand(args.excludePaths),
36
103
  triggers: args.triggers,
37
104
  dir: require_stable_dir.stableDir(args.monorepoRoot),
38
105
  environment: {
@@ -48,4 +115,6 @@ var VercelDeploy = class extends _pulumi_pulumi.ComponentResource {
48
115
 
49
116
  //#endregion
50
117
  exports.VercelDeploy = VercelDeploy;
118
+ exports.buildVercelDeployCommand = buildVercelDeployCommand;
119
+ exports.buildVercelIgnore = buildVercelIgnore;
51
120
  //# sourceMappingURL=deploy.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.cjs","names":["pulumi","command","stableDir"],"sources":["../../src/vercel/deploy.ts"],"sourcesContent":["import * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { stableDir } from \"../stable-dir\";\nimport type { VercelProject } from \"./project\";\nimport type { VercelProvider } from \"./provider\";\n\n/** Options type for VercelDeploy — replaces Pulumi's native `provider` field. */\ntype VercelDeployOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n\n\t/**\n\t * VercelProject resource to source the project ID from.\n\t * When provided, `args.projectId` is optional and ignored if both are given.\n\t */\n\tproject?: VercelProject;\n};\n\n/** Args for VercelDeploy. */\nexport interface VercelDeployArgs {\n\t/**\n\t * Vercel project ID.\n\t * Required when `opts.project` is not provided.\n\t */\n\tprojectId?: pulumi.Input<string>;\n\n\t/**\n\t * Absolute path to the monorepo root (working directory for `vercel deploy`).\n\t * Stored relative to the Pulumi program directory so the command stays stable\n\t * across machines and CI (see {@link stableDir}).\n\t */\n\tmonorepoRoot: string;\n\n\t/** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n}\n\n/**\n * Deploys a Vercel project via `vercel deploy --prod --yes` CLI.\n *\n * Triggers on source hash (computed from the app directory) and env content hash\n * (from `VercelVariable.contentHash`). When an env value changes — whether from\n * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes\n * and a redeploy is triggered.\n *\n * @example\n * ```typescript\n * new VercelDeploy(\"nexus-deploy\", {\n * projectId: vercelProject.id,\n * rootDirectory: \"apps/nexus\",\n * monorepoRoot,\n * env: { NEXT_PUBLIC_API_URL: meshUrl },\n * }, { provider });\n * ```\n */\nexport class VercelDeploy extends pulumi.ComponentResource {\n\t/**\n\t * The production deployment URL printed by `vercel deploy` (its final stdout line).\n\t * Surfaces the deployed link in stack outputs after `pulumi up` without visiting the dashboard.\n\t */\n\tpublic readonly deploymentUrl: pulumi.Output<string>;\n\n\tconstructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions) {\n\t\tconst { provider, project, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst projectId = project\n\t\t\t? project.id\n\t\t\t: (args.projectId as pulumi.Input<string>);\n\n\t\tif (!projectId) {\n\t\t\tthrow new Error(\n\t\t\t\t\"VercelDeploy: either `args.projectId` or `opts.project` must be provided\",\n\t\t\t);\n\t\t}\n\n\t\tconst deployCmd = new command.local.Command(\n\t\t\t`${name}-deploy`,\n\t\t\t{\n\t\t\t\tcreate: \"vercel deploy --prod --yes\",\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\tdir: stableDir(args.monorepoRoot),\n\t\t\t\tenvironment: {\n\t\t\t\t\tVERCEL_TOKEN: provider.token,\n\t\t\t\t\tVERCEL_ORG_ID: provider.teamId,\n\t\t\t\t\tVERCEL_PROJECT_ID: projectId,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.deploymentUrl = deployCmd.stdout.apply(\n\t\t\t(out) => out.trim().split(\"\\n\").pop() ?? \"\",\n\t\t);\n\n\t\tthis.registerOutputs({ deploymentUrl: this.deploymentUrl });\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,IAAa,eAAb,cAAkCA,eAAO,kBAAkB;CAO1D,YAAY,MAAc,MAAwB,MAA2B;EAC5E,MAAM,EAAE,UAAU,SAAS,GAAG,eAAe;EAE7C,MAAM,4BAA4B,MAAM,CAAC,GAAG,UAAU;EAEtD,MAAM,YAAY,UACf,QAAQ,KACP,KAAK;EAET,IAAI,CAAC,WACJ,MAAM,IAAI,MACT,0EACD;EAGD,MAAM,YAAY,IAAIC,gBAAQ,MAAM,QACnC,GAAG,KAAK,UACR;GACC,QAAQ;GACR,UAAU,KAAK;GACf,KAAKC,6BAAU,KAAK,YAAY;GAChC,aAAa;IACZ,cAAc,SAAS;IACvB,eAAe,SAAS;IACxB,mBAAmB;GACpB;EACD,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,UAAU,OAAO,OACpC,QAAQ,IAAI,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,KAAK,EAC1C;EAEA,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,CAAC;CAC3D;AACD"}
1
+ {"version":3,"file":"deploy.cjs","names":["GUARD_DIR","LEGACY_GUARD_DIRS","pulumi","command","stableDir"],"sources":["../../src/vercel/deploy.ts"],"sourcesContent":["import * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { GUARD_DIR, LEGACY_GUARD_DIRS } from \"../git-guard\";\nimport { stableDir } from \"../stable-dir\";\nimport type { VercelProject } from \"./project\";\nimport type { VercelProvider } from \"./provider\";\n\n/** Options type for VercelDeploy — replaces Pulumi's native `provider` field. */\ntype VercelDeployOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n\n\t/**\n\t * VercelProject resource to source the project ID from.\n\t * When provided, `args.projectId` is optional and ignored if both are given.\n\t */\n\tproject?: VercelProject;\n};\n\n/** Args for VercelDeploy. */\nexport interface VercelDeployArgs {\n\t/**\n\t * Vercel project ID.\n\t * Required when `opts.project` is not provided.\n\t */\n\tprojectId?: pulumi.Input<string>;\n\n\t/**\n\t * Absolute path to the monorepo root (working directory for `vercel deploy`).\n\t * Stored relative to the Pulumi program directory so the command stays stable\n\t * across machines and CI (see {@link stableDir}).\n\t */\n\tmonorepoRoot: string;\n\n\t/** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\n\t/**\n\t * Paths to exclude from the upload via a generated `.vercelignore`, mirroring\n\t * `RailwayDeployArgs.excludePaths`. An `apps/<name>` entry excludes that app's\n\t * code but keeps its `package.json`, so the workspace graph still resolves during\n\t * the monorepo build. The gitGuard guard dir is always excluded regardless of this\n\t * list — see {@link buildVercelIgnore}.\n\t */\n\texcludePaths?: string[];\n}\n\n/** mkdir-based lock serializing the brief window when `.vercelignore` is written. */\nconst LOCK_DIR = \"/tmp/.vercel-upload-lock\";\n\n/** Where a committed `.vercelignore` is parked while the engine owns the file. */\nconst IGNORE_BACKUP = \".vercelignore.infracraft-bak\";\n\n/**\n * Patterns the engine ALWAYS writes to `.vercelignore`, independent of `excludePaths`.\n *\n * `gitGuard` hides the real `.git` by renaming it to {@link GUARD_DIR} (or a\n * {@link LEGACY_GUARD_DIRS} name) for the duration of a deploy, leaving a stub `.git`.\n * Vercel default-ignores `.git` but NOT the renamed guard dir, and — unlike the Railway\n * CLI — Vercel never reads `.gitignore` (where gitGuard also records the guard dir).\n * Without this exclusion the renamed real `.git`, with its 100MB+ pack files, is\n * uploaded and trips Vercel's 100MB-per-file limit. The transient `.vercelignore*` (the\n * generated file and its {@link IGNORE_BACKUP}) is excluded too, so the engine's own\n * scratch files never ship.\n */\nconst ALWAYS_IGNORE: readonly string[] = [\n\tGUARD_DIR,\n\t...LEGACY_GUARD_DIRS,\n\t\".vercelignore*\",\n];\n\n/**\n * Builds the newline-joined `.vercelignore` body for the deploy.\n *\n * Prepends {@link ALWAYS_IGNORE} (the guard dir and scratch files) to the consumer's\n * `excludePaths`, applying the workspace-preserving `apps/<name>` rule so other apps'\n * code is dropped while their `package.json` stays for the build's dependency graph.\n *\n * @param excludePaths Consumer-supplied paths to exclude (optional)\n * @returns The ignore body, one pattern per line\n * @example\n * ```typescript\n * buildVercelIgnore([\"apps/mesh\", \"docs\"]);\n * // \".git-infracraft-pulumi-guard\\n…\\napps/mesh/**\\n!apps/mesh/package.json\\ndocs\"\n * ```\n */\nexport function buildVercelIgnore(excludePaths?: string[]): string {\n\tconst excludeLines = (excludePaths ?? []).map((entry) =>\n\t\tentry.startsWith(\"apps/\") ? `${entry}/**\\n!${entry}/package.json` : entry,\n\t);\n\n\treturn [...ALWAYS_IGNORE, ...excludeLines].join(\"\\n\");\n}\n\n/**\n * Builds the `create` shell command that wraps `vercel deploy` in the ignore engine.\n *\n * The engine acquires a mkdir lock, parks any committed `.vercelignore` at\n * {@link IGNORE_BACKUP}, writes the generated body (`printf` expands the encoded `\\n`\n * sequences), runs the deploy, and — in a background timer so a slow remote build does\n * not block sibling deploys — restores the parked file and releases the lock. The\n * deploy's exit status is captured into `EXIT` before `wait`, so the Pulumi resource\n * fails when the deploy fails.\n *\n * @param excludePaths Consumer-supplied paths to exclude (optional)\n * @returns A single-line shell command (no raw newlines)\n */\nexport function buildVercelDeployCommand(excludePaths?: string[]): string {\n\tconst ignoreBody = buildVercelIgnore(excludePaths).replace(/\\n/g, \"\\\\n\");\n\n\tconst restore = `rm -f .vercelignore; [ -f ${IGNORE_BACKUP} ] && mv ${IGNORE_BACKUP} .vercelignore`;\n\n\treturn (\n\t\t`while ! mkdir ${LOCK_DIR} 2>/dev/null; do sleep 1; done; ` +\n\t\t`if [ -f .vercelignore ]; then mv .vercelignore ${IGNORE_BACKUP}; fi; ` +\n\t\t`printf '${ignoreBody}\\\\n' > .vercelignore; ` +\n\t\t`{ sleep 8; ${restore}; rmdir ${LOCK_DIR} 2>/dev/null; } & ` +\n\t\t`vercel deploy --prod --yes; EXIT=$?; wait; exit $EXIT`\n\t);\n}\n\n/**\n * Deploys a Vercel project via `vercel deploy --prod --yes`, generating a\n * `.vercelignore` around the upload — the same exclude engine as `RailwayDeploy`.\n *\n * Triggers on source hash (computed from the app directory) and env content hash\n * (from `VercelVariable.contentHash`). When an env value changes — whether from\n * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes\n * and a redeploy is triggered.\n *\n * The upload is wrapped in an ignore engine mirroring Railway's: a mkdir lock\n * serializes the brief window in which `.vercelignore` must be consistent, the file is\n * written (gitGuard guard dir + {@link VercelDeployArgs.excludePaths}), then a\n * background timer restores the repository and releases the lock so concurrent deploys\n * stream. Any committed `.vercelignore` is parked at {@link IGNORE_BACKUP} and restored\n * afterward, so this never destroys a consumer's file — and because a committed\n * `.vercelignore` is git-tracked, a hard-killed run is recoverable with\n * `git checkout .vercelignore`.\n *\n * @example\n * ```typescript\n * new VercelDeploy(\"nexus-deploy\", {\n * projectId: vercelProject.id,\n * monorepoRoot,\n * triggers: [sourceHash, envHash],\n * excludePaths: [\"apps/mesh\", \"docs\"],\n * }, { provider });\n * ```\n */\nexport class VercelDeploy extends pulumi.ComponentResource {\n\t/**\n\t * The production deployment URL printed by `vercel deploy` (its final stdout line).\n\t * Surfaces the deployed link in stack outputs after `pulumi up` without visiting the dashboard.\n\t */\n\tpublic readonly deploymentUrl: pulumi.Output<string>;\n\n\tconstructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions) {\n\t\tconst { provider, project, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst projectId = project\n\t\t\t? project.id\n\t\t\t: (args.projectId as pulumi.Input<string>);\n\n\t\tif (!projectId) {\n\t\t\tthrow new Error(\n\t\t\t\t\"VercelDeploy: either `args.projectId` or `opts.project` must be provided\",\n\t\t\t);\n\t\t}\n\n\t\tconst deployCmd = new command.local.Command(\n\t\t\t`${name}-deploy`,\n\t\t\t{\n\t\t\t\tcreate: buildVercelDeployCommand(args.excludePaths),\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\tdir: stableDir(args.monorepoRoot),\n\t\t\t\tenvironment: {\n\t\t\t\t\tVERCEL_TOKEN: provider.token,\n\t\t\t\t\tVERCEL_ORG_ID: provider.teamId,\n\t\t\t\t\tVERCEL_PROJECT_ID: projectId,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.deploymentUrl = deployCmd.stdout.apply(\n\t\t\t(out) => out.trim().split(\"\\n\").pop() ?? \"\",\n\t\t);\n\n\t\tthis.registerOutputs({ deploymentUrl: this.deploymentUrl });\n\t}\n}\n"],"mappings":";;;;;;;;;;;AAgDA,MAAM,WAAW;;AAGjB,MAAM,gBAAgB;;;;;;;;;;;;;AActB,MAAM,gBAAmC;CACxCA;CACA,GAAGC;CACH;AACD;;;;;;;;;;;;;;;;AAiBA,SAAgB,kBAAkB,cAAiC;CAClE,MAAM,gBAAgB,gBAAgB,CAAC,GAAG,KAAK,UAC9C,MAAM,WAAW,OAAO,IAAI,GAAG,MAAM,QAAQ,MAAM,iBAAiB,KACrE;CAEA,OAAO,CAAC,GAAG,eAAe,GAAG,YAAY,EAAE,KAAK,IAAI;AACrD;;;;;;;;;;;;;;AAeA,SAAgB,yBAAyB,cAAiC;CAKzE,OACC,iBAAiB,SAAS,iFACwB,cAAc,gBAN9C,kBAAkB,YAAY,EAAE,QAAQ,OAAO,KAO7C,EAAE,mCACR,6BAN8B,cAAc,WAAW,cAAc,gBAM7D,UAAU,SAAS;AAG3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,IAAa,eAAb,cAAkCC,eAAO,kBAAkB;CAO1D,YAAY,MAAc,MAAwB,MAA2B;EAC5E,MAAM,EAAE,UAAU,SAAS,GAAG,eAAe;EAE7C,MAAM,4BAA4B,MAAM,CAAC,GAAG,UAAU;EAEtD,MAAM,YAAY,UACf,QAAQ,KACP,KAAK;EAET,IAAI,CAAC,WACJ,MAAM,IAAI,MACT,0EACD;EAGD,MAAM,YAAY,IAAIC,gBAAQ,MAAM,QACnC,GAAG,KAAK,UACR;GACC,QAAQ,yBAAyB,KAAK,YAAY;GAClD,UAAU,KAAK;GACf,KAAKC,6BAAU,KAAK,YAAY;GAChC,aAAa;IACZ,cAAc,SAAS;IACvB,eAAe,SAAS;IACxB,mBAAmB;GACpB;EACD,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,UAAU,OAAO,OACpC,QAAQ,IAAI,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,KAAK,EAC1C;EAEA,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,CAAC;CAC3D;AACD"}
@@ -28,22 +28,70 @@ interface VercelDeployArgs {
28
28
  monorepoRoot: string;
29
29
  /** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */
30
30
  triggers: pulumi.Input<pulumi.Input<string>[]>;
31
+ /**
32
+ * Paths to exclude from the upload via a generated `.vercelignore`, mirroring
33
+ * `RailwayDeployArgs.excludePaths`. An `apps/<name>` entry excludes that app's
34
+ * code but keeps its `package.json`, so the workspace graph still resolves during
35
+ * the monorepo build. The gitGuard guard dir is always excluded regardless of this
36
+ * list — see {@link buildVercelIgnore}.
37
+ */
38
+ excludePaths?: string[];
31
39
  }
32
40
  /**
33
- * Deploys a Vercel project via `vercel deploy --prod --yes` CLI.
41
+ * Builds the newline-joined `.vercelignore` body for the deploy.
42
+ *
43
+ * Prepends {@link ALWAYS_IGNORE} (the guard dir and scratch files) to the consumer's
44
+ * `excludePaths`, applying the workspace-preserving `apps/<name>` rule so other apps'
45
+ * code is dropped while their `package.json` stays for the build's dependency graph.
46
+ *
47
+ * @param excludePaths Consumer-supplied paths to exclude (optional)
48
+ * @returns The ignore body, one pattern per line
49
+ * @example
50
+ * ```typescript
51
+ * buildVercelIgnore(["apps/mesh", "docs"]);
52
+ * // ".git-infracraft-pulumi-guard\n…\napps/mesh/**\n!apps/mesh/package.json\ndocs"
53
+ * ```
54
+ */
55
+ declare function buildVercelIgnore(excludePaths?: string[]): string;
56
+ /**
57
+ * Builds the `create` shell command that wraps `vercel deploy` in the ignore engine.
58
+ *
59
+ * The engine acquires a mkdir lock, parks any committed `.vercelignore` at
60
+ * {@link IGNORE_BACKUP}, writes the generated body (`printf` expands the encoded `\n`
61
+ * sequences), runs the deploy, and — in a background timer so a slow remote build does
62
+ * not block sibling deploys — restores the parked file and releases the lock. The
63
+ * deploy's exit status is captured into `EXIT` before `wait`, so the Pulumi resource
64
+ * fails when the deploy fails.
65
+ *
66
+ * @param excludePaths Consumer-supplied paths to exclude (optional)
67
+ * @returns A single-line shell command (no raw newlines)
68
+ */
69
+ declare function buildVercelDeployCommand(excludePaths?: string[]): string;
70
+ /**
71
+ * Deploys a Vercel project via `vercel deploy --prod --yes`, generating a
72
+ * `.vercelignore` around the upload — the same exclude engine as `RailwayDeploy`.
34
73
  *
35
74
  * Triggers on source hash (computed from the app directory) and env content hash
36
75
  * (from `VercelVariable.contentHash`). When an env value changes — whether from
37
76
  * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes
38
77
  * and a redeploy is triggered.
39
78
  *
79
+ * The upload is wrapped in an ignore engine mirroring Railway's: a mkdir lock
80
+ * serializes the brief window in which `.vercelignore` must be consistent, the file is
81
+ * written (gitGuard guard dir + {@link VercelDeployArgs.excludePaths}), then a
82
+ * background timer restores the repository and releases the lock so concurrent deploys
83
+ * stream. Any committed `.vercelignore` is parked at {@link IGNORE_BACKUP} and restored
84
+ * afterward, so this never destroys a consumer's file — and because a committed
85
+ * `.vercelignore` is git-tracked, a hard-killed run is recoverable with
86
+ * `git checkout .vercelignore`.
87
+ *
40
88
  * @example
41
89
  * ```typescript
42
90
  * new VercelDeploy("nexus-deploy", {
43
91
  * projectId: vercelProject.id,
44
- * rootDirectory: "apps/nexus",
45
92
  * monorepoRoot,
46
- * env: { NEXT_PUBLIC_API_URL: meshUrl },
93
+ * triggers: [sourceHash, envHash],
94
+ * excludePaths: ["apps/mesh", "docs"],
47
95
  * }, { provider });
48
96
  * ```
49
97
  */
@@ -56,5 +104,5 @@ declare class VercelDeploy extends pulumi.ComponentResource {
56
104
  constructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions);
57
105
  }
58
106
  //#endregion
59
- export { VercelDeploy, VercelDeployArgs };
107
+ export { VercelDeploy, VercelDeployArgs, buildVercelDeployCommand, buildVercelIgnore };
60
108
  //# sourceMappingURL=deploy.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/vercel/deploy.ts"],"mappings":";;;;;;;KAOK,mBAAA,GAAsB,IAAA,CAAK,MAAA,CAAO,wBAAA;uCAEtC,QAAA,EAAU,cAAA;EAFa;;;;EAQvB,OAAA,GAAU,aAAA;AAAA;;UAIM,gBAAA;EAZU;;;;EAiB1B,SAAA,GAAY,MAAA,CAAO,KAAA;EATnB;;;AAAuB;AAIxB;EAYC,YAAA;;EAGA,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;;;AAAK;AAqBpC;;;;cAAa,YAAA,SAAqB,MAAA,CAAO,iBAAA;EAOgB;;;;EAAA,SAFxC,aAAA,EAAe,MAAA,CAAO,MAAA;cAE1B,IAAA,UAAc,IAAA,EAAM,gBAAA,EAAkB,IAAA,EAAM,mBAAA;AAAA"}
1
+ {"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/vercel/deploy.ts"],"mappings":";;;;;;;KAQK,mBAAA,GAAsB,IAAA,CAAK,MAAA,CAAO,wBAAA;uCAEtC,QAAA,EAAU,cAAA;EAFa;;;;EAQvB,OAAA,GAAU,aAAA;AAAA;;UAIM,gBAAA;EAZU;;;;EAiB1B,SAAA,GAAY,MAAA,CAAO,KAAA;EATnB;;;AAAuB;AAIxB;EAYC,YAAA;;EAGA,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAAP;;;;;;;EASvB,YAAA;AAAA;;;;;;;AAAY;AA0Cb;;;;AAAyD;AAqBzD;;;iBArBgB,iBAAA,CAAkB,YAAuB;AAqBO;AA0ChE;;;;;;;;;;;;AA1CgE,iBAAhD,wBAAA,CAAyB,YAAuB;;;;;;;;;AAiDY;;;;;;;;;;;;;;;;;;;;cAP/D,YAAA,SAAqB,MAAA,CAAO,iBAAA;;;;;WAKxB,aAAA,EAAe,MAAA,CAAO,MAAA;cAE1B,IAAA,UAAc,IAAA,EAAM,gBAAA,EAAkB,IAAA,EAAM,mBAAA;AAAA"}
@@ -28,22 +28,70 @@ interface VercelDeployArgs {
28
28
  monorepoRoot: string;
29
29
  /** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */
30
30
  triggers: pulumi.Input<pulumi.Input<string>[]>;
31
+ /**
32
+ * Paths to exclude from the upload via a generated `.vercelignore`, mirroring
33
+ * `RailwayDeployArgs.excludePaths`. An `apps/<name>` entry excludes that app's
34
+ * code but keeps its `package.json`, so the workspace graph still resolves during
35
+ * the monorepo build. The gitGuard guard dir is always excluded regardless of this
36
+ * list — see {@link buildVercelIgnore}.
37
+ */
38
+ excludePaths?: string[];
31
39
  }
32
40
  /**
33
- * Deploys a Vercel project via `vercel deploy --prod --yes` CLI.
41
+ * Builds the newline-joined `.vercelignore` body for the deploy.
42
+ *
43
+ * Prepends {@link ALWAYS_IGNORE} (the guard dir and scratch files) to the consumer's
44
+ * `excludePaths`, applying the workspace-preserving `apps/<name>` rule so other apps'
45
+ * code is dropped while their `package.json` stays for the build's dependency graph.
46
+ *
47
+ * @param excludePaths Consumer-supplied paths to exclude (optional)
48
+ * @returns The ignore body, one pattern per line
49
+ * @example
50
+ * ```typescript
51
+ * buildVercelIgnore(["apps/mesh", "docs"]);
52
+ * // ".git-infracraft-pulumi-guard\n…\napps/mesh/**\n!apps/mesh/package.json\ndocs"
53
+ * ```
54
+ */
55
+ declare function buildVercelIgnore(excludePaths?: string[]): string;
56
+ /**
57
+ * Builds the `create` shell command that wraps `vercel deploy` in the ignore engine.
58
+ *
59
+ * The engine acquires a mkdir lock, parks any committed `.vercelignore` at
60
+ * {@link IGNORE_BACKUP}, writes the generated body (`printf` expands the encoded `\n`
61
+ * sequences), runs the deploy, and — in a background timer so a slow remote build does
62
+ * not block sibling deploys — restores the parked file and releases the lock. The
63
+ * deploy's exit status is captured into `EXIT` before `wait`, so the Pulumi resource
64
+ * fails when the deploy fails.
65
+ *
66
+ * @param excludePaths Consumer-supplied paths to exclude (optional)
67
+ * @returns A single-line shell command (no raw newlines)
68
+ */
69
+ declare function buildVercelDeployCommand(excludePaths?: string[]): string;
70
+ /**
71
+ * Deploys a Vercel project via `vercel deploy --prod --yes`, generating a
72
+ * `.vercelignore` around the upload — the same exclude engine as `RailwayDeploy`.
34
73
  *
35
74
  * Triggers on source hash (computed from the app directory) and env content hash
36
75
  * (from `VercelVariable.contentHash`). When an env value changes — whether from
37
76
  * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes
38
77
  * and a redeploy is triggered.
39
78
  *
79
+ * The upload is wrapped in an ignore engine mirroring Railway's: a mkdir lock
80
+ * serializes the brief window in which `.vercelignore` must be consistent, the file is
81
+ * written (gitGuard guard dir + {@link VercelDeployArgs.excludePaths}), then a
82
+ * background timer restores the repository and releases the lock so concurrent deploys
83
+ * stream. Any committed `.vercelignore` is parked at {@link IGNORE_BACKUP} and restored
84
+ * afterward, so this never destroys a consumer's file — and because a committed
85
+ * `.vercelignore` is git-tracked, a hard-killed run is recoverable with
86
+ * `git checkout .vercelignore`.
87
+ *
40
88
  * @example
41
89
  * ```typescript
42
90
  * new VercelDeploy("nexus-deploy", {
43
91
  * projectId: vercelProject.id,
44
- * rootDirectory: "apps/nexus",
45
92
  * monorepoRoot,
46
- * env: { NEXT_PUBLIC_API_URL: meshUrl },
93
+ * triggers: [sourceHash, envHash],
94
+ * excludePaths: ["apps/mesh", "docs"],
47
95
  * }, { provider });
48
96
  * ```
49
97
  */
@@ -56,5 +104,5 @@ declare class VercelDeploy extends pulumi.ComponentResource {
56
104
  constructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions);
57
105
  }
58
106
  //#endregion
59
- export { VercelDeploy, VercelDeployArgs };
107
+ export { VercelDeploy, VercelDeployArgs, buildVercelDeployCommand, buildVercelIgnore };
60
108
  //# sourceMappingURL=deploy.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/vercel/deploy.ts"],"mappings":";;;;;;;KAOK,mBAAA,GAAsB,IAAA,CAAK,MAAA,CAAO,wBAAA;uCAEtC,QAAA,EAAU,cAAA;EAFa;;;;EAQvB,OAAA,GAAU,aAAA;AAAA;;UAIM,gBAAA;EAZU;;;;EAiB1B,SAAA,GAAY,MAAA,CAAO,KAAA;EATnB;;;AAAuB;AAIxB;EAYC,YAAA;;EAGA,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;;;;;AAAK;AAqBpC;;;;cAAa,YAAA,SAAqB,MAAA,CAAO,iBAAA;EAOgB;;;;EAAA,SAFxC,aAAA,EAAe,MAAA,CAAO,MAAA;cAE1B,IAAA,UAAc,IAAA,EAAM,gBAAA,EAAkB,IAAA,EAAM,mBAAA;AAAA"}
1
+ {"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/vercel/deploy.ts"],"mappings":";;;;;;;KAQK,mBAAA,GAAsB,IAAA,CAAK,MAAA,CAAO,wBAAA;uCAEtC,QAAA,EAAU,cAAA;EAFa;;;;EAQvB,OAAA,GAAU,aAAA;AAAA;;UAIM,gBAAA;EAZU;;;;EAiB1B,SAAA,GAAY,MAAA,CAAO,KAAA;EATnB;;;AAAuB;AAIxB;EAYC,YAAA;;EAGA,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAAP;;;;;;;EASvB,YAAA;AAAA;;;;;;;AAAY;AA0Cb;;;;AAAyD;AAqBzD;;;iBArBgB,iBAAA,CAAkB,YAAuB;AAqBO;AA0ChE;;;;;;;;;;;;AA1CgE,iBAAhD,wBAAA,CAAyB,YAAuB;;;;;;;;;AAiDY;;;;;;;;;;;;;;;;;;;;cAP/D,YAAA,SAAqB,MAAA,CAAO,iBAAA;;;;;WAKxB,aAAA,EAAe,MAAA,CAAO,MAAA;cAE1B,IAAA,UAAc,IAAA,EAAM,gBAAA,EAAkB,IAAA,EAAM,mBAAA;AAAA"}
@@ -1,24 +1,91 @@
1
1
  import { t as __name } from "../chunk-OPjESj5l.mjs";
2
+ import { GUARD_DIR, LEGACY_GUARD_DIRS } from "../git-guard.mjs";
2
3
  import { stableDir } from "../stable-dir.mjs";
3
4
  import * as command from "@pulumi/command";
4
5
  import * as pulumi from "@pulumi/pulumi";
5
6
 
6
7
  //#region src/vercel/deploy.ts
8
+ /** mkdir-based lock serializing the brief window when `.vercelignore` is written. */
9
+ const LOCK_DIR = "/tmp/.vercel-upload-lock";
10
+ /** Where a committed `.vercelignore` is parked while the engine owns the file. */
11
+ const IGNORE_BACKUP = ".vercelignore.infracraft-bak";
7
12
  /**
8
- * Deploys a Vercel project via `vercel deploy --prod --yes` CLI.
13
+ * Patterns the engine ALWAYS writes to `.vercelignore`, independent of `excludePaths`.
14
+ *
15
+ * `gitGuard` hides the real `.git` by renaming it to {@link GUARD_DIR} (or a
16
+ * {@link LEGACY_GUARD_DIRS} name) for the duration of a deploy, leaving a stub `.git`.
17
+ * Vercel default-ignores `.git` but NOT the renamed guard dir, and — unlike the Railway
18
+ * CLI — Vercel never reads `.gitignore` (where gitGuard also records the guard dir).
19
+ * Without this exclusion the renamed real `.git`, with its 100MB+ pack files, is
20
+ * uploaded and trips Vercel's 100MB-per-file limit. The transient `.vercelignore*` (the
21
+ * generated file and its {@link IGNORE_BACKUP}) is excluded too, so the engine's own
22
+ * scratch files never ship.
23
+ */
24
+ const ALWAYS_IGNORE = [
25
+ GUARD_DIR,
26
+ ...LEGACY_GUARD_DIRS,
27
+ ".vercelignore*"
28
+ ];
29
+ /**
30
+ * Builds the newline-joined `.vercelignore` body for the deploy.
31
+ *
32
+ * Prepends {@link ALWAYS_IGNORE} (the guard dir and scratch files) to the consumer's
33
+ * `excludePaths`, applying the workspace-preserving `apps/<name>` rule so other apps'
34
+ * code is dropped while their `package.json` stays for the build's dependency graph.
35
+ *
36
+ * @param excludePaths Consumer-supplied paths to exclude (optional)
37
+ * @returns The ignore body, one pattern per line
38
+ * @example
39
+ * ```typescript
40
+ * buildVercelIgnore(["apps/mesh", "docs"]);
41
+ * // ".git-infracraft-pulumi-guard\n…\napps/mesh/**\n!apps/mesh/package.json\ndocs"
42
+ * ```
43
+ */
44
+ function buildVercelIgnore(excludePaths) {
45
+ const excludeLines = (excludePaths ?? []).map((entry) => entry.startsWith("apps/") ? `${entry}/**\n!${entry}/package.json` : entry);
46
+ return [...ALWAYS_IGNORE, ...excludeLines].join("\n");
47
+ }
48
+ /**
49
+ * Builds the `create` shell command that wraps `vercel deploy` in the ignore engine.
50
+ *
51
+ * The engine acquires a mkdir lock, parks any committed `.vercelignore` at
52
+ * {@link IGNORE_BACKUP}, writes the generated body (`printf` expands the encoded `\n`
53
+ * sequences), runs the deploy, and — in a background timer so a slow remote build does
54
+ * not block sibling deploys — restores the parked file and releases the lock. The
55
+ * deploy's exit status is captured into `EXIT` before `wait`, so the Pulumi resource
56
+ * fails when the deploy fails.
57
+ *
58
+ * @param excludePaths Consumer-supplied paths to exclude (optional)
59
+ * @returns A single-line shell command (no raw newlines)
60
+ */
61
+ function buildVercelDeployCommand(excludePaths) {
62
+ return `while ! mkdir ${LOCK_DIR} 2>/dev/null; do sleep 1; done; if [ -f .vercelignore ]; then mv .vercelignore ${IGNORE_BACKUP}; fi; printf '${buildVercelIgnore(excludePaths).replace(/\n/g, "\\n")}\\n' > .vercelignore; { sleep 8; ${`rm -f .vercelignore; [ -f ${IGNORE_BACKUP} ] && mv ${IGNORE_BACKUP} .vercelignore`}; rmdir ${LOCK_DIR} 2>/dev/null; } & vercel deploy --prod --yes; EXIT=$?; wait; exit $EXIT`;
63
+ }
64
+ /**
65
+ * Deploys a Vercel project via `vercel deploy --prod --yes`, generating a
66
+ * `.vercelignore` around the upload — the same exclude engine as `RailwayDeploy`.
9
67
  *
10
68
  * Triggers on source hash (computed from the app directory) and env content hash
11
69
  * (from `VercelVariable.contentHash`). When an env value changes — whether from
12
70
  * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes
13
71
  * and a redeploy is triggered.
14
72
  *
73
+ * The upload is wrapped in an ignore engine mirroring Railway's: a mkdir lock
74
+ * serializes the brief window in which `.vercelignore` must be consistent, the file is
75
+ * written (gitGuard guard dir + {@link VercelDeployArgs.excludePaths}), then a
76
+ * background timer restores the repository and releases the lock so concurrent deploys
77
+ * stream. Any committed `.vercelignore` is parked at {@link IGNORE_BACKUP} and restored
78
+ * afterward, so this never destroys a consumer's file — and because a committed
79
+ * `.vercelignore` is git-tracked, a hard-killed run is recoverable with
80
+ * `git checkout .vercelignore`.
81
+ *
15
82
  * @example
16
83
  * ```typescript
17
84
  * new VercelDeploy("nexus-deploy", {
18
85
  * projectId: vercelProject.id,
19
- * rootDirectory: "apps/nexus",
20
86
  * monorepoRoot,
21
- * env: { NEXT_PUBLIC_API_URL: meshUrl },
87
+ * triggers: [sourceHash, envHash],
88
+ * excludePaths: ["apps/mesh", "docs"],
22
89
  * }, { provider });
23
90
  * ```
24
91
  */
@@ -29,7 +96,7 @@ var VercelDeploy = class extends pulumi.ComponentResource {
29
96
  const projectId = project ? project.id : args.projectId;
30
97
  if (!projectId) throw new Error("VercelDeploy: either `args.projectId` or `opts.project` must be provided");
31
98
  const deployCmd = new command.local.Command(`${name}-deploy`, {
32
- create: "vercel deploy --prod --yes",
99
+ create: buildVercelDeployCommand(args.excludePaths),
33
100
  triggers: args.triggers,
34
101
  dir: stableDir(args.monorepoRoot),
35
102
  environment: {
@@ -44,5 +111,5 @@ var VercelDeploy = class extends pulumi.ComponentResource {
44
111
  };
45
112
 
46
113
  //#endregion
47
- export { VercelDeploy };
114
+ export { VercelDeploy, buildVercelDeployCommand, buildVercelIgnore };
48
115
  //# sourceMappingURL=deploy.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.mjs","names":[],"sources":["../../src/vercel/deploy.ts"],"sourcesContent":["import * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { stableDir } from \"../stable-dir\";\nimport type { VercelProject } from \"./project\";\nimport type { VercelProvider } from \"./provider\";\n\n/** Options type for VercelDeploy — replaces Pulumi's native `provider` field. */\ntype VercelDeployOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n\n\t/**\n\t * VercelProject resource to source the project ID from.\n\t * When provided, `args.projectId` is optional and ignored if both are given.\n\t */\n\tproject?: VercelProject;\n};\n\n/** Args for VercelDeploy. */\nexport interface VercelDeployArgs {\n\t/**\n\t * Vercel project ID.\n\t * Required when `opts.project` is not provided.\n\t */\n\tprojectId?: pulumi.Input<string>;\n\n\t/**\n\t * Absolute path to the monorepo root (working directory for `vercel deploy`).\n\t * Stored relative to the Pulumi program directory so the command stays stable\n\t * across machines and CI (see {@link stableDir}).\n\t */\n\tmonorepoRoot: string;\n\n\t/** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n}\n\n/**\n * Deploys a Vercel project via `vercel deploy --prod --yes` CLI.\n *\n * Triggers on source hash (computed from the app directory) and env content hash\n * (from `VercelVariable.contentHash`). When an env value changes — whether from\n * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes\n * and a redeploy is triggered.\n *\n * @example\n * ```typescript\n * new VercelDeploy(\"nexus-deploy\", {\n * projectId: vercelProject.id,\n * rootDirectory: \"apps/nexus\",\n * monorepoRoot,\n * env: { NEXT_PUBLIC_API_URL: meshUrl },\n * }, { provider });\n * ```\n */\nexport class VercelDeploy extends pulumi.ComponentResource {\n\t/**\n\t * The production deployment URL printed by `vercel deploy` (its final stdout line).\n\t * Surfaces the deployed link in stack outputs after `pulumi up` without visiting the dashboard.\n\t */\n\tpublic readonly deploymentUrl: pulumi.Output<string>;\n\n\tconstructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions) {\n\t\tconst { provider, project, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst projectId = project\n\t\t\t? project.id\n\t\t\t: (args.projectId as pulumi.Input<string>);\n\n\t\tif (!projectId) {\n\t\t\tthrow new Error(\n\t\t\t\t\"VercelDeploy: either `args.projectId` or `opts.project` must be provided\",\n\t\t\t);\n\t\t}\n\n\t\tconst deployCmd = new command.local.Command(\n\t\t\t`${name}-deploy`,\n\t\t\t{\n\t\t\t\tcreate: \"vercel deploy --prod --yes\",\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\tdir: stableDir(args.monorepoRoot),\n\t\t\t\tenvironment: {\n\t\t\t\t\tVERCEL_TOKEN: provider.token,\n\t\t\t\t\tVERCEL_ORG_ID: provider.teamId,\n\t\t\t\t\tVERCEL_PROJECT_ID: projectId,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.deploymentUrl = deployCmd.stdout.apply(\n\t\t\t(out) => out.trim().split(\"\\n\").pop() ?? \"\",\n\t\t);\n\n\t\tthis.registerOutputs({ deploymentUrl: this.deploymentUrl });\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAuDA,IAAa,eAAb,cAAkC,OAAO,kBAAkB;CAO1D,YAAY,MAAc,MAAwB,MAA2B;EAC5E,MAAM,EAAE,UAAU,SAAS,GAAG,eAAe;EAE7C,MAAM,4BAA4B,MAAM,CAAC,GAAG,UAAU;EAEtD,MAAM,YAAY,UACf,QAAQ,KACP,KAAK;EAET,IAAI,CAAC,WACJ,MAAM,IAAI,MACT,0EACD;EAGD,MAAM,YAAY,IAAI,QAAQ,MAAM,QACnC,GAAG,KAAK,UACR;GACC,QAAQ;GACR,UAAU,KAAK;GACf,KAAK,UAAU,KAAK,YAAY;GAChC,aAAa;IACZ,cAAc,SAAS;IACvB,eAAe,SAAS;IACxB,mBAAmB;GACpB;EACD,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,UAAU,OAAO,OACpC,QAAQ,IAAI,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,KAAK,EAC1C;EAEA,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,CAAC;CAC3D;AACD"}
1
+ {"version":3,"file":"deploy.mjs","names":[],"sources":["../../src/vercel/deploy.ts"],"sourcesContent":["import * as command from \"@pulumi/command\";\nimport * as pulumi from \"@pulumi/pulumi\";\nimport { GUARD_DIR, LEGACY_GUARD_DIRS } from \"../git-guard\";\nimport { stableDir } from \"../stable-dir\";\nimport type { VercelProject } from \"./project\";\nimport type { VercelProvider } from \"./provider\";\n\n/** Options type for VercelDeploy — replaces Pulumi's native `provider` field. */\ntype VercelDeployOptions = Omit<pulumi.ComponentResourceOptions, \"provider\"> & {\n\t/** Vercel authentication context. */\n\tprovider: VercelProvider;\n\n\t/**\n\t * VercelProject resource to source the project ID from.\n\t * When provided, `args.projectId` is optional and ignored if both are given.\n\t */\n\tproject?: VercelProject;\n};\n\n/** Args for VercelDeploy. */\nexport interface VercelDeployArgs {\n\t/**\n\t * Vercel project ID.\n\t * Required when `opts.project` is not provided.\n\t */\n\tprojectId?: pulumi.Input<string>;\n\n\t/**\n\t * Absolute path to the monorepo root (working directory for `vercel deploy`).\n\t * Stored relative to the Pulumi program directory so the command stays stable\n\t * across machines and CI (see {@link stableDir}).\n\t */\n\tmonorepoRoot: string;\n\n\t/** Values that trigger a redeploy when changed (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\n\t/**\n\t * Paths to exclude from the upload via a generated `.vercelignore`, mirroring\n\t * `RailwayDeployArgs.excludePaths`. An `apps/<name>` entry excludes that app's\n\t * code but keeps its `package.json`, so the workspace graph still resolves during\n\t * the monorepo build. The gitGuard guard dir is always excluded regardless of this\n\t * list — see {@link buildVercelIgnore}.\n\t */\n\texcludePaths?: string[];\n}\n\n/** mkdir-based lock serializing the brief window when `.vercelignore` is written. */\nconst LOCK_DIR = \"/tmp/.vercel-upload-lock\";\n\n/** Where a committed `.vercelignore` is parked while the engine owns the file. */\nconst IGNORE_BACKUP = \".vercelignore.infracraft-bak\";\n\n/**\n * Patterns the engine ALWAYS writes to `.vercelignore`, independent of `excludePaths`.\n *\n * `gitGuard` hides the real `.git` by renaming it to {@link GUARD_DIR} (or a\n * {@link LEGACY_GUARD_DIRS} name) for the duration of a deploy, leaving a stub `.git`.\n * Vercel default-ignores `.git` but NOT the renamed guard dir, and — unlike the Railway\n * CLI — Vercel never reads `.gitignore` (where gitGuard also records the guard dir).\n * Without this exclusion the renamed real `.git`, with its 100MB+ pack files, is\n * uploaded and trips Vercel's 100MB-per-file limit. The transient `.vercelignore*` (the\n * generated file and its {@link IGNORE_BACKUP}) is excluded too, so the engine's own\n * scratch files never ship.\n */\nconst ALWAYS_IGNORE: readonly string[] = [\n\tGUARD_DIR,\n\t...LEGACY_GUARD_DIRS,\n\t\".vercelignore*\",\n];\n\n/**\n * Builds the newline-joined `.vercelignore` body for the deploy.\n *\n * Prepends {@link ALWAYS_IGNORE} (the guard dir and scratch files) to the consumer's\n * `excludePaths`, applying the workspace-preserving `apps/<name>` rule so other apps'\n * code is dropped while their `package.json` stays for the build's dependency graph.\n *\n * @param excludePaths Consumer-supplied paths to exclude (optional)\n * @returns The ignore body, one pattern per line\n * @example\n * ```typescript\n * buildVercelIgnore([\"apps/mesh\", \"docs\"]);\n * // \".git-infracraft-pulumi-guard\\n…\\napps/mesh/**\\n!apps/mesh/package.json\\ndocs\"\n * ```\n */\nexport function buildVercelIgnore(excludePaths?: string[]): string {\n\tconst excludeLines = (excludePaths ?? []).map((entry) =>\n\t\tentry.startsWith(\"apps/\") ? `${entry}/**\\n!${entry}/package.json` : entry,\n\t);\n\n\treturn [...ALWAYS_IGNORE, ...excludeLines].join(\"\\n\");\n}\n\n/**\n * Builds the `create` shell command that wraps `vercel deploy` in the ignore engine.\n *\n * The engine acquires a mkdir lock, parks any committed `.vercelignore` at\n * {@link IGNORE_BACKUP}, writes the generated body (`printf` expands the encoded `\\n`\n * sequences), runs the deploy, and — in a background timer so a slow remote build does\n * not block sibling deploys — restores the parked file and releases the lock. The\n * deploy's exit status is captured into `EXIT` before `wait`, so the Pulumi resource\n * fails when the deploy fails.\n *\n * @param excludePaths Consumer-supplied paths to exclude (optional)\n * @returns A single-line shell command (no raw newlines)\n */\nexport function buildVercelDeployCommand(excludePaths?: string[]): string {\n\tconst ignoreBody = buildVercelIgnore(excludePaths).replace(/\\n/g, \"\\\\n\");\n\n\tconst restore = `rm -f .vercelignore; [ -f ${IGNORE_BACKUP} ] && mv ${IGNORE_BACKUP} .vercelignore`;\n\n\treturn (\n\t\t`while ! mkdir ${LOCK_DIR} 2>/dev/null; do sleep 1; done; ` +\n\t\t`if [ -f .vercelignore ]; then mv .vercelignore ${IGNORE_BACKUP}; fi; ` +\n\t\t`printf '${ignoreBody}\\\\n' > .vercelignore; ` +\n\t\t`{ sleep 8; ${restore}; rmdir ${LOCK_DIR} 2>/dev/null; } & ` +\n\t\t`vercel deploy --prod --yes; EXIT=$?; wait; exit $EXIT`\n\t);\n}\n\n/**\n * Deploys a Vercel project via `vercel deploy --prod --yes`, generating a\n * `.vercelignore` around the upload — the same exclude engine as `RailwayDeploy`.\n *\n * Triggers on source hash (computed from the app directory) and env content hash\n * (from `VercelVariable.contentHash`). When an env value changes — whether from\n * code, a new mesh URL, or a drift fix after `pulumi refresh` — the hash changes\n * and a redeploy is triggered.\n *\n * The upload is wrapped in an ignore engine mirroring Railway's: a mkdir lock\n * serializes the brief window in which `.vercelignore` must be consistent, the file is\n * written (gitGuard guard dir + {@link VercelDeployArgs.excludePaths}), then a\n * background timer restores the repository and releases the lock so concurrent deploys\n * stream. Any committed `.vercelignore` is parked at {@link IGNORE_BACKUP} and restored\n * afterward, so this never destroys a consumer's file — and because a committed\n * `.vercelignore` is git-tracked, a hard-killed run is recoverable with\n * `git checkout .vercelignore`.\n *\n * @example\n * ```typescript\n * new VercelDeploy(\"nexus-deploy\", {\n * projectId: vercelProject.id,\n * monorepoRoot,\n * triggers: [sourceHash, envHash],\n * excludePaths: [\"apps/mesh\", \"docs\"],\n * }, { provider });\n * ```\n */\nexport class VercelDeploy extends pulumi.ComponentResource {\n\t/**\n\t * The production deployment URL printed by `vercel deploy` (its final stdout line).\n\t * Surfaces the deployed link in stack outputs after `pulumi up` without visiting the dashboard.\n\t */\n\tpublic readonly deploymentUrl: pulumi.Output<string>;\n\n\tconstructor(name: string, args: VercelDeployArgs, opts: VercelDeployOptions) {\n\t\tconst { provider, project, ...pulumiOpts } = opts;\n\n\t\tsuper(\"infracraft:vercel:Deploy\", name, {}, pulumiOpts);\n\n\t\tconst projectId = project\n\t\t\t? project.id\n\t\t\t: (args.projectId as pulumi.Input<string>);\n\n\t\tif (!projectId) {\n\t\t\tthrow new Error(\n\t\t\t\t\"VercelDeploy: either `args.projectId` or `opts.project` must be provided\",\n\t\t\t);\n\t\t}\n\n\t\tconst deployCmd = new command.local.Command(\n\t\t\t`${name}-deploy`,\n\t\t\t{\n\t\t\t\tcreate: buildVercelDeployCommand(args.excludePaths),\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\tdir: stableDir(args.monorepoRoot),\n\t\t\t\tenvironment: {\n\t\t\t\t\tVERCEL_TOKEN: provider.token,\n\t\t\t\t\tVERCEL_ORG_ID: provider.teamId,\n\t\t\t\t\tVERCEL_PROJECT_ID: projectId,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ parent: this },\n\t\t);\n\n\t\tthis.deploymentUrl = deployCmd.stdout.apply(\n\t\t\t(out) => out.trim().split(\"\\n\").pop() ?? \"\",\n\t\t);\n\n\t\tthis.registerOutputs({ deploymentUrl: this.deploymentUrl });\n\t}\n}\n"],"mappings":";;;;;;;;AAgDA,MAAM,WAAW;;AAGjB,MAAM,gBAAgB;;;;;;;;;;;;;AActB,MAAM,gBAAmC;CACxC;CACA,GAAG;CACH;AACD;;;;;;;;;;;;;;;;AAiBA,SAAgB,kBAAkB,cAAiC;CAClE,MAAM,gBAAgB,gBAAgB,CAAC,GAAG,KAAK,UAC9C,MAAM,WAAW,OAAO,IAAI,GAAG,MAAM,QAAQ,MAAM,iBAAiB,KACrE;CAEA,OAAO,CAAC,GAAG,eAAe,GAAG,YAAY,EAAE,KAAK,IAAI;AACrD;;;;;;;;;;;;;;AAeA,SAAgB,yBAAyB,cAAiC;CAKzE,OACC,iBAAiB,SAAS,iFACwB,cAAc,gBAN9C,kBAAkB,YAAY,EAAE,QAAQ,OAAO,KAO7C,EAAE,mCACR,6BAN8B,cAAc,WAAW,cAAc,gBAM7D,UAAU,SAAS;AAG3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,IAAa,eAAb,cAAkC,OAAO,kBAAkB;CAO1D,YAAY,MAAc,MAAwB,MAA2B;EAC5E,MAAM,EAAE,UAAU,SAAS,GAAG,eAAe;EAE7C,MAAM,4BAA4B,MAAM,CAAC,GAAG,UAAU;EAEtD,MAAM,YAAY,UACf,QAAQ,KACP,KAAK;EAET,IAAI,CAAC,WACJ,MAAM,IAAI,MACT,0EACD;EAGD,MAAM,YAAY,IAAI,QAAQ,MAAM,QACnC,GAAG,KAAK,UACR;GACC,QAAQ,yBAAyB,KAAK,YAAY;GAClD,UAAU,KAAK;GACf,KAAK,UAAU,KAAK,YAAY;GAChC,aAAa;IACZ,cAAc,SAAS;IACvB,eAAe,SAAS;IACxB,mBAAmB;GACpB;EACD,GACA,EAAE,QAAQ,KAAK,CAChB;EAEA,KAAK,gBAAgB,UAAU,OAAO,OACpC,QAAQ,IAAI,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,KAAK,EAC1C;EAEA,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,CAAC;CAC3D;AACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infracraft/pulumi",
3
- "version": "1.13.1",
3
+ "version": "1.14.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {