@absolutejs/deploy 0.22.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 +199 -121
- 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 +180 -0
- package/dist/googlePlay.js +10897 -0
- package/dist/googlePlay.js.map +90 -0
- package/dist/index.d.ts +16 -12
- package/dist/index.js +11460 -14
- package/dist/index.js.map +86 -4
- package/dist/nativeRelease.d.ts +18 -1
- package/dist/nativeRelease.js +53 -13
- package/dist/nativeRelease.js.map +3 -3
- package/package.json +12 -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
|
|
|
@@ -48,6 +47,71 @@ both publication and promotion for intentionally non-publishable local-testing
|
|
|
48
47
|
artifacts. The registry uses the structural BlobStore shape, so Deploy does not
|
|
49
48
|
take a runtime dependency on `@absolutejs/blob` or a cloud SDK.
|
|
50
49
|
|
|
50
|
+
### Google Play distribution (0.23.0)
|
|
51
|
+
|
|
52
|
+
`@absolutejs/deploy/google-play` composes the native registry with Google Play.
|
|
53
|
+
The same BlobStore persists stage receipts before external effects, allowing a
|
|
54
|
+
retry to reuse an unexpired edit or resumable upload and to reconcile a commit
|
|
55
|
+
whose successful response was lost.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { createGooglePlayReleasePublisher } from "@absolutejs/deploy/google-play";
|
|
59
|
+
|
|
60
|
+
export default createGooglePlayReleasePublisher({
|
|
61
|
+
receiptStore: store,
|
|
62
|
+
registry: releases,
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
AbsoluteJS supplies the requested track and rollout intent when the developer
|
|
67
|
+
runs `absolute mobile publish android --play-track internal`. Google
|
|
68
|
+
Application Default Credentials must have Android Publisher access to the app.
|
|
69
|
+
Before Gradle runs, the publisher inspects Play's existing bundles and returns
|
|
70
|
+
the next version code. It persists that allocation by application and complete
|
|
71
|
+
build identity, so retries and promotion to another track reuse the same code.
|
|
72
|
+
AbsoluteJS injects it into the AAB and the publisher verifies the upload response
|
|
73
|
+
returned that exact code. Serialize publication jobs for one application because
|
|
74
|
+
Google Play provides monotonic version codes but no reservation operation.
|
|
75
|
+
The default commit behavior is `ERROR_IF_IN_REVIEW`; opting into cancellation
|
|
76
|
+
of an existing review must therefore be explicit. Production staged rollouts
|
|
77
|
+
use `status: 'inProgress'` with `0 < userFraction < 1`; subsequent calls for the
|
|
78
|
+
same release can increase the fraction, halt or resume it, or mark it completed
|
|
79
|
+
without uploading the AAB again.
|
|
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
|
+
|
|
51
115
|
## Infrastructure providers (0.14.0)
|
|
52
116
|
|
|
53
117
|
Control planes use the normalized `InfrastructureProvider` contract from
|
|
@@ -56,25 +120,27 @@ live in this package beside their deploy targets so providers never become
|
|
|
56
120
|
scattered across host applications.
|
|
57
121
|
|
|
58
122
|
```ts
|
|
59
|
-
import { createDigitalOceanInfrastructureProvider } from
|
|
123
|
+
import { createDigitalOceanInfrastructureProvider } from "@absolutejs/deploy/digitalocean-infrastructure";
|
|
60
124
|
|
|
61
125
|
const provider = createDigitalOceanInfrastructureProvider({
|
|
62
126
|
token: process.env.DIGITALOCEAN_TOKEN!,
|
|
63
|
-
tag:
|
|
64
|
-
regions: [
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
],
|
|
71
137
|
agent: { preferPrivateNetwork: true, port: 8081 },
|
|
72
138
|
});
|
|
73
139
|
|
|
74
140
|
await provider.listNodes();
|
|
75
141
|
await provider.provisionNode({
|
|
76
142
|
idempotencyKey: crypto.randomUUID(),
|
|
77
|
-
name:
|
|
143
|
+
name: "absolutejs-node-01",
|
|
78
144
|
});
|
|
79
145
|
```
|
|
80
146
|
|
|
@@ -100,26 +166,26 @@ and idempotent removal. Provider resource construction stays here instead of
|
|
|
100
166
|
leaking DigitalOcean or GCP APIs into a control plane.
|
|
101
167
|
|
|
102
168
|
```ts
|
|
103
|
-
import { createDigitalOceanIngressProvider } from
|
|
169
|
+
import { createDigitalOceanIngressProvider } from "@absolutejs/deploy/digitalocean-ingress";
|
|
104
170
|
|
|
105
171
|
const ingress = createDigitalOceanIngressProvider({
|
|
106
172
|
token: process.env.DIGITALOCEAN_TOKEN!,
|
|
107
173
|
});
|
|
108
174
|
|
|
109
175
|
await ingress.reconcileIngress({
|
|
110
|
-
name:
|
|
176
|
+
name: "absolutejs-edge",
|
|
111
177
|
idempotencyKey: crypto.randomUUID(),
|
|
112
178
|
backends: [
|
|
113
|
-
{ region:
|
|
114
|
-
{ region:
|
|
179
|
+
{ region: "nyc3", resourceId: "regional-lb-east", priority: 1 },
|
|
180
|
+
{ region: "sfo3", resourceId: "regional-lb-west", priority: 2 },
|
|
115
181
|
],
|
|
116
182
|
listener: {
|
|
117
183
|
port: 443,
|
|
118
|
-
protocol:
|
|
184
|
+
protocol: "https",
|
|
119
185
|
targetPort: 443,
|
|
120
186
|
tlsPassthrough: true,
|
|
121
187
|
},
|
|
122
|
-
healthCheck: { protocol:
|
|
188
|
+
healthCheck: { protocol: "tcp", port: 443 },
|
|
123
189
|
});
|
|
124
190
|
```
|
|
125
191
|
|
|
@@ -132,26 +198,25 @@ operations before advancing dependent resources. Creating an adapter does not
|
|
|
132
198
|
provision anything; only `reconcileIngress()` mutates provider state.
|
|
133
199
|
|
|
134
200
|
```ts
|
|
135
|
-
import {
|
|
136
|
-
createDeployer,
|
|
137
|
-
sshTarget,
|
|
138
|
-
systemdManager,
|
|
139
|
-
} from '@absolutejs/deploy';
|
|
201
|
+
import { createDeployer, sshTarget, systemdManager } from "@absolutejs/deploy";
|
|
140
202
|
|
|
141
203
|
const deployer = createDeployer({
|
|
142
|
-
appName:
|
|
204
|
+
appName: "my-app",
|
|
143
205
|
target: sshTarget({
|
|
144
|
-
host:
|
|
145
|
-
user:
|
|
146
|
-
identity:
|
|
206
|
+
host: "droplet-1.example.com",
|
|
207
|
+
user: "deploy",
|
|
208
|
+
identity: "~/.ssh/id_ed25519",
|
|
147
209
|
}),
|
|
148
|
-
source: { kind:
|
|
149
|
-
env: { PORT:
|
|
150
|
-
processManager: systemdManager({ user:
|
|
151
|
-
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" },
|
|
152
214
|
hooks: {
|
|
153
215
|
onStepStart: ({ name, releaseId }) => console.log(`▸ ${releaseId} ${name}`),
|
|
154
|
-
onLog: (line, stream, step) =>
|
|
216
|
+
onLog: (line, stream, step) =>
|
|
217
|
+
process[stream === "stderr" ? "stderr" : "stdout"].write(
|
|
218
|
+
`[${step}] ${line}\n`,
|
|
219
|
+
),
|
|
155
220
|
},
|
|
156
221
|
});
|
|
157
222
|
|
|
@@ -182,9 +247,9 @@ await deployer.stop();
|
|
|
182
247
|
|
|
183
248
|
### Targets
|
|
184
249
|
|
|
185
|
-
| Adapter
|
|
186
|
-
|
|
187
|
-
| `localTarget({ root, env? })`
|
|
250
|
+
| Adapter | Use |
|
|
251
|
+
| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
252
|
+
| `localTarget({ root, env? })` | Tests, local-dev, and "deploy to the same box" workflows. |
|
|
188
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. |
|
|
189
254
|
|
|
190
255
|
A `Target` is just:
|
|
@@ -192,8 +257,15 @@ A `Target` is just:
|
|
|
192
257
|
```ts
|
|
193
258
|
type Target = {
|
|
194
259
|
description: string;
|
|
195
|
-
exec(
|
|
196
|
-
|
|
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>;
|
|
197
269
|
close?(): Promise<void>;
|
|
198
270
|
};
|
|
199
271
|
```
|
|
@@ -202,10 +274,10 @@ If you can implement those two methods, you can deploy through `@absolutejs/depl
|
|
|
202
274
|
|
|
203
275
|
### Process managers
|
|
204
276
|
|
|
205
|
-
| Manager
|
|
206
|
-
|
|
207
|
-
| `bareManager({ command? })`
|
|
208
|
-
| `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. |
|
|
209
281
|
|
|
210
282
|
A `ProcessManager` is just `{ reload, stop?, status? }`. Wrap PM2, supervisord, runit, or even `@absolutejs/runtime` — whatever your remote uses.
|
|
211
283
|
|
|
@@ -249,25 +321,25 @@ up a droplet by name; creates it via the v2 API if absent; waits for
|
|
|
249
321
|
hand to `createDeployer`.
|
|
250
322
|
|
|
251
323
|
```ts
|
|
252
|
-
import { createDeployer } from
|
|
253
|
-
import { digitalOceanTarget } from
|
|
324
|
+
import { createDeployer } from "@absolutejs/deploy";
|
|
325
|
+
import { digitalOceanTarget } from "@absolutejs/deploy/digitalocean";
|
|
254
326
|
|
|
255
327
|
const target = await digitalOceanTarget({
|
|
256
328
|
token: process.env.DO_TOKEN!,
|
|
257
|
-
name:
|
|
258
|
-
region:
|
|
259
|
-
size:
|
|
260
|
-
image:
|
|
329
|
+
name: "absolutejs-prod-1", // idempotency key
|
|
330
|
+
region: "nyc3",
|
|
331
|
+
size: "s-1vcpu-1gb",
|
|
332
|
+
image: "ubuntu-22-04-x64",
|
|
261
333
|
sshKeys: [process.env.DO_KEY_FINGERPRINT!],
|
|
262
|
-
tags: [
|
|
263
|
-
userData:
|
|
334
|
+
tags: ["absolutejs"],
|
|
335
|
+
userData: "#!/bin/bash\ncurl -fsSL https://bun.sh/install | bash",
|
|
264
336
|
onLog: (line) => console.log(line),
|
|
265
337
|
});
|
|
266
338
|
|
|
267
339
|
console.log(`droplet ${target.dropletId} at ${target.ipv4}`);
|
|
268
340
|
|
|
269
|
-
const deployer = createDeployer({ appName:
|
|
270
|
-
await deployer.deploy({ source: { kind:
|
|
341
|
+
const deployer = createDeployer({ appName: "my-app", target });
|
|
342
|
+
await deployer.deploy({ source: { kind: "directory", path: "./build" } });
|
|
271
343
|
|
|
272
344
|
// Tear it down when you're done:
|
|
273
345
|
await target.destroy();
|
|
@@ -294,22 +366,22 @@ mappings underneath. Hetzner-specific differences: locations
|
|
|
294
366
|
and public-net IPv4/IPv6 are independently toggleable.
|
|
295
367
|
|
|
296
368
|
```ts
|
|
297
|
-
import { createDeployer } from
|
|
298
|
-
import { hetznerTarget } from
|
|
369
|
+
import { createDeployer } from "@absolutejs/deploy";
|
|
370
|
+
import { hetznerTarget } from "@absolutejs/deploy/hetzner";
|
|
299
371
|
|
|
300
372
|
const target = await hetznerTarget({
|
|
301
373
|
token: process.env.HETZNER_TOKEN!,
|
|
302
|
-
name:
|
|
303
|
-
location:
|
|
304
|
-
serverType:
|
|
305
|
-
image:
|
|
374
|
+
name: "absolutejs-prod-1",
|
|
375
|
+
location: "nbg1",
|
|
376
|
+
serverType: "cx22",
|
|
377
|
+
image: "ubuntu-22.04",
|
|
306
378
|
sshKeys: [process.env.HETZNER_KEY_FINGERPRINT!],
|
|
307
|
-
labels: { env:
|
|
308
|
-
userData:
|
|
379
|
+
labels: { env: "prod", team: "platform" },
|
|
380
|
+
userData: "#!/bin/bash\ncurl -fsSL https://bun.sh/install | bash",
|
|
309
381
|
});
|
|
310
382
|
|
|
311
|
-
const deployer = createDeployer({ appName:
|
|
312
|
-
await deployer.deploy({ source: { kind:
|
|
383
|
+
const deployer = createDeployer({ appName: "my-app", target });
|
|
384
|
+
await deployer.deploy({ source: { kind: "directory", path: "./build" } });
|
|
313
385
|
await target.destroy();
|
|
314
386
|
```
|
|
315
387
|
|
|
@@ -327,11 +399,13 @@ implements the shared `DnsProvider` contract from
|
|
|
327
399
|
`@absolutejs/deploy/dns`.
|
|
328
400
|
|
|
329
401
|
```ts
|
|
330
|
-
import { hetznerTarget } from
|
|
331
|
-
import { cloudflareProvider } from
|
|
332
|
-
import { ensureDnsForTarget } from
|
|
402
|
+
import { hetznerTarget } from "@absolutejs/deploy/hetzner";
|
|
403
|
+
import { cloudflareProvider } from "@absolutejs/deploy/cloudflare";
|
|
404
|
+
import { ensureDnsForTarget } from "@absolutejs/deploy/dns";
|
|
333
405
|
|
|
334
|
-
const target = await hetznerTarget({
|
|
406
|
+
const target = await hetznerTarget({
|
|
407
|
+
/* … */
|
|
408
|
+
});
|
|
335
409
|
const dns = cloudflareProvider({
|
|
336
410
|
token: process.env.CLOUDFLARE_TOKEN!,
|
|
337
411
|
zoneId: process.env.CLOUDFLARE_ZONE_ID!,
|
|
@@ -339,7 +413,7 @@ const dns = cloudflareProvider({
|
|
|
339
413
|
|
|
340
414
|
// Idempotent — create or update so the A record points at target.ipv4.
|
|
341
415
|
await ensureDnsForTarget(dns, {
|
|
342
|
-
name:
|
|
416
|
+
name: "api.example.com",
|
|
343
417
|
target,
|
|
344
418
|
ttl: 60,
|
|
345
419
|
proxied: false,
|
|
@@ -374,36 +448,38 @@ against Bun's `crypto.subtle`. The audit surface stays in this
|
|
|
374
448
|
repo.
|
|
375
449
|
|
|
376
450
|
```ts
|
|
377
|
-
import { hetznerTarget } from
|
|
378
|
-
import { cloudflareProvider } from
|
|
379
|
-
import { ensureDnsForTarget } from
|
|
451
|
+
import { hetznerTarget } from "@absolutejs/deploy/hetzner";
|
|
452
|
+
import { cloudflareProvider } from "@absolutejs/deploy/cloudflare";
|
|
453
|
+
import { ensureDnsForTarget } from "@absolutejs/deploy/dns";
|
|
380
454
|
import {
|
|
381
455
|
issueCertificate,
|
|
382
456
|
installCertificateOnTarget,
|
|
383
457
|
LETSENCRYPT_PRODUCTION,
|
|
384
|
-
} from
|
|
458
|
+
} from "@absolutejs/deploy/tls";
|
|
385
459
|
|
|
386
|
-
const target = await hetznerTarget({
|
|
460
|
+
const target = await hetznerTarget({
|
|
461
|
+
/* … */
|
|
462
|
+
});
|
|
387
463
|
const dns = cloudflareProvider({
|
|
388
464
|
token: process.env.CLOUDFLARE_TOKEN!,
|
|
389
465
|
zoneId: process.env.CLOUDFLARE_ZONE_ID!,
|
|
390
466
|
});
|
|
391
467
|
|
|
392
468
|
// 1. Point DNS at the box.
|
|
393
|
-
await ensureDnsForTarget(dns, { name:
|
|
469
|
+
await ensureDnsForTarget(dns, { name: "api.example.com", target, ttl: 60 });
|
|
394
470
|
|
|
395
471
|
// 2. Issue a cert via DNS-01.
|
|
396
472
|
const cert = await issueCertificate({
|
|
397
|
-
domains: [
|
|
473
|
+
domains: ["api.example.com"],
|
|
398
474
|
dnsProvider: dns,
|
|
399
|
-
email:
|
|
475
|
+
email: "ops@example.com",
|
|
400
476
|
directoryUrl: LETSENCRYPT_PRODUCTION,
|
|
401
477
|
onLog: (line) => console.log(line),
|
|
402
478
|
});
|
|
403
479
|
|
|
404
480
|
// 3. Install on the box.
|
|
405
481
|
await installCertificateOnTarget(target, cert, {
|
|
406
|
-
reload:
|
|
482
|
+
reload: "systemctl reload nginx",
|
|
407
483
|
});
|
|
408
484
|
```
|
|
409
485
|
|
|
@@ -414,9 +490,9 @@ map the provider write while leaving propagation checks on the public name:
|
|
|
414
490
|
|
|
415
491
|
```ts
|
|
416
492
|
const cert = await issueCertificate({
|
|
417
|
-
domains: [
|
|
493
|
+
domains: ["app.customer.com"],
|
|
418
494
|
dnsProvider: platformValidationDns,
|
|
419
|
-
email:
|
|
495
|
+
email: "ops@platform.example",
|
|
420
496
|
mapDnsChallengeRecord: ({ domain }) =>
|
|
421
497
|
`${stableDomainToken(domain)}.acme.platform.example`,
|
|
422
498
|
});
|
|
@@ -442,43 +518,43 @@ this module handles the deploy-side (push values to remote env
|
|
|
442
518
|
files, atomic swap, conditional service reload).
|
|
443
519
|
|
|
444
520
|
```ts
|
|
445
|
-
import { createSecretBroker, inMemoryAdapter } from
|
|
446
|
-
import { hetznerTarget } from
|
|
521
|
+
import { createSecretBroker, inMemoryAdapter } from "@absolutejs/secrets";
|
|
522
|
+
import { hetznerTarget } from "@absolutejs/deploy/hetzner";
|
|
447
523
|
import {
|
|
448
524
|
syncSecretsToDeployments,
|
|
449
525
|
deploymentsUsing,
|
|
450
526
|
type EnvDeployment,
|
|
451
|
-
} from
|
|
527
|
+
} from "@absolutejs/deploy/env";
|
|
452
528
|
|
|
453
529
|
// One source of truth — the SecretBroker. Swap in whatever adapter
|
|
454
530
|
// (env, file, vault, etc.) makes sense for your team.
|
|
455
531
|
const broker = createSecretBroker({
|
|
456
532
|
adapter: inMemoryAdapter({
|
|
457
533
|
initial: {
|
|
458
|
-
DATABASE_URL:
|
|
459
|
-
STRIPE_KEY:
|
|
534
|
+
DATABASE_URL: "postgres://prod-db",
|
|
535
|
+
STRIPE_KEY: "sk_live_old",
|
|
460
536
|
},
|
|
461
537
|
}),
|
|
462
538
|
});
|
|
463
539
|
|
|
464
540
|
// Each deployed service is one EnvDeployment.
|
|
465
|
-
const api = await hetznerTarget({ name:
|
|
466
|
-
const worker = await hetznerTarget({ name:
|
|
541
|
+
const api = await hetznerTarget({ name: "api-1" /* … */ });
|
|
542
|
+
const worker = await hetznerTarget({ name: "worker-1" /* … */ });
|
|
467
543
|
|
|
468
544
|
const deployments: EnvDeployment[] = [
|
|
469
545
|
{
|
|
470
546
|
target: api,
|
|
471
|
-
remotePath:
|
|
472
|
-
secretNames: [
|
|
473
|
-
extras: { NODE_ENV:
|
|
474
|
-
reload:
|
|
547
|
+
remotePath: "/etc/api.env",
|
|
548
|
+
secretNames: ["STRIPE_KEY", "DATABASE_URL"],
|
|
549
|
+
extras: { NODE_ENV: "production", PORT: "3000" },
|
|
550
|
+
reload: "systemctl reload api",
|
|
475
551
|
},
|
|
476
552
|
{
|
|
477
553
|
target: worker,
|
|
478
|
-
remotePath:
|
|
479
|
-
secretNames: [
|
|
480
|
-
extras: { NODE_ENV:
|
|
481
|
-
reload:
|
|
554
|
+
remotePath: "/etc/worker.env",
|
|
555
|
+
secretNames: ["DATABASE_URL"],
|
|
556
|
+
extras: { NODE_ENV: "production" },
|
|
557
|
+
reload: "systemctl restart worker",
|
|
482
558
|
},
|
|
483
559
|
];
|
|
484
560
|
|
|
@@ -486,10 +562,10 @@ const deployments: EnvDeployment[] = [
|
|
|
486
562
|
await syncSecretsToDeployments(broker, deployments);
|
|
487
563
|
|
|
488
564
|
// Rotate STRIPE_KEY everywhere it's used:
|
|
489
|
-
await broker.rotate(
|
|
565
|
+
await broker.rotate("STRIPE_KEY");
|
|
490
566
|
await syncSecretsToDeployments(
|
|
491
567
|
broker,
|
|
492
|
-
deploymentsUsing(
|
|
568
|
+
deploymentsUsing("STRIPE_KEY", deployments),
|
|
493
569
|
);
|
|
494
570
|
```
|
|
495
571
|
|
|
@@ -522,33 +598,35 @@ import {
|
|
|
522
598
|
renewCertificate,
|
|
523
599
|
installCertificateOnTarget,
|
|
524
600
|
importAccount,
|
|
525
|
-
} from
|
|
526
|
-
import { readFile, writeFile } from
|
|
601
|
+
} from "@absolutejs/deploy/tls";
|
|
602
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
527
603
|
|
|
528
|
-
const currentCertificatePem = await readFile(
|
|
604
|
+
const currentCertificatePem = await readFile("./cert.pem", "utf8").catch(
|
|
605
|
+
() => undefined,
|
|
606
|
+
);
|
|
529
607
|
const account = await importAccount(
|
|
530
|
-
JSON.parse(await readFile(
|
|
608
|
+
JSON.parse(await readFile("./account.json", "utf8")),
|
|
531
609
|
);
|
|
532
610
|
|
|
533
611
|
const result = await renewCertificate({
|
|
534
612
|
currentCertificatePem,
|
|
535
|
-
domains: [
|
|
613
|
+
domains: ["api.example.com"],
|
|
536
614
|
dnsProvider: dns,
|
|
537
|
-
email:
|
|
615
|
+
email: "ops@example.com",
|
|
538
616
|
account,
|
|
539
|
-
renewWhenDaysRemaining: 30,
|
|
617
|
+
renewWhenDaysRemaining: 30, // default
|
|
540
618
|
});
|
|
541
619
|
|
|
542
620
|
if (result.renewed) {
|
|
543
621
|
console.log(`renewed (${result.reason})`);
|
|
544
622
|
await installCertificateOnTarget(target, result.certificate, {
|
|
545
|
-
reload:
|
|
623
|
+
reload: "systemctl reload nginx",
|
|
546
624
|
});
|
|
547
|
-
await writeFile(
|
|
548
|
-
await writeFile(
|
|
625
|
+
await writeFile("./cert.pem", result.certificate.certificatePem);
|
|
626
|
+
await writeFile("./key.pem", result.certificate.privateKeyPem);
|
|
549
627
|
} else {
|
|
550
628
|
console.log(
|
|
551
|
-
`still fresh — ${result.inspection.daysRemaining} days remaining
|
|
629
|
+
`still fresh — ${result.inspection.daysRemaining} days remaining`,
|
|
552
630
|
);
|
|
553
631
|
}
|
|
554
632
|
```
|
|
@@ -606,17 +684,17 @@ and traffic cutover remain control-plane responsibilities.
|
|
|
606
684
|
|
|
607
685
|
Compute (`Target` via `createCloudTarget`):
|
|
608
686
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
687
|
+
- `@absolutejs/deploy/digitalocean` — droplets
|
|
688
|
+
- `@absolutejs/deploy/hetzner` — Hetzner Cloud servers
|
|
689
|
+
- `@absolutejs/deploy/linode` — Linode instances
|
|
690
|
+
- `@absolutejs/deploy/vultr` — Vultr instances
|
|
613
691
|
|
|
614
692
|
DNS (`DnsProvider`):
|
|
615
693
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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.
|
|
620
698
|
|
|
621
699
|
All compute adapters share the same `createCloudTarget` machinery
|
|
622
700
|
(find-or-create + wait-for-ready + wait-for-SSH + sshTarget wrap)
|