@astrale-os/sdk 0.6.0-beta.21 → 0.6.0-beta.22
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 +8 -1
- 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/index.d.ts +1 -0
- package/dist/deployment/adapter/release/list.d.ts +11 -10
- package/dist/deployment/address/index.d.ts +1 -1
- package/dist/deployment/address/index.js +0 -1
- 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/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/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 +1 -1
package/README.md
CHANGED
|
@@ -345,10 +345,6 @@ did not derive.
|
|
|
345
345
|
- **Record.** `acceptDeploymentRecord` admits a `DeploymentRecordV1` only when its provider
|
|
346
346
|
script is the label that its own fields derive. A host admits the record a deploy declares with
|
|
347
347
|
it before storing it, so every stored record decodes.
|
|
348
|
-
- **Platform summary.** `acceptPlatformDeploymentSummary` admits the `summary:<label>` a platform
|
|
349
|
-
dispatch namespace keeps beside `record:<label>`: the deployment's `DeploymentSummaryV1` without
|
|
350
|
-
its record, owned by `platform`, named by its label, with no provider version id and no
|
|
351
|
-
`lastCallAt` or `expiresAt`. `encodePlatformDeploymentSummary` gives its one canonical text.
|
|
352
348
|
|
|
353
349
|
```ts
|
|
354
350
|
import {
|
|
@@ -36,25 +36,29 @@ export interface DurableExecution {
|
|
|
36
36
|
*/
|
|
37
37
|
export interface DurableStepAuthority<DomainValue extends Domain = Domain> {
|
|
38
38
|
/**
|
|
39
|
-
* Kernel Session acting as the Domain. Each `invoke` without an explicit
|
|
40
|
-
* one derived from the Execution, the Step and its position, so a
|
|
41
|
-
* durable child Execution.
|
|
39
|
+
* Kernel Session acting as the Domain. Each `invoke` of a durable callable without an explicit
|
|
40
|
+
* `idempotencyKey` carries one derived from the Execution, the Step and its position, so a
|
|
41
|
+
* retried Step reaches the same durable child Execution. An `invoke` of any other callable
|
|
42
|
+
* carries no key, which the Kernel would refuse, and runs again when the Step is retried.
|
|
42
43
|
*/
|
|
43
44
|
readonly kernel: BoundClientSession;
|
|
44
45
|
readonly query: QueryExecutor<DomainValue>;
|
|
45
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Each Mutation carries a key derived from the Execution, the Step and its position. The Kernel
|
|
48
|
+
* commits a receipt with the write and answers a retried Step with its first result.
|
|
49
|
+
*/
|
|
46
50
|
readonly mutate: MutationExecutor<DomainValue>;
|
|
47
51
|
}
|
|
48
52
|
export interface DurableStep<DomainValue extends Domain = Domain> {
|
|
49
53
|
/**
|
|
50
54
|
* Run `execute` once and journal its portable output; a replay returns the journaled output. A
|
|
51
|
-
* Step may run again when its outcome was not journaled
|
|
55
|
+
* Step may run again when its outcome was not journaled: its Mutations and durable calls are
|
|
56
|
+
* answered from their keys, and any other effect must be idempotent.
|
|
52
57
|
*/
|
|
53
58
|
run<Output>(id: string, execute: (authority: DurableStepAuthority<DomainValue>) => Output | Promise<Output>): Promise<Output>;
|
|
54
59
|
/**
|
|
55
|
-
* One journaled Mutation.
|
|
56
|
-
*
|
|
57
|
-
* own effect instead of repeating it.
|
|
60
|
+
* One journaled Mutation. A Step replayed after its commit receives the result of that commit
|
|
61
|
+
* from the Kernel's receipt instead of writing again.
|
|
58
62
|
*/
|
|
59
63
|
mutate<Input, Output>(id: string, mutation: Mutation<DomainValue, Input, Output>, input: Input): Promise<Output>;
|
|
60
64
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FrozenConfigurationV1 } from '../address/index.js';
|
|
2
2
|
import type { Bundler } from './bundle/index.js';
|
|
3
3
|
import type { ArtifactSource, DeployContext, DeployResult, PrepareContext } from './legacy/index.js';
|
|
4
|
-
import type { ConfigureContext, ListContext, ListedDeployment, Placement, PlacementContext, ReleaseDeployContext, ReleaseDeployResult, RetainContext } from './release/index.js';
|
|
4
|
+
import type { AdapterCommand, ConfigureContext, ListContext, ListedDeployment, Placement, PlacementContext, ReleaseDeployContext, ReleaseDeployResult, RetainContext } from './release/index.js';
|
|
5
5
|
/** What `admit` receives when `defineProject` binds an adapter to one Environment. */
|
|
6
6
|
export interface AdapterEnvironmentContext {
|
|
7
7
|
readonly environment: string;
|
|
@@ -44,6 +44,12 @@ export interface Adapter<Parameters extends object = object> {
|
|
|
44
44
|
* transient. Absent: the registry marks it (Admin's Services).
|
|
45
45
|
*/
|
|
46
46
|
retain?(parameters: Parameters, context: RetainContext): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Provider commands, by name: `astrale-domain <adapter name> <command> <environment>` runs one
|
|
49
|
+
* on an Environment this adapter deploys (adapter-cloudflare's `plan` and `init` provision the
|
|
50
|
+
* host on the operator's own account). Every effect is the adapter's; the CLI only routes.
|
|
51
|
+
*/
|
|
52
|
+
readonly commands?: Readonly<Record<string, AdapterCommand<Parameters>>>;
|
|
47
53
|
/**
|
|
48
54
|
* @deprecated Legacy direct-mode path (CT33) only: place a build at a stable target. Replacement:
|
|
49
55
|
* `bundler`, `configure` and `placement`. Deleted with the legacy path (D10).
|
|
@@ -67,6 +73,7 @@ export interface AdapterInput<Parameters extends object> {
|
|
|
67
73
|
deployRelease(parameters: Parameters, context: ReleaseDeployContext): Promise<ReleaseDeployResult>;
|
|
68
74
|
list?(parameters: Parameters, context: ListContext): Promise<readonly ListedDeployment[]>;
|
|
69
75
|
retain?(parameters: Parameters, context: RetainContext): Promise<void>;
|
|
76
|
+
readonly commands?: Readonly<Record<string, AdapterCommand<Parameters>>>;
|
|
70
77
|
/** @deprecated Legacy direct-mode path (CT33) only. Replacement: `configure`, `placement`. */
|
|
71
78
|
prepare?(parameters: Parameters, context: PrepareContext): Promise<ArtifactSource>;
|
|
72
79
|
/** @deprecated Legacy direct-mode path (CT33) only. Replacement: `deployRelease`. */
|
|
@@ -3,7 +3,7 @@ import type { Adapter, AdapterInput } from './adapter.js';
|
|
|
3
3
|
import type { LegacyAdapter, LegacyAdapterInput } from './legacy/index.js';
|
|
4
4
|
/**
|
|
5
5
|
* Capture one receiver-bound provider adapter (adapter interface v2): a required `bundler`,
|
|
6
|
-
* `configure`, `placement` and `deployRelease`, optionally `list` and `
|
|
6
|
+
* `configure`, `placement` and `deployRelease`, optionally `list`, `retain` and provider `commands`, and `prepare` with
|
|
7
7
|
* `deploy` only while it also serves the legacy direct-mode path.
|
|
8
8
|
*/
|
|
9
9
|
export declare function defineAdapter<Parameters extends object>(input: AdapterInput<Parameters>): Adapter<Parameters>;
|
|
@@ -12,6 +12,7 @@ const FIELDS = [
|
|
|
12
12
|
'deployRelease',
|
|
13
13
|
'list',
|
|
14
14
|
'retain',
|
|
15
|
+
'commands',
|
|
15
16
|
'prepare',
|
|
16
17
|
'deploy',
|
|
17
18
|
'secretsFile',
|
|
@@ -22,7 +23,9 @@ const OPTIONAL_RELEASE_OPERATIONS = ['list', 'retain'];
|
|
|
22
23
|
/** The operations of the legacy direct-mode path. */
|
|
23
24
|
const STABLE_TARGET_OPERATIONS = ['prepare', 'deploy'];
|
|
24
25
|
/** Every operation an adapter may declare, in the order an admitted adapter lists them. */
|
|
25
|
-
const OPERATIONS = FIELDS.filter((key) => key !== 'name' && key !== 'version' && key !== 'bundler');
|
|
26
|
+
const OPERATIONS = FIELDS.filter((key) => key !== 'name' && key !== 'version' && key !== 'bundler' && key !== 'commands');
|
|
27
|
+
/** A provider command name, `astrale-domain <adapter> <command>`, and an option name. */
|
|
28
|
+
const COMMAND_NAME = /^[a-z][a-z0-9-]{0,31}$/u;
|
|
26
29
|
export function defineAdapter(input) {
|
|
27
30
|
const value = record(input, 'Adapter definition');
|
|
28
31
|
if (Reflect.ownKeys(value).some((key) => !FIELDS.includes(key))) {
|
|
@@ -47,12 +50,16 @@ export function defineAdapter(input) {
|
|
|
47
50
|
key,
|
|
48
51
|
capture(receiver, key),
|
|
49
52
|
]));
|
|
53
|
+
if (value.commands !== undefined && !release) {
|
|
54
|
+
throw new TypeError(`Adapter ${name} declares commands without adapter interface v2.`);
|
|
55
|
+
}
|
|
50
56
|
const adapter = Object.freeze({
|
|
51
57
|
kind: 'adapter',
|
|
52
58
|
name,
|
|
53
59
|
version,
|
|
54
60
|
bundler,
|
|
55
61
|
...operations,
|
|
62
|
+
...(value.commands === undefined ? {} : { commands: admitCommands(name, value.commands) }),
|
|
56
63
|
});
|
|
57
64
|
admittedAdapters.add(adapter);
|
|
58
65
|
return adapter;
|
|
@@ -131,6 +138,38 @@ function missingBundler(name) {
|
|
|
131
138
|
'to the adapter as its Bundle (a Cloudflare Worker adapter declares ' +
|
|
132
139
|
'`cloudflareWorkerBundler` from `@astrale-os/adapter-cloudflare/build`).');
|
|
133
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Capture the provider commands of one adapter: each a lower-case name, a one-line `summary`,
|
|
143
|
+
* optional `options` (name to one line of help) and a receiver-bound `run`.
|
|
144
|
+
*/
|
|
145
|
+
function admitCommands(adapter, input) {
|
|
146
|
+
const commands = record(input, `Adapter ${adapter} commands`);
|
|
147
|
+
const admitted = {};
|
|
148
|
+
for (const name of Reflect.ownKeys(commands)) {
|
|
149
|
+
if (typeof name !== 'string' || !COMMAND_NAME.test(name)) {
|
|
150
|
+
throw new TypeError(`Adapter ${adapter} declares an invalid command name.`);
|
|
151
|
+
}
|
|
152
|
+
const command = record(commands[name], `Adapter ${adapter} command ${name}`);
|
|
153
|
+
const options = command.options === undefined ? {} : record(command.options, 'Command options');
|
|
154
|
+
if (Reflect.ownKeys(command).some((key) => key !== 'summary' && key !== 'options' && key !== 'run') ||
|
|
155
|
+
typeof command.summary !== 'string' ||
|
|
156
|
+
command.summary.length === 0 ||
|
|
157
|
+
typeof command.run !== 'function' ||
|
|
158
|
+
Reflect.ownKeys(options).some((key) => typeof key !== 'string' ||
|
|
159
|
+
!COMMAND_NAME.test(key) ||
|
|
160
|
+
key === 'json' ||
|
|
161
|
+
typeof options[key] !== 'string')) {
|
|
162
|
+
throw new TypeError(`Adapter ${adapter} command ${name} must declare a summary, string options other than ` +
|
|
163
|
+
'json and run.');
|
|
164
|
+
}
|
|
165
|
+
admitted[name] = Object.freeze({
|
|
166
|
+
summary: command.summary,
|
|
167
|
+
options: Object.freeze({ ...options }),
|
|
168
|
+
run: command.run.bind(command),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return Object.freeze(admitted);
|
|
172
|
+
}
|
|
134
173
|
function invalidFields() {
|
|
135
174
|
throw new TypeError('Adapter definition contains missing or unknown fields.');
|
|
136
175
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One command an adapter declares for its provider (`astrale-domain <adapter> <command>
|
|
3
|
+
* <environment>`), such as adapter-cloudflare's `plan` and `init`, which provision the host on the
|
|
4
|
+
* operator's own account. The CLI only routes: it loads the Project, selects the Environment, checks
|
|
5
|
+
* that its adapter is the one named and declares the command, and admits the options the command
|
|
6
|
+
* declares; everything the command does is the adapter's.
|
|
7
|
+
*/
|
|
8
|
+
export interface AdapterCommand<Parameters extends object = object> {
|
|
9
|
+
/** One line: what the command does, for help. */
|
|
10
|
+
readonly summary: string;
|
|
11
|
+
/** The options it takes, `--<name> <value>`, each with one line of help. */
|
|
12
|
+
readonly options?: Readonly<Record<string, string>>;
|
|
13
|
+
run(parameters: Parameters, context: AdapterCommandContext): Promise<AdapterCommandResult>;
|
|
14
|
+
}
|
|
15
|
+
/** What an adapter command receives. */
|
|
16
|
+
export interface AdapterCommandContext {
|
|
17
|
+
readonly environment: string;
|
|
18
|
+
/** Parent-owned lifecycle cancellation. */
|
|
19
|
+
readonly signal: AbortSignal;
|
|
20
|
+
/** The options given, each one the command declares. */
|
|
21
|
+
readonly options: Readonly<Record<string, string>>;
|
|
22
|
+
}
|
|
23
|
+
/** What an adapter command answers; a command that cannot run rejects instead. */
|
|
24
|
+
export interface AdapterCommandResult {
|
|
25
|
+
/** 0 done; 1 refused or blocked (the command ran, and says why in `warnings`). */
|
|
26
|
+
readonly exitCode: 0 | 1;
|
|
27
|
+
/** Lines for a person, printed on stdout without `--json`. */
|
|
28
|
+
readonly lines: readonly string[];
|
|
29
|
+
/** Lines for stderr, printed in either case. */
|
|
30
|
+
readonly warnings?: readonly string[];
|
|
31
|
+
/** The JSON-serializable result `--json` prints. */
|
|
32
|
+
readonly result: unknown;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export type { ConfigureContext, ListContext, Placement, PlacementContext, ReleaseDeployContext, RetainContext, } from './context.js';
|
|
2
|
+
export type { AdapterCommand, AdapterCommandContext, AdapterCommandResult } from './command.js';
|
|
2
3
|
export type { DeploymentCallTarget, ListedDeployment } from './list.js';
|
|
3
4
|
export type { DeploymentSecretsState, FailedReleaseDeployResult, MutationKnowledge, ReadyReleaseDeployResult, ReleaseDeploymentPhase, ReleaseDeployResult, } from './result.js';
|
|
@@ -11,8 +11,9 @@ import type { DeploymentSummaryV1 } from '../../address/index.js';
|
|
|
11
11
|
* and the deployment it currently serves: `promote` (this deployment's label rolls the Service
|
|
12
12
|
* back or forward to it), `setSecret` and `secrets`, `setSchedule` and `schedules`, `logs`,
|
|
13
13
|
* `transfer` and `delete`.
|
|
14
|
-
* - `namespace`: a deployment of a
|
|
15
|
-
*
|
|
14
|
+
* - `namespace`: a deployment of a Cloudflare dispatch namespace (adapter-cloudflare's namespace
|
|
15
|
+
* mode), which has no Services node: the namespace and the script, which the hosting kit
|
|
16
|
+
* (`@astrale-os/adapter-cloudflare/hosting`) retires, points a name at or schedules.
|
|
16
17
|
*/
|
|
17
18
|
export type DeploymentCallTarget = {
|
|
18
19
|
readonly kind: 'services';
|
|
@@ -27,11 +28,11 @@ export type DeploymentCallTarget = {
|
|
|
27
28
|
};
|
|
28
29
|
/**
|
|
29
30
|
* One deployment as an adapter's `list` returns it: what its host knows of it, its record as the
|
|
30
|
-
* host stores it, and where it is called.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
31
|
+
* host stores it, and where it is called. Admin's Services keeps every deployment until it is
|
|
32
|
+
* retired and counts no call: its deployments are `published` and `withdrawn` once retiring. A
|
|
33
|
+
* Cloudflare dispatch namespace keeps no retention: its rows carry none, and carry their creation
|
|
34
|
+
* instant and key only while their script exists. No first-party adapter reports a last call or an
|
|
35
|
+
* expiry any more; the CLI still prints one an older adapter reports.
|
|
35
36
|
*
|
|
36
37
|
* The adapter does not admit the record. The listing admits it with `acceptDeploymentRecord`, and a
|
|
37
38
|
* record it refuses (one a host on an older SDK still admits, say) never fails the listing: that
|
|
@@ -40,13 +41,13 @@ export type DeploymentCallTarget = {
|
|
|
40
41
|
export interface ListedDeployment extends Omit<DeploymentSummaryV1, 'record'> {
|
|
41
42
|
/**
|
|
42
43
|
* The record as the host stores it, unadmitted, whatever value that is (JSON `null` included).
|
|
43
|
-
* Absent when the host keeps no record of the deployment: a
|
|
44
|
-
* it
|
|
44
|
+
* Absent when the host keeps no record of the deployment: a dispatch namespace keeps none for a
|
|
45
|
+
* label it only retired.
|
|
45
46
|
*/
|
|
46
47
|
readonly record?: unknown;
|
|
47
48
|
/**
|
|
48
49
|
* Whether the stable URL of the deployment's Service serves it. Present only where the host has
|
|
49
|
-
* Services (Admin's Services); a
|
|
50
|
+
* Services (Admin's Services); a dispatch namespace has none.
|
|
50
51
|
*/
|
|
51
52
|
readonly current?: boolean;
|
|
52
53
|
readonly callTarget: DeploymentCallTarget;
|
|
@@ -2,4 +2,4 @@ export { configurationDigest, frozenConfiguration, FROZEN_CONFIGURATION_FORMAT,
|
|
|
2
2
|
export { DEPLOYMENT_LABEL, deploymentContent, deploymentLabel, deploymentUrl, parseDeploymentLabel, type DeploymentLabelParts, } from './label.js';
|
|
3
3
|
export { deploymentLine, lineFingerprint, readablePart, type DeploymentLineInput, type LineAddressing, } from './line.js';
|
|
4
4
|
export { acceptDeploymentRecord, DEPLOYMENT_RECORD_FORMAT, DEPLOYMENT_RECORD_VERSION, type DeploymentCommitV1, type DeploymentRecordV1, } from './record.js';
|
|
5
|
-
export {
|
|
5
|
+
export type { DeploymentSummaryV1 } from './summary.js';
|
|
@@ -2,4 +2,3 @@ export { configurationDigest, frozenConfiguration, FROZEN_CONFIGURATION_FORMAT,
|
|
|
2
2
|
export { DEPLOYMENT_LABEL, deploymentContent, deploymentLabel, deploymentUrl, parseDeploymentLabel, } from './label.js';
|
|
3
3
|
export { deploymentLine, lineFingerprint, readablePart, } from './line.js';
|
|
4
4
|
export { acceptDeploymentRecord, DEPLOYMENT_RECORD_FORMAT, DEPLOYMENT_RECORD_VERSION, } from './record.js';
|
|
5
|
-
export { acceptPlatformDeploymentSummary, encodePlatformDeploymentSummary, PLATFORM_OWNER, } from './summary.js';
|
|
@@ -1,57 +1,34 @@
|
|
|
1
1
|
import type { DeploymentRecordV1 } from './record.js';
|
|
2
2
|
/**
|
|
3
3
|
* What a host knows of one immutable deployment beside the record its deployer declared: where it
|
|
4
|
-
* answers, who owns its line
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* answers, who owns its line and its lifecycle. It names a deployment by its label and URL, never
|
|
5
|
+
* by a provider version id. An adapter's `list` returns these; the host owns every field but
|
|
6
|
+
* `record`, which it stores as declared.
|
|
7
7
|
*/
|
|
8
8
|
export interface DeploymentSummaryV1 {
|
|
9
|
-
/** The host's own identifier of the deployment (a graph node id on Admin's Services). */
|
|
9
|
+
/** The host's own identifier of the deployment (a graph node id on Admin's Services, the label on a dispatch namespace). */
|
|
10
10
|
readonly id: string;
|
|
11
11
|
readonly url: string;
|
|
12
12
|
readonly label: string;
|
|
13
13
|
readonly line: string;
|
|
14
|
-
/**
|
|
14
|
+
/** Who owns the line: a node path on Admin's Services, the dispatch namespace's name on Cloudflare. */
|
|
15
15
|
readonly owner: string;
|
|
16
16
|
readonly state: 'deploying' | 'active' | 'retiring' | 'retired' | 'failed';
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* `published` on Admin's Services, which keeps every deployment until it is retired. Absent on a
|
|
19
|
+
* dispatch namespace, which keeps no retention.
|
|
20
|
+
*/
|
|
21
|
+
readonly retention?: 'preview' | 'published';
|
|
22
|
+
/** When the host created it; absent when the host no longer knows (its script is gone). */
|
|
23
|
+
readonly createdAt?: string;
|
|
24
|
+
/** An older adapter's report of the last call; no first-party adapter reports it any more. */
|
|
19
25
|
readonly lastCallAt?: string;
|
|
20
|
-
/**
|
|
26
|
+
/** An older adapter's preview expiry; no first-party adapter reports it any more. */
|
|
21
27
|
readonly expiresAt?: string;
|
|
22
28
|
readonly retiredAt?: string;
|
|
29
|
+
/** `withdrawn` once its owner retired it (`expired` only from an older adapter). */
|
|
23
30
|
readonly retirement?: 'expired' | 'withdrawn';
|
|
24
|
-
|
|
31
|
+
/** The RFC 7638 thumbprint of its key; absent when the host no longer knows (its script is gone). */
|
|
32
|
+
readonly signingKeyId?: string;
|
|
25
33
|
readonly record: DeploymentRecordV1;
|
|
26
34
|
}
|
|
27
|
-
/** The owner every summary of a platform namespace names: the platform holds all of its lines. */
|
|
28
|
-
export declare const PLATFORM_OWNER = "platform";
|
|
29
|
-
/**
|
|
30
|
-
* `summary:<label>` in the routing KV of a platform dispatch namespace (CT20, AM-131): the
|
|
31
|
-
* `DeploymentSummaryV1` of one deployment without its record, which stays at `record:<label>`.
|
|
32
|
-
*
|
|
33
|
-
* Its writers are the deployer (`cloudflare({ namespace })`), which writes it at activation,
|
|
34
|
-
* before `record:<label>`, names in it the new key of a script it recreates under the label after
|
|
35
|
-
* an operator deleted it, and marks it published (`retain`), and the platform retirement tooling
|
|
36
|
-
* (SV6), which records a withdrawal. So it names the deployment by its label (`id` is the label),
|
|
37
|
-
* is owned by `platform`, is `active` until withdrawn, and carries no `lastCallAt` or `expiresAt`:
|
|
38
|
-
* nothing on the platform namespace keeps those clocks, and listings read them from the calls
|
|
39
|
-
* dataset (AM-133). It never carries a provider version id or a secret digest: an in-place secret
|
|
40
|
-
* write mints a new version id, which identifies nothing (OPS-2 X1).
|
|
41
|
-
*/
|
|
42
|
-
export type PlatformDeploymentSummaryV1 = Omit<DeploymentSummaryV1, 'record' | 'lastCallAt' | 'expiresAt' | 'state' | 'retirement'> & {
|
|
43
|
-
readonly state: 'active' | 'retiring' | 'retired';
|
|
44
|
-
readonly retirement?: 'withdrawn';
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* Admit one platform deployment summary exactly. Any member outside its shape is refused, a
|
|
48
|
-
* provider version id or a clock among them; so is a summary whose id, line or URL is not its
|
|
49
|
-
* label's, whose owner is not the platform, or whose state and retirement disagree. A reader that
|
|
50
|
-
* meets a refused summary lists that label apart, never acting on it.
|
|
51
|
-
*/
|
|
52
|
-
export declare function acceptPlatformDeploymentSummary(input: unknown): PlatformDeploymentSummaryV1;
|
|
53
|
-
/**
|
|
54
|
-
* The canonical JSON (RFC 8785) of one admitted platform summary, as `summary:<label>` holds it:
|
|
55
|
-
* one summary has one text, so a writer that reads it back compares texts.
|
|
56
|
-
*/
|
|
57
|
-
export declare function encodePlatformDeploymentSummary(summary: PlatformDeploymentSummaryV1): string;
|
|
@@ -1,117 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { deploymentUrl, parseDeploymentLabel } from './label.js';
|
|
3
|
-
/** The owner every summary of a platform namespace names: the platform holds all of its lines. */
|
|
4
|
-
export const PLATFORM_OWNER = 'platform';
|
|
5
|
-
const REQUIRED = [
|
|
6
|
-
'id',
|
|
7
|
-
'url',
|
|
8
|
-
'label',
|
|
9
|
-
'line',
|
|
10
|
-
'owner',
|
|
11
|
-
'state',
|
|
12
|
-
'retention',
|
|
13
|
-
'createdAt',
|
|
14
|
-
'signingKeyId',
|
|
15
|
-
];
|
|
16
|
-
const RETIREMENT = ['retiredAt', 'retirement'];
|
|
17
|
-
const STATES = ['active', 'retiring', 'retired'];
|
|
18
|
-
const RETENTIONS = ['preview', 'published'];
|
|
19
|
-
/** An instant as `Date.prototype.toISOString` writes it, in UTC. */
|
|
20
|
-
const INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
|
|
21
|
-
/** An RFC 7638 JWK thumbprint: the base64url SHA-256 of the public key, 43 characters. */
|
|
22
|
-
const KEY_ID = /^[A-Za-z0-9_-]{43}$/u;
|
|
23
|
-
/**
|
|
24
|
-
* Admit one platform deployment summary exactly. Any member outside its shape is refused, a
|
|
25
|
-
* provider version id or a clock among them; so is a summary whose id, line or URL is not its
|
|
26
|
-
* label's, whose owner is not the platform, or whose state and retirement disagree. A reader that
|
|
27
|
-
* meets a refused summary lists that label apart, never acting on it.
|
|
28
|
-
*/
|
|
29
|
-
export function acceptPlatformDeploymentSummary(input) {
|
|
30
|
-
if (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
31
|
-
invalid('members');
|
|
32
|
-
const value = input;
|
|
33
|
-
const keys = Reflect.ownKeys(value);
|
|
34
|
-
const retired = RETIREMENT.some((key) => Object.hasOwn(value, key));
|
|
35
|
-
const expected = retired ? [...REQUIRED, ...RETIREMENT] : REQUIRED;
|
|
36
|
-
if (keys.length !== expected.length || expected.some((key) => !Object.hasOwn(value, key))) {
|
|
37
|
-
invalid('members');
|
|
38
|
-
}
|
|
39
|
-
const label = text(value.label, 'label');
|
|
40
|
-
let line;
|
|
41
|
-
try {
|
|
42
|
-
line = parseDeploymentLabel(label).line;
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
invalid('label');
|
|
46
|
-
}
|
|
47
|
-
if (value.id !== label)
|
|
48
|
-
invalid('id');
|
|
49
|
-
if (value.line !== line)
|
|
50
|
-
invalid('line');
|
|
51
|
-
const url = text(value.url, 'url');
|
|
52
|
-
if (!sameDeploymentUrl(url, label))
|
|
53
|
-
invalid('url');
|
|
54
|
-
if (value.owner !== PLATFORM_OWNER)
|
|
55
|
-
invalid('owner');
|
|
56
|
-
const state = value.state;
|
|
57
|
-
if (typeof state !== 'string' || !STATES.includes(state))
|
|
58
|
-
invalid('state');
|
|
59
|
-
if (retired !== (state !== 'active'))
|
|
60
|
-
invalid('state');
|
|
61
|
-
const retention = value.retention;
|
|
62
|
-
if (typeof retention !== 'string' || !RETENTIONS.includes(retention))
|
|
63
|
-
invalid('retention');
|
|
64
|
-
const createdAt = instant(value.createdAt, 'createdAt');
|
|
65
|
-
const signingKeyId = text(value.signingKeyId, 'signingKeyId');
|
|
66
|
-
if (!KEY_ID.test(signingKeyId))
|
|
67
|
-
invalid('signingKeyId');
|
|
68
|
-
if (retired && value.retirement !== 'withdrawn')
|
|
69
|
-
invalid('retirement');
|
|
70
|
-
return Object.freeze({
|
|
71
|
-
id: label,
|
|
72
|
-
url,
|
|
73
|
-
label,
|
|
74
|
-
line,
|
|
75
|
-
owner: PLATFORM_OWNER,
|
|
76
|
-
state: state,
|
|
77
|
-
retention: retention,
|
|
78
|
-
createdAt,
|
|
79
|
-
signingKeyId,
|
|
80
|
-
...(retired
|
|
81
|
-
? { retiredAt: instant(value.retiredAt, 'retiredAt'), retirement: 'withdrawn' }
|
|
82
|
-
: {}),
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* The canonical JSON (RFC 8785) of one admitted platform summary, as `summary:<label>` holds it:
|
|
87
|
-
* one summary has one text, so a writer that reads it back compares texts.
|
|
88
|
-
*/
|
|
89
|
-
export function encodePlatformDeploymentSummary(summary) {
|
|
90
|
-
return json.serialize(acceptPlatformDeploymentSummary(summary));
|
|
91
|
-
}
|
|
92
|
-
/** Whether `url` is exactly `https://<label>.<routing domain>` for some routing domain. */
|
|
93
|
-
function sameDeploymentUrl(url, label) {
|
|
94
|
-
const prefix = `https://${label}.`;
|
|
95
|
-
if (!url.startsWith(prefix))
|
|
96
|
-
return false;
|
|
97
|
-
try {
|
|
98
|
-
return deploymentUrl(label, url.slice(prefix.length)) === url;
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
function text(input, member) {
|
|
105
|
-
if (typeof input !== 'string' || input.length === 0)
|
|
106
|
-
invalid(member);
|
|
107
|
-
return input;
|
|
108
|
-
}
|
|
109
|
-
function instant(input, member) {
|
|
110
|
-
const value = text(input, member);
|
|
111
|
-
if (!INSTANT.test(value) || new Date(value).toISOString() !== value)
|
|
112
|
-
invalid(member);
|
|
113
|
-
return value;
|
|
114
|
-
}
|
|
115
|
-
function invalid(member) {
|
|
116
|
-
throw new TypeError(`Platform deployment summary has invalid ${member}.`);
|
|
117
|
-
}
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { defineAdapter, defineDeployment, isAdapter } from './adapter/index.js';
|
|
2
|
-
export type { Adapter, AdapterEnvironmentContext, AdapterInput, AddressingPlan, ArtifactFile, ArtifactSource, Bundle, BundleContext, Bundler, BundleSource, ConfigureContext, DeployContext, DeploymentCallTarget, DeploymentPhase, DeploymentProgress, DeploymentSecretsState, DeployResult, FailedDeployResult, FailedReleaseDeployResult, LegacyAdapter, LegacyAdapterInput, ListContext, ListedDeployment, MutationKnowledge, Placement, PlacementContext, PreparedArtifact, PreparedArtifactFile, PrepareContext, ReadyDeployResult, ReadyReleaseDeployResult, ReleaseDeployContext, ReleaseDeploymentPhase, ReleaseDeployResult, RetainContext, } from './adapter/index.js';
|
|
2
|
+
export type { Adapter, AdapterCommand, AdapterCommandContext, AdapterCommandResult, AdapterEnvironmentContext, AdapterInput, AddressingPlan, ArtifactFile, ArtifactSource, Bundle, BundleContext, Bundler, BundleSource, ConfigureContext, DeployContext, DeploymentCallTarget, DeploymentPhase, DeploymentProgress, DeploymentSecretsState, DeployResult, FailedDeployResult, FailedReleaseDeployResult, LegacyAdapter, LegacyAdapterInput, ListContext, ListedDeployment, MutationKnowledge, Placement, PlacementContext, PreparedArtifact, PreparedArtifactFile, PrepareContext, ReadyDeployResult, ReadyReleaseDeployResult, ReleaseDeployContext, ReleaseDeploymentPhase, ReleaseDeployResult, RetainContext, } from './adapter/index.js';
|
|
3
3
|
export { compile, isBuild } from './build/index.js';
|
|
4
4
|
export type { Build, BuildBundle, BuildDigest, BuildManifestV1, BuildSchema, } from './build/index.js';
|
|
5
5
|
export { deploy } from './deploy.js';
|
|
@@ -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,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')
|
|
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { admitRuntime, compile, resolveRuntime } from '../../deployment/index.js';
|
|
4
4
|
import { packageDomain } from '../packaging/index.js';
|
|
5
|
+
import { runAdapterCommand } from './adapter-command.js';
|
|
5
6
|
import { parseArgs } from './arguments.js';
|
|
6
7
|
import { buildResult, bundleBuild, bundlerCopyWarnings, projectBundlers } from './build.js';
|
|
7
8
|
import { BunRequiredError } from './bun.js';
|
|
@@ -64,6 +65,23 @@ export async function run(argv) {
|
|
|
64
65
|
const project = await loadProject(configPath);
|
|
65
66
|
if (parsed.command === 'list')
|
|
66
67
|
return list(parsed, project, projectDir);
|
|
68
|
+
if (parsed.command === 'adapter') {
|
|
69
|
+
const lifecycle = deploymentLifecycle(`${parsed.adapter} ${parsed.adapterCommand} interrupted.`);
|
|
70
|
+
try {
|
|
71
|
+
return await runAdapterCommand({
|
|
72
|
+
adapter: parsed.adapter,
|
|
73
|
+
command: parsed.adapterCommand,
|
|
74
|
+
environment: parsed.env,
|
|
75
|
+
definition: selectEnvironment(project, parsed.env),
|
|
76
|
+
options: parsed.options ?? {},
|
|
77
|
+
json: parsed.json === true,
|
|
78
|
+
signal: lifecycle.signal,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
lifecycle.dispose();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
67
85
|
if (parsed.command === 'yank')
|
|
68
86
|
return yank(parsed, project);
|
|
69
87
|
if (parsed.command === 'publish') {
|
|
@@ -63,10 +63,10 @@ export declare const VERIFY_TIMEOUT_MS = 60000;
|
|
|
63
63
|
* 6. Ask Admin for the Publication; a version Admin gave another release meanwhile is refused,
|
|
64
64
|
* naming the winner's release, and this deployment stays a preview. Any other failure says
|
|
65
65
|
* what CT29 says of it: Admin may have created the Publication (a rerun settles it), Admin made
|
|
66
|
-
* none and a rerun can help, or Admin refused and a rerun meets the same refusal. Then,
|
|
67
|
-
* the host
|
|
68
|
-
* keeps the published deployment
|
|
69
|
-
*
|
|
66
|
+
* none and a rerun can help, or Admin refused and a rerun meets the same refusal. Then, when
|
|
67
|
+
* the adapter declares `retain` (a host with no registry principal to keep it, CT36), it
|
|
68
|
+
* keeps the published deployment. No first-party adapter declares it: Admin's Services and a
|
|
69
|
+
* Cloudflare dispatch namespace keep every deployment until its owner retires it.
|
|
70
70
|
*
|
|
71
71
|
* Steps 0 to 3 make no deploy call. Never installs.
|
|
72
72
|
*/
|
|
@@ -44,10 +44,10 @@ const DEFAULT_DEPENDENCIES = Object.freeze({
|
|
|
44
44
|
* 6. Ask Admin for the Publication; a version Admin gave another release meanwhile is refused,
|
|
45
45
|
* naming the winner's release, and this deployment stays a preview. Any other failure says
|
|
46
46
|
* what CT29 says of it: Admin may have created the Publication (a rerun settles it), Admin made
|
|
47
|
-
* none and a rerun can help, or Admin refused and a rerun meets the same refusal. Then,
|
|
48
|
-
* the host
|
|
49
|
-
* keeps the published deployment
|
|
50
|
-
*
|
|
47
|
+
* none and a rerun can help, or Admin refused and a rerun meets the same refusal. Then, when
|
|
48
|
+
* the adapter declares `retain` (a host with no registry principal to keep it, CT36), it
|
|
49
|
+
* keeps the published deployment. No first-party adapter declares it: Admin's Services and a
|
|
50
|
+
* Cloudflare dispatch namespace keep every deployment until its owner retires it.
|
|
51
51
|
*
|
|
52
52
|
* Steps 0 to 3 make no deploy call. Never installs.
|
|
53
53
|
*/
|
|
@@ -294,11 +294,10 @@ function described(cause) {
|
|
|
294
294
|
return cause instanceof RegistryCliError ? `${cause.code}: ${cause.message}` : message(cause);
|
|
295
295
|
}
|
|
296
296
|
/**
|
|
297
|
-
* Keep the published deployment where its host has no registry principal to mark it
|
|
298
|
-
* declares `retain`
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* retries.
|
|
297
|
+
* Keep the published deployment where its host has no registry principal to mark it, when the
|
|
298
|
+
* adapter declares `retain` (CT36); an adapter without it keeps every deployment until it is
|
|
299
|
+
* retired, as adapter-astrale and adapter-cloudflare do. A `RetainRefusal` (absent or retired) is
|
|
300
|
+
* `refused`, which no rerun changes; any other rejection `failed`, which a rerun retries.
|
|
302
301
|
*/
|
|
303
302
|
async function retain(plan, signal) {
|
|
304
303
|
const { adapter } = plan;
|