@infracraft/pulumi 1.17.0 β 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 +21 -0
- package/README.md +429 -0
- package/package.json +1 -1
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
|