@absolutejs/deploy 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -123
- package/dist/appStoreConnect.d.ts +182 -0
- package/dist/appStoreConnect.js +585 -0
- package/dist/appStoreConnect.js.map +10 -0
- package/dist/googlePlay.d.ts +10 -2
- package/dist/googlePlay.js +4 -1
- package/dist/googlePlay.js.map +3 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.js +604 -15
- package/dist/index.js.map +6 -5
- package/dist/nativeRelease.d.ts +17 -1
- package/dist/nativeRelease.js +53 -14
- package/dist/nativeRelease.js.map +3 -3
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -15,29 +15,28 @@ Zero `ssh2` / `node-ssh` dependency — `sshTarget` shells out to the system
|
|
|
15
15
|
## Native application releases (0.22.0)
|
|
16
16
|
|
|
17
17
|
`@absolutejs/deploy/native-release` publishes the immutable release directory
|
|
18
|
-
created by `absolute mobile build android`
|
|
19
|
-
adapter. It verifies the local AAB's declared size and SHA-256 digest again,
|
|
18
|
+
created by `absolute mobile build android` or `absolute mobile build ios` through
|
|
19
|
+
any `@absolutejs/blob` adapter. It verifies the local AAB/IPA's declared size and SHA-256 digest again,
|
|
20
20
|
requires a signed build by default, and writes the binary only once under its
|
|
21
21
|
content-derived release identity.
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
|
-
import { createNativeReleaseRegistry } from
|
|
25
|
-
import { s3BlobStore } from
|
|
24
|
+
import { createNativeReleaseRegistry } from "@absolutejs/deploy/native-release";
|
|
25
|
+
import { s3BlobStore } from "@absolutejs/blob/s3";
|
|
26
26
|
|
|
27
|
-
const store = s3BlobStore({ client, bucket:
|
|
27
|
+
const store = s3BlobStore({ client, bucket: "absolute-releases" });
|
|
28
28
|
const releases = createNativeReleaseRegistry({ store });
|
|
29
29
|
|
|
30
30
|
const published = await releases.publish({
|
|
31
|
-
releaseRoot:
|
|
32
|
-
|
|
33
|
-
channel: 'internal'
|
|
31
|
+
releaseRoot: ".absolutejs/mobile/releases/android/amobile_android_<sha256>",
|
|
32
|
+
channel: "internal",
|
|
34
33
|
});
|
|
35
34
|
|
|
36
35
|
await releases.promote({
|
|
37
36
|
appId: published.record.metadata.appId,
|
|
38
|
-
platform:
|
|
37
|
+
platform: "android",
|
|
39
38
|
releaseId: published.record.metadata.releaseId,
|
|
40
|
-
channel:
|
|
39
|
+
channel: "production",
|
|
41
40
|
});
|
|
42
41
|
```
|
|
43
42
|
|
|
@@ -56,11 +55,11 @@ retry to reuse an unexpired edit or resumable upload and to reconcile a commit
|
|
|
56
55
|
whose successful response was lost.
|
|
57
56
|
|
|
58
57
|
```ts
|
|
59
|
-
import { createGooglePlayReleasePublisher } from
|
|
58
|
+
import { createGooglePlayReleasePublisher } from "@absolutejs/deploy/google-play";
|
|
60
59
|
|
|
61
60
|
export default createGooglePlayReleasePublisher({
|
|
62
61
|
receiptStore: store,
|
|
63
|
-
registry: releases
|
|
62
|
+
registry: releases,
|
|
64
63
|
});
|
|
65
64
|
```
|
|
66
65
|
|
|
@@ -79,6 +78,40 @@ use `status: 'inProgress'` with `0 < userFraction < 1`; subsequent calls for the
|
|
|
79
78
|
same release can increase the fraction, halt or resume it, or mark it completed
|
|
80
79
|
without uploading the AAB again.
|
|
81
80
|
|
|
81
|
+
### App Store Connect and TestFlight distribution (0.24.0)
|
|
82
|
+
|
|
83
|
+
`@absolutejs/deploy/app-store-connect` uses Apple's current Build Upload API,
|
|
84
|
+
then waits for the processed build and assigns it to named or ID-addressed
|
|
85
|
+
TestFlight groups. It stores only durable Apple resource IDs in retry receipts;
|
|
86
|
+
time-limited upload URLs and API credentials are never persisted.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { createAppStoreConnectReleasePublisher } from "@absolutejs/deploy/app-store-connect";
|
|
90
|
+
import { createGooglePlayReleasePublisher } from "@absolutejs/deploy/google-play";
|
|
91
|
+
|
|
92
|
+
const google = createGooglePlayReleasePublisher({
|
|
93
|
+
receiptStore: store,
|
|
94
|
+
registry: releases,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export default createAppStoreConnectReleasePublisher({
|
|
98
|
+
auth: {
|
|
99
|
+
issuerId: process.env.APP_STORE_CONNECT_ISSUER_ID!,
|
|
100
|
+
keyId: process.env.APP_STORE_CONNECT_KEY_ID!,
|
|
101
|
+
privateKey: process.env.APP_STORE_CONNECT_PRIVATE_KEY!,
|
|
102
|
+
},
|
|
103
|
+
receiptStore: store,
|
|
104
|
+
registry: google,
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Provider wrappers compose, so this one module serves Android and iOS. AbsoluteJS
|
|
109
|
+
asks the adapter for a stable next integer build number before Xcode archives;
|
|
110
|
+
the allocation is tied to the web build, native-source fingerprint, and marketing
|
|
111
|
+
version. Serialize release jobs for one Apple app because App Store Connect has
|
|
112
|
+
no build-number reservation operation. Internal groups need no review. External
|
|
113
|
+
review is submitted only when explicitly requested.
|
|
114
|
+
|
|
82
115
|
## Infrastructure providers (0.14.0)
|
|
83
116
|
|
|
84
117
|
Control planes use the normalized `InfrastructureProvider` contract from
|
|
@@ -87,25 +120,27 @@ live in this package beside their deploy targets so providers never become
|
|
|
87
120
|
scattered across host applications.
|
|
88
121
|
|
|
89
122
|
```ts
|
|
90
|
-
import { createDigitalOceanInfrastructureProvider } from
|
|
123
|
+
import { createDigitalOceanInfrastructureProvider } from "@absolutejs/deploy/digitalocean-infrastructure";
|
|
91
124
|
|
|
92
125
|
const provider = createDigitalOceanInfrastructureProvider({
|
|
93
126
|
token: process.env.DIGITALOCEAN_TOKEN!,
|
|
94
|
-
tag:
|
|
95
|
-
regions: [
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
127
|
+
tag: "absolutejs-paas-node",
|
|
128
|
+
regions: [
|
|
129
|
+
{
|
|
130
|
+
region: "nyc3",
|
|
131
|
+
size: "s-2vcpu-4gb",
|
|
132
|
+
image: "ubuntu-24-04-x64",
|
|
133
|
+
sshKeys: [process.env.DIGITALOCEAN_SSH_KEY!],
|
|
134
|
+
userData: process.env.ABSOLUTEJS_NODE_CLOUD_INIT,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
102
137
|
agent: { preferPrivateNetwork: true, port: 8081 },
|
|
103
138
|
});
|
|
104
139
|
|
|
105
140
|
await provider.listNodes();
|
|
106
141
|
await provider.provisionNode({
|
|
107
142
|
idempotencyKey: crypto.randomUUID(),
|
|
108
|
-
name:
|
|
143
|
+
name: "absolutejs-node-01",
|
|
109
144
|
});
|
|
110
145
|
```
|
|
111
146
|
|
|
@@ -131,26 +166,26 @@ and idempotent removal. Provider resource construction stays here instead of
|
|
|
131
166
|
leaking DigitalOcean or GCP APIs into a control plane.
|
|
132
167
|
|
|
133
168
|
```ts
|
|
134
|
-
import { createDigitalOceanIngressProvider } from
|
|
169
|
+
import { createDigitalOceanIngressProvider } from "@absolutejs/deploy/digitalocean-ingress";
|
|
135
170
|
|
|
136
171
|
const ingress = createDigitalOceanIngressProvider({
|
|
137
172
|
token: process.env.DIGITALOCEAN_TOKEN!,
|
|
138
173
|
});
|
|
139
174
|
|
|
140
175
|
await ingress.reconcileIngress({
|
|
141
|
-
name:
|
|
176
|
+
name: "absolutejs-edge",
|
|
142
177
|
idempotencyKey: crypto.randomUUID(),
|
|
143
178
|
backends: [
|
|
144
|
-
{ region:
|
|
145
|
-
{ region:
|
|
179
|
+
{ region: "nyc3", resourceId: "regional-lb-east", priority: 1 },
|
|
180
|
+
{ region: "sfo3", resourceId: "regional-lb-west", priority: 2 },
|
|
146
181
|
],
|
|
147
182
|
listener: {
|
|
148
183
|
port: 443,
|
|
149
|
-
protocol:
|
|
184
|
+
protocol: "https",
|
|
150
185
|
targetPort: 443,
|
|
151
186
|
tlsPassthrough: true,
|
|
152
187
|
},
|
|
153
|
-
healthCheck: { protocol:
|
|
188
|
+
healthCheck: { protocol: "tcp", port: 443 },
|
|
154
189
|
});
|
|
155
190
|
```
|
|
156
191
|
|
|
@@ -163,26 +198,25 @@ operations before advancing dependent resources. Creating an adapter does not
|
|
|
163
198
|
provision anything; only `reconcileIngress()` mutates provider state.
|
|
164
199
|
|
|
165
200
|
```ts
|
|
166
|
-
import {
|
|
167
|
-
createDeployer,
|
|
168
|
-
sshTarget,
|
|
169
|
-
systemdManager,
|
|
170
|
-
} from '@absolutejs/deploy';
|
|
201
|
+
import { createDeployer, sshTarget, systemdManager } from "@absolutejs/deploy";
|
|
171
202
|
|
|
172
203
|
const deployer = createDeployer({
|
|
173
|
-
appName:
|
|
204
|
+
appName: "my-app",
|
|
174
205
|
target: sshTarget({
|
|
175
|
-
host:
|
|
176
|
-
user:
|
|
177
|
-
identity:
|
|
206
|
+
host: "droplet-1.example.com",
|
|
207
|
+
user: "deploy",
|
|
208
|
+
identity: "~/.ssh/id_ed25519",
|
|
178
209
|
}),
|
|
179
|
-
source: { kind:
|
|
180
|
-
env: { PORT:
|
|
181
|
-
processManager: systemdManager({ user:
|
|
182
|
-
verify: { kind:
|
|
210
|
+
source: { kind: "directory", root: "./" },
|
|
211
|
+
env: { PORT: "3000", DATABASE_URL: process.env.DATABASE_URL! },
|
|
212
|
+
processManager: systemdManager({ user: "deploy" }),
|
|
213
|
+
verify: { kind: "http", url: "http://localhost:3000/health" },
|
|
183
214
|
hooks: {
|
|
184
215
|
onStepStart: ({ name, releaseId }) => console.log(`▸ ${releaseId} ${name}`),
|
|
185
|
-
onLog: (line, stream, step) =>
|
|
216
|
+
onLog: (line, stream, step) =>
|
|
217
|
+
process[stream === "stderr" ? "stderr" : "stdout"].write(
|
|
218
|
+
`[${step}] ${line}\n`,
|
|
219
|
+
),
|
|
186
220
|
},
|
|
187
221
|
});
|
|
188
222
|
|
|
@@ -213,9 +247,9 @@ await deployer.stop();
|
|
|
213
247
|
|
|
214
248
|
### Targets
|
|
215
249
|
|
|
216
|
-
| Adapter
|
|
217
|
-
|
|
218
|
-
| `localTarget({ root, env? })`
|
|
250
|
+
| Adapter | Use |
|
|
251
|
+
| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
252
|
+
| `localTarget({ root, env? })` | Tests, local-dev, and "deploy to the same box" workflows. |
|
|
219
253
|
| `sshTarget({ host, user?, port?, identity?, sshFlags?, rsync? })` | Any VPS — DigitalOcean Droplets, Linode, Hetzner, Vultr, Lightsail, Scaleway. Uses the system `ssh` / `rsync` — no npm dep. |
|
|
220
254
|
|
|
221
255
|
A `Target` is just:
|
|
@@ -223,8 +257,15 @@ A `Target` is just:
|
|
|
223
257
|
```ts
|
|
224
258
|
type Target = {
|
|
225
259
|
description: string;
|
|
226
|
-
exec(
|
|
227
|
-
|
|
260
|
+
exec(
|
|
261
|
+
cmd: string,
|
|
262
|
+
opts?: { cwd?; env?; timeoutMs?; onLog?; stdin? },
|
|
263
|
+
): Promise<{ stdout; stderr; exitCode }>;
|
|
264
|
+
upload(
|
|
265
|
+
local: string,
|
|
266
|
+
remote: string,
|
|
267
|
+
opts?: { exclude?; deleteOrphans? },
|
|
268
|
+
): Promise<void>;
|
|
228
269
|
close?(): Promise<void>;
|
|
229
270
|
};
|
|
230
271
|
```
|
|
@@ -233,10 +274,10 @@ If you can implement those two methods, you can deploy through `@absolutejs/depl
|
|
|
233
274
|
|
|
234
275
|
### Process managers
|
|
235
276
|
|
|
236
|
-
| Manager
|
|
237
|
-
|
|
238
|
-
| `bareManager({ command? })`
|
|
239
|
-
| `systemdManager({ user?, group?, execStart?, restart?, ... })` | Templated systemd unit pointing at `current/`, `daemon-reload` + `restart`. The production answer for VMs.
|
|
277
|
+
| Manager | What it does |
|
|
278
|
+
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
279
|
+
| `bareManager({ command? })` | Default. `nohup bun run start &`, pid file under `/var/lib/<appName>/`, logs to `/var/log/<appName>/`. Zero remote dependency. |
|
|
280
|
+
| `systemdManager({ user?, group?, execStart?, restart?, ... })` | Templated systemd unit pointing at `current/`, `daemon-reload` + `restart`. The production answer for VMs. |
|
|
240
281
|
|
|
241
282
|
A `ProcessManager` is just `{ reload, stop?, status? }`. Wrap PM2, supervisord, runit, or even `@absolutejs/runtime` — whatever your remote uses.
|
|
242
283
|
|
|
@@ -280,25 +321,25 @@ up a droplet by name; creates it via the v2 API if absent; waits for
|
|
|
280
321
|
hand to `createDeployer`.
|
|
281
322
|
|
|
282
323
|
```ts
|
|
283
|
-
import { createDeployer } from
|
|
284
|
-
import { digitalOceanTarget } from
|
|
324
|
+
import { createDeployer } from "@absolutejs/deploy";
|
|
325
|
+
import { digitalOceanTarget } from "@absolutejs/deploy/digitalocean";
|
|
285
326
|
|
|
286
327
|
const target = await digitalOceanTarget({
|
|
287
328
|
token: process.env.DO_TOKEN!,
|
|
288
|
-
name:
|
|
289
|
-
region:
|
|
290
|
-
size:
|
|
291
|
-
image:
|
|
329
|
+
name: "absolutejs-prod-1", // idempotency key
|
|
330
|
+
region: "nyc3",
|
|
331
|
+
size: "s-1vcpu-1gb",
|
|
332
|
+
image: "ubuntu-22-04-x64",
|
|
292
333
|
sshKeys: [process.env.DO_KEY_FINGERPRINT!],
|
|
293
|
-
tags: [
|
|
294
|
-
userData:
|
|
334
|
+
tags: ["absolutejs"],
|
|
335
|
+
userData: "#!/bin/bash\ncurl -fsSL https://bun.sh/install | bash",
|
|
295
336
|
onLog: (line) => console.log(line),
|
|
296
337
|
});
|
|
297
338
|
|
|
298
339
|
console.log(`droplet ${target.dropletId} at ${target.ipv4}`);
|
|
299
340
|
|
|
300
|
-
const deployer = createDeployer({ appName:
|
|
301
|
-
await deployer.deploy({ source: { kind:
|
|
341
|
+
const deployer = createDeployer({ appName: "my-app", target });
|
|
342
|
+
await deployer.deploy({ source: { kind: "directory", path: "./build" } });
|
|
302
343
|
|
|
303
344
|
// Tear it down when you're done:
|
|
304
345
|
await target.destroy();
|
|
@@ -325,22 +366,22 @@ mappings underneath. Hetzner-specific differences: locations
|
|
|
325
366
|
and public-net IPv4/IPv6 are independently toggleable.
|
|
326
367
|
|
|
327
368
|
```ts
|
|
328
|
-
import { createDeployer } from
|
|
329
|
-
import { hetznerTarget } from
|
|
369
|
+
import { createDeployer } from "@absolutejs/deploy";
|
|
370
|
+
import { hetznerTarget } from "@absolutejs/deploy/hetzner";
|
|
330
371
|
|
|
331
372
|
const target = await hetznerTarget({
|
|
332
373
|
token: process.env.HETZNER_TOKEN!,
|
|
333
|
-
name:
|
|
334
|
-
location:
|
|
335
|
-
serverType:
|
|
336
|
-
image:
|
|
374
|
+
name: "absolutejs-prod-1",
|
|
375
|
+
location: "nbg1",
|
|
376
|
+
serverType: "cx22",
|
|
377
|
+
image: "ubuntu-22.04",
|
|
337
378
|
sshKeys: [process.env.HETZNER_KEY_FINGERPRINT!],
|
|
338
|
-
labels: { env:
|
|
339
|
-
userData:
|
|
379
|
+
labels: { env: "prod", team: "platform" },
|
|
380
|
+
userData: "#!/bin/bash\ncurl -fsSL https://bun.sh/install | bash",
|
|
340
381
|
});
|
|
341
382
|
|
|
342
|
-
const deployer = createDeployer({ appName:
|
|
343
|
-
await deployer.deploy({ source: { kind:
|
|
383
|
+
const deployer = createDeployer({ appName: "my-app", target });
|
|
384
|
+
await deployer.deploy({ source: { kind: "directory", path: "./build" } });
|
|
344
385
|
await target.destroy();
|
|
345
386
|
```
|
|
346
387
|
|
|
@@ -358,11 +399,13 @@ implements the shared `DnsProvider` contract from
|
|
|
358
399
|
`@absolutejs/deploy/dns`.
|
|
359
400
|
|
|
360
401
|
```ts
|
|
361
|
-
import { hetznerTarget } from
|
|
362
|
-
import { cloudflareProvider } from
|
|
363
|
-
import { ensureDnsForTarget } from
|
|
402
|
+
import { hetznerTarget } from "@absolutejs/deploy/hetzner";
|
|
403
|
+
import { cloudflareProvider } from "@absolutejs/deploy/cloudflare";
|
|
404
|
+
import { ensureDnsForTarget } from "@absolutejs/deploy/dns";
|
|
364
405
|
|
|
365
|
-
const target = await hetznerTarget({
|
|
406
|
+
const target = await hetznerTarget({
|
|
407
|
+
/* … */
|
|
408
|
+
});
|
|
366
409
|
const dns = cloudflareProvider({
|
|
367
410
|
token: process.env.CLOUDFLARE_TOKEN!,
|
|
368
411
|
zoneId: process.env.CLOUDFLARE_ZONE_ID!,
|
|
@@ -370,7 +413,7 @@ const dns = cloudflareProvider({
|
|
|
370
413
|
|
|
371
414
|
// Idempotent — create or update so the A record points at target.ipv4.
|
|
372
415
|
await ensureDnsForTarget(dns, {
|
|
373
|
-
name:
|
|
416
|
+
name: "api.example.com",
|
|
374
417
|
target,
|
|
375
418
|
ttl: 60,
|
|
376
419
|
proxied: false,
|
|
@@ -405,36 +448,38 @@ against Bun's `crypto.subtle`. The audit surface stays in this
|
|
|
405
448
|
repo.
|
|
406
449
|
|
|
407
450
|
```ts
|
|
408
|
-
import { hetznerTarget } from
|
|
409
|
-
import { cloudflareProvider } from
|
|
410
|
-
import { ensureDnsForTarget } from
|
|
451
|
+
import { hetznerTarget } from "@absolutejs/deploy/hetzner";
|
|
452
|
+
import { cloudflareProvider } from "@absolutejs/deploy/cloudflare";
|
|
453
|
+
import { ensureDnsForTarget } from "@absolutejs/deploy/dns";
|
|
411
454
|
import {
|
|
412
455
|
issueCertificate,
|
|
413
456
|
installCertificateOnTarget,
|
|
414
457
|
LETSENCRYPT_PRODUCTION,
|
|
415
|
-
} from
|
|
458
|
+
} from "@absolutejs/deploy/tls";
|
|
416
459
|
|
|
417
|
-
const target = await hetznerTarget({
|
|
460
|
+
const target = await hetznerTarget({
|
|
461
|
+
/* … */
|
|
462
|
+
});
|
|
418
463
|
const dns = cloudflareProvider({
|
|
419
464
|
token: process.env.CLOUDFLARE_TOKEN!,
|
|
420
465
|
zoneId: process.env.CLOUDFLARE_ZONE_ID!,
|
|
421
466
|
});
|
|
422
467
|
|
|
423
468
|
// 1. Point DNS at the box.
|
|
424
|
-
await ensureDnsForTarget(dns, { name:
|
|
469
|
+
await ensureDnsForTarget(dns, { name: "api.example.com", target, ttl: 60 });
|
|
425
470
|
|
|
426
471
|
// 2. Issue a cert via DNS-01.
|
|
427
472
|
const cert = await issueCertificate({
|
|
428
|
-
domains: [
|
|
473
|
+
domains: ["api.example.com"],
|
|
429
474
|
dnsProvider: dns,
|
|
430
|
-
email:
|
|
475
|
+
email: "ops@example.com",
|
|
431
476
|
directoryUrl: LETSENCRYPT_PRODUCTION,
|
|
432
477
|
onLog: (line) => console.log(line),
|
|
433
478
|
});
|
|
434
479
|
|
|
435
480
|
// 3. Install on the box.
|
|
436
481
|
await installCertificateOnTarget(target, cert, {
|
|
437
|
-
reload:
|
|
482
|
+
reload: "systemctl reload nginx",
|
|
438
483
|
});
|
|
439
484
|
```
|
|
440
485
|
|
|
@@ -445,9 +490,9 @@ map the provider write while leaving propagation checks on the public name:
|
|
|
445
490
|
|
|
446
491
|
```ts
|
|
447
492
|
const cert = await issueCertificate({
|
|
448
|
-
domains: [
|
|
493
|
+
domains: ["app.customer.com"],
|
|
449
494
|
dnsProvider: platformValidationDns,
|
|
450
|
-
email:
|
|
495
|
+
email: "ops@platform.example",
|
|
451
496
|
mapDnsChallengeRecord: ({ domain }) =>
|
|
452
497
|
`${stableDomainToken(domain)}.acme.platform.example`,
|
|
453
498
|
});
|
|
@@ -473,43 +518,43 @@ this module handles the deploy-side (push values to remote env
|
|
|
473
518
|
files, atomic swap, conditional service reload).
|
|
474
519
|
|
|
475
520
|
```ts
|
|
476
|
-
import { createSecretBroker, inMemoryAdapter } from
|
|
477
|
-
import { hetznerTarget } from
|
|
521
|
+
import { createSecretBroker, inMemoryAdapter } from "@absolutejs/secrets";
|
|
522
|
+
import { hetznerTarget } from "@absolutejs/deploy/hetzner";
|
|
478
523
|
import {
|
|
479
524
|
syncSecretsToDeployments,
|
|
480
525
|
deploymentsUsing,
|
|
481
526
|
type EnvDeployment,
|
|
482
|
-
} from
|
|
527
|
+
} from "@absolutejs/deploy/env";
|
|
483
528
|
|
|
484
529
|
// One source of truth — the SecretBroker. Swap in whatever adapter
|
|
485
530
|
// (env, file, vault, etc.) makes sense for your team.
|
|
486
531
|
const broker = createSecretBroker({
|
|
487
532
|
adapter: inMemoryAdapter({
|
|
488
533
|
initial: {
|
|
489
|
-
DATABASE_URL:
|
|
490
|
-
STRIPE_KEY:
|
|
534
|
+
DATABASE_URL: "postgres://prod-db",
|
|
535
|
+
STRIPE_KEY: "sk_live_old",
|
|
491
536
|
},
|
|
492
537
|
}),
|
|
493
538
|
});
|
|
494
539
|
|
|
495
540
|
// Each deployed service is one EnvDeployment.
|
|
496
|
-
const api = await hetznerTarget({ name:
|
|
497
|
-
const worker = await hetznerTarget({ name:
|
|
541
|
+
const api = await hetznerTarget({ name: "api-1" /* … */ });
|
|
542
|
+
const worker = await hetznerTarget({ name: "worker-1" /* … */ });
|
|
498
543
|
|
|
499
544
|
const deployments: EnvDeployment[] = [
|
|
500
545
|
{
|
|
501
546
|
target: api,
|
|
502
|
-
remotePath:
|
|
503
|
-
secretNames: [
|
|
504
|
-
extras: { NODE_ENV:
|
|
505
|
-
reload:
|
|
547
|
+
remotePath: "/etc/api.env",
|
|
548
|
+
secretNames: ["STRIPE_KEY", "DATABASE_URL"],
|
|
549
|
+
extras: { NODE_ENV: "production", PORT: "3000" },
|
|
550
|
+
reload: "systemctl reload api",
|
|
506
551
|
},
|
|
507
552
|
{
|
|
508
553
|
target: worker,
|
|
509
|
-
remotePath:
|
|
510
|
-
secretNames: [
|
|
511
|
-
extras: { NODE_ENV:
|
|
512
|
-
reload:
|
|
554
|
+
remotePath: "/etc/worker.env",
|
|
555
|
+
secretNames: ["DATABASE_URL"],
|
|
556
|
+
extras: { NODE_ENV: "production" },
|
|
557
|
+
reload: "systemctl restart worker",
|
|
513
558
|
},
|
|
514
559
|
];
|
|
515
560
|
|
|
@@ -517,10 +562,10 @@ const deployments: EnvDeployment[] = [
|
|
|
517
562
|
await syncSecretsToDeployments(broker, deployments);
|
|
518
563
|
|
|
519
564
|
// Rotate STRIPE_KEY everywhere it's used:
|
|
520
|
-
await broker.rotate(
|
|
565
|
+
await broker.rotate("STRIPE_KEY");
|
|
521
566
|
await syncSecretsToDeployments(
|
|
522
567
|
broker,
|
|
523
|
-
deploymentsUsing(
|
|
568
|
+
deploymentsUsing("STRIPE_KEY", deployments),
|
|
524
569
|
);
|
|
525
570
|
```
|
|
526
571
|
|
|
@@ -553,33 +598,35 @@ import {
|
|
|
553
598
|
renewCertificate,
|
|
554
599
|
installCertificateOnTarget,
|
|
555
600
|
importAccount,
|
|
556
|
-
} from
|
|
557
|
-
import { readFile, writeFile } from
|
|
601
|
+
} from "@absolutejs/deploy/tls";
|
|
602
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
558
603
|
|
|
559
|
-
const currentCertificatePem = await readFile(
|
|
604
|
+
const currentCertificatePem = await readFile("./cert.pem", "utf8").catch(
|
|
605
|
+
() => undefined,
|
|
606
|
+
);
|
|
560
607
|
const account = await importAccount(
|
|
561
|
-
JSON.parse(await readFile(
|
|
608
|
+
JSON.parse(await readFile("./account.json", "utf8")),
|
|
562
609
|
);
|
|
563
610
|
|
|
564
611
|
const result = await renewCertificate({
|
|
565
612
|
currentCertificatePem,
|
|
566
|
-
domains: [
|
|
613
|
+
domains: ["api.example.com"],
|
|
567
614
|
dnsProvider: dns,
|
|
568
|
-
email:
|
|
615
|
+
email: "ops@example.com",
|
|
569
616
|
account,
|
|
570
|
-
renewWhenDaysRemaining: 30,
|
|
617
|
+
renewWhenDaysRemaining: 30, // default
|
|
571
618
|
});
|
|
572
619
|
|
|
573
620
|
if (result.renewed) {
|
|
574
621
|
console.log(`renewed (${result.reason})`);
|
|
575
622
|
await installCertificateOnTarget(target, result.certificate, {
|
|
576
|
-
reload:
|
|
623
|
+
reload: "systemctl reload nginx",
|
|
577
624
|
});
|
|
578
|
-
await writeFile(
|
|
579
|
-
await writeFile(
|
|
625
|
+
await writeFile("./cert.pem", result.certificate.certificatePem);
|
|
626
|
+
await writeFile("./key.pem", result.certificate.privateKeyPem);
|
|
580
627
|
} else {
|
|
581
628
|
console.log(
|
|
582
|
-
`still fresh — ${result.inspection.daysRemaining} days remaining
|
|
629
|
+
`still fresh — ${result.inspection.daysRemaining} days remaining`,
|
|
583
630
|
);
|
|
584
631
|
}
|
|
585
632
|
```
|
|
@@ -637,17 +684,17 @@ and traffic cutover remain control-plane responsibilities.
|
|
|
637
684
|
|
|
638
685
|
Compute (`Target` via `createCloudTarget`):
|
|
639
686
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
687
|
+
- `@absolutejs/deploy/digitalocean` — droplets
|
|
688
|
+
- `@absolutejs/deploy/hetzner` — Hetzner Cloud servers
|
|
689
|
+
- `@absolutejs/deploy/linode` — Linode instances
|
|
690
|
+
- `@absolutejs/deploy/vultr` — Vultr instances
|
|
644
691
|
|
|
645
692
|
DNS (`DnsProvider`):
|
|
646
693
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
694
|
+
- `@absolutejs/deploy/cloudflare`
|
|
695
|
+
- `@absolutejs/deploy/digitalocean-dns`
|
|
696
|
+
- `@absolutejs/deploy/hetzner-dns`
|
|
697
|
+
- `@absolutejs/deploy/route53` — narrow client interface; BYO `@aws-sdk/client-route-53` via a 4-line shim, or hand-roll a SigV4 fetch client.
|
|
651
698
|
|
|
652
699
|
All compute adapters share the same `createCloudTarget` machinery
|
|
653
700
|
(find-or-create + wait-for-ready + wait-for-SSH + sshTarget wrap)
|