@infracraft/pulumi 1.16.8 β†’ 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AndrΓ© "Dezzy" Victor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,429 @@
1
+ <p align="center">
2
+ <b>infracraft</b>
3
+ <br />
4
+ <i>Pulumi providers for platforms that don't have one.</i>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@infracraft/pulumi"><img src="https://img.shields.io/npm/v/@infracraft/pulumi?style=flat&colorA=18181b&colorB=18181b" alt="npm" /></a>
9
+ <a href="https://www.npmjs.com/package/@infracraft/pulumi"><img src="https://img.shields.io/npm/dm/@infracraft/pulumi?style=flat&colorA=18181b&colorB=18181b" alt="downloads" /></a>
10
+ <a href="https://github.com/andredezzy/infracraft/blob/main/LICENSE"><img src="https://img.shields.io/github/license/andredezzy/infracraft?style=flat&colorA=18181b&colorB=18181b" alt="license" /></a>
11
+ </p>
12
+
13
+ ---
14
+
15
+ Native Pulumi providers with adopt-or-create semantics and deploy orchestration. No Terraform bridge.
16
+
17
+ ## Providers
18
+
19
+ | | Provider | Import | What it does |
20
+ |---|---|---|---|
21
+ | πŸš‚ | **Railway** | `@infracraft/pulumi/railway` | The only Pulumi provider for Railway. Projects, environments, services, variables, volumes, domains, deploys. |
22
+ | 🐘 | **Neon** | `@infracraft/pulumi/neon` | Adopt-or-create layer for Neon Postgres. Projects, branches, endpoints, roles, databases. |
23
+ | β–² | **Vercel** | `@infracraft/pulumi/vercel` | Projects with adopt-or-create, deploy orchestration, marketplace resources, and sensitive env var drift detection. |
24
+ | 🎯 | **Fly.io** | `@infracraft/pulumi/fly` | App, Secret, Volume, Certificate, IP, and Deploy resources via the Machines REST API and Fly GraphQL API. |
25
+ | πŸ€– | **Agents** | `@infracraft/pulumi/agents` | Emit operating hints for AI coding agents working on the stack. |
26
+ | #️⃣ | **Hash** | `@infracraft/pulumi/hash` | Deterministic directory/env-var/app hashing for deploy triggers. |
27
+ | πŸ“¦ | **Sandbox** | `@infracraft/pulumi/sandbox` | Isolated `/tmp` working copies for CLI deploys β€” opt in via `dependsOn`. |
28
+ | πŸ”’ | **Git Guard** | `@infracraft/pulumi/git-guard` | Metadata-free `.git` stub for deploys β€” keeps commit SHAs and author emails off third-party platforms. |
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ npm i @infracraft/pulumi
34
+ # or
35
+ bun add @infracraft/pulumi
36
+ ```
37
+
38
+ Peer dependencies: `@pulumi/pulumi` ^3, `@pulumi/command` ^1 (optional)
39
+
40
+ ## Railway
41
+
42
+ ```typescript
43
+ import {
44
+ RailwayProvider,
45
+ RailwayProject,
46
+ RailwayEnvironment,
47
+ RailwayService,
48
+ RailwayBuilder,
49
+ RailwayVariable,
50
+ RailwayProjectToken,
51
+ RailwayDeploy,
52
+ } from "@infracraft/pulumi/railway"
53
+ import { hash } from "@infracraft/pulumi/hash"
54
+
55
+ const provider = new RailwayProvider("railway", {
56
+ token: config.requireSecret("railwayToken"),
57
+ })
58
+
59
+ const project = new RailwayProject("my-project", {
60
+ name: "my-app",
61
+ }, { provider })
62
+
63
+ const environment = new RailwayEnvironment("production", {
64
+ name: "production",
65
+ }, { provider, project })
66
+
67
+ const service = new RailwayService("api", {
68
+ name: "api",
69
+ builder: RailwayBuilder.RAILPACK,
70
+ startCommand: "node dist/index.js",
71
+ }, { provider, project, environment })
72
+
73
+ const env = { DATABASE_URL: dbUrl }
74
+
75
+ new RailwayVariable("api-vars", {
76
+ variables: env,
77
+ }, { provider, project, environment, service })
78
+
79
+ const deployToken = new RailwayProjectToken("api-token", {
80
+ name: "api-deploy",
81
+ }, { provider, project, environment })
82
+
83
+ new RailwayDeploy("api-deploy", {
84
+ triggers: [hash("apps/api"), hash(env)], // hash(env): a non-secret digest, not raw secret values
85
+ }, { provider, project, environment, service, projectToken: deployToken.token })
86
+ ```
87
+
88
+ ### Railway API surface
89
+
90
+ | Class | Key outputs | Notes |
91
+ |---|---|---|
92
+ | `RailwayProvider` | β€” | Pass as `provider` option to every Railway resource |
93
+ | `RailwayProject` | `.id` (project UUID) | Adopt-or-create by name |
94
+ | `RailwayEnvironment` | `.id` | Optional `source` env to fork from |
95
+ | `RailwayService` | `.id` | Full instance config: builder, healthcheck, restart policy |
96
+ | `RailwayDomain` | `.fqdn` | Omit `customDomain` for an auto-generated domain |
97
+ | `RailwayVariable` | β€” | Batch upsert; uses `skipDeploys` to avoid snapshot errors |
98
+ | `RailwayVolume` | `.id` | Persistent volume; `mountPath` must be absolute |
99
+ | `RailwayProjectToken` | `.token` (secret) | Environment-scoped deploy token; feed into `RailwayDeploy` |
100
+ | `RailwayDeploy` | β€” | Runs `railway up --detach`, then monitors the deployment via the Railway API |
101
+
102
+ **Enums:** `RailwayBuilder` (`RAILPACK`, `NIXPACKS`, `DOCKERFILE`, `HEROKU`, `PAKETO`), `RailwayRestartPolicy` (`ON_FAILURE`, `ALWAYS`, `NEVER`)
103
+
104
+ ## Neon
105
+
106
+ ```typescript
107
+ import {
108
+ NeonProvider,
109
+ NeonProject,
110
+ NeonBranch,
111
+ NeonRole,
112
+ NeonEndpoint,
113
+ NeonDatabase,
114
+ } from "@infracraft/pulumi/neon"
115
+
116
+ const provider = new NeonProvider("neon", {
117
+ apiKey: config.requireSecret("neonApiKey"),
118
+ })
119
+
120
+ const project = new NeonProject("db", { name: "my-app" }, { provider })
121
+
122
+ // Copy-on-write branch from "main"
123
+ const branch = new NeonBranch("prod", {
124
+ name: "production",
125
+ parent: "main",
126
+ }, { provider, project })
127
+
128
+ const role = new NeonRole("owner", {
129
+ name: "neondb_owner",
130
+ resetPassword: true, // isolate COW branch from parent's password
131
+ }, { provider, project, branch })
132
+
133
+ const endpoint = new NeonEndpoint("prod", {
134
+ minCu: 0.25,
135
+ maxCu: 1,
136
+ suspendTimeout: 300,
137
+ }, { provider, project, branch })
138
+
139
+ const db = new NeonDatabase("app-db", {
140
+ name: "app",
141
+ ownerName: "neondb_owner",
142
+ }, { provider, project, branch })
143
+
144
+ const roleName = "neondb_owner"
145
+ const dbName = "app"
146
+ const connectionString = pulumi.interpolate`postgresql://${roleName}:${role.password}@${endpoint.host}/${dbName}`
147
+ ```
148
+
149
+ ### Neon API surface
150
+
151
+ | Class | Key outputs | Notes |
152
+ |---|---|---|
153
+ | `NeonProvider` | β€” | `apiKey` + optional `orgId` |
154
+ | `NeonProject` | `.id` | Adopt-or-create by name |
155
+ | `NeonBranch` | `.id` | Optional `parent` for copy-on-write branching |
156
+ | `NeonEndpoint` | `.host` | Read-write compute endpoint; use `.host` in connection strings |
157
+ | `NeonRole` | `.password` (secret) | `resetPassword: true` isolates COW branch passwords from parent |
158
+ | `NeonDatabase` | β€” | `name` + `ownerName` |
159
+
160
+ ## Vercel
161
+
162
+ ```typescript
163
+ import {
164
+ VercelProvider,
165
+ VercelProject,
166
+ VercelVariable,
167
+ VercelDeploy,
168
+ VercelIntegration,
169
+ VercelMarketplaceResource,
170
+ VercelResourceConnection,
171
+ } from "@infracraft/pulumi/vercel"
172
+ import { hash } from "@infracraft/pulumi/hash"
173
+
174
+ const provider = new VercelProvider("vercel", {
175
+ token: config.requireSecret("vercelToken"),
176
+ teamId: "team_xxx",
177
+ })
178
+
179
+ const project = new VercelProject("web", {
180
+ name: "my-web-app",
181
+ framework: "nextjs",
182
+ rootDirectory: "apps/web",
183
+ }, { provider })
184
+
185
+ // project.url resolves to the custom domain or <name>.vercel.app
186
+ export const url = project.url
187
+
188
+ const vars = new VercelVariable("web-vars", {
189
+ variables: { NEXT_PUBLIC_API_URL: apiUrl },
190
+ }, { provider, project })
191
+
192
+ new VercelDeploy("web-deploy", {
193
+ triggers: [hash("apps/web"), vars.contentHash],
194
+ }, { provider, project })
195
+
196
+ // Marketplace example: provision an Upstash KV store
197
+ const integration = new VercelIntegration("upstash", {
198
+ slug: "upstash",
199
+ }, { provider })
200
+
201
+ const store = new VercelMarketplaceResource("kv", {
202
+ integrationConfigurationId: integration.configurationId,
203
+ name: "my-kv",
204
+ type: "kv",
205
+ externalId: "my-kv",
206
+ }, { provider })
207
+
208
+ new VercelResourceConnection("kv-conn", {
209
+ storeId: store.id,
210
+ projectId: project.id,
211
+ targets: ["production", "preview"],
212
+ }, { provider })
213
+ ```
214
+
215
+ ### Vercel API surface
216
+
217
+ | Class | Key outputs | Notes |
218
+ |---|---|---|
219
+ | `VercelProvider` | β€” | `token` + `teamId` |
220
+ | `VercelProject` | `.id`, `.url` | `.url` prefers custom domain over `*.vercel.app` |
221
+ | `VercelVariable` | `.contentHash` | Use as a deploy trigger to redeploy on env var changes |
222
+ | `VercelDeploy` | β€” | Runs `vercel deploy --prod --yes` |
223
+ | `VercelIntegration` | `.configurationId` (`icfg_…`) | Resolves an installed marketplace integration by slug |
224
+ | `VercelMarketplaceResource` | `.id`, `.externalResourceId`, `.status` | Provisions a marketplace store |
225
+ | `VercelResourceConnection` | β€” | Wires a store to a project; injects env vars into target environments |
226
+
227
+ **Helpers:** `VERCEL_FRAMEWORKS` (const array), `VercelFramework` (derived union type)
228
+
229
+ ## Fly.io
230
+
231
+ ```typescript
232
+ import {
233
+ FlyProvider,
234
+ FlyApp,
235
+ FlySecret,
236
+ FlyVolume,
237
+ FlyCertificate,
238
+ FlyIp,
239
+ FlyIpType,
240
+ FlyDeploy,
241
+ FlyDeployStrategy,
242
+ } from "@infracraft/pulumi/fly"
243
+ import { hash } from "@infracraft/pulumi/hash"
244
+
245
+ // Provider β€” auth context (token + optional default org)
246
+ const provider = new FlyProvider("fly", {
247
+ token: config.requireSecret("flyToken"),
248
+ organization: "personal",
249
+ })
250
+
251
+ // App β€” adopt-or-create; `.id` is the app name
252
+ const app = new FlyApp("api", { name: "rby-api" }, { provider })
253
+
254
+ // Secrets β€” managed via the Machines REST secrets API.
255
+ // `.version` changes only when the secret set changes.
256
+ const secrets = new FlySecret("api-secrets", {
257
+ secrets: { JWT_SECRET: jwt, DATABASE_URL: dbUrl },
258
+ }, { provider, app })
259
+
260
+ // Volume β€” persistent storage (grow-only)
261
+ new FlyVolume("api-data", {
262
+ name: "data",
263
+ region: "iad",
264
+ sizeGb: 10,
265
+ }, { provider, app })
266
+
267
+ // Certificate β€” ACME cert for a custom hostname
268
+ new FlyCertificate("api-cert", {
269
+ hostname: "api.example.com",
270
+ }, { provider, app })
271
+
272
+ // Dedicated/shared IP (Fly GraphQL API)
273
+ new FlyIp("api-ip", { type: FlyIpType.SHARED_V4 }, { provider, app })
274
+
275
+ // Deploy β€” `fly deploy --remote-only` with consumer-controlled triggers.
276
+ // The generated fly.toml content is included in the triggers automatically.
277
+ new FlyDeploy("api-deploy", {
278
+ config: {
279
+ app: "rby-api",
280
+ primaryRegion: "iad",
281
+ build: { dockerfile: "apps/api/Dockerfile" },
282
+ httpService: {
283
+ internalPort: 3333,
284
+ forceHttps: true,
285
+ minMachinesRunning: 1,
286
+ checks: [{ method: "GET", path: "/health", interval: "30s", timeout: "10s" }],
287
+ },
288
+ deploy: { strategy: FlyDeployStrategy.ROLLING },
289
+ vm: [{ size: "shared-cpu-1x", memory: "512mb", cpus: 1 }],
290
+ },
291
+ triggers: [hash("apps/api"), secrets.version],
292
+ }, { provider, app, dependsOn: [secrets] })
293
+ ```
294
+
295
+ **Requirements:** `flyctl` must be installed on the machine running `pulumi up` (used by `FlyDeploy`). Generate a token with `fly tokens create deploy`. Dedicated IP allocation uses the Fly GraphQL API; everything else uses the Machines REST API.
296
+
297
+ ### Fly.io API surface
298
+
299
+ | Class | Key outputs | Notes |
300
+ |---|---|---|
301
+ | `FlyApp` | `.id` (app name) | Adopt-or-create |
302
+ | `FlySecret` | `.version` | Feed into `FlyDeploy` triggers to redeploy on secret changes |
303
+ | `FlyVolume` | `.id` (`vol_…`) | `sizeGb` can only grow |
304
+ | `FlyCertificate` | `.id` (hostname), `.configured`, `.dnsRequirements` | `.dnsRequirements` contains ACME validation records |
305
+ | `FlyIp` | `.id` (IP address) | `type`: `FlyIpType.V4`, `V6`, `SHARED_V4`, `PRIVATE_V6` |
306
+ | `FlyDeploy` | β€” | Writes fly.toml at deploy time; triggers on config + source hash |
307
+
308
+ **Enums:** `FlyIpType`, `FlyDeployStrategy` (`ROLLING`, `IMMEDIATE`, `CANARY`, `BLUEGREEN`), `FlyRestartPolicy` (`ALWAYS`, `ON_FAILURE`, `NEVER`), `FlyAutoStopMachines` (`OFF`, `STOP`, `SUSPEND`), `FlyConcurrencyType` (`CONNECTIONS`, `REQUESTS`), `FlyServiceProtocol` (`TCP`, `UDP`), `FlyPortHandler` (`HTTP`, `TLS`, `PG_TLS`, `PROXY_PROTO`, `EDGE_HTTP`), `FlyCpuKind` (`SHARED`, `PERFORMANCE`), `FlyCheckType` (`HTTP`, `TCP`)
309
+
310
+ **Constants:** `FLY_REGIONS` (IATA codes array), `FlyRegion` (derived type), `FLY_VM_SIZES` (size preset array), `FlyVmSize` (derived type)
311
+
312
+ **fly.toml types:** `FlyTomlConfig`, `FlyBuildConfig`, `FlyHttpService`, `FlyService`, `FlyServicePort`, `FlyMount`, `FlyVm`, `FlyDeployConfig`, `FlyRestartConfig`, `FlyCheck`, `FlyConcurrency`
313
+
314
+ **Helper:** `generateFlyToml(config)` serializes a `FlyTomlConfig` to fly.toml text (camelCase to snake_case, deterministic output).
315
+
316
+ ## Agents
317
+
318
+ Emit operating reminders for AI coding agents (Claude Code, Copilot, etc.) working on the stack. Auto-detects an agent via `CLAUDECODE` / `AI_AGENT` env vars β€” a no-op for humans unless `enabled` is forced.
319
+
320
+ ```typescript
321
+ import * as agents from "@infracraft/pulumi/agents"
322
+
323
+ agents.hint({
324
+ project: [
325
+ "Production branch is `main` β€” never destroy it.",
326
+ "All Railway services share one project; only environments are per-feature.",
327
+ ],
328
+ // channel: "stderr" (default) | "pulumi-log"
329
+ })
330
+ ```
331
+
332
+ Hints are emitted inside a `<infracraft-hint>` block with infracraft defaults (adopt-or-create, no-op deletes for shared resources, protect-for-shared) plus caller-supplied `project` reminders.
333
+
334
+ ### Agents API surface
335
+
336
+ | Export | Kind | Notes |
337
+ |---|---|---|
338
+ | `hint(options?)` | function | Emits the hint block; no-op outside agent context |
339
+ | `AgentHintOptions` | type | `project?` (string[]), `enabled?` (boolean), `channel?` (`"stderr"` or `"pulumi-log"`) |
340
+
341
+ ## Hash
342
+
343
+ Produce a stable digest to use as a deploy trigger element. Accepts a source directory path (synchronous, returns `string`), a key-value env var map (returns `Output<string>`), or β€” via `hashApp` β€” an app directory plus its transitive workspace dependencies.
344
+
345
+ ```typescript
346
+ import { hash, hashApp } from "@infracraft/pulumi/hash"
347
+
348
+ // Hash a source directory
349
+ const sourceHash = hash("apps/api")
350
+
351
+ // Hash an app + every workspace package it depends on (transitively) β€”
352
+ // a change to a shared packages/* the app uses retriggers its deploy
353
+ const appHash = hashApp(monorepoRoot, "apps/api")
354
+
355
+ // Hash an env var map into a single non-secret digest (Output<string>)
356
+ const envHash = hash({ DATABASE_URL: dbUrl, JWT_SECRET: jwt })
357
+
358
+ new RailwayDeploy("api-deploy", {
359
+ triggers: [appHash, envHash],
360
+ }, { provider, project, environment, service, projectToken: deployToken.token })
361
+ ```
362
+
363
+ ### Hash API surface
364
+
365
+ | Export | Kind | Notes |
366
+ |---|---|---|
367
+ | `hash(directory, options?)` | function | Recursive file name + content digest; skips build/VCS directories |
368
+ | `hash(env)` | function | Sorted-key digest of resolved values; returned `Output<string>` is non-secret |
369
+ | `hashApp(monorepoRoot, appDirectory, options?)` | function | Hashes the app and its transitive `apps/*`/`packages/*` workspace dependencies |
370
+
371
+ ## Sandbox & Git Guard
372
+
373
+ Deploy isolation as `dependsOn` markers. Listing a `DeploySandbox` in a deploy's `dependsOn` runs that deploy's CLI from an isolated copy of the repo's tracked files under `/tmp/infracraft` (stale sandboxes are garbage-collected automatically). Adding a `GitGuard` swaps the copy's `.git` for a metadata-free stub (`git init` + `git add -A`, unborn HEAD) β€” so no commit SHA or author email is sent to the platform.
374
+
375
+ ```typescript
376
+ import { DeploySandbox } from "@infracraft/pulumi/sandbox"
377
+ import { GitGuard } from "@infracraft/pulumi/git-guard"
378
+ import { hash } from "@infracraft/pulumi/hash"
379
+
380
+ const sandbox = new DeploySandbox("sandbox")
381
+ const guard = new GitGuard("git-guard")
382
+
383
+ // Runs `vercel deploy` from an isolated copy with a stub `.git`
384
+ new VercelDeploy("web-deploy", {
385
+ triggers: [hash("apps/web")],
386
+ excludePaths: ["apps/docs"], // drop other apps from the upload (stub mode only)
387
+ }, { provider, project, dependsOn: [sandbox, guard] })
388
+ ```
389
+
390
+ | `dependsOn` markers | Working copy | `.git` sent to the platform |
391
+ |---|---|---|
392
+ | none | Live repo tree | The real one β€” whatever the platform CLI picks up |
393
+ | `DeploySandbox` | Isolated `/tmp/infracraft` copy | Real `.git` (copy-on-write copy) |
394
+ | `DeploySandbox` + `GitGuard` | Isolated copy, `excludePaths` applied | Metadata-free stub β€” no commit SHA, no author |
395
+ | `GitGuard` alone | β€” | Throws: the guard needs a sandbox to act on |
396
+
397
+ ### Sandbox & Git Guard API surface
398
+
399
+ | Export | Kind | Notes |
400
+ |---|---|---|
401
+ | `DeploySandbox` | ComponentResource | Isolation marker + workspace lifecycle; GCs sandboxes older than 3h |
402
+ | `GitGuard` | ComponentResource | Metadata-protection marker; requires a `DeploySandbox` alongside it |
403
+ | `SandboxMode` | enum | `NONE`, `ORIGINAL`, `STUB` β€” derived from the markers by the deploy seam |
404
+ | `buildSandboxScript(options)` | function | Builds the sandboxed shell a deploy command runs (used by the deploy resources) |
405
+ | `buildSandboxFileFilter(excludePaths)` | function | Portable awk filter applied to `git ls-files` before the copy |
406
+ | `isDeploySandbox(value)` / `isGitGuard(value)` | functions | Bundle-safe marker checks |
407
+
408
+ ## Design
409
+
410
+ **Context-based**: Resources inherit auth, project, and environment from their options β€” no manual ID passing.
411
+
412
+ **Adopt-or-create**: Existing infrastructure is discovered by name and adopted into Pulumi state. Run `pulumi up` against a pre-existing project and it just works.
413
+
414
+ **Consumer-controlled protection**: Use `protect: true` on shared/production resources to prevent accidental deletion. Deploy resources accept a `triggers` array β€” you decide what causes a redeploy.
415
+
416
+ **Consumer-controlled triggers**: Hash source directories, env values, or anything else. Pass results into `triggers` arrays.
417
+
418
+ ## Why
419
+
420
+ | Provider | Existing options | Gap |
421
+ |---|---|---|
422
+ | Railway | Nothing. Zero Pulumi providers exist. | **We are the Railway Pulumi provider.** |
423
+ | Neon | Bridged TF provider β€” fails on pre-existing resources | Adopt-or-create without manual `import` blocks |
424
+ | Vercel | `@pulumiverse/vercel` β€” no adopt-or-create, no CLI deploys | Adopt-or-create projects + consumer-controlled deploy triggers |
425
+ | Fly.io | `@ediri/pulumi-fly` β€” bridges a Terraform provider Fly archived March 2024; no secrets support | Hand-rolled `dynamic` resources matching every other provider β€” secrets, adopt-or-create, consumer-controlled deploys; no unmaintained upstream |
426
+
427
+ ## License
428
+
429
+ MIT
@@ -0,0 +1,38 @@
1
+ const require_chunk = require('../../chunk-BVYJZCqc.cjs');
2
+ const require_railway_deployment_monitor = require('../deployment-monitor.cjs');
3
+
4
+ //#region src/railway/bin/monitor-deployment.ts
5
+ /**
6
+ * Runnable entry for the Railway deploy monitor β€” invoked by `RailwayDeploy` as
7
+ * `node <dist>/railway/bin/monitor-deployment.mjs` after `railway up --detach`.
8
+ *
9
+ * Reads the deploy context from `IC_*` env vars, wires the real `fetch`/`setTimeout`/stderr
10
+ * collaborators, and exits non-zero iff the Railway API reports a failed (or never-resolved-
11
+ * after-failed-upload) deployment. All decision logic lives in the unit-tested
12
+ * `deployment-monitor` module; this file is only IO + process glue.
13
+ */
14
+ async function main() {
15
+ const result = await require_railway_deployment_monitor.monitorRailwayDeployment({
16
+ apiUrl: process.env.IC_API_URL || void 0,
17
+ projectToken: process.env.IC_TOK ?? "",
18
+ projectId: process.env.IC_PROJ ?? "",
19
+ environmentId: process.env.IC_ENV ?? "",
20
+ serviceId: process.env.IC_SVC ?? "",
21
+ deploymentId: process.env.IC_DEPLOY_ID || void 0,
22
+ uploadOutput: process.env.IC_UP_OUT,
23
+ uploadExitCode: Number(process.env.IC_UP_EXIT ?? "0"),
24
+ since: Number(process.env.IC_SINCE ?? "0")
25
+ }, {
26
+ fetch: globalThis.fetch,
27
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
28
+ log: (line) => process.stderr.write(`${line}\n`)
29
+ });
30
+ process.exit(result.failed ? 1 : 0);
31
+ }
32
+ main().catch((error) => {
33
+ process.stderr.write(`[infracraft] deploy monitor crashed: ${error instanceof Error ? error.message : String(error)}\n`);
34
+ process.exit(1);
35
+ });
36
+
37
+ //#endregion
38
+ //# sourceMappingURL=monitor-deployment.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monitor-deployment.cjs","names":["monitorRailwayDeployment"],"sources":["../../../src/railway/bin/monitor-deployment.ts"],"sourcesContent":["/**\n * Runnable entry for the Railway deploy monitor β€” invoked by `RailwayDeploy` as\n * `node <dist>/railway/bin/monitor-deployment.mjs` after `railway up --detach`.\n *\n * Reads the deploy context from `IC_*` env vars, wires the real `fetch`/`setTimeout`/stderr\n * collaborators, and exits non-zero iff the Railway API reports a failed (or never-resolved-\n * after-failed-upload) deployment. All decision logic lives in the unit-tested\n * `deployment-monitor` module; this file is only IO + process glue.\n */\nimport { monitorRailwayDeployment } from \"../deployment-monitor\";\n\nasync function main(): Promise<void> {\n\tconst result = await monitorRailwayDeployment(\n\t\t{\n\t\t\t// Defaults to Railway's public API; override only to point at a proxy or a test server.\n\t\t\tapiUrl: process.env.IC_API_URL || undefined,\n\t\t\tprojectToken: process.env.IC_TOK ?? \"\",\n\t\t\tprojectId: process.env.IC_PROJ ?? \"\",\n\t\t\tenvironmentId: process.env.IC_ENV ?? \"\",\n\t\t\tserviceId: process.env.IC_SVC ?? \"\",\n\t\t\tdeploymentId: process.env.IC_DEPLOY_ID || undefined,\n\t\t\tuploadOutput: process.env.IC_UP_OUT,\n\t\t\tuploadExitCode: Number(process.env.IC_UP_EXIT ?? \"0\"),\n\t\t\tsince: Number(process.env.IC_SINCE ?? \"0\"),\n\t\t},\n\t\t{\n\t\t\tfetch: globalThis.fetch,\n\t\t\tsleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n\t\t\tlog: (line) => process.stderr.write(`${line}\\n`),\n\t\t},\n\t);\n\n\tprocess.exit(result.failed ? 1 : 0);\n}\n\nmain().catch((error) => {\n\tprocess.stderr.write(\n\t\t`[infracraft] deploy monitor crashed: ${error instanceof Error ? error.message : String(error)}\\n`,\n\t);\n\n\tprocess.exit(1);\n});\n"],"mappings":";;;;;;;;;;;;;AAWA,eAAe,OAAsB;CACpC,MAAM,SAAS,MAAMA,4DACpB;EAEC,QAAQ,QAAQ,IAAI,cAAc;EAClC,cAAc,QAAQ,IAAI,UAAU;EACpC,WAAW,QAAQ,IAAI,WAAW;EAClC,eAAe,QAAQ,IAAI,UAAU;EACrC,WAAW,QAAQ,IAAI,UAAU;EACjC,cAAc,QAAQ,IAAI,gBAAgB;EAC1C,cAAc,QAAQ,IAAI;EAC1B,gBAAgB,OAAO,QAAQ,IAAI,cAAc,GAAG;EACpD,OAAO,OAAO,QAAQ,IAAI,YAAY,GAAG;CAC1C,GACA;EACC,OAAO,WAAW;EAClB,QAAQ,OAAO,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;EAC/D,MAAM,SAAS,QAAQ,OAAO,MAAM,GAAG,KAAK,GAAG;CAChD,CACD;CAEA,QAAQ,KAAK,OAAO,SAAS,IAAI,CAAC;AACnC;AAEA,KAAK,EAAE,OAAO,UAAU;CACvB,QAAQ,OAAO,MACd,wCAAwC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAChG;CAEA,QAAQ,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,39 @@
1
+ import { t as __name } from "../../chunk-OPjESj5l.mjs";
2
+ import { monitorRailwayDeployment } from "../deployment-monitor.mjs";
3
+
4
+ //#region src/railway/bin/monitor-deployment.ts
5
+ /**
6
+ * Runnable entry for the Railway deploy monitor β€” invoked by `RailwayDeploy` as
7
+ * `node <dist>/railway/bin/monitor-deployment.mjs` after `railway up --detach`.
8
+ *
9
+ * Reads the deploy context from `IC_*` env vars, wires the real `fetch`/`setTimeout`/stderr
10
+ * collaborators, and exits non-zero iff the Railway API reports a failed (or never-resolved-
11
+ * after-failed-upload) deployment. All decision logic lives in the unit-tested
12
+ * `deployment-monitor` module; this file is only IO + process glue.
13
+ */
14
+ async function main() {
15
+ const result = await monitorRailwayDeployment({
16
+ apiUrl: process.env.IC_API_URL || void 0,
17
+ projectToken: process.env.IC_TOK ?? "",
18
+ projectId: process.env.IC_PROJ ?? "",
19
+ environmentId: process.env.IC_ENV ?? "",
20
+ serviceId: process.env.IC_SVC ?? "",
21
+ deploymentId: process.env.IC_DEPLOY_ID || void 0,
22
+ uploadOutput: process.env.IC_UP_OUT,
23
+ uploadExitCode: Number(process.env.IC_UP_EXIT ?? "0"),
24
+ since: Number(process.env.IC_SINCE ?? "0")
25
+ }, {
26
+ fetch: globalThis.fetch,
27
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
28
+ log: (line) => process.stderr.write(`${line}\n`)
29
+ });
30
+ process.exit(result.failed ? 1 : 0);
31
+ }
32
+ main().catch((error) => {
33
+ process.stderr.write(`[infracraft] deploy monitor crashed: ${error instanceof Error ? error.message : String(error)}\n`);
34
+ process.exit(1);
35
+ });
36
+
37
+ //#endregion
38
+ export { };
39
+ //# sourceMappingURL=monitor-deployment.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monitor-deployment.mjs","names":[],"sources":["../../../src/railway/bin/monitor-deployment.ts"],"sourcesContent":["/**\n * Runnable entry for the Railway deploy monitor β€” invoked by `RailwayDeploy` as\n * `node <dist>/railway/bin/monitor-deployment.mjs` after `railway up --detach`.\n *\n * Reads the deploy context from `IC_*` env vars, wires the real `fetch`/`setTimeout`/stderr\n * collaborators, and exits non-zero iff the Railway API reports a failed (or never-resolved-\n * after-failed-upload) deployment. All decision logic lives in the unit-tested\n * `deployment-monitor` module; this file is only IO + process glue.\n */\nimport { monitorRailwayDeployment } from \"../deployment-monitor\";\n\nasync function main(): Promise<void> {\n\tconst result = await monitorRailwayDeployment(\n\t\t{\n\t\t\t// Defaults to Railway's public API; override only to point at a proxy or a test server.\n\t\t\tapiUrl: process.env.IC_API_URL || undefined,\n\t\t\tprojectToken: process.env.IC_TOK ?? \"\",\n\t\t\tprojectId: process.env.IC_PROJ ?? \"\",\n\t\t\tenvironmentId: process.env.IC_ENV ?? \"\",\n\t\t\tserviceId: process.env.IC_SVC ?? \"\",\n\t\t\tdeploymentId: process.env.IC_DEPLOY_ID || undefined,\n\t\t\tuploadOutput: process.env.IC_UP_OUT,\n\t\t\tuploadExitCode: Number(process.env.IC_UP_EXIT ?? \"0\"),\n\t\t\tsince: Number(process.env.IC_SINCE ?? \"0\"),\n\t\t},\n\t\t{\n\t\t\tfetch: globalThis.fetch,\n\t\t\tsleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n\t\t\tlog: (line) => process.stderr.write(`${line}\\n`),\n\t\t},\n\t);\n\n\tprocess.exit(result.failed ? 1 : 0);\n}\n\nmain().catch((error) => {\n\tprocess.stderr.write(\n\t\t`[infracraft] deploy monitor crashed: ${error instanceof Error ? error.message : String(error)}\\n`,\n\t);\n\n\tprocess.exit(1);\n});\n"],"mappings":";;;;;;;;;;;;;AAWA,eAAe,OAAsB;CACpC,MAAM,SAAS,MAAM,yBACpB;EAEC,QAAQ,QAAQ,IAAI,cAAc;EAClC,cAAc,QAAQ,IAAI,UAAU;EACpC,WAAW,QAAQ,IAAI,WAAW;EAClC,eAAe,QAAQ,IAAI,UAAU;EACrC,WAAW,QAAQ,IAAI,UAAU;EACjC,cAAc,QAAQ,IAAI,gBAAgB;EAC1C,cAAc,QAAQ,IAAI;EAC1B,gBAAgB,OAAO,QAAQ,IAAI,cAAc,GAAG;EACpD,OAAO,OAAO,QAAQ,IAAI,YAAY,GAAG;CAC1C,GACA;EACC,OAAO,WAAW;EAClB,QAAQ,OAAO,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;EAC/D,MAAM,SAAS,QAAQ,OAAO,MAAM,GAAG,KAAK,GAAG;CAChD,CACD;CAEA,QAAQ,KAAK,OAAO,SAAS,IAAI,CAAC;AACnC;AAEA,KAAK,EAAE,OAAO,UAAU;CACvB,QAAQ,OAAO,MACd,wCAAwC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAChG;CAEA,QAAQ,KAAK,CAAC;AACf,CAAC"}
@@ -3,23 +3,16 @@ const require_chunk = require('../chunk-BVYJZCqc.cjs');
3
3
  const require_commands_deploy = require('../commands/deploy.cjs');
4
4
  let _pulumi_pulumi = require("@pulumi/pulumi");
5
5
  _pulumi_pulumi = require_chunk.__toESM(_pulumi_pulumi, 1);
6
+ let node_url = require("node:url");
6
7
 
7
8
  //#region src/railway/deploy.ts
8
9
  /**
9
- * The deploy-wait poller. `railway up --ci` only blocks until upload; this polls
10
- * the Railway GraphQL API to a terminal status and fails the resource on
11
- * FAILED/CRASHED/REMOVED. Runs under `node -e`, single-quoted in the shell, so it
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.
10
+ * Absolute path to the runnable deploy monitor, resolved next to this module in `dist`.
11
+ * `railway up` only uploads + triggers; this bin makes the Railway GraphQL API β€” not the
12
+ * CLI's exit code β€” the source of truth for pass/fail, and dumps build + deploy logs on
13
+ * failure. Its logic lives in the unit-tested `deployment-monitor` module.
21
14
  */
22
- const 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
+ const MONITOR_BIN = (0, node_url.fileURLToPath)(new URL("./bin/monitor-deployment.mjs", require("url").pathToFileURL(__filename).href));
23
16
  /**
24
17
  * Deploys a Railway service and waits for a terminal status. Isolation/git are the
25
18
  * seam's job (list a `DeploySandbox` and optionally a `GitGuard` in `dependsOn`).
@@ -34,7 +27,7 @@ var RailwayDeploy = class extends _pulumi_pulumi.ComponentResource {
34
27
  constructor(name, args, opts) {
35
28
  const { provider, project, environment, service, projectToken, ...pulumiOpts } = opts;
36
29
  super("infracraft:railway:Deploy", name, {}, pulumiOpts);
37
- const cli = _pulumi_pulumi.interpolate`IC_SINCE=$(node -e "process.stdout.write(String(Date.now()))"); RAILWAY_TOKEN=${projectToken} railway up --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}'`;
30
+ const cli = _pulumi_pulumi.interpolate`IC_SINCE=$(node -e "process.stdout.write(String(Date.now()))"); IC_UP_OUT=$(RAILWAY_TOKEN=${projectToken} railway up --detach --json --project ${project.id} --service ${service.id} --environment ${environment.id} 2>&1); IC_UP_EXIT=$?; printf '%s\\n' "$IC_UP_OUT"; if [ -n "$INFRACRAFT_SKIP_DEPLOY_WAIT" ]; then exit "$IC_UP_EXIT"; fi; IC_UP_OUT="$IC_UP_OUT" IC_UP_EXIT=$IC_UP_EXIT IC_TOK=${projectToken} IC_PROJ=${project.id} IC_ENV=${environment.id} IC_SVC=${service.id} IC_SINCE=$IC_SINCE node "${MONITOR_BIN}"`;
38
31
  const setup = args.railpackConfig ? `printf '%s' '${JSON.stringify(args.railpackConfig).replace(/'/g, "'\\''")}' > railpack.json` : void 0;
39
32
  const { deploymentUrl } = require_commands_deploy.createDeployCommand({
40
33
  name,
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.cjs","names":["pulumi","createDeployCommand"],"sources":["../../src/railway/deploy.ts"],"sourcesContent":["// src/railway/deploy.ts (replace entire file)\nimport * as pulumi from \"@pulumi/pulumi\";\n\nimport { createDeployCommand } from \"../commands/deploy\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport { RailwayBuilder, type RailwayService } from \"./service\";\n\nexport interface RailwayDeployConfig {\n\tbuilder?: RailwayBuilder;\n\tstartCommand?: string;\n\tpreDeployCommand?: string;\n}\n\nexport interface RailwayDeployArgs {\n\t/** Redeploy triggers (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\t/** Paths excluded from the upload when running with `DeploySandbox` + `GitGuard`. */\n\texcludePaths?: string[];\n\t/** Railpack configuration written to `railpack.json` before deploy. */\n\trailpackConfig?: Record<string, unknown>;\n}\n\ntype RailwayDeployOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\tprovider: RailwayProvider;\n\tproject: RailwayProject;\n\tenvironment: RailwayEnvironment;\n\tservice: RailwayService;\n\t/** Environment-scoped Railway deploy token (provision via RailwayProjectToken). */\n\tprojectToken: pulumi.Input<string>;\n};\n\n/**\n * 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"}
1
+ {"version":3,"file":"deploy.cjs","names":["pulumi","createDeployCommand"],"sources":["../../src/railway/deploy.ts"],"sourcesContent":["// src/railway/deploy.ts (replace entire file)\nimport { fileURLToPath } from \"node:url\";\n\nimport * as pulumi from \"@pulumi/pulumi\";\n\nimport { createDeployCommand } from \"../commands/deploy\";\nimport type { RailwayEnvironment } from \"./environment\";\nimport type { RailwayProject } from \"./project\";\nimport type { RailwayProvider } from \"./provider\";\nimport { RailwayBuilder, type RailwayService } from \"./service\";\n\nexport interface RailwayDeployConfig {\n\tbuilder?: RailwayBuilder;\n\tstartCommand?: string;\n\tpreDeployCommand?: string;\n}\n\nexport interface RailwayDeployArgs {\n\t/** Redeploy triggers (e.g. source hashes, env hashes). */\n\ttriggers: pulumi.Input<pulumi.Input<string>[]>;\n\t/** Paths excluded from the upload when running with `DeploySandbox` + `GitGuard`. */\n\texcludePaths?: string[];\n\t/** Railpack configuration written to `railpack.json` before deploy. */\n\trailpackConfig?: Record<string, unknown>;\n}\n\ntype RailwayDeployOptions = Omit<\n\tpulumi.ComponentResourceOptions,\n\t\"provider\"\n> & {\n\tprovider: RailwayProvider;\n\tproject: RailwayProject;\n\tenvironment: RailwayEnvironment;\n\tservice: RailwayService;\n\t/** Environment-scoped Railway deploy token (provision via RailwayProjectToken). */\n\tprojectToken: pulumi.Input<string>;\n};\n\n/**\n * Absolute path to the runnable deploy monitor, resolved next to this module in `dist`.\n * `railway up` only uploads + triggers; this bin makes the Railway GraphQL API β€” not the\n * CLI's exit code β€” the source of truth for pass/fail, and dumps build + deploy logs on\n * failure. Its logic lives in the unit-tested `deployment-monitor` module.\n */\nconst MONITOR_BIN = fileURLToPath(\n\tnew URL(\"./bin/monitor-deployment.mjs\", import.meta.url),\n);\n\n/**\n * Deploys a Railway service and waits for a terminal status. Isolation/git are the\n * seam's job (list a `DeploySandbox` and optionally a `GitGuard` in `dependsOn`).\n *\n * @example\n * ```typescript\n * new RailwayDeploy(\"mesh\", { triggers: [sourceHash], railpackConfig: { apt: [\"libatomic1\"] } },\n * { provider, project, environment, service, projectToken: token.token, dependsOn: [sandbox, gitGuard] });\n * ```\n */\nexport class RailwayDeploy extends pulumi.ComponentResource {\n\t/** The deploy CLI's final stdout line (Railway service URL when emitted). */\n\tpublic readonly deploymentUrl: pulumi.Output<string>;\n\n\tconstructor(\n\t\tname: string,\n\t\targs: RailwayDeployArgs,\n\t\topts: RailwayDeployOptions,\n\t) {\n\t\tconst {\n\t\t\tprovider,\n\t\t\tproject,\n\t\t\tenvironment,\n\t\t\tservice,\n\t\t\tprojectToken,\n\t\t\t...pulumiOpts\n\t\t} = opts;\n\n\t\tsuper(\"infracraft:railway:Deploy\", name, {}, pulumiOpts);\n\n\t\t// `railway up --detach` uploads + triggers WITHOUT attaching to the build-log\n\t\t// stream β€” that long-lived stream is what intermittently times out and makes the\n\t\t// CLI exit non-zero even when the deploy actually succeeds. We capture its `--json`\n\t\t// output (for the exact deployment id) and exit code, re-emit it for visibility,\n\t\t// then hand off to the monitor bin which polls the Railway API to a terminal status.\n\t\t// The API β€” not the CLI exit code β€” decides pass/fail. Token is inlined (not via the\n\t\t// env map): an unknown secret in `environment` makes `pulumi preview` fail. IC_SINCE\n\t\t// is captured just before `railway up` as a createdAt fallback for id resolution.\n\t\tconst cli = pulumi.interpolate`IC_SINCE=$(node -e \"process.stdout.write(String(Date.now()))\"); IC_UP_OUT=$(RAILWAY_TOKEN=${projectToken} railway up --detach --json --project ${project.id} --service ${service.id} --environment ${environment.id} 2>&1); IC_UP_EXIT=$?; printf '%s\\\\n' \"$IC_UP_OUT\"; if [ -n \"$INFRACRAFT_SKIP_DEPLOY_WAIT\" ]; then exit \"$IC_UP_EXIT\"; fi; IC_UP_OUT=\"$IC_UP_OUT\" IC_UP_EXIT=$IC_UP_EXIT IC_TOK=${projectToken} IC_PROJ=${project.id} IC_ENV=${environment.id} IC_SVC=${service.id} IC_SINCE=$IC_SINCE node \"${MONITOR_BIN}\"`;\n\n\t\t// `printf '%s'` (not a bare format string) so railpack values containing %\n\t\t// are literal; the JSON is single-quote-escaped the POSIX way (' -> '\\'').\n\t\tconst setup = args.railpackConfig\n\t\t\t? `printf '%s' '${JSON.stringify(args.railpackConfig).replace(/'/g, \"'\\\\''\")}' > railpack.json`\n\t\t\t: undefined;\n\n\t\tconst { deploymentUrl } = createDeployCommand(\n\t\t\t{\n\t\t\t\tname,\n\t\t\t\tcli,\n\t\t\t\ttriggers: args.triggers,\n\t\t\t\texcludePaths: args.excludePaths,\n\t\t\t\tsetup,\n\t\t\t},\n\t\t\t{ parent: this, ...pulumiOpts },\n\t\t);\n\n\t\tthis.deploymentUrl = deploymentUrl;\n\n\t\tthis.registerOutputs({ deploymentUrl: this.deploymentUrl });\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;AA4CA,MAAM,0CACL,IAAI,IAAI,6EAA+C,CACxD;;;;;;;;;;;AAYA,IAAa,gBAAb,cAAmCA,eAAO,kBAAkB;CAI3D,YACC,MACA,MACA,MACC;EACD,MAAM,EACL,UACA,SACA,aACA,SACA,cACA,GAAG,eACA;EAEJ,MAAM,6BAA6B,MAAM,CAAC,GAAG,UAAU;EAUvD,MAAM,MAAM,eAAO,WAAW,6FAA6F,aAAa,wCAAwC,QAAQ,GAAG,aAAa,QAAQ,GAAG,iBAAiB,YAAY,GAAG,kLAAkL,aAAa,WAAW,QAAQ,GAAG,UAAU,YAAY,GAAG,UAAU,QAAQ,GAAG,4BAA4B,YAAY;EAI9hB,MAAM,QAAQ,KAAK,iBAChB,gBAAgB,KAAK,UAAU,KAAK,cAAc,EAAE,QAAQ,MAAM,OAAO,EAAE,qBAC3E;EAEH,MAAM,EAAE,kBAAkBC,4CACzB;GACC;GACA;GACA,UAAU,KAAK;GACf,cAAc,KAAK;GACnB;EACD,GACA;GAAE,QAAQ;GAAM,GAAG;EAAW,CAC/B;EAEA,KAAK,gBAAgB;EAErB,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,CAAC;CAC3D;AACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/railway/deploy.ts"],"mappings":";;;;;;;;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"}
1
+ {"version":3,"file":"deploy.d.cts","names":[],"sources":["../../src/railway/deploy.ts"],"mappings":";;;;;;;;UAWiB,mBAAA;EAChB,OAAA,GAAU,cAAc;EACxB,YAAA;EACA,gBAAA;AAAA;AAAA,UAGgB,iBAAA;EALQ;EAOxB,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAPpB;EASV,YAAA;EAPA;EASA,cAAA,GAAiB,MAAA;AAAA;AAAA,KAGb,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAGP,QAAA,EAAU,eAAA;EACV,OAAA,EAAS,cAAA;EACT,WAAA,EAAa,kBAAA;EACb,OAAA,EAAS,cAAA,EAVQ;EAYjB,YAAA,EAAc,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;AAZE;cAmCX,aAAA,SAAsB,MAAA,CAAO,iBAAA;EAhCjB;EAAA,SAkCR,aAAA,EAAe,MAAA,CAAO,MAAA;cAGrC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/railway/deploy.ts"],"mappings":";;;;;;;;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"}
1
+ {"version":3,"file":"deploy.d.mts","names":[],"sources":["../../src/railway/deploy.ts"],"mappings":";;;;;;;;UAWiB,mBAAA;EAChB,OAAA,GAAU,cAAc;EACxB,YAAA;EACA,gBAAA;AAAA;AAAA,UAGgB,iBAAA;EALQ;EAOxB,QAAA,EAAU,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,KAAA;EAPpB;EASV,YAAA;EAPA;EASA,cAAA,GAAiB,MAAA;AAAA;AAAA,KAGb,oBAAA,GAAuB,IAAA,CAC3B,MAAA,CAAO,wBAAA;EAGP,QAAA,EAAU,eAAA;EACV,OAAA,EAAS,cAAA;EACT,WAAA,EAAa,kBAAA;EACb,OAAA,EAAS,cAAA,EAVQ;EAYjB,YAAA,EAAc,MAAA,CAAO,KAAA;AAAA;;;;;;;;;;AAZE;cAmCX,aAAA,SAAsB,MAAA,CAAO,iBAAA;EAhCjB;EAAA,SAkCR,aAAA,EAAe,MAAA,CAAO,MAAA;cAGrC,IAAA,UACA,IAAA,EAAM,iBAAA,EACN,IAAA,EAAM,oBAAA;AAAA"}