@astrale-os/sdk 0.6.0-beta.15 → 0.6.0-beta.16
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/dist/deployment/adapter/adapter.d.ts +3 -2
- package/dist/deployment/adapter/index.d.ts +1 -0
- package/dist/deployment/adapter/index.js +1 -0
- package/dist/deployment/adapter/release/refusal.d.ts +10 -0
- package/dist/deployment/adapter/release/refusal.js +14 -0
- package/dist/tooling/cli/arguments.d.ts +7 -1
- package/dist/tooling/cli/arguments.js +67 -5
- package/dist/tooling/cli/commit.d.ts +12 -0
- package/dist/tooling/cli/commit.js +19 -0
- package/dist/tooling/cli/deploy-mode.d.ts +18 -6
- package/dist/tooling/cli/deploy-mode.js +28 -1
- package/dist/tooling/cli/diff/declared.d.ts +7 -0
- package/dist/tooling/cli/diff/declared.js +27 -0
- package/dist/tooling/cli/diff/index.d.ts +30 -0
- package/dist/tooling/cli/diff/index.js +58 -0
- package/dist/tooling/cli/diff/render.d.ts +15 -0
- package/dist/tooling/cli/diff/render.js +131 -0
- package/dist/tooling/cli/diff/report.d.ts +88 -0
- package/dist/tooling/cli/diff/report.js +154 -0
- package/dist/tooling/cli/help.js +98 -0
- package/dist/tooling/cli/immutable-deployment.d.ts +65 -9
- package/dist/tooling/cli/immutable-deployment.js +59 -19
- package/dist/tooling/cli/index.d.ts +8 -2
- package/dist/tooling/cli/index.js +6 -2
- package/dist/tooling/cli/orchestrate.js +104 -2
- package/dist/tooling/cli/publish/index.d.ts +74 -0
- package/dist/tooling/cli/publish/index.js +364 -0
- package/dist/tooling/cli/publish/report.d.ts +49 -0
- package/dist/tooling/cli/publish/report.js +15 -0
- package/dist/tooling/cli/registry/client.d.ts +127 -0
- package/dist/tooling/cli/registry/client.js +328 -0
- package/dist/tooling/cli/registry/executable.d.ts +20 -0
- package/dist/tooling/cli/registry/executable.js +79 -0
- package/dist/tooling/cli/registry/index.d.ts +6 -0
- package/dist/tooling/cli/registry/index.js +6 -0
- package/dist/tooling/cli/registry/process.d.ts +21 -0
- package/dist/tooling/cli/registry/process.js +84 -0
- package/dist/tooling/cli/yank.d.ts +34 -0
- package/dist/tooling/cli/yank.js +32 -0
- package/package.json +2 -2
|
@@ -39,8 +39,9 @@ export interface Adapter<Parameters extends object = object> {
|
|
|
39
39
|
list?(parameters: Parameters, context: ListContext): Promise<readonly ListedDeployment[]>;
|
|
40
40
|
/**
|
|
41
41
|
* Keep one published deployment where the host has no registry principal to mark it (a platform
|
|
42
|
-
* namespace). Called only after its Publication commits.
|
|
43
|
-
*
|
|
42
|
+
* namespace). Called only after its Publication commits. Rejects with a `RetainRefusal` when the
|
|
43
|
+
* host can no longer keep it (absent or retired), which no rerun changes; any other rejection is
|
|
44
|
+
* transient. Absent: the registry marks it (Admin's Services).
|
|
44
45
|
*/
|
|
45
46
|
retain?(parameters: Parameters, context: RetainContext): Promise<void>;
|
|
46
47
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { Adapter, AdapterEnvironmentContext, AdapterInput } from './adapter.js';
|
|
2
2
|
export { defineAdapter, defineDeployment, isAdapter } from './define.js';
|
|
3
|
+
export { RetainRefusal } from './release/refusal.js';
|
|
3
4
|
export type * from './bundle/index.js';
|
|
4
5
|
export type * from './release/index.js';
|
|
5
6
|
/** @deprecated The contract before adapter interface v2; see `./legacy/index.ts`. */
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why the host can no longer keep one published deployment (`Adapter.retain`): it is `absent`
|
|
3
|
+
* (the host holds no deployment of that label, or it was deleted) or `retired` (withdrawn, never
|
|
4
|
+
* served again). No rerun keeps it; a new version publishes a new deployment. Any other rejection
|
|
5
|
+
* of `retain` is transient: once it has passed, a rerun keeps the deployment.
|
|
6
|
+
*/
|
|
7
|
+
export declare class RetainRefusal extends Error {
|
|
8
|
+
readonly reason: 'absent' | 'retired';
|
|
9
|
+
constructor(reason: RetainRefusal['reason'], message: string);
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why the host can no longer keep one published deployment (`Adapter.retain`): it is `absent`
|
|
3
|
+
* (the host holds no deployment of that label, or it was deleted) or `retired` (withdrawn, never
|
|
4
|
+
* served again). No rerun keeps it; a new version publishes a new deployment. Any other rejection
|
|
5
|
+
* of `retain` is transient: once it has passed, a rerun keeps the deployment.
|
|
6
|
+
*/
|
|
7
|
+
export class RetainRefusal extends Error {
|
|
8
|
+
reason;
|
|
9
|
+
constructor(reason, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'RetainRefusal';
|
|
12
|
+
this.reason = reason;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LintReportFormat } from '../linter/index.js';
|
|
2
|
-
|
|
2
|
+
import { type Version } from '../../platform/versioning/index.js';
|
|
3
|
+
export declare const COMMANDS: readonly ['dev', 'build', 'deploy', 'diff', 'list', 'publish', 'test', 'lint', 'package', 'upgrade', 'yank'];
|
|
3
4
|
export type Command = (typeof COMMANDS)[number];
|
|
4
5
|
export interface ParsedArgs {
|
|
5
6
|
readonly command: Command;
|
|
@@ -13,6 +14,11 @@ export interface ParsedArgs {
|
|
|
13
14
|
readonly channel?: string;
|
|
14
15
|
readonly check?: boolean;
|
|
15
16
|
readonly json?: boolean;
|
|
17
|
+
/** `yank`: the published version it names. */
|
|
18
|
+
readonly version?: Version;
|
|
19
|
+
/** `yank --undo`: restore the version instead of yanking it. */
|
|
20
|
+
readonly undo?: boolean;
|
|
21
|
+
readonly allowDirty?: boolean;
|
|
16
22
|
}
|
|
17
23
|
/** Parse the frozen `astrale-domain` command grammar without performing effects. */
|
|
18
24
|
export declare function parseArgs(argv: readonly string[]): ParsedArgs;
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { isAbsolute, normalize } from 'node:path';
|
|
2
|
+
import { acceptVersion } from '../../platform/versioning/index.js';
|
|
2
3
|
import { installCommand } from '../../project/install-command.js';
|
|
3
4
|
export const COMMANDS = [
|
|
4
5
|
'dev',
|
|
5
6
|
'build',
|
|
6
7
|
'deploy',
|
|
8
|
+
'diff',
|
|
7
9
|
'list',
|
|
10
|
+
'publish',
|
|
8
11
|
'test',
|
|
9
12
|
'lint',
|
|
10
13
|
'package',
|
|
11
14
|
'upgrade',
|
|
15
|
+
'yank',
|
|
12
16
|
];
|
|
13
17
|
/** Parse the frozen `astrale-domain` command grammar without performing effects. */
|
|
14
18
|
export function parseArgs(argv) {
|
|
@@ -18,6 +22,8 @@ export function parseArgs(argv) {
|
|
|
18
22
|
const fix = rest.includes('--fix');
|
|
19
23
|
const deployOnly = rest.includes('--deploy-only');
|
|
20
24
|
const json = rest.includes('--json');
|
|
25
|
+
const undo = rest.includes('--undo');
|
|
26
|
+
const allowDirty = rest.includes('--allow-dirty');
|
|
21
27
|
let secretsFile;
|
|
22
28
|
let credentialFile;
|
|
23
29
|
let identity;
|
|
@@ -63,7 +69,11 @@ export function parseArgs(argv) {
|
|
|
63
69
|
else if (argument.startsWith('--suite=')) {
|
|
64
70
|
suite = requiredValue('--suite', argument.slice('--suite='.length));
|
|
65
71
|
}
|
|
66
|
-
else if (argument === '--fix' ||
|
|
72
|
+
else if (argument === '--fix' ||
|
|
73
|
+
argument === '--deploy-only' ||
|
|
74
|
+
argument === '--json' ||
|
|
75
|
+
argument === '--undo' ||
|
|
76
|
+
argument === '--allow-dirty') {
|
|
67
77
|
continue;
|
|
68
78
|
}
|
|
69
79
|
else if (argument.startsWith('-')) {
|
|
@@ -75,9 +85,20 @@ export function parseArgs(argv) {
|
|
|
75
85
|
}
|
|
76
86
|
if (fix && command !== 'lint')
|
|
77
87
|
throw new Error('`--fix` is only valid for `lint`.');
|
|
78
|
-
if (json &&
|
|
79
|
-
|
|
88
|
+
if (json &&
|
|
89
|
+
command !== 'build' &&
|
|
90
|
+
command !== 'deploy' &&
|
|
91
|
+
command !== 'diff' &&
|
|
92
|
+
command !== 'list' &&
|
|
93
|
+
command !== 'publish' &&
|
|
94
|
+
command !== 'yank') {
|
|
95
|
+
throw new Error('`--json` is only valid for `build`, `deploy`, `diff`, `list`, `publish` or `yank`.');
|
|
80
96
|
}
|
|
97
|
+
if (allowDirty && command !== 'publish') {
|
|
98
|
+
throw new Error('`--allow-dirty` is only valid for `publish`.');
|
|
99
|
+
}
|
|
100
|
+
if (undo && command !== 'yank')
|
|
101
|
+
throw new Error('`--undo` is only valid for `yank`.');
|
|
81
102
|
if (format !== undefined && command !== 'lint') {
|
|
82
103
|
throw new Error('`--format` is only valid for `lint`.');
|
|
83
104
|
}
|
|
@@ -89,8 +110,13 @@ export function parseArgs(argv) {
|
|
|
89
110
|
}
|
|
90
111
|
if (deployOnly && command !== 'dev' && command !== 'deploy')
|
|
91
112
|
refuseDeployOnly(undefined);
|
|
92
|
-
if (identity !== undefined &&
|
|
93
|
-
|
|
113
|
+
if (identity !== undefined &&
|
|
114
|
+
command !== 'dev' &&
|
|
115
|
+
command !== 'deploy' &&
|
|
116
|
+
command !== 'diff' &&
|
|
117
|
+
command !== 'list' &&
|
|
118
|
+
command !== 'publish') {
|
|
119
|
+
throw new Error('`--as` is only valid for `dev`, `deploy`, `diff`, `list` or `publish`.');
|
|
94
120
|
}
|
|
95
121
|
if (environment !== undefined &&
|
|
96
122
|
command !== 'dev' &&
|
|
@@ -144,6 +170,30 @@ export function parseArgs(argv) {
|
|
|
144
170
|
throw new Error(`\`${command}\` does not accept positional arguments.`);
|
|
145
171
|
}
|
|
146
172
|
return { command, env: '', ...(json ? { json: true } : {}) };
|
|
173
|
+
case 'diff':
|
|
174
|
+
// The version is package.json's: no command takes a version number (tech Versioning).
|
|
175
|
+
if (positionals.length > 0) {
|
|
176
|
+
throw new Error('`diff` does not accept positional arguments; it reads package.json.');
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
command: 'diff',
|
|
180
|
+
env: '',
|
|
181
|
+
...(json ? { json: true } : {}),
|
|
182
|
+
...(identity === undefined ? {} : { identity }),
|
|
183
|
+
};
|
|
184
|
+
case 'publish': {
|
|
185
|
+
// The version is package.json's: the one positional names the Environment (tech Versioning).
|
|
186
|
+
if (positionals.length !== 1) {
|
|
187
|
+
throw new Error('`publish` takes exactly one Environment, e.g. `publish production`; it reads the version from package.json.');
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
command: 'publish',
|
|
191
|
+
env: positionals[0],
|
|
192
|
+
...(json ? { json: true } : {}),
|
|
193
|
+
...(allowDirty ? { allowDirty: true } : {}),
|
|
194
|
+
...(identity === undefined ? {} : { identity }),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
147
197
|
case 'test':
|
|
148
198
|
if (positionals.length > 0) {
|
|
149
199
|
throw new Error('`test` does not accept positional arguments; use `--environment`.');
|
|
@@ -153,6 +203,18 @@ export function parseArgs(argv) {
|
|
|
153
203
|
env: environment ?? '',
|
|
154
204
|
...(suite === undefined ? {} : { suite }),
|
|
155
205
|
};
|
|
206
|
+
case 'yank': {
|
|
207
|
+
if (positionals.length !== 1) {
|
|
208
|
+
throw new Error('`yank` takes exactly one published version, e.g. `yank 1.5.0`.');
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
command: 'yank',
|
|
212
|
+
env: '',
|
|
213
|
+
version: acceptVersion(positionals[0]),
|
|
214
|
+
...(undo ? { undo: true } : {}),
|
|
215
|
+
...(json ? { json: true } : {}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
156
218
|
case 'lint':
|
|
157
219
|
if (positionals.length > 0)
|
|
158
220
|
throw new Error('`lint` does not accept positional arguments.');
|
|
@@ -21,3 +21,15 @@ export declare function deploymentCommit(input: {
|
|
|
21
21
|
readonly runtimeModule: string;
|
|
22
22
|
readonly signal: AbortSignal;
|
|
23
23
|
}): Promise<DeploymentCommitV1 | null>;
|
|
24
|
+
/**
|
|
25
|
+
* Whether a remote-tracking branch of the repository already holds a commit. A Publication records
|
|
26
|
+
* the commit it was built from, and release-please computes the next number from the pushed
|
|
27
|
+
* history: publishing a commit nobody pushed risks the same number being computed again (tech
|
|
28
|
+
* Versioning [.73872]). Nothing when git cannot tell. Reads the local remote-tracking refs only,
|
|
29
|
+
* never the network.
|
|
30
|
+
*/
|
|
31
|
+
export declare function commitPushed(input: {
|
|
32
|
+
readonly projectDir: string;
|
|
33
|
+
readonly sha: string;
|
|
34
|
+
readonly signal: AbortSignal;
|
|
35
|
+
}): Promise<boolean | undefined>;
|
|
@@ -46,6 +46,25 @@ export async function deploymentCommit(input) {
|
|
|
46
46
|
...(base === undefined ? {} : { base }),
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether a remote-tracking branch of the repository already holds a commit. A Publication records
|
|
51
|
+
* the commit it was built from, and release-please computes the next number from the pushed
|
|
52
|
+
* history: publishing a commit nobody pushed risks the same number being computed again (tech
|
|
53
|
+
* Versioning [.73872]). Nothing when git cannot tell. Reads the local remote-tracking refs only,
|
|
54
|
+
* never the network.
|
|
55
|
+
*/
|
|
56
|
+
export async function commitPushed(input) {
|
|
57
|
+
if (!SHA.test(input.sha))
|
|
58
|
+
return undefined;
|
|
59
|
+
const branches = await git(input.projectDir, input.signal, [
|
|
60
|
+
'branch',
|
|
61
|
+
'--remotes',
|
|
62
|
+
'--contains',
|
|
63
|
+
input.sha,
|
|
64
|
+
'--format=%(refname)',
|
|
65
|
+
]);
|
|
66
|
+
return branches === undefined ? undefined : branches.length > 0;
|
|
67
|
+
}
|
|
49
68
|
/**
|
|
50
69
|
* The prefixes of the release tags of the package the Domain is released from, or nothing when
|
|
51
70
|
* no release line holds the Domain.
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { LoadedEnvironment } from './project.js';
|
|
2
1
|
/**
|
|
3
2
|
* The deploy mode of one Environment for one command (CT33), chosen by the configuration itself:
|
|
4
3
|
* an unchanged configuration keeps its mode, and so its behaviour, across an SDK upgrade, as long as
|
|
@@ -41,12 +40,15 @@ export type LegacyDirectReason =
|
|
|
41
40
|
};
|
|
42
41
|
export type CloudflareDirectParameter = 'route' | 'workerName' | 'identityIssuer' | 'addressing' | 'signingIdentity';
|
|
43
42
|
export interface DeployRefusal {
|
|
44
|
-
readonly reason: 'astrale-instance' | 'astrale-signing-identity' | 'mixed-parameters' | 'scheduled-triggers' | 'development-canonical';
|
|
43
|
+
readonly reason: 'astrale-instance' | 'astrale-signing-identity' | 'mixed-parameters' | 'scheduled-triggers' | 'development-canonical' | 'publish-legacy-direct';
|
|
45
44
|
/** The configuration or command to write instead, for the author. */
|
|
46
45
|
readonly guidance: string;
|
|
47
46
|
}
|
|
48
|
-
/**
|
|
49
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The commands that select a deploy mode. `publish` names only an immutable deployment, so it
|
|
49
|
+
* refuses every Environment that is not canonical (CT33, R-G03).
|
|
50
|
+
*/
|
|
51
|
+
export type DeployModeCommand = 'deploy' | 'dev' | 'publish';
|
|
50
52
|
/**
|
|
51
53
|
* Select the deploy mode of one Environment (CT33). It reads the configuration and the command
|
|
52
54
|
* only, and has no effect.
|
|
@@ -56,7 +58,8 @@ export type DeployModeCommand = 'deploy' | 'dev';
|
|
|
56
58
|
* before any effect, since their one consumer pins an older SDK (AM-72). Otherwise the Environment
|
|
57
59
|
* is on the legacy direct-mode path when adapter-cloudflare names a direct-mode parameter or no
|
|
58
60
|
* `namespace`, or when its adapter declares no `deployRelease`. Everything else is canonical,
|
|
59
|
-
* except under `dev`, which serves the legacy direct-mode path only.
|
|
61
|
+
* except under `dev`, which serves the legacy direct-mode path only. `publish` refuses the legacy
|
|
62
|
+
* direct-mode path: a stable target serves no release, so no version can name it.
|
|
60
63
|
*
|
|
61
64
|
* Within canonical adapter-cloudflare (with `namespace`), a direct-mode parameter is refused
|
|
62
65
|
* rather than silently selecting either mode, and so is `wrangler.triggers`: an immutable
|
|
@@ -66,5 +69,14 @@ export type DeployModeCommand = 'deploy' | 'dev';
|
|
|
66
69
|
export declare function selectDeployMode(input: {
|
|
67
70
|
readonly command: DeployModeCommand;
|
|
68
71
|
readonly environment: string;
|
|
69
|
-
|
|
72
|
+
/** The Environment's definition, of which selection reads only the adapter and its parameters. */
|
|
73
|
+
readonly definition: {
|
|
74
|
+
readonly deployment: {
|
|
75
|
+
readonly adapter: {
|
|
76
|
+
readonly name: string;
|
|
77
|
+
readonly deployRelease?: unknown;
|
|
78
|
+
};
|
|
79
|
+
readonly parameters: object;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
70
82
|
}): DeployMode;
|
|
@@ -22,7 +22,8 @@ const CLOUDFLARE_DIRECT_PARAMETERS = [
|
|
|
22
22
|
* before any effect, since their one consumer pins an older SDK (AM-72). Otherwise the Environment
|
|
23
23
|
* is on the legacy direct-mode path when adapter-cloudflare names a direct-mode parameter or no
|
|
24
24
|
* `namespace`, or when its adapter declares no `deployRelease`. Everything else is canonical,
|
|
25
|
-
* except under `dev`, which serves the legacy direct-mode path only.
|
|
25
|
+
* except under `dev`, which serves the legacy direct-mode path only. `publish` refuses the legacy
|
|
26
|
+
* direct-mode path: a stable target serves no release, so no version can name it.
|
|
26
27
|
*
|
|
27
28
|
* Within canonical adapter-cloudflare (with `namespace`), a direct-mode parameter is refused
|
|
28
29
|
* rather than silently selecting either mode, and so is `wrangler.triggers`: an immutable
|
|
@@ -83,6 +84,14 @@ export function selectDeployMode(input) {
|
|
|
83
84
|
reasons.push({ reason: 'no-deploy-release' });
|
|
84
85
|
if (refusals.length > 0)
|
|
85
86
|
return Object.freeze({ kind: 'refused', refusals: freeze(refusals) });
|
|
87
|
+
if (reasons.length > 0 && command === 'publish') {
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
kind: 'refused',
|
|
90
|
+
refusals: freeze([
|
|
91
|
+
{ reason: 'publish-legacy-direct', guidance: publishGuidance(environment, reasons) },
|
|
92
|
+
]),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
86
95
|
if (reasons.length > 0)
|
|
87
96
|
return Object.freeze({ kind: 'legacy-direct', reasons: freeze(reasons) });
|
|
88
97
|
if (command === 'dev') {
|
|
@@ -124,6 +133,24 @@ function organizationGuidance(parameters, environment) {
|
|
|
124
133
|
? ''
|
|
125
134
|
: ", and name the organisation that holds the deployment line in `astrale({ organization: '<Identity id>' })`";
|
|
126
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Why a legacy direct-mode Environment cannot be published, and the configuration that makes it
|
|
138
|
+
* publishable: a version names an immutable deployment's release, which a stable target never
|
|
139
|
+
* serves.
|
|
140
|
+
*/
|
|
141
|
+
function publishGuidance(environment, reasons) {
|
|
142
|
+
const deploys = `\`astrale-domain publish ${environment}\` names an immutable deployment's release, and ${environment} deploys`;
|
|
143
|
+
if (reasons.some(({ reason }) => reason === 'no-deploy-release')) {
|
|
144
|
+
return (`${deploys} through an adapter without deployRelease, which replaces a stable target in ` +
|
|
145
|
+
'place and serves no release. Use an adapter that implements deployRelease (adapter ' +
|
|
146
|
+
"interface v2), such as astrale() on Admin's Services.");
|
|
147
|
+
}
|
|
148
|
+
const direct = reasons.flatMap((reason) => reason.reason === 'direct-parameter' ? [reason.parameter] : []);
|
|
149
|
+
return (`${deploys} on cloudflare()'s legacy direct mode, which serves no release. Deploy it into a ` +
|
|
150
|
+
'dispatch namespace with cloudflare({ namespace })' +
|
|
151
|
+
(direct.length === 0 ? '' : ` and remove ${direct.join(', ')}`) +
|
|
152
|
+
", or on Admin's Services with astrale().");
|
|
153
|
+
}
|
|
127
154
|
/** A parameter is declared when its value is not `undefined`, as optional adapter parameters read. */
|
|
128
155
|
function declares(parameters, name) {
|
|
129
156
|
return Object.hasOwn(parameters, name) && parameters[name] !== undefined;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Version } from '../../../platform/versioning/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* The version the author declares for the next release: `package.json` `version` in the Project
|
|
4
|
+
* directory, as release-please and `npm version` write it (tech Versioning [.21210]). No command
|
|
5
|
+
* takes a version number.
|
|
6
|
+
*/
|
|
7
|
+
export declare function declaredVersion(projectDir: string): Promise<Version>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { acceptVersion } from '../../../platform/versioning/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* The version the author declares for the next release: `package.json` `version` in the Project
|
|
6
|
+
* directory, as release-please and `npm version` write it (tech Versioning [.21210]). No command
|
|
7
|
+
* takes a version number.
|
|
8
|
+
*/
|
|
9
|
+
export async function declaredVersion(projectDir) {
|
|
10
|
+
const path = join(projectDir, 'package.json');
|
|
11
|
+
let manifest;
|
|
12
|
+
try {
|
|
13
|
+
manifest = JSON.parse(await readFile(path, 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
catch (cause) {
|
|
16
|
+
throw new TypeError(`Cannot read the Domain's version: ${path} is not a readable JSON file.`, {
|
|
17
|
+
cause,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const version = typeof manifest === 'object' && manifest !== null && !Array.isArray(manifest)
|
|
21
|
+
? manifest.version
|
|
22
|
+
: undefined;
|
|
23
|
+
if (typeof version !== 'string') {
|
|
24
|
+
throw new TypeError(`${path} declares no version. The Domain's version is the package.json version, for example written by npm version minor.`);
|
|
25
|
+
}
|
|
26
|
+
return acceptVersion(version);
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Build } from '../../../deployment/index.js';
|
|
2
|
+
import type { DomainRegistry } from '../registry/index.js';
|
|
3
|
+
export type { DiffChangeV1, DiffReportV1 } from './report.js';
|
|
4
|
+
export interface DiffCommandInput {
|
|
5
|
+
readonly projectDir: string;
|
|
6
|
+
readonly build: Build;
|
|
7
|
+
/** The build digest of every distinct bundler of the Project, bundled on demand. */
|
|
8
|
+
readonly builds: () => Promise<readonly string[]>;
|
|
9
|
+
readonly identity?: string;
|
|
10
|
+
readonly json: boolean;
|
|
11
|
+
readonly signal: AbortSignal;
|
|
12
|
+
}
|
|
13
|
+
export interface DiffCommandDependencies {
|
|
14
|
+
readonly open: (input: {
|
|
15
|
+
readonly projectDir: string;
|
|
16
|
+
readonly identity?: string;
|
|
17
|
+
readonly signal: AbortSignal;
|
|
18
|
+
}) => Promise<DomainRegistry>;
|
|
19
|
+
readonly stdout: (text: string) => void;
|
|
20
|
+
readonly stderr: (text: string) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `astrale-domain diff [--as <identity>] [--json]`: the changes since the highest published stable
|
|
24
|
+
* version up to the declared one, and the smallest version they admit (tech Versioning [.72565]).
|
|
25
|
+
* On a feature branch the declared version is still the published one and the report is
|
|
26
|
+
* informational (exit 0); on a release PR an unpublished version that is too low fails (exit 1)
|
|
27
|
+
* with the [.105820] message. Reads the registry through the Astrale CLI under the caller's own
|
|
28
|
+
* credential and changes nothing.
|
|
29
|
+
*/
|
|
30
|
+
export declare function runDiff(input: DiffCommandInput, dependencies?: DiffCommandDependencies): Promise<number>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as bundle from '@astrale-os/kernel-dsl/v1/bundle/transport';
|
|
2
|
+
import { DIM, RED, RESET } from '../log.js';
|
|
3
|
+
import { RegistryCliError, openDomainRegistry } from '../registry/index.js';
|
|
4
|
+
import { declaredVersion } from './declared.js';
|
|
5
|
+
import { diffExitCode, renderDiff } from './render.js';
|
|
6
|
+
import { diffReport, publishedSchema } from './report.js';
|
|
7
|
+
/**
|
|
8
|
+
* `astrale-domain diff [--as <identity>] [--json]`: the changes since the highest published stable
|
|
9
|
+
* version up to the declared one, and the smallest version they admit (tech Versioning [.72565]).
|
|
10
|
+
* On a feature branch the declared version is still the published one and the report is
|
|
11
|
+
* informational (exit 0); on a release PR an unpublished version that is too low fails (exit 1)
|
|
12
|
+
* with the [.105820] message. Reads the registry through the Astrale CLI under the caller's own
|
|
13
|
+
* credential and changes nothing.
|
|
14
|
+
*/
|
|
15
|
+
export async function runDiff(input, dependencies = {
|
|
16
|
+
open: openDomainRegistry,
|
|
17
|
+
stdout: (text) => void process.stdout.write(text),
|
|
18
|
+
stderr: (text) => void process.stderr.write(text),
|
|
19
|
+
}) {
|
|
20
|
+
const root = input.build.schema.compiled.root;
|
|
21
|
+
let report;
|
|
22
|
+
try {
|
|
23
|
+
const declared = await declaredVersion(input.projectDir);
|
|
24
|
+
const registry = await dependencies.open({
|
|
25
|
+
projectDir: input.projectDir,
|
|
26
|
+
...(input.identity === undefined ? {} : { identity: input.identity }),
|
|
27
|
+
signal: input.signal,
|
|
28
|
+
});
|
|
29
|
+
report = await diffReport({
|
|
30
|
+
origin: root.origin,
|
|
31
|
+
declared,
|
|
32
|
+
revision: root.revision,
|
|
33
|
+
index: await registry.index(root.origin),
|
|
34
|
+
current: () => bundle.decode(input.build.schema.bundle.bytes()).root,
|
|
35
|
+
builds: input.builds,
|
|
36
|
+
published: async (publication) => publishedSchema(await registry.bundle(root.origin, publication), publication, root.origin),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (cause) {
|
|
40
|
+
if (!(cause instanceof RegistryCliError))
|
|
41
|
+
throw cause;
|
|
42
|
+
// Nothing on stdout: a JSON consumer reads one report or nothing.
|
|
43
|
+
dependencies.stderr(`${RED}✗${RESET} Domain registry: ${cause.code}: ${cause.message}\n`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
const rendered = renderDiff(report);
|
|
47
|
+
// With --json, stdout carries only the report and the summary moves to stderr, as for deploy.
|
|
48
|
+
const summary = input.json ? dependencies.stderr : dependencies.stdout;
|
|
49
|
+
// The sentence takes the progress mark; the member rows below it keep their indentation.
|
|
50
|
+
for (const line of rendered.summary) {
|
|
51
|
+
summary(line.startsWith(' ') ? `${line}\n` : `${DIM}›${RESET} ${line}\n`);
|
|
52
|
+
}
|
|
53
|
+
if (rendered.refusal.length > 0)
|
|
54
|
+
dependencies.stderr(`${rendered.refusal.join('\n')}\n`);
|
|
55
|
+
if (input.json)
|
|
56
|
+
dependencies.stdout(`${JSON.stringify(report, null, 2)}\n`);
|
|
57
|
+
return diffExitCode(report);
|
|
58
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DiffReportV1 } from './report.js';
|
|
2
|
+
/**
|
|
3
|
+
* 0 when the declared version is accepted, already published (the report is then informational:
|
|
4
|
+
* a feature branch keeps the last published number until its release), or has nothing to compare
|
|
5
|
+
* with; 1 when an unpublished declared version is too low.
|
|
6
|
+
*/
|
|
7
|
+
export declare function diffExitCode(report: DiffReportV1): 0 | 1;
|
|
8
|
+
/**
|
|
9
|
+
* The report for a person. `summary` describes the comparison; `refusal` is the [.105820] message
|
|
10
|
+
* for an unpublished version that is too low (tech Versioning), printed as an error.
|
|
11
|
+
*/
|
|
12
|
+
export declare function renderDiff(report: DiffReportV1): {
|
|
13
|
+
readonly summary: readonly string[];
|
|
14
|
+
readonly refusal: readonly string[];
|
|
15
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/** What a change of each level is, in the [.105820] message. */
|
|
2
|
+
const NATURE = Object.freeze({
|
|
3
|
+
major: 'breaking change',
|
|
4
|
+
minor: 'addition',
|
|
5
|
+
patch: 'change',
|
|
6
|
+
});
|
|
7
|
+
const LEVELS = ['patch', 'minor', 'major'];
|
|
8
|
+
/**
|
|
9
|
+
* 0 when the declared version is accepted, already published (the report is then informational:
|
|
10
|
+
* a feature branch keeps the last published number until its release), or has nothing to compare
|
|
11
|
+
* with; 1 when an unpublished declared version is too low.
|
|
12
|
+
*/
|
|
13
|
+
export function diffExitCode(report) {
|
|
14
|
+
return report.published || report.accepted ? 0 : 1;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The report for a person. `summary` describes the comparison; `refusal` is the [.105820] message
|
|
18
|
+
* for an unpublished version that is too low (tech Versioning), printed as an error.
|
|
19
|
+
*/
|
|
20
|
+
export function renderDiff(report) {
|
|
21
|
+
const subject = `${report.origin} ${report.declared}`;
|
|
22
|
+
if (report.registry === 'unregistered') {
|
|
23
|
+
return {
|
|
24
|
+
summary: [
|
|
25
|
+
`${report.origin} is not in the Domain registry, or you may not read it: nothing to compare.`,
|
|
26
|
+
],
|
|
27
|
+
refusal: [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (report.baseline === undefined) {
|
|
31
|
+
return {
|
|
32
|
+
summary: [
|
|
33
|
+
`${subject}: no stable version up to ${report.declared} is published, nothing to compare.`,
|
|
34
|
+
],
|
|
35
|
+
refusal: [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const level = report.level;
|
|
39
|
+
const required = `at least ${report.next} (${NATURE[level]})`;
|
|
40
|
+
const rows = changeRows(report.changes, report.origin);
|
|
41
|
+
if (report.changes.length === 0) {
|
|
42
|
+
const state = report.published ? 'is published' : 'is accepted';
|
|
43
|
+
return {
|
|
44
|
+
summary: [`${subject} ${state}: no change since ${report.baseline}.`],
|
|
45
|
+
refusal: [],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (report.published) {
|
|
49
|
+
return {
|
|
50
|
+
summary: [
|
|
51
|
+
`${subject} is published. The changes since ${report.baseline} need ${required}:`,
|
|
52
|
+
...rows,
|
|
53
|
+
],
|
|
54
|
+
refusal: [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (report.accepted) {
|
|
58
|
+
return {
|
|
59
|
+
summary: [
|
|
60
|
+
`${subject} is accepted. The changes since ${report.baseline} need ${required}:`,
|
|
61
|
+
...rows,
|
|
62
|
+
],
|
|
63
|
+
refusal: [],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Only the changes that set the level are listed, as [.105820] lists the breaking ones.
|
|
67
|
+
const binding = report.changes.filter((change) => change.bump === level);
|
|
68
|
+
const others = report.changes.length - binding.length;
|
|
69
|
+
const declare = report.suggestion ?? report.next;
|
|
70
|
+
return {
|
|
71
|
+
summary: [],
|
|
72
|
+
refusal: [
|
|
73
|
+
`error: ${report.declared} is too low, ${NATURE[level]} since ${report.baseline}: at least ${report.next} is required`,
|
|
74
|
+
...changeRows(binding, report.origin),
|
|
75
|
+
...(others === 0 ? [] : [` (and ${others} other ${others === 1 ? 'change' : 'changes'})`]),
|
|
76
|
+
level === 'major'
|
|
77
|
+
? `Declare ${declare}, or keep the old member (expand then contract).`
|
|
78
|
+
: `Declare ${declare}.`,
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* One line per changed member, `<kind> <name> <states> (<details>)`, in report order. A member the
|
|
84
|
+
* engine observes through several facets (a removed Method loses its existence and its signature)
|
|
85
|
+
* is one line.
|
|
86
|
+
*/
|
|
87
|
+
function changeRows(changes, origin) {
|
|
88
|
+
const members = new Map();
|
|
89
|
+
for (const change of changes) {
|
|
90
|
+
const key = `${change.facet === 'build' || change.facet === 'schema' ? change.facet : ''}\u0000${change.subject}`;
|
|
91
|
+
const member = members.get(key) ?? {
|
|
92
|
+
...memberName(change, origin),
|
|
93
|
+
states: [],
|
|
94
|
+
details: [],
|
|
95
|
+
bump: 'patch',
|
|
96
|
+
};
|
|
97
|
+
if (!member.states.includes(change.state))
|
|
98
|
+
member.states.push(change.state);
|
|
99
|
+
if (change.detail !== undefined && !member.details.includes(change.detail)) {
|
|
100
|
+
member.details.push(change.detail);
|
|
101
|
+
}
|
|
102
|
+
if (LEVELS.indexOf(change.bump) > LEVELS.indexOf(member.bump))
|
|
103
|
+
member.bump = change.bump;
|
|
104
|
+
members.set(key, member);
|
|
105
|
+
}
|
|
106
|
+
const rows = [...members.values()];
|
|
107
|
+
const kindWidth = Math.max(0, ...rows.map(({ kind }) => kind.length)) + 1;
|
|
108
|
+
const nameWidth = Math.max(0, ...rows.map(({ name }) => name.length)) + 3;
|
|
109
|
+
return rows.map(({ kind, name, states, details }) => ` ${kind.padEnd(kindWidth)}${name.padEnd(nameWidth)}${states.join(', ')}${details.length === 0 ? '' : ` (${details.join('; ')})`}`);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* `class.Project.method.archive` reads `method Project.archive`. A Key of another origin keeps
|
|
113
|
+
* its origin; a whole-Domain change names the origin.
|
|
114
|
+
*/
|
|
115
|
+
function memberName(change, origin) {
|
|
116
|
+
if (change.facet === 'build' || change.facet === 'schema') {
|
|
117
|
+
return { kind: change.facet, name: change.subject };
|
|
118
|
+
}
|
|
119
|
+
const separator = change.subject.indexOf(':');
|
|
120
|
+
const local = change.subject.slice(separator + 1);
|
|
121
|
+
const prefix = separator > 0 && change.subject.slice(0, separator) !== origin
|
|
122
|
+
? `${change.subject.slice(0, separator)}:`
|
|
123
|
+
: '';
|
|
124
|
+
const member = /^class\.([^.]+)\.(property|method)\.(.+)$/u.exec(local);
|
|
125
|
+
if (member !== null)
|
|
126
|
+
return { kind: member[2], name: `${prefix}${member[1]}.${member[3]}` };
|
|
127
|
+
const dot = local.indexOf('.');
|
|
128
|
+
return dot > 0
|
|
129
|
+
? { kind: local.slice(0, dot), name: `${prefix}${local.slice(dot + 1)}` }
|
|
130
|
+
: { kind: 'member', name: change.subject };
|
|
131
|
+
}
|