@git.zone/cli 6.6.2 → 6.7.1
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/.smartconfig.json +2 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/gitzone.cli.js +2 -2
- package/dist_ts/helpers.workflow.d.ts +3 -0
- package/dist_ts/helpers.workflow.js +14 -1
- package/dist_ts/mod_config/index.js +4 -1
- package/dist_ts/mod_deprecate/helpers.deprecation.d.ts +12 -0
- package/dist_ts/mod_deprecate/helpers.deprecation.js +87 -0
- package/dist_ts/mod_deprecate/index.d.ts +1 -1
- package/dist_ts/mod_deprecate/index.js +77 -40
- package/dist_ts/mod_format/classes.baseformatter.d.ts +6 -0
- package/dist_ts/mod_format/classes.baseformatter.js +61 -1
- package/dist_ts/mod_format/classes.formatplanner.js +14 -3
- package/dist_ts/mod_format/classes.formatstats.d.ts +4 -1
- package/dist_ts/mod_format/classes.formatstats.js +11 -1
- package/dist_ts/mod_format/formatters/readme.formatter.d.ts +2 -1
- package/dist_ts/mod_format/formatters/readme.formatter.js +80 -14
- package/dist_ts/mod_format/index.js +2 -1
- package/dist_ts/mod_format/interfaces.format.d.ts +6 -2
- package/dist_ts/mod_format/interfaces.format.js +1 -1
- package/dist_ts/mod_release/classes.releasejournal.d.ts +17 -3
- package/dist_ts/mod_release/classes.releasejournal.js +144 -12
- package/dist_ts/mod_release/helpers.npmartifact.d.ts +3 -1
- package/dist_ts/mod_release/helpers.npmartifact.js +64 -5
- package/dist_ts/mod_release/helpers.releasepublication.js +21 -19
- package/dist_ts/mod_release/index.d.ts +24 -0
- package/dist_ts/mod_release/index.js +82 -22
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/package.json +2 -2
- package/readme.md +72 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/gitzone.cli.ts +1 -1
- package/ts/helpers.workflow.ts +17 -0
- package/ts/mod_config/index.ts +3 -0
- package/ts/mod_deprecate/helpers.deprecation.ts +151 -0
- package/ts/mod_deprecate/index.ts +92 -41
- package/ts/mod_format/classes.baseformatter.ts +71 -0
- package/ts/mod_format/classes.formatplanner.ts +16 -3
- package/ts/mod_format/classes.formatstats.ts +14 -1
- package/ts/mod_format/formatters/readme.formatter.ts +89 -14
- package/ts/mod_format/index.ts +1 -0
- package/ts/mod_format/interfaces.format.ts +7 -2
- package/ts/mod_release/classes.releasejournal.ts +219 -20
- package/ts/mod_release/helpers.npmartifact.ts +120 -23
- package/ts/mod_release/helpers.releasepublication.ts +49 -15
- package/ts/mod_release/index.ts +127 -30
- package/ts/plugins.ts +2 -0
- package/readme.hints.md +0 -596
- package/readme.plan.md +0 -176
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseFormatter } from '../classes.baseformatter.js';
|
|
2
|
-
import type { IPlannedChange } from '../interfaces.format.js';
|
|
2
|
+
import type { IFormatWarning, IPlannedChange } from '../interfaces.format.js';
|
|
3
3
|
import * as plugins from '../mod.plugins.js';
|
|
4
4
|
import { logger } from '../../gitzone.logging.js';
|
|
5
5
|
|
|
@@ -7,9 +7,33 @@ const DEFAULT_README_CONTENT = `# Project Readme
|
|
|
7
7
|
|
|
8
8
|
This is the initial readme file.`;
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const DEFAULT_PLAN_CONTENT = `# Project Plan
|
|
11
11
|
|
|
12
|
-
This is the initial
|
|
12
|
+
This is the initial plan file. It holds implementation plans and is not published.`;
|
|
13
|
+
|
|
14
|
+
const DEFAULT_HINTS_CONTENT = `# Project Hints
|
|
15
|
+
|
|
16
|
+
This is the initial hints file. It holds development findings and is not published.`;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Development documents used to live in readme.plan.md and readme.hints.md.
|
|
20
|
+
* npm-packlist force-includes every root file whose name starts with "readme",
|
|
21
|
+
* regardless of the package.json files array or .npmignore, so those documents
|
|
22
|
+
* shipped inside every published tarball. plan.md and hints.md carry the same
|
|
23
|
+
* content without being force-included.
|
|
24
|
+
*/
|
|
25
|
+
const DEVELOPMENT_DOCUMENTS = [
|
|
26
|
+
{
|
|
27
|
+
legacyPath: 'readme.plan.md',
|
|
28
|
+
path: 'plan.md',
|
|
29
|
+
defaultContent: DEFAULT_PLAN_CONTENT,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
legacyPath: 'readme.hints.md',
|
|
33
|
+
path: 'hints.md',
|
|
34
|
+
defaultContent: DEFAULT_HINTS_CONTENT,
|
|
35
|
+
},
|
|
36
|
+
] as const;
|
|
13
37
|
|
|
14
38
|
export class ReadmeFormatter extends BaseFormatter {
|
|
15
39
|
get name(): string {
|
|
@@ -19,7 +43,7 @@ export class ReadmeFormatter extends BaseFormatter {
|
|
|
19
43
|
async analyze(): Promise<IPlannedChange[]> {
|
|
20
44
|
const changes: IPlannedChange[] = [];
|
|
21
45
|
|
|
22
|
-
// Check readme.md
|
|
46
|
+
// Check readme.md - this one is meant to be published, keep it as is
|
|
23
47
|
const readmeExists = await plugins.smartfs.file('readme.md').exists();
|
|
24
48
|
if (!readmeExists) {
|
|
25
49
|
changes.push({
|
|
@@ -31,22 +55,73 @@ export class ReadmeFormatter extends BaseFormatter {
|
|
|
31
55
|
});
|
|
32
56
|
}
|
|
33
57
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
58
|
+
for (const document of DEVELOPMENT_DOCUMENTS) {
|
|
59
|
+
const legacyExists = await plugins.smartfs
|
|
60
|
+
.file(document.legacyPath)
|
|
61
|
+
.exists();
|
|
62
|
+
const currentExists = await plugins.smartfs.file(document.path).exists();
|
|
63
|
+
|
|
64
|
+
if (legacyExists && currentExists) {
|
|
65
|
+
// Conflict: reported by validate(), never resolved by clobbering.
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (legacyExists) {
|
|
70
|
+
changes.push({
|
|
71
|
+
type: 'rename',
|
|
72
|
+
path: document.path,
|
|
73
|
+
fromPath: document.legacyPath,
|
|
74
|
+
module: this.name,
|
|
75
|
+
description: `Rename ${document.legacyPath} to ${document.path} so it is not published to npm`,
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!currentExists) {
|
|
81
|
+
changes.push({
|
|
82
|
+
type: 'create',
|
|
83
|
+
path: document.path,
|
|
84
|
+
module: this.name,
|
|
85
|
+
description: `Create ${document.path}`,
|
|
86
|
+
content: document.defaultContent,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
44
89
|
}
|
|
45
90
|
|
|
46
91
|
return changes;
|
|
47
92
|
}
|
|
48
93
|
|
|
94
|
+
async validate(): Promise<IFormatWarning[]> {
|
|
95
|
+
const warnings: IFormatWarning[] = [];
|
|
96
|
+
|
|
97
|
+
for (const document of DEVELOPMENT_DOCUMENTS) {
|
|
98
|
+
const legacyExists = await plugins.smartfs
|
|
99
|
+
.file(document.legacyPath)
|
|
100
|
+
.exists();
|
|
101
|
+
const currentExists = await plugins.smartfs.file(document.path).exists();
|
|
102
|
+
if (legacyExists && currentExists) {
|
|
103
|
+
warnings.push({
|
|
104
|
+
level: 'error',
|
|
105
|
+
module: this.name,
|
|
106
|
+
message:
|
|
107
|
+
`Both ${document.legacyPath} and ${document.path} exist. ` +
|
|
108
|
+
`${document.legacyPath} is force-included in every npm tarball and must go away, ` +
|
|
109
|
+
`but ${document.path} would be overwritten. Merge them by hand, then remove ${document.legacyPath}.`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return warnings;
|
|
115
|
+
}
|
|
116
|
+
|
|
49
117
|
async applyChange(change: IPlannedChange): Promise<void> {
|
|
118
|
+
if (change.type === 'rename') {
|
|
119
|
+
if (!change.fromPath) return;
|
|
120
|
+
await this.renameFile(change.fromPath, change.path);
|
|
121
|
+
logger.log('info', `Renamed ${change.fromPath} to ${change.path}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
50
125
|
if (change.type !== 'create' || !change.content) return;
|
|
51
126
|
|
|
52
127
|
await this.createFile(change.path, change.content);
|
package/ts/mod_format/index.ts
CHANGED
|
@@ -4,35 +4,40 @@ export type IFormatWarning = {
|
|
|
4
4
|
module: string;
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
+
export type TPlannedChangeType = 'create' | 'modify' | 'delete' | 'rename';
|
|
8
|
+
|
|
7
9
|
export type IFormatPlan = {
|
|
8
10
|
summary: {
|
|
9
11
|
totalFiles: number;
|
|
10
12
|
filesAdded: number;
|
|
11
13
|
filesModified: number;
|
|
12
14
|
filesRemoved: number;
|
|
15
|
+
filesRenamed: number;
|
|
13
16
|
};
|
|
14
17
|
changes: IPlannedChange[];
|
|
15
18
|
warnings: IFormatWarning[];
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
export type IPlannedChange = {
|
|
19
|
-
type:
|
|
22
|
+
type: TPlannedChangeType;
|
|
20
23
|
path: string;
|
|
21
24
|
module: string;
|
|
22
25
|
description: string;
|
|
23
26
|
content?: string; // New content for create/modify operations
|
|
24
27
|
mode?: string; // Optional file mode, e.g. 0755 for generated executables
|
|
28
|
+
fromPath?: string; // Source path for rename operations, path is the destination
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
export interface ICheckResult {
|
|
28
32
|
hasDiff: boolean;
|
|
29
33
|
diffs: Array<{
|
|
30
34
|
path: string;
|
|
31
|
-
type:
|
|
35
|
+
type: TPlannedChangeType;
|
|
32
36
|
before?: string;
|
|
33
37
|
after?: string;
|
|
34
38
|
modeBefore?: string;
|
|
35
39
|
modeAfter?: string;
|
|
40
|
+
fromPath?: string;
|
|
36
41
|
}>;
|
|
37
42
|
}
|
|
38
43
|
|
|
@@ -47,7 +47,7 @@ export interface IReleaseTargetStatus {
|
|
|
47
47
|
|
|
48
48
|
export interface IReleaseArtifact {
|
|
49
49
|
kind: "npm-tarball";
|
|
50
|
-
file:
|
|
50
|
+
file: string;
|
|
51
51
|
packageName: string;
|
|
52
52
|
version: string;
|
|
53
53
|
size: number;
|
|
@@ -64,6 +64,7 @@ export interface IReleaseGitJournal extends IReleaseTargetStatus {
|
|
|
64
64
|
|
|
65
65
|
export interface IReleaseNpmRegistryJournal extends IReleaseTargetStatus {
|
|
66
66
|
registry: string;
|
|
67
|
+
packageName?: string;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
export interface IReleaseJournalV1 {
|
|
@@ -135,7 +136,49 @@ export interface IReleaseJournalV2 {
|
|
|
135
136
|
completedAt: string | null;
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
|
|
139
|
+
/** Ordered, independently packed modules from one tspublish repository release. */
|
|
140
|
+
export interface IReleasePackage {
|
|
141
|
+
folder: string;
|
|
142
|
+
artifact: IReleaseArtifact;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface IReleaseJournalV3 extends Omit<
|
|
146
|
+
IReleaseJournalV1,
|
|
147
|
+
"schemaVersion" | "artifact"
|
|
148
|
+
> {
|
|
149
|
+
schemaVersion: 3;
|
|
150
|
+
artifact: null;
|
|
151
|
+
packages: IReleasePackage[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type TReleaseJournal =
|
|
155
|
+
IReleaseJournalV1 | IReleaseJournalV2 | IReleaseJournalV3;
|
|
156
|
+
|
|
157
|
+
export const releasePackageArtifactFile = (packageNameArg: string): string =>
|
|
158
|
+
`package.${plugins.crypto.createHash("sha256").update(packageNameArg).digest("hex")}.tgz`;
|
|
159
|
+
|
|
160
|
+
export const getReleaseArtifacts = (
|
|
161
|
+
journalArg: TReleaseJournal,
|
|
162
|
+
): IReleaseArtifact[] =>
|
|
163
|
+
journalArg.schemaVersion === 3
|
|
164
|
+
? journalArg.packages.map((entryArg) => entryArg.artifact)
|
|
165
|
+
: journalArg.artifact
|
|
166
|
+
? [journalArg.artifact]
|
|
167
|
+
: [];
|
|
168
|
+
|
|
169
|
+
export const getReleaseNpmArtifact = (
|
|
170
|
+
journalArg: TReleaseJournal,
|
|
171
|
+
packageNameArg?: string,
|
|
172
|
+
): IReleaseArtifact => {
|
|
173
|
+
const artifact =
|
|
174
|
+
journalArg.schemaVersion === 3
|
|
175
|
+
? journalArg.packages.find(
|
|
176
|
+
(entryArg) => entryArg.artifact.packageName === packageNameArg,
|
|
177
|
+
)?.artifact
|
|
178
|
+
: journalArg.artifact;
|
|
179
|
+
if (!artifact) throw new Error("npm release target has no exact artifact.");
|
|
180
|
+
return artifact;
|
|
181
|
+
};
|
|
139
182
|
|
|
140
183
|
const releaseVersionRegex = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
|
|
141
184
|
const gitOidRegex = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
@@ -356,7 +399,7 @@ const isComplete = (journalArg: TReleaseJournal): boolean => {
|
|
|
356
399
|
].every((stateArg) => stateArg === "verified" || stateArg === "skipped");
|
|
357
400
|
if (
|
|
358
401
|
!commonComplete ||
|
|
359
|
-
journalArg.schemaVersion
|
|
402
|
+
journalArg.schemaVersion !== releaseJournalSchemaVersion
|
|
360
403
|
) {
|
|
361
404
|
return commonComplete;
|
|
362
405
|
}
|
|
@@ -375,6 +418,9 @@ const immutableJournalIdentity = (journalArg: TReleaseJournal): string =>
|
|
|
375
418
|
schemaVersion: journalArg.schemaVersion,
|
|
376
419
|
release: journalArg.release,
|
|
377
420
|
artifact: journalArg.artifact,
|
|
421
|
+
...(journalArg.schemaVersion === 3
|
|
422
|
+
? { packages: journalArg.packages }
|
|
423
|
+
: {}),
|
|
378
424
|
git: {
|
|
379
425
|
remote: journalArg.git.remote,
|
|
380
426
|
destinationHash: journalArg.git.destinationHash,
|
|
@@ -384,8 +430,10 @@ const immutableJournalIdentity = (journalArg: TReleaseJournal): string =>
|
|
|
384
430
|
access: journalArg.npm.access,
|
|
385
431
|
tag: journalArg.npm.tag,
|
|
386
432
|
alreadyPublished: journalArg.npm.alreadyPublished,
|
|
387
|
-
registries: journalArg.npm.registries.map(
|
|
388
|
-
|
|
433
|
+
registries: journalArg.npm.registries.map((registryArg) =>
|
|
434
|
+
journalArg.schemaVersion === 3
|
|
435
|
+
? [registryArg.packageName, registryArg.registry]
|
|
436
|
+
: registryArg.registry,
|
|
389
437
|
),
|
|
390
438
|
},
|
|
391
439
|
docker:
|
|
@@ -994,6 +1042,138 @@ const assertReleaseJournalV2 = (valueArg: unknown): IReleaseJournalV2 => {
|
|
|
994
1042
|
return valueArg as unknown as IReleaseJournalV2;
|
|
995
1043
|
};
|
|
996
1044
|
|
|
1045
|
+
const assertReleaseJournalV3 = (
|
|
1046
|
+
valueArg: Record<string, unknown>,
|
|
1047
|
+
): IReleaseJournalV3 => {
|
|
1048
|
+
assertExactKeys(
|
|
1049
|
+
valueArg,
|
|
1050
|
+
[
|
|
1051
|
+
"kind",
|
|
1052
|
+
"schemaVersion",
|
|
1053
|
+
"revision",
|
|
1054
|
+
"release",
|
|
1055
|
+
"artifact",
|
|
1056
|
+
"packages",
|
|
1057
|
+
"git",
|
|
1058
|
+
"npm",
|
|
1059
|
+
"createdAt",
|
|
1060
|
+
"updatedAt",
|
|
1061
|
+
"completedAt",
|
|
1062
|
+
],
|
|
1063
|
+
"Release journal",
|
|
1064
|
+
);
|
|
1065
|
+
if (
|
|
1066
|
+
valueArg.artifact !== null ||
|
|
1067
|
+
!Array.isArray(valueArg.packages) ||
|
|
1068
|
+
!valueArg.packages.length ||
|
|
1069
|
+
valueArg.packages.length > 1000 ||
|
|
1070
|
+
!isPlainObject(valueArg.npm) ||
|
|
1071
|
+
!Array.isArray(valueArg.npm.registries)
|
|
1072
|
+
) {
|
|
1073
|
+
throw new Error("Schema 3 requires an ordered npm package set.");
|
|
1074
|
+
}
|
|
1075
|
+
const names = new Set<string>();
|
|
1076
|
+
const folders = new Set<string>();
|
|
1077
|
+
const targets = valueArg.npm.registries;
|
|
1078
|
+
let expectedRegistries: string[] | undefined;
|
|
1079
|
+
let targetOffset = 0;
|
|
1080
|
+
for (const entry of valueArg.packages) {
|
|
1081
|
+
if (!isPlainObject(entry) || !isPlainObject(entry.artifact))
|
|
1082
|
+
throw new Error("Invalid release package.");
|
|
1083
|
+
assertExactKeys(entry, ["folder", "artifact"], "Release package");
|
|
1084
|
+
if (
|
|
1085
|
+
typeof entry.folder !== "string" ||
|
|
1086
|
+
!/^ts(?:_[a-z0-9_]+)?$/.test(entry.folder) ||
|
|
1087
|
+
folders.has(entry.folder)
|
|
1088
|
+
) {
|
|
1089
|
+
throw new Error(
|
|
1090
|
+
"Release package folders must be unique canonical source folders.",
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
folders.add(entry.folder);
|
|
1094
|
+
const artifact = assertArtifact({
|
|
1095
|
+
...entry.artifact,
|
|
1096
|
+
file: releaseArtifactFileName,
|
|
1097
|
+
})!;
|
|
1098
|
+
if (
|
|
1099
|
+
names.has(artifact.packageName) ||
|
|
1100
|
+
entry.artifact.file !== releasePackageArtifactFile(artifact.packageName)
|
|
1101
|
+
) {
|
|
1102
|
+
throw new Error(
|
|
1103
|
+
"Release packages require unique names and canonical artifact filenames.",
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
names.add(artifact.packageName);
|
|
1107
|
+
const registries: IReleaseNpmRegistryJournal[] = [];
|
|
1108
|
+
while (
|
|
1109
|
+
targetOffset < targets.length &&
|
|
1110
|
+
isPlainObject(targets[targetOffset]) &&
|
|
1111
|
+
targets[targetOffset].packageName === artifact.packageName
|
|
1112
|
+
) {
|
|
1113
|
+
const target = targets[targetOffset++];
|
|
1114
|
+
assertExactKeys(
|
|
1115
|
+
target,
|
|
1116
|
+
["state", "attempts", "attempt", "error", "registry", "packageName"],
|
|
1117
|
+
"Release package target",
|
|
1118
|
+
);
|
|
1119
|
+
const { packageName, ...registry } = target;
|
|
1120
|
+
registries.push(registry as unknown as IReleaseNpmRegistryJournal);
|
|
1121
|
+
}
|
|
1122
|
+
const registryUrls = registries.map((target) => target.registry);
|
|
1123
|
+
if (
|
|
1124
|
+
!registries.length ||
|
|
1125
|
+
(expectedRegistries &&
|
|
1126
|
+
JSON.stringify(expectedRegistries) !== JSON.stringify(registryUrls))
|
|
1127
|
+
) {
|
|
1128
|
+
throw new Error(
|
|
1129
|
+
"Release package targets must retain one complete, ordered registry matrix.",
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
expectedRegistries = registryUrls;
|
|
1133
|
+
// Reuse the established identity, destination, state and timestamp validators.
|
|
1134
|
+
// Each projection is validation-only; schema 1/2 data is never rewritten.
|
|
1135
|
+
const { packages, ...common } = valueArg;
|
|
1136
|
+
const complete = [
|
|
1137
|
+
valueArg.git as IReleaseTargetStatus,
|
|
1138
|
+
...registries,
|
|
1139
|
+
].every(
|
|
1140
|
+
(target) => target?.state === "verified" || target?.state === "skipped",
|
|
1141
|
+
);
|
|
1142
|
+
assertReleaseJournalV1({
|
|
1143
|
+
...common,
|
|
1144
|
+
schemaVersion: 1,
|
|
1145
|
+
artifact,
|
|
1146
|
+
npm: { ...valueArg.npm, registries },
|
|
1147
|
+
completedAt: complete ? valueArg.updatedAt : null,
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
if (targetOffset !== targets.length)
|
|
1151
|
+
throw new Error("Unexpected or out-of-order release package target.");
|
|
1152
|
+
const statuses = [
|
|
1153
|
+
valueArg.git as IReleaseTargetStatus,
|
|
1154
|
+
...(targets as IReleaseTargetStatus[]),
|
|
1155
|
+
];
|
|
1156
|
+
if (statuses.filter((target) => target.state === "publishing").length > 1) {
|
|
1157
|
+
throw new Error("Schema 3 permits at most one active publication owner.");
|
|
1158
|
+
}
|
|
1159
|
+
const journal = valueArg as unknown as IReleaseJournalV3;
|
|
1160
|
+
if (valueArg.completedAt !== null) {
|
|
1161
|
+
const completedAt = assertIsoTimestamp(
|
|
1162
|
+
valueArg.completedAt,
|
|
1163
|
+
"Release journal completedAt",
|
|
1164
|
+
);
|
|
1165
|
+
if (
|
|
1166
|
+
completedAt < journal.createdAt ||
|
|
1167
|
+
completedAt > journal.updatedAt ||
|
|
1168
|
+
!isComplete(journal)
|
|
1169
|
+
) {
|
|
1170
|
+
throw new Error("Release journal completion state is inconsistent.");
|
|
1171
|
+
}
|
|
1172
|
+
} else if (isComplete(journal))
|
|
1173
|
+
throw new Error("Complete release journal must record completedAt.");
|
|
1174
|
+
return journal;
|
|
1175
|
+
};
|
|
1176
|
+
|
|
997
1177
|
export const assertReleaseJournal = (valueArg: unknown): TReleaseJournal => {
|
|
998
1178
|
if (!isPlainObject(valueArg)) {
|
|
999
1179
|
throw new Error("Release journal root must be a JSON object.");
|
|
@@ -1004,6 +1184,7 @@ export const assertReleaseJournal = (valueArg: unknown): TReleaseJournal => {
|
|
|
1004
1184
|
if (valueArg.schemaVersion === releaseJournalSchemaVersion) {
|
|
1005
1185
|
return assertReleaseJournalV2(valueArg);
|
|
1006
1186
|
}
|
|
1187
|
+
if (valueArg.schemaVersion === 3) return assertReleaseJournalV3(valueArg);
|
|
1007
1188
|
throw new Error("Release journal header is invalid or unsupported.");
|
|
1008
1189
|
};
|
|
1009
1190
|
|
|
@@ -1014,7 +1195,7 @@ const prepareReleaseJournalSerialization = (
|
|
|
1014
1195
|
journalArg: TReleaseJournal,
|
|
1015
1196
|
): TReleaseJournal => {
|
|
1016
1197
|
const journal = assertReleaseJournal(journalArg);
|
|
1017
|
-
if (journal.schemaVersion
|
|
1198
|
+
if (journal.schemaVersion !== releaseJournalSchemaVersion) {
|
|
1018
1199
|
return journal;
|
|
1019
1200
|
}
|
|
1020
1201
|
return {
|
|
@@ -1292,11 +1473,17 @@ export class ReleaseJournalStore {
|
|
|
1292
1473
|
return plugins.path.join(this.rootPath, `v${version}`);
|
|
1293
1474
|
}
|
|
1294
1475
|
|
|
1295
|
-
public getArtifactPath(
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1476
|
+
public getArtifactPath(
|
|
1477
|
+
versionArg: string,
|
|
1478
|
+
fileArg: string = releaseArtifactFileName,
|
|
1479
|
+
): string {
|
|
1480
|
+
if (
|
|
1481
|
+
fileArg !== releaseArtifactFileName &&
|
|
1482
|
+
!/^package\.[0-9a-f]{64}\.tgz$/.test(fileArg)
|
|
1483
|
+
) {
|
|
1484
|
+
throw new Error("Invalid release artifact filename.");
|
|
1485
|
+
}
|
|
1486
|
+
return plugins.path.join(this.getReleaseDirectory(versionArg), fileArg);
|
|
1300
1487
|
}
|
|
1301
1488
|
|
|
1302
1489
|
public async createTemporaryDirectory(versionArg: string): Promise<string> {
|
|
@@ -1379,6 +1566,21 @@ export class ReleaseJournalStore {
|
|
|
1379
1566
|
if (journal.revision !== 1) {
|
|
1380
1567
|
throw new Error("New release journals must start at revision 1.");
|
|
1381
1568
|
}
|
|
1569
|
+
if (
|
|
1570
|
+
journal.schemaVersion === 3 &&
|
|
1571
|
+
(journal.completedAt !== null ||
|
|
1572
|
+
[journal.git, ...journal.npm.registries].some(
|
|
1573
|
+
(target) =>
|
|
1574
|
+
!["pending", "skipped"].includes(target.state) ||
|
|
1575
|
+
target.attempts !== 0 ||
|
|
1576
|
+
target.attempt !== null ||
|
|
1577
|
+
target.error !== null,
|
|
1578
|
+
))
|
|
1579
|
+
) {
|
|
1580
|
+
throw new Error(
|
|
1581
|
+
"New schema 3 journals require unclaimed publication targets.",
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1382
1584
|
if (journal.schemaVersion === releaseJournalSchemaVersion) {
|
|
1383
1585
|
const commonTargets = [journal.git, ...journal.npm.registries];
|
|
1384
1586
|
const commonTargetsUnclaimed = commonTargets.every(
|
|
@@ -1419,28 +1621,25 @@ export class ReleaseJournalStore {
|
|
|
1419
1621
|
"Prepared release directory is outside canonical storage.",
|
|
1420
1622
|
);
|
|
1421
1623
|
}
|
|
1422
|
-
|
|
1423
|
-
const artifactPath = plugins.path.join(
|
|
1424
|
-
temporaryDirectory,
|
|
1425
|
-
releaseArtifactFileName,
|
|
1426
|
-
);
|
|
1624
|
+
for (const artifact of getReleaseArtifacts(journal)) {
|
|
1625
|
+
const artifactPath = plugins.path.join(temporaryDirectory, artifact.file);
|
|
1427
1626
|
const artifactStat = await plugins.fs.lstat(artifactPath);
|
|
1428
1627
|
if (!artifactStat.isFile() || artifactStat.isSymbolicLink()) {
|
|
1429
1628
|
throw new Error("Prepared release artifact must be a regular file.");
|
|
1430
1629
|
}
|
|
1431
1630
|
const artifactBytes = await plugins.fs.readFile(artifactPath);
|
|
1432
1631
|
if (
|
|
1433
|
-
artifactBytes.byteLength !==
|
|
1632
|
+
artifactBytes.byteLength !== artifact.size ||
|
|
1434
1633
|
plugins.crypto
|
|
1435
1634
|
.createHash("sha1")
|
|
1436
1635
|
.update(artifactBytes)
|
|
1437
|
-
.digest("hex") !==
|
|
1636
|
+
.digest("hex") !== artifact.sha1 ||
|
|
1438
1637
|
plugins.crypto
|
|
1439
1638
|
.createHash("sha256")
|
|
1440
1639
|
.update(artifactBytes)
|
|
1441
|
-
.digest("hex") !==
|
|
1640
|
+
.digest("hex") !== artifact.sha256 ||
|
|
1442
1641
|
`sha512-${plugins.crypto.createHash("sha512").update(artifactBytes).digest("base64")}` !==
|
|
1443
|
-
|
|
1642
|
+
artifact.integrity
|
|
1444
1643
|
) {
|
|
1445
1644
|
throw new Error(
|
|
1446
1645
|
"Prepared release artifact does not match its journal identity.",
|