@astrale-os/sdk 0.6.0-beta.21 → 0.6.0-beta.23
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 +0 -4
- package/dist/application/workflow/durable.d.ts +12 -8
- package/dist/deployment/adapter/adapter.d.ts +16 -5
- package/dist/deployment/adapter/define.d.ts +1 -1
- package/dist/deployment/adapter/define.js +40 -1
- package/dist/deployment/adapter/release/command.d.ts +33 -0
- package/dist/deployment/adapter/release/command.js +1 -0
- package/dist/deployment/adapter/release/context.d.ts +3 -4
- package/dist/deployment/adapter/release/index.d.ts +1 -0
- package/dist/deployment/adapter/release/list.d.ts +11 -10
- package/dist/deployment/address/configuration.d.ts +12 -0
- package/dist/deployment/address/configuration.js +11 -0
- package/dist/deployment/address/index.d.ts +3 -3
- package/dist/deployment/address/index.js +2 -3
- package/dist/deployment/address/label.d.ts +5 -5
- package/dist/deployment/address/label.js +5 -5
- package/dist/deployment/address/record.d.ts +21 -0
- package/dist/deployment/address/record.js +34 -0
- package/dist/deployment/address/summary.d.ts +17 -40
- package/dist/deployment/address/summary.js +1 -117
- package/dist/deployment/index.d.ts +1 -1
- package/dist/deployment/verify/index.d.ts +1 -1
- package/dist/deployment/verify/index.js +1 -1
- package/dist/deployment/verify/readiness/index.d.ts +7 -0
- package/dist/deployment/verify/readiness/index.js +17 -0
- package/dist/deployment/worker/address.d.ts +26 -0
- package/dist/deployment/worker/address.js +43 -0
- package/dist/deployment/worker/build.d.ts +60 -0
- package/dist/deployment/worker/build.js +172 -0
- package/dist/deployment/worker/configuration.d.ts +73 -0
- package/dist/deployment/worker/configuration.js +165 -0
- package/dist/deployment/worker/content.d.ts +38 -0
- package/dist/deployment/worker/content.js +169 -0
- package/dist/deployment/worker/index.d.ts +29 -0
- package/dist/deployment/worker/index.js +29 -0
- package/dist/deployment/worker/limits.d.ts +30 -0
- package/dist/deployment/worker/limits.js +30 -0
- package/dist/deployment/worker/record.d.ts +29 -0
- package/dist/deployment/worker/record.js +69 -0
- package/dist/deployment/worker/request.d.ts +118 -0
- package/dist/deployment/worker/request.js +280 -0
- package/dist/deployment/worker/response.d.ts +104 -0
- package/dist/deployment/worker/response.js +213 -0
- package/dist/execution/workflows/run.js +18 -6
- package/dist/tooling/cli/adapter-command.d.ts +35 -0
- package/dist/tooling/cli/adapter-command.js +66 -0
- package/dist/tooling/cli/arguments.d.ts +7 -1
- package/dist/tooling/cli/arguments.js +53 -1
- package/dist/tooling/cli/help.js +11 -8
- package/dist/tooling/cli/immutable-deployment.d.ts +2 -2
- package/dist/tooling/cli/index.d.ts +4 -2
- package/dist/tooling/cli/index.js +3 -2
- package/dist/tooling/cli/list.d.ts +5 -5
- package/dist/tooling/cli/list.js +16 -14
- package/dist/tooling/cli/orchestrate.js +18 -0
- package/dist/tooling/cli/publish/index.d.ts +4 -4
- package/dist/tooling/cli/publish/index.js +8 -9
- package/package.json +5 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { parseDeploymentLabel } from '../address/label.js';
|
|
2
|
+
import { serviceName } from './address.js';
|
|
3
|
+
import { decodeWorkerRecord } from './record.js';
|
|
4
|
+
export const WORKER_DEPLOYMENT_REJECTIONS = Object.freeze([
|
|
5
|
+
'invalid-deployment',
|
|
6
|
+
'address-mismatch',
|
|
7
|
+
'build-mismatch',
|
|
8
|
+
'service-owned-elsewhere',
|
|
9
|
+
'deployment-conflict',
|
|
10
|
+
'deployment-retired',
|
|
11
|
+
'hosting-unavailable',
|
|
12
|
+
'service-unavailable',
|
|
13
|
+
]);
|
|
14
|
+
const STATES = Object.freeze([
|
|
15
|
+
'deploying',
|
|
16
|
+
'active',
|
|
17
|
+
'retiring',
|
|
18
|
+
'retired',
|
|
19
|
+
'failed',
|
|
20
|
+
]);
|
|
21
|
+
/** Which secret states each deployment state reports. */
|
|
22
|
+
const SECRETS_BY_STATE = Object.freeze({
|
|
23
|
+
created: Object.freeze(['bound']),
|
|
24
|
+
reused: Object.freeze(['updated', 'unchanged']),
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Admit the answer of a host's `hosting`: its routing domain. The `addressing: 'opaque'` an older
|
|
28
|
+
* Services answers beside it is admitted and dropped: every Service name is opaque.
|
|
29
|
+
*/
|
|
30
|
+
export function decodeWorkerHosting(input) {
|
|
31
|
+
const value = exact(input, ['routingDomain'], ['addressing'], 'hosting');
|
|
32
|
+
if (typeof value.routingDomain !== 'string' || !isDnsName(value.routingDomain)) {
|
|
33
|
+
invalid('hosting', 'routingDomain');
|
|
34
|
+
}
|
|
35
|
+
if (value.addressing !== undefined && value.addressing !== 'opaque') {
|
|
36
|
+
invalid('hosting', 'addressing');
|
|
37
|
+
}
|
|
38
|
+
return Object.freeze({ routingDomain: value.routingDomain });
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Admit the answer of a deploy exactly: a deployment whose state reports the secret state it
|
|
42
|
+
* pairs with, or a refusal a host can give.
|
|
43
|
+
*/
|
|
44
|
+
export function decodeWorkerDeploymentResponse(input) {
|
|
45
|
+
const value = exact(input, ['kind'], ['state', 'secrets', 'deployment', 'reason', 'message', 'retryAfter'], 'deploy');
|
|
46
|
+
if (value.kind === 'rejected') {
|
|
47
|
+
exact(value, ['kind', 'reason', 'message'], ['retryAfter'], 'refusal');
|
|
48
|
+
if (!WORKER_DEPLOYMENT_REJECTIONS.includes(value.reason)) {
|
|
49
|
+
invalid('refusal', 'reason');
|
|
50
|
+
}
|
|
51
|
+
if (typeof value.message !== 'string' ||
|
|
52
|
+
value.message.length < 1 ||
|
|
53
|
+
value.message.length > 500) {
|
|
54
|
+
invalid('refusal', 'message');
|
|
55
|
+
}
|
|
56
|
+
if (value.retryAfter !== undefined && !isInstant(value.retryAfter)) {
|
|
57
|
+
invalid('refusal', 'retryAfter');
|
|
58
|
+
}
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
kind: 'rejected',
|
|
61
|
+
reason: value.reason,
|
|
62
|
+
message: value.message,
|
|
63
|
+
...(value.retryAfter === undefined ? {} : { retryAfter: value.retryAfter }),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
exact(value, ['kind', 'state', 'secrets', 'deployment'], [], 'deploy');
|
|
67
|
+
if (value.kind !== 'deployed')
|
|
68
|
+
invalid('deploy', 'kind');
|
|
69
|
+
if (value.state !== 'created' && value.state !== 'reused')
|
|
70
|
+
invalid('deploy', 'state');
|
|
71
|
+
const secrets = exact(value.secrets, ['state'], [], 'secrets');
|
|
72
|
+
const allowed = SECRETS_BY_STATE[value.state];
|
|
73
|
+
if (typeof secrets.state !== 'string' || !allowed.includes(secrets.state)) {
|
|
74
|
+
invalid('deploy', 'secrets.state');
|
|
75
|
+
}
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
kind: 'deployed',
|
|
78
|
+
state: value.state,
|
|
79
|
+
secrets: Object.freeze({ state: secrets.state }),
|
|
80
|
+
deployment: decodeWorkerDeploymentSummary(value.deployment),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Admit the answer of a host's `list`: the summaries of the caller's deployments, in its order. */
|
|
84
|
+
export function decodeWorkerDeploymentList(input) {
|
|
85
|
+
const value = exact(input, ['deployments'], [], 'list');
|
|
86
|
+
if (!Array.isArray(value.deployments))
|
|
87
|
+
invalid('list', 'deployments');
|
|
88
|
+
return Object.freeze(value.deployments.map((deployment) => decodeWorkerDeploymentSummary(deployment)));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Admit one summary exactly: node ids and names a person can read, a label whose first part is
|
|
92
|
+
* the Service's name, the URL of that label, a known state, instants, digests, and a record that
|
|
93
|
+
* decodes.
|
|
94
|
+
*/
|
|
95
|
+
export function decodeWorkerDeploymentSummary(input) {
|
|
96
|
+
const value = exact(input, [
|
|
97
|
+
'id',
|
|
98
|
+
'url',
|
|
99
|
+
'label',
|
|
100
|
+
'serviceId',
|
|
101
|
+
'service',
|
|
102
|
+
'project',
|
|
103
|
+
'environment',
|
|
104
|
+
'owner',
|
|
105
|
+
'state',
|
|
106
|
+
'current',
|
|
107
|
+
'createdAt',
|
|
108
|
+
'buildDigest',
|
|
109
|
+
'configuration',
|
|
110
|
+
], ['retiredAt', 'signingKeyId', 'record'], 'deployment');
|
|
111
|
+
for (const key of [
|
|
112
|
+
'id',
|
|
113
|
+
'url',
|
|
114
|
+
'label',
|
|
115
|
+
'serviceId',
|
|
116
|
+
'service',
|
|
117
|
+
'project',
|
|
118
|
+
'environment',
|
|
119
|
+
'owner',
|
|
120
|
+
]) {
|
|
121
|
+
if (typeof value[key] !== 'string' || !isPrintable(value[key]))
|
|
122
|
+
invalid('deployment', key);
|
|
123
|
+
}
|
|
124
|
+
if (value.signingKeyId !== undefined) {
|
|
125
|
+
if (typeof value.signingKeyId !== 'string' || !isPrintable(value.signingKeyId)) {
|
|
126
|
+
invalid('deployment', 'signingKeyId');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
let line;
|
|
130
|
+
try {
|
|
131
|
+
line = parseDeploymentLabel(value.label).line;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
invalid('deployment', 'label');
|
|
135
|
+
}
|
|
136
|
+
if (value.service !== line)
|
|
137
|
+
invalid('deployment', 'service');
|
|
138
|
+
let named;
|
|
139
|
+
try {
|
|
140
|
+
named = serviceName(value.project, value.environment);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
invalid('deployment', 'project');
|
|
144
|
+
}
|
|
145
|
+
if (named !== line)
|
|
146
|
+
invalid('deployment', 'service');
|
|
147
|
+
if (!isDeploymentUrl(value.url, value.label))
|
|
148
|
+
invalid('deployment', 'url');
|
|
149
|
+
if (!STATES.includes(value.state))
|
|
150
|
+
invalid('deployment', 'state');
|
|
151
|
+
if (typeof value.current !== 'boolean')
|
|
152
|
+
invalid('deployment', 'current');
|
|
153
|
+
if (!isInstant(value.createdAt))
|
|
154
|
+
invalid('deployment', 'createdAt');
|
|
155
|
+
if (value.retiredAt !== undefined && !isInstant(value.retiredAt))
|
|
156
|
+
invalid('deployment', 'retiredAt');
|
|
157
|
+
for (const key of ['buildDigest', 'configuration']) {
|
|
158
|
+
if (typeof value[key] !== 'string' || !/^sha256:[0-9a-f]{64}$/u.test(value[key])) {
|
|
159
|
+
invalid('deployment', key);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
let record;
|
|
163
|
+
if (value.record !== undefined) {
|
|
164
|
+
try {
|
|
165
|
+
record = decodeWorkerRecord(value.record);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
invalid('deployment', 'record');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const summary = { ...value };
|
|
172
|
+
delete summary.record;
|
|
173
|
+
return Object.freeze({
|
|
174
|
+
...summary,
|
|
175
|
+
...(record === undefined ? {} : { record }),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** `https://<label>.<routing domain>`, nothing more. */
|
|
179
|
+
function isDeploymentUrl(url, label) {
|
|
180
|
+
const prefix = `https://${label}.`;
|
|
181
|
+
return url.startsWith(prefix) && isDnsName(url.slice(prefix.length));
|
|
182
|
+
}
|
|
183
|
+
/** Text a person can read in a listing: at most 512 characters, none of them a control character. */
|
|
184
|
+
function isPrintable(value) {
|
|
185
|
+
return value.length > 0 && value.length <= 512 && !/[\p{Cc}]/u.test(value);
|
|
186
|
+
}
|
|
187
|
+
function isInstant(value) {
|
|
188
|
+
return (typeof value === 'string' &&
|
|
189
|
+
value.length > 0 &&
|
|
190
|
+
value.length <= 64 &&
|
|
191
|
+
!Number.isNaN(Date.parse(value)));
|
|
192
|
+
}
|
|
193
|
+
function isDnsName(value) {
|
|
194
|
+
return (value.length <= 253 &&
|
|
195
|
+
value.split('.').every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(label)));
|
|
196
|
+
}
|
|
197
|
+
function exact(input, required, optional, label) {
|
|
198
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
199
|
+
invalid(label);
|
|
200
|
+
const value = input;
|
|
201
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
202
|
+
if (typeof key !== 'string' || (!required.includes(key) && !optional.includes(key))) {
|
|
203
|
+
invalid(label, String(key));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const key of required)
|
|
207
|
+
if (!Object.hasOwn(value, key))
|
|
208
|
+
invalid(label, key);
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
function invalid(label, member) {
|
|
212
|
+
throw new TypeError(`The Worker host returned an invalid ${label} answer${member === undefined ? '' : ` (${member})`}.`);
|
|
213
|
+
}
|
|
@@ -86,7 +86,7 @@ function durableContext(input, context, envelope) {
|
|
|
86
86
|
const deadline = Date.now() + serving.stepTimeoutMs;
|
|
87
87
|
const session = input.executor(execution.kernel, execution.id, { deadline, signal });
|
|
88
88
|
try {
|
|
89
|
-
const kernel = keyed(bindSessionOf(session, deadline, signal), await keySequence(execution.id, name));
|
|
89
|
+
const kernel = keyed(bindSessionOf(session, deadline, signal), domain, await keySequence(execution.id, name));
|
|
90
90
|
const output = await execute(Object.freeze({
|
|
91
91
|
kernel,
|
|
92
92
|
query: bindQuery(kernel, domain),
|
|
@@ -171,17 +171,29 @@ function bindSessionOf(session, deadline, signal) {
|
|
|
171
171
|
return bindSession(session, { deadline, signal });
|
|
172
172
|
}
|
|
173
173
|
/**
|
|
174
|
-
* Every `invoke`
|
|
175
|
-
* sequence. The sequence restarts with every attempt, so a retried Step
|
|
176
|
-
* the Kernel answers with
|
|
174
|
+
* Every Mutation and every `invoke` of a durable callable of one Step without its own key carries
|
|
175
|
+
* the next key of the Step's sequence. The sequence restarts with every attempt, so a retried Step
|
|
176
|
+
* replays the same keys: the Kernel answers a Mutation with its receipt and a durable call with the
|
|
177
|
+
* Execution it started, or refuses a request that changed under its key. Any other `invoke` carries
|
|
178
|
+
* no key, because the Kernel accepts one only for a durable Function, and runs again with the Step.
|
|
177
179
|
*/
|
|
178
|
-
function keyed(session, next) {
|
|
180
|
+
function keyed(session, domain, next) {
|
|
179
181
|
const invokeSource = session.invoke;
|
|
180
182
|
const mutateSource = session.mutate;
|
|
181
|
-
const invoke = ((reference, value, options) => invokeSource(reference, value, withKey(options, next)));
|
|
183
|
+
const invoke = ((reference, value, options) => invokeSource(reference, value, declaredDurable(domain, reference) ? withKey(options, next) : options));
|
|
182
184
|
const mutate = ((author, options) => mutateSource(author, withKey(options, next)));
|
|
183
185
|
return Object.freeze({ ...session, invoke, mutate });
|
|
184
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Whether the Domain's Schema closure declares the referenced callable durable. An untyped Path
|
|
189
|
+
* names no declaration and a callable outside the closure has none here: both need their own key.
|
|
190
|
+
*/
|
|
191
|
+
function declaredDurable(domain, reference) {
|
|
192
|
+
if (reference === null || typeof reference !== 'object')
|
|
193
|
+
return false;
|
|
194
|
+
const key = reference.callable;
|
|
195
|
+
return (typeof key === 'string' && domain.callable(key)?.execution?.durable === true);
|
|
196
|
+
}
|
|
185
197
|
function withKey(options, next) {
|
|
186
198
|
if (options?.idempotencyKey !== undefined || options?.body !== undefined)
|
|
187
199
|
return options;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { LoadedEnvironment } from './project.js';
|
|
2
|
+
/**
|
|
3
|
+
* `astrale-domain <adapter> <command> <environment> --json` on stdout: what one provider command
|
|
4
|
+
* of the Environment's adapter answered (adapter-cloudflare: `cloudflare plan` and
|
|
5
|
+
* `cloudflare init`, which provision its host on the operator's own account).
|
|
6
|
+
*/
|
|
7
|
+
export interface AdapterCommandReportV1 {
|
|
8
|
+
readonly format: 'astrale.adapter-command';
|
|
9
|
+
readonly version: 1;
|
|
10
|
+
/** `<adapter name>@<adapter version>` */
|
|
11
|
+
readonly adapter: string;
|
|
12
|
+
readonly command: string;
|
|
13
|
+
readonly environment: string;
|
|
14
|
+
/** 0 done; 1 refused or blocked. */
|
|
15
|
+
readonly exitCode: 0 | 1;
|
|
16
|
+
/** What the adapter's command answered, as it answered it. */
|
|
17
|
+
readonly result: unknown;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* `astrale-domain <adapter> <command> <environment> [--<option> <value>]... [--json]`: run one
|
|
21
|
+
* provider command an adapter declares, on an Environment that adapter deploys. The CLI only
|
|
22
|
+
* routes: it refuses an Environment whose adapter has another name, a command the adapter does
|
|
23
|
+
* not declare and an option the command does not declare, each before any effect, and prints what
|
|
24
|
+
* the command answers. Exit status: the command's (0 done, 1 refused or blocked), 1 when it could
|
|
25
|
+
* not run.
|
|
26
|
+
*/
|
|
27
|
+
export declare function runAdapterCommand(input: {
|
|
28
|
+
readonly adapter: string;
|
|
29
|
+
readonly command: string;
|
|
30
|
+
readonly environment: string;
|
|
31
|
+
readonly definition: LoadedEnvironment;
|
|
32
|
+
readonly options: Readonly<Record<string, string>>;
|
|
33
|
+
readonly json: boolean;
|
|
34
|
+
readonly signal: AbortSignal;
|
|
35
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { error, info, warn } from './log.js';
|
|
2
|
+
/**
|
|
3
|
+
* `astrale-domain <adapter> <command> <environment> [--<option> <value>]... [--json]`: run one
|
|
4
|
+
* provider command an adapter declares, on an Environment that adapter deploys. The CLI only
|
|
5
|
+
* routes: it refuses an Environment whose adapter has another name, a command the adapter does
|
|
6
|
+
* not declare and an option the command does not declare, each before any effect, and prints what
|
|
7
|
+
* the command answers. Exit status: the command's (0 done, 1 refused or blocked), 1 when it could
|
|
8
|
+
* not run.
|
|
9
|
+
*/
|
|
10
|
+
export async function runAdapterCommand(input) {
|
|
11
|
+
const { adapter, parameters } = input.definition.deployment;
|
|
12
|
+
const name = `${adapter.name}@${adapter.version}`;
|
|
13
|
+
if (adapter.name !== input.adapter) {
|
|
14
|
+
error(`Environment ${input.environment} deploys through adapter ${adapter.name}, not ` +
|
|
15
|
+
`${input.adapter}: run \`astrale-domain ${adapter.name} <command> ${input.environment}\`.`);
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
const commands = 'commands' in adapter ? adapter.commands : undefined;
|
|
19
|
+
const command = commands?.[input.command];
|
|
20
|
+
if (command === undefined) {
|
|
21
|
+
const declared = Object.entries(commands ?? {}).map(([candidate, { summary }]) => `${candidate} (${summary})`);
|
|
22
|
+
error(`Adapter ${name} declares no command ${input.command}` +
|
|
23
|
+
(declared.length === 0 ? '.' : `; its commands: ${declared.join('; ')}.`));
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
const declaredOptions = command.options ?? {};
|
|
27
|
+
const unknown = Object.keys(input.options).filter((option) => !Object.hasOwn(declaredOptions, option));
|
|
28
|
+
if (unknown.length > 0) {
|
|
29
|
+
const known = Object.entries(declaredOptions).map(([option, description]) => `--${option} (${description})`);
|
|
30
|
+
error(`${input.adapter} ${input.command} takes no option ${unknown.map((option) => `--${option}`).join(', ')}` +
|
|
31
|
+
(known.length === 0 ? '.' : `; its options: ${known.join('; ')}.`));
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
let answered;
|
|
35
|
+
try {
|
|
36
|
+
answered = await command.run(parameters, {
|
|
37
|
+
environment: input.environment,
|
|
38
|
+
signal: input.signal,
|
|
39
|
+
options: Object.freeze({ ...input.options }),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (cause) {
|
|
43
|
+
error(`${input.adapter} ${input.command} ${input.environment} (${name}) failed: ` +
|
|
44
|
+
(cause instanceof Error ? cause.message : String(cause)));
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
for (const line of answered.warnings ?? [])
|
|
48
|
+
warn(line);
|
|
49
|
+
if (input.json) {
|
|
50
|
+
const report = Object.freeze({
|
|
51
|
+
format: 'astrale.adapter-command',
|
|
52
|
+
version: 1,
|
|
53
|
+
adapter: name,
|
|
54
|
+
command: input.command,
|
|
55
|
+
environment: input.environment,
|
|
56
|
+
exitCode: answered.exitCode,
|
|
57
|
+
result: answered.result,
|
|
58
|
+
});
|
|
59
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
for (const line of answered.lines)
|
|
63
|
+
info(line);
|
|
64
|
+
}
|
|
65
|
+
return answered.exitCode;
|
|
66
|
+
}
|
|
@@ -3,7 +3,8 @@ import { type Version } from '../../platform/versioning/index.js';
|
|
|
3
3
|
export declare const COMMANDS: readonly ['build', 'deploy', 'diff', 'list', 'publish', 'test', 'lint', 'package', 'upgrade', 'yank'];
|
|
4
4
|
export type Command = (typeof COMMANDS)[number];
|
|
5
5
|
export interface ParsedArgs {
|
|
6
|
-
|
|
6
|
+
/** `adapter`: a provider command of an Environment's adapter (`<adapter> <command> <env>`). */
|
|
7
|
+
readonly command: Command | 'adapter';
|
|
7
8
|
readonly env: string;
|
|
8
9
|
readonly fix?: boolean;
|
|
9
10
|
readonly format?: LintReportFormat;
|
|
@@ -19,6 +20,11 @@ export interface ParsedArgs {
|
|
|
19
20
|
readonly allowDirty?: boolean;
|
|
20
21
|
/** `list`: instances to ask what they run, in argument order. */
|
|
21
22
|
readonly instances?: readonly string[];
|
|
23
|
+
/** `<adapter> <command> <env>`: the adapter name and its command. */
|
|
24
|
+
readonly adapter?: string;
|
|
25
|
+
readonly adapterCommand?: string;
|
|
26
|
+
/** `<adapter> <command> <env> --<option> <value>`: the options, by name. */
|
|
27
|
+
readonly options?: Readonly<Record<string, string>>;
|
|
22
28
|
}
|
|
23
29
|
/** Parse the frozen `astrale-domain` command grammar without performing effects. */
|
|
24
30
|
export declare function parseArgs(argv: readonly string[]): ParsedArgs;
|
|
@@ -12,11 +12,18 @@ export const COMMANDS = [
|
|
|
12
12
|
'upgrade',
|
|
13
13
|
'yank',
|
|
14
14
|
];
|
|
15
|
+
/** An adapter name or one of its command and option names, as `defineAdapter` admits them. */
|
|
16
|
+
const PROVIDER_NAME = /^[a-z][a-z0-9-]{0,31}$/u;
|
|
15
17
|
/** Parse the frozen `astrale-domain` command grammar without performing effects. */
|
|
16
18
|
export function parseArgs(argv) {
|
|
17
19
|
const [command, ...rest] = argv;
|
|
18
20
|
if (command === 'upgrade')
|
|
19
21
|
return parseUpgrade(rest);
|
|
22
|
+
if (command !== undefined && !COMMANDS.includes(command)) {
|
|
23
|
+
const provider = parseAdapterCommand(command, rest);
|
|
24
|
+
if (provider !== undefined)
|
|
25
|
+
return provider;
|
|
26
|
+
}
|
|
20
27
|
const fix = rest.includes('--fix');
|
|
21
28
|
const deployOnly = rest.includes('--deploy-only');
|
|
22
29
|
const json = rest.includes('--json');
|
|
@@ -84,7 +91,7 @@ export function parseArgs(argv) {
|
|
|
84
91
|
command !== 'list' &&
|
|
85
92
|
command !== 'publish' &&
|
|
86
93
|
command !== 'yank') {
|
|
87
|
-
throw new Error('`--json` is only valid for `build`, `deploy`, `diff`, `list`, `publish` or
|
|
94
|
+
throw new Error('`--json` is only valid for `build`, `deploy`, `diff`, `list`, `publish`, `yank` or an adapter command.');
|
|
88
95
|
}
|
|
89
96
|
if (allowDirty && command !== 'publish') {
|
|
90
97
|
throw new Error('`--allow-dirty` is only valid for `publish`.');
|
|
@@ -222,6 +229,51 @@ function refuseDeployOnly(command, environment) {
|
|
|
222
229
|
throw new Error(`${refusal} Run \`astrale-domain ${command} ${environment}\` without it, then install the ` +
|
|
223
230
|
`URL it prints with \`${installCommand()}\`.`);
|
|
224
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* `astrale-domain <adapter> <command> <environment> [--<option> <value>]... [--json]`: a provider
|
|
234
|
+
* command, routed once the Project is loaded to the adapter of that Environment. Anything that is
|
|
235
|
+
* not exactly an adapter name, a command and an Environment is no command at all (undefined).
|
|
236
|
+
*/
|
|
237
|
+
function parseAdapterCommand(adapter, args) {
|
|
238
|
+
if (!PROVIDER_NAME.test(adapter))
|
|
239
|
+
return undefined;
|
|
240
|
+
const positionals = [];
|
|
241
|
+
const options = {};
|
|
242
|
+
let json = false;
|
|
243
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
244
|
+
const argument = args[index];
|
|
245
|
+
if (argument === '--json') {
|
|
246
|
+
json = true;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (!argument.startsWith('-')) {
|
|
250
|
+
positionals.push(argument);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const equals = argument.indexOf('=');
|
|
254
|
+
const name = argument.slice(2, equals === -1 ? undefined : equals);
|
|
255
|
+
if (!argument.startsWith('--') || !PROVIDER_NAME.test(name)) {
|
|
256
|
+
throw new Error(`Unknown flag "${argument}".`);
|
|
257
|
+
}
|
|
258
|
+
if (Object.hasOwn(options, name))
|
|
259
|
+
throw new Error(`--${name} is given twice.`);
|
|
260
|
+
options[name] =
|
|
261
|
+
equals === -1
|
|
262
|
+
? requiredValue(`--${name}`, args[++index])
|
|
263
|
+
: requiredValue(`--${name}`, argument.slice(equals + 1));
|
|
264
|
+
}
|
|
265
|
+
const [adapterCommand, env] = positionals;
|
|
266
|
+
if (positionals.length !== 2 || !PROVIDER_NAME.test(adapterCommand))
|
|
267
|
+
return undefined;
|
|
268
|
+
return {
|
|
269
|
+
command: 'adapter',
|
|
270
|
+
env: env,
|
|
271
|
+
adapter,
|
|
272
|
+
adapterCommand: adapterCommand,
|
|
273
|
+
options,
|
|
274
|
+
...(json ? { json: true } : {}),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
225
277
|
function parseUpgrade(args) {
|
|
226
278
|
let channel = 'beta';
|
|
227
279
|
let check = false;
|
package/dist/tooling/cli/help.js
CHANGED
|
@@ -30,11 +30,15 @@ Commands:
|
|
|
30
30
|
package Prepare public declarations for publication.
|
|
31
31
|
upgrade Update Astrale dependencies through pnpm (default: beta).
|
|
32
32
|
yank <version> Take a published version out of resolution, or put it back.
|
|
33
|
+
<adapter> <command> <environment>
|
|
34
|
+
Run a command the Environment's adapter declares for its provider,
|
|
35
|
+
such as cloudflare plan production or cloudflare init production.
|
|
33
36
|
|
|
34
37
|
Options:
|
|
35
38
|
-h, --help Show help.
|
|
36
39
|
|
|
37
|
-
Run "astrale-domain <command> --help" for command-specific help.
|
|
40
|
+
Run "astrale-domain <command> --help" for command-specific help. An adapter command that
|
|
41
|
+
does not exist names the commands and options the Environment's adapter declares.
|
|
38
42
|
`;
|
|
39
43
|
const COMMAND_HELP = Object.freeze({
|
|
40
44
|
upgrade: `Update existing Astrale dependencies in a pnpm project or workspace.
|
|
@@ -140,19 +144,18 @@ Behavior:
|
|
|
140
144
|
The registry and the instances are read through the Astrale CLI (${MINIMUM_INSTALLATIONS_CLI_VERSION} or
|
|
141
145
|
newer) with the caller's identity, only once a deployment is listed.
|
|
142
146
|
A registry that cannot be read fails the listing; an unknown instance never does.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
dispatch namespace reads them from its calls dataset, and without Account Analytics
|
|
147
|
-
Read lists its deployments without last calls or expiries.
|
|
147
|
+
Admin's Services and a Cloudflare dispatch namespace keep every deployment until its
|
|
148
|
+
owner retires it, and report no last call or expiry; LAST CALL and EXPIRES show one
|
|
149
|
+
only when an older adapter reports it.
|
|
148
150
|
Listing changes nothing, at any host, registry or instance.
|
|
149
151
|
An Environment in legacy direct mode, refused by deploy, or whose adapter lists no
|
|
150
152
|
deployments is reported on stderr and, with --json, under unlisted. A deployment
|
|
151
153
|
whose record this SDK does not admit is still listed, from what its host knows.
|
|
152
154
|
With --json, each deployment carries its installedOn and its call target: the path of
|
|
153
155
|
its node on Admin's Services, for astrale call "<path>.method.setSecret" --admin, or the
|
|
154
|
-
namespace and script of a
|
|
155
|
-
|
|
156
|
+
namespace and script of a dispatch namespace, which the hosting kit
|
|
157
|
+
(@astrale-os/adapter-cloudflare/hosting) retires; installations says what of
|
|
158
|
+
INSTALLED ON is unknown.
|
|
156
159
|
Exit status: 0 listed, 1 a host or the registry could not be read (with --json,
|
|
157
160
|
nothing on stdout), 2 usage error.
|
|
158
161
|
`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DeploymentCommitV1,
|
|
1
|
+
import type { DeploymentCommitV1, DeploymentConfiguration } from '../../deployment/address/index.js';
|
|
2
2
|
import type { Adapter, Build, Bundle, LegacyAdapter, Placement, Release, ResolvedRuntimeReference } from '../../deployment/index.js';
|
|
3
3
|
import type { DeployResultV1 } from './deploy-result.js';
|
|
4
4
|
/**
|
|
@@ -15,7 +15,7 @@ export interface ImmutableDeploymentPlan {
|
|
|
15
15
|
readonly identity?: string;
|
|
16
16
|
readonly bundle: Bundle;
|
|
17
17
|
readonly secrets: Readonly<Record<string, string>>;
|
|
18
|
-
readonly configuration:
|
|
18
|
+
readonly configuration: DeploymentConfiguration;
|
|
19
19
|
readonly placement: Placement;
|
|
20
20
|
/** `<line>-<content>`: the deployment's DNS label and provider script. */
|
|
21
21
|
readonly label: string;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@astrale-os/sdk/cli` — the `astrale-domain` CLI
|
|
3
|
-
* (build | deploy | diff | list | publish | test | lint | package | upgrade | yank
|
|
4
|
-
* the deploy mode each command selects for an Environment (CT33), and the explicit
|
|
3
|
+
* (build | deploy | diff | list | publish | test | lint | package | upgrade | yank, and the
|
|
4
|
+
* provider commands adapters declare), its JSON results, the deploy mode each command selects for an Environment (CT33), and the explicit
|
|
5
|
+
* dotenv boundary.
|
|
5
6
|
*
|
|
6
7
|
* Node-only: the CLI imports `node:fs`/`node:module`/`node:url` and runs under
|
|
7
8
|
* Bun (it imports the project's `astrale.config.ts` directly). This subpath is
|
|
@@ -14,6 +15,7 @@ export { run } from './run.js';
|
|
|
14
15
|
export type { BuildResultV1 } from './build.js';
|
|
15
16
|
export type { DeployResultV1, DeploymentSecretsState } from './deploy-result.js';
|
|
16
17
|
export type { DiffChangeV1, DiffReportV1 } from './diff/index.js';
|
|
18
|
+
export type { AdapterCommandReportV1 } from './adapter-command.js';
|
|
17
19
|
export type { InstallationSourcesV1, InstallationUnknownReason, InstalledOnV1, InstanceSourceV1, ListedDeploymentV1, ListResultV1, UnlistedEnvironmentV1, } from './list.js';
|
|
18
20
|
export type { PublishReportV1 } from './publish/index.js';
|
|
19
21
|
export type { YankReportV1 } from './yank.js';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@astrale-os/sdk/cli` — the `astrale-domain` CLI
|
|
3
|
-
* (build | deploy | diff | list | publish | test | lint | package | upgrade | yank
|
|
4
|
-
* the deploy mode each command selects for an Environment (CT33), and the explicit
|
|
3
|
+
* (build | deploy | diff | list | publish | test | lint | package | upgrade | yank, and the
|
|
4
|
+
* provider commands adapters declare), its JSON results, the deploy mode each command selects for an Environment (CT33), and the explicit
|
|
5
|
+
* dotenv boundary.
|
|
5
6
|
*
|
|
6
7
|
* Node-only: the CLI imports `node:fs`/`node:module`/`node:url` and runs under
|
|
7
8
|
* Bun (it imports the project's `astrale.config.ts` directly). This subpath is
|
|
@@ -26,11 +26,11 @@ export interface ListResultV1 {
|
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* One deployment: what its host knows (`DeploymentSummaryV1`), the Environment it was listed for
|
|
29
|
-
* and its adapter, its computed name, and its call target.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
29
|
+
* and its adapter, its computed name, and its call target. Admin's Services (adapter-astrale) keeps
|
|
30
|
+
* every deployment until it is retired: its rows are `published` and say whether their Service
|
|
31
|
+
* serves them (`current`). A Cloudflare dispatch namespace (adapter-cloudflare) keeps no
|
|
32
|
+
* retention, and knows a deployment's creation instant and key only while its script exists. A
|
|
33
|
+
* last call and an expiry are printed only when an older adapter still reports them.
|
|
34
34
|
*/
|
|
35
35
|
export type ListedDeploymentV1 = Omit<DeploymentSummaryV1, 'record'> & {
|
|
36
36
|
/** The Project Environment whose line holds the deployment, as its host listed it. */
|
package/dist/tooling/cli/list.js
CHANGED
|
@@ -189,13 +189,13 @@ function listedDeployment(hosted, publications, installations) {
|
|
|
189
189
|
line: deployment.line,
|
|
190
190
|
owner: deployment.owner,
|
|
191
191
|
state: deployment.state,
|
|
192
|
-
retention: deployment.retention,
|
|
193
|
-
createdAt: deployment.createdAt,
|
|
192
|
+
...(deployment.retention === undefined ? {} : { retention: deployment.retention }),
|
|
193
|
+
...(deployment.createdAt === undefined ? {} : { createdAt: deployment.createdAt }),
|
|
194
194
|
...(deployment.lastCallAt === undefined ? {} : { lastCallAt: deployment.lastCallAt }),
|
|
195
195
|
...(deployment.expiresAt === undefined ? {} : { expiresAt: deployment.expiresAt }),
|
|
196
196
|
...(deployment.retiredAt === undefined ? {} : { retiredAt: deployment.retiredAt }),
|
|
197
197
|
...(deployment.retirement === undefined ? {} : { retirement: deployment.retirement }),
|
|
198
|
-
signingKeyId: deployment.signingKeyId,
|
|
198
|
+
...(deployment.signingKeyId === undefined ? {} : { signingKeyId: deployment.signingKeyId }),
|
|
199
199
|
record,
|
|
200
200
|
...(recordRefusal === undefined ? {} : { recordRefusal }),
|
|
201
201
|
...(deployment.current === undefined ? {} : { current: deployment.current }),
|
|
@@ -203,11 +203,11 @@ function listedDeployment(hosted, publications, installations) {
|
|
|
203
203
|
});
|
|
204
204
|
}
|
|
205
205
|
/**
|
|
206
|
-
* Why a deployment its host keeps no record of has none: the adapter lists it without `record`, as
|
|
207
|
-
*
|
|
206
|
+
* Why a deployment its host keeps no record of has none: the adapter lists it without `record`, as
|
|
207
|
+
* a dispatch namespace lists a label it retired and never activated.
|
|
208
208
|
*/
|
|
209
|
-
const NO_RECORD = 'Its host keeps no record of it: it
|
|
210
|
-
'
|
|
209
|
+
const NO_RECORD = 'Its host keeps no record of it: it was never activated (a deploy still in progress, one that ' +
|
|
210
|
+
'stopped before activating it, or a label retired before it served).';
|
|
211
211
|
/** Why an admitted record does not describe the listed deployment, if it does not. */
|
|
212
212
|
function describesAnother(record, label, listing) {
|
|
213
213
|
if (record.providerScript !== label) {
|
|
@@ -259,18 +259,21 @@ function admitListed(deployment, adapter) {
|
|
|
259
259
|
throw new TypeError(`Adapter ${adapter} listed an invalid deployment (${member}).`);
|
|
260
260
|
};
|
|
261
261
|
const value = members(deployment) ?? invalid('deployment');
|
|
262
|
-
for (const key of ['id', 'url', 'label', 'line', 'owner'
|
|
262
|
+
for (const key of ['id', 'url', 'label', 'line', 'owner']) {
|
|
263
263
|
if (!text(value[key]))
|
|
264
264
|
invalid(key);
|
|
265
265
|
}
|
|
266
|
-
for (const key of ['lastCallAt', 'expiresAt', 'retiredAt']) {
|
|
266
|
+
for (const key of ['createdAt', 'signingKeyId', 'lastCallAt', 'expiresAt', 'retiredAt']) {
|
|
267
267
|
if (value[key] !== undefined && !text(value[key]))
|
|
268
268
|
invalid(key);
|
|
269
269
|
}
|
|
270
270
|
if (!STATES.includes(value.state))
|
|
271
271
|
invalid('state');
|
|
272
|
-
if (value.retention !==
|
|
272
|
+
if (value.retention !== undefined &&
|
|
273
|
+
value.retention !== 'preview' &&
|
|
274
|
+
value.retention !== 'published') {
|
|
273
275
|
invalid('retention');
|
|
276
|
+
}
|
|
274
277
|
if (value.retirement !== undefined &&
|
|
275
278
|
value.retirement !== 'expired' &&
|
|
276
279
|
value.retirement !== 'withdrawn') {
|
|
@@ -352,10 +355,9 @@ function shortDigest(digest) {
|
|
|
352
355
|
return `${digest.slice(0, 'sha256:'.length + 7)}…`;
|
|
353
356
|
}
|
|
354
357
|
/**
|
|
355
|
-
* When a deployment stops serving: `never` once published, else
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
* retired it yet; for a retired deployment, why it was retired.
|
|
358
|
+
* When a deployment stops serving: `never` once published, else the expiry an older adapter
|
|
359
|
+
* reports (`due` once that instant passed), else `—` (a dispatch namespace keeps a deployment
|
|
360
|
+
* until its owner retires it); for a retired deployment, why it was retired.
|
|
359
361
|
*/
|
|
360
362
|
function expiry(deployment, now) {
|
|
361
363
|
if (deployment.state === 'retiring')
|