@ykdz/template 0.0.33 → 0.0.36
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/application.d.ts +2 -1
- package/dist/application.js +64 -75
- package/dist/main.js +6 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/foundation.d.ts +50 -36
- package/node_modules/@ykdz/template-builtin-presets/dist/src/foundation.js +970 -365
- package/node_modules/@ykdz/template-builtin-presets/dist/src/generation-record.d.ts +24 -0
- package/node_modules/@ykdz/template-builtin-presets/dist/src/generation-record.js +154 -0
- package/node_modules/@ykdz/template-builtin-presets/dist/src/index.d.ts +2 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/index.js +1 -1
- package/node_modules/@ykdz/template-builtin-presets/dist/src/rust-bin/definition.d.ts +31 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/rust-bin/definition.js +29 -33
- package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/development-container.d.ts +1 -0
- package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/development-container.js +17 -0
- package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/typescript.d.ts +4 -3
- package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/typescript.js +9 -5
- package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/vue.d.ts +1 -1
- package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/vue.js +1 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/ts-cli/definition.d.ts +31 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/ts-cli/definition.js +79 -58
- package/node_modules/@ykdz/template-builtin-presets/dist/src/ts-lib/definition.d.ts +31 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/ts-lib/definition.js +31 -23
- package/node_modules/@ykdz/template-builtin-presets/dist/src/vike-app/definition.d.ts +22 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/vike-app/definition.js +61 -35
- package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-app/definition.d.ts +26 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-app/definition.js +19 -8
- package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-hono-app/definition.d.ts +22 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-hono-app/definition.js +31 -21
- package/node_modules/@ykdz/template-builtin-presets/package.json +12 -11
- package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check-deployment-diagnostics.yml +1 -1
- package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check-deployment.yml +1 -1
- package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.pnpmfile.mjs +19 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/foundation/gitignore +1 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/foundation/tsconfig.packing-hook.json +8 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/shared/devcontainer/github-cli.Dockerfile +9 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/CHANGELOG.md.template +17 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/RELEASING.md +17 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/artifact.ts +1271 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/changelog.ts +283 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/check-artifact.ts +99 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/check-readiness.ts +66 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/cli-command-identity.ts +4 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/handoff.ts +450 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/publish.ts +483 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/readiness.ts +1125 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/release.ts +819 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/release.yml +119 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/root-tsconfig.json +13 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/tsconfig.json +4 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/turbo.json +6 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/README.md +48 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/assets/CHANGELOG.md.template +17 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/assets/LICENSE-APACHE-2.0.txt +202 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/assets/LICENSE-MIT.txt +21 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/assets/README.md.template +5 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/bridge.ts +3336 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/setup.sh +221 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/src/cli-command-identity.ts +133 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/src/cli.ts +4 -3
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/src/main.ts +5 -4
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/test/integration/command.test.ts +14 -13
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/test/unit/cli-command-identity.test.ts +76 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/tsconfig.build.json +2 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/src/readiness.ts +1 -1
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/test/global-setup.ts +21 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/tsconfig.json +6 -1
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/vitest.config.ts +7 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db-migrations/drizzle.config.ts +5 -1
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/playwright.config.ts +102 -22
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/test/playwright-teardown.ts +12 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/vite.config.ts +39 -1
- package/node_modules/@ykdz/template-builtin-presets/templates/vue-app/playwright.config.ts +48 -7
- package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/web/playwright.config.ts +59 -8
- package/node_modules/@ykdz/template-core/dist/module-graph.d.ts +15 -53
- package/node_modules/@ykdz/template-core/dist/module-graph.js +134 -101
- package/node_modules/@ykdz/template-core/dist/package-contribution.d.ts +13 -1
- package/node_modules/@ykdz/template-core/dist/package-contribution.js +92 -0
- package/node_modules/@ykdz/template-core/dist/pnpm-workspace.yaml +14 -10
- package/node_modules/@ykdz/template-core/dist/preset-definition.d.ts +71 -13
- package/node_modules/@ykdz/template-core/dist/preset-definition.js +16 -1
- package/node_modules/@ykdz/template-core/dist/project-blueprint.d.ts +56 -0
- package/node_modules/@ykdz/template-core/dist/project-blueprint.js +271 -0
- package/node_modules/@ykdz/template-core/dist/project-linking-v2.d.ts +2 -2
- package/node_modules/@ykdz/template-core/dist/project-linking-v2.js +2 -2
- package/node_modules/@ykdz/template-core/dist/project-projection.d.ts +2 -1
- package/node_modules/@ykdz/template-core/dist/project-projection.js +107 -20
- package/node_modules/@ykdz/template-core/dist/renderer.d.ts +0 -2
- package/node_modules/@ykdz/template-core/dist/renderer.js +17 -38
- package/node_modules/@ykdz/template-core/dist/template-boundary-check.d.ts +2 -2
- package/node_modules/@ykdz/template-core/dist/template-boundary-check.js +6 -0
- package/node_modules/@ykdz/template-core/dist/toolchain-resolution.d.ts +2 -0
- package/node_modules/@ykdz/template-core/dist/toolchain-resolution.js +9 -3
- package/package.json +3 -3
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db-migrations/scripts/prepare-database.ts +0 -45
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/scripts/run-playwright.ts +0 -151
- package/node_modules/@ykdz/template-builtin-presets/templates/vue-app/scripts/run-playwright.ts +0 -62
- package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/web/scripts/run-playwright.ts +0 -103
- package/node_modules/@ykdz/template-core/dist/project-blueprint-v2.d.ts +0 -33
- package/node_modules/@ykdz/template-core/dist/project-blueprint-v2.js +0 -182
|
@@ -0,0 +1,1271 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
lstat,
|
|
5
|
+
mkdtemp,
|
|
6
|
+
readFile,
|
|
7
|
+
readdir,
|
|
8
|
+
realpath,
|
|
9
|
+
rename,
|
|
10
|
+
rm,
|
|
11
|
+
stat,
|
|
12
|
+
writeFile,
|
|
13
|
+
} from "node:fs/promises";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
|
|
17
|
+
import { validRange } from "semver";
|
|
18
|
+
import { list } from "tar";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
inspectNpmPublicationReadiness,
|
|
22
|
+
type NpmPublicationReadiness,
|
|
23
|
+
} from "./readiness.ts";
|
|
24
|
+
|
|
25
|
+
type JsonObject = Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
export type PublicationArtifactFailureCode =
|
|
28
|
+
| "artifact-output-invalid"
|
|
29
|
+
| "artifact-temporary-output-failed"
|
|
30
|
+
| "artifact-pack-failed"
|
|
31
|
+
| "artifact-pack-output-invalid"
|
|
32
|
+
| "artifact-archive-unsafe"
|
|
33
|
+
| "artifact-manifest-invalid"
|
|
34
|
+
| "artifact-manifest-contract"
|
|
35
|
+
| "artifact-files-contract"
|
|
36
|
+
| "artifact-bin-contract"
|
|
37
|
+
| "consumer-install-failed"
|
|
38
|
+
| "consumer-smoke-failed"
|
|
39
|
+
| "artifact-receipt-failed"
|
|
40
|
+
| "artifact-cleanup-failed";
|
|
41
|
+
|
|
42
|
+
export type PublicationArtifactFailure = {
|
|
43
|
+
readonly code: PublicationArtifactFailureCode;
|
|
44
|
+
readonly observed: string;
|
|
45
|
+
readonly expected: string;
|
|
46
|
+
readonly nextAction: string;
|
|
47
|
+
readonly command?: {
|
|
48
|
+
readonly executable: string;
|
|
49
|
+
readonly args: readonly string[];
|
|
50
|
+
readonly exitCode: number;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type VerifiedPublicationArtifactReceipt = {
|
|
55
|
+
readonly schemaVersion: 1;
|
|
56
|
+
readonly artifact: {
|
|
57
|
+
readonly file: string;
|
|
58
|
+
readonly checksumFile: "SHA512SUMS";
|
|
59
|
+
readonly size: number;
|
|
60
|
+
readonly integrity: string;
|
|
61
|
+
};
|
|
62
|
+
readonly publication: {
|
|
63
|
+
readonly packageName: string;
|
|
64
|
+
readonly version: string;
|
|
65
|
+
readonly commandName: string;
|
|
66
|
+
readonly repository: string;
|
|
67
|
+
readonly releaseDate: string;
|
|
68
|
+
readonly releaseNotes: string;
|
|
69
|
+
};
|
|
70
|
+
readonly packedManifest: Readonly<JsonObject>;
|
|
71
|
+
readonly files: readonly {
|
|
72
|
+
readonly path: string;
|
|
73
|
+
readonly mode: number;
|
|
74
|
+
readonly size: number;
|
|
75
|
+
}[];
|
|
76
|
+
readonly bin: {
|
|
77
|
+
readonly path: "package/dist/cli.js";
|
|
78
|
+
readonly shebang: "#!/usr/bin/env node";
|
|
79
|
+
readonly mode: number;
|
|
80
|
+
readonly posixExecutableChecked: boolean;
|
|
81
|
+
};
|
|
82
|
+
readonly smokes: readonly [
|
|
83
|
+
{
|
|
84
|
+
readonly name: "runtime-import";
|
|
85
|
+
readonly args: readonly [];
|
|
86
|
+
readonly stdout: "";
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
readonly name: "help";
|
|
90
|
+
readonly args: readonly ["--help"];
|
|
91
|
+
readonly stdout: string;
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
readonly name: "version";
|
|
95
|
+
readonly args: readonly ["--version"];
|
|
96
|
+
readonly stdout: string;
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
readonly name: "greet";
|
|
100
|
+
readonly args: readonly ["greet", " Ada Lovelace "];
|
|
101
|
+
readonly stdout: string;
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type VerifiedPublicationArtifactResult =
|
|
107
|
+
| {
|
|
108
|
+
readonly kind: "blocked";
|
|
109
|
+
readonly readiness: Extract<
|
|
110
|
+
NpmPublicationReadiness,
|
|
111
|
+
{ readonly kind: "blocked" }
|
|
112
|
+
>;
|
|
113
|
+
}
|
|
114
|
+
| { readonly kind: "failed"; readonly failure: PublicationArtifactFailure }
|
|
115
|
+
| {
|
|
116
|
+
readonly kind: "verified";
|
|
117
|
+
readonly artifactPath: string;
|
|
118
|
+
readonly checksumPath: string;
|
|
119
|
+
readonly receiptPath: string;
|
|
120
|
+
readonly receipt: VerifiedPublicationArtifactReceipt;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
type CommandResult = {
|
|
124
|
+
readonly exitCode: number;
|
|
125
|
+
readonly stdout: string;
|
|
126
|
+
readonly stderr: string;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
type ArchiveFile = {
|
|
130
|
+
readonly path: string;
|
|
131
|
+
readonly mode: number;
|
|
132
|
+
readonly size: number;
|
|
133
|
+
readonly body?: Buffer;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
type ArchiveInspection = {
|
|
137
|
+
readonly files: readonly ArchiveFile[];
|
|
138
|
+
readonly manifest: JsonObject;
|
|
139
|
+
readonly cliSource: Buffer;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
type ValidatedArtifactPaths = {
|
|
143
|
+
readonly repositoryRoot: string;
|
|
144
|
+
readonly packageRoot: string;
|
|
145
|
+
readonly outputDirectory: string;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
type CommandPlan = {
|
|
149
|
+
readonly executable: string;
|
|
150
|
+
readonly args: readonly string[];
|
|
151
|
+
readonly environment: NodeJS.ProcessEnv;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const expectedFiles = [
|
|
155
|
+
"package/CHANGELOG.md",
|
|
156
|
+
"package/LICENSE",
|
|
157
|
+
"package/README.md",
|
|
158
|
+
"package/dist/cli-command-identity.js",
|
|
159
|
+
"package/dist/cli.js",
|
|
160
|
+
"package/dist/main.js",
|
|
161
|
+
"package/package.json",
|
|
162
|
+
] as const;
|
|
163
|
+
|
|
164
|
+
const allowedPackedManifestFields = new Set([
|
|
165
|
+
"bin",
|
|
166
|
+
"bugs",
|
|
167
|
+
"dependencies",
|
|
168
|
+
"description",
|
|
169
|
+
"engines",
|
|
170
|
+
"files",
|
|
171
|
+
"homepage",
|
|
172
|
+
"license",
|
|
173
|
+
"name",
|
|
174
|
+
"publishConfig",
|
|
175
|
+
"repository",
|
|
176
|
+
"type",
|
|
177
|
+
"version",
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
const inheritedChildEnvironmentKeys = [
|
|
181
|
+
"ComSpec",
|
|
182
|
+
"HTTP_PROXY",
|
|
183
|
+
"HTTPS_PROXY",
|
|
184
|
+
"LANG",
|
|
185
|
+
"LC_ALL",
|
|
186
|
+
"LC_CTYPE",
|
|
187
|
+
"NO_PROXY",
|
|
188
|
+
"PATH",
|
|
189
|
+
"PATHEXT",
|
|
190
|
+
"SSL_CERT_DIR",
|
|
191
|
+
"SSL_CERT_FILE",
|
|
192
|
+
"SystemRoot",
|
|
193
|
+
"TEMP",
|
|
194
|
+
"TMP",
|
|
195
|
+
"TMPDIR",
|
|
196
|
+
"http_proxy",
|
|
197
|
+
"https_proxy",
|
|
198
|
+
"no_proxy",
|
|
199
|
+
] as const;
|
|
200
|
+
|
|
201
|
+
const installedBinEnvironmentKey = "TEMPLATE_VERIFIED_PUBLICATION_BIN";
|
|
202
|
+
|
|
203
|
+
function isObject(value: unknown): value is JsonObject {
|
|
204
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function oneLine(value: unknown): string {
|
|
208
|
+
return String(value)
|
|
209
|
+
.replaceAll(/\s*\r?\n\s*/gu, " ")
|
|
210
|
+
.slice(0, 1_000);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function failure(
|
|
214
|
+
code: PublicationArtifactFailureCode,
|
|
215
|
+
observed: unknown,
|
|
216
|
+
expected: string,
|
|
217
|
+
nextAction: string,
|
|
218
|
+
command?: PublicationArtifactFailure["command"],
|
|
219
|
+
): PublicationArtifactFailure {
|
|
220
|
+
return {
|
|
221
|
+
code,
|
|
222
|
+
observed: oneLine(observed),
|
|
223
|
+
expected,
|
|
224
|
+
nextAction,
|
|
225
|
+
...(command === undefined ? {} : { command }),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function commandFailure(
|
|
230
|
+
code:
|
|
231
|
+
| "artifact-pack-failed"
|
|
232
|
+
| "consumer-install-failed"
|
|
233
|
+
| "consumer-smoke-failed",
|
|
234
|
+
executable: string,
|
|
235
|
+
args: readonly string[],
|
|
236
|
+
result: CommandResult,
|
|
237
|
+
expected: string,
|
|
238
|
+
nextAction: string,
|
|
239
|
+
): PublicationArtifactFailure {
|
|
240
|
+
return failure(
|
|
241
|
+
code,
|
|
242
|
+
result.stderr || result.stdout || `exit ${result.exitCode}`,
|
|
243
|
+
expected,
|
|
244
|
+
nextAction,
|
|
245
|
+
{ executable, args, exitCode: result.exitCode },
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function runCommand(options: {
|
|
250
|
+
readonly executable: string;
|
|
251
|
+
readonly args: readonly string[];
|
|
252
|
+
readonly cwd: string;
|
|
253
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
254
|
+
}): Promise<CommandResult> {
|
|
255
|
+
return new Promise((resolve) => {
|
|
256
|
+
const child = spawn(options.executable, [...options.args], {
|
|
257
|
+
cwd: options.cwd,
|
|
258
|
+
env: options.env ?? process.env,
|
|
259
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
260
|
+
});
|
|
261
|
+
let stdout = "";
|
|
262
|
+
let stderr = "";
|
|
263
|
+
child.stdout.setEncoding("utf8");
|
|
264
|
+
child.stderr.setEncoding("utf8");
|
|
265
|
+
child.stdout.on("data", (chunk: string) => {
|
|
266
|
+
stdout += chunk;
|
|
267
|
+
});
|
|
268
|
+
child.stderr.on("data", (chunk: string) => {
|
|
269
|
+
stderr += chunk;
|
|
270
|
+
});
|
|
271
|
+
child.once("error", (error) => {
|
|
272
|
+
resolve({ exitCode: 1, stdout, stderr: `${stderr}\n${error.message}` });
|
|
273
|
+
});
|
|
274
|
+
child.once("close", (exitCode) => {
|
|
275
|
+
resolve({ exitCode: exitCode ?? 1, stdout, stderr });
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function pathContains(parent: string, child: string): boolean {
|
|
281
|
+
const relative = path.relative(parent, child);
|
|
282
|
+
return (
|
|
283
|
+
relative === "" ||
|
|
284
|
+
(!relative.startsWith("..") && !path.isAbsolute(relative))
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function validateOutputDirectory(options: {
|
|
289
|
+
readonly repositoryRoot: string;
|
|
290
|
+
readonly packageRoot: string;
|
|
291
|
+
readonly outputDirectory: string;
|
|
292
|
+
}): Promise<ValidatedArtifactPaths | PublicationArtifactFailure> {
|
|
293
|
+
try {
|
|
294
|
+
const [repositoryRoot, packageRoot, outputDirectory] = await Promise.all([
|
|
295
|
+
realpath(options.repositoryRoot),
|
|
296
|
+
realpath(options.packageRoot),
|
|
297
|
+
realpath(options.outputDirectory),
|
|
298
|
+
]);
|
|
299
|
+
const [repository, packageDirectory, output] = await Promise.all([
|
|
300
|
+
stat(repositoryRoot),
|
|
301
|
+
stat(packageRoot),
|
|
302
|
+
stat(outputDirectory),
|
|
303
|
+
]);
|
|
304
|
+
if (!repository.isDirectory())
|
|
305
|
+
throw new Error("repository path is not a directory");
|
|
306
|
+
if (!packageDirectory.isDirectory())
|
|
307
|
+
throw new Error("package path is not a directory");
|
|
308
|
+
if (!output.isDirectory())
|
|
309
|
+
throw new Error("output path is not a directory");
|
|
310
|
+
if (!pathContains(repositoryRoot, packageRoot)) {
|
|
311
|
+
throw new Error("package directory is outside the repository");
|
|
312
|
+
}
|
|
313
|
+
if ((await readdir(outputDirectory)).length !== 0) {
|
|
314
|
+
throw new Error("output directory is not empty");
|
|
315
|
+
}
|
|
316
|
+
if (
|
|
317
|
+
pathContains(repositoryRoot, outputDirectory) ||
|
|
318
|
+
pathContains(outputDirectory, repositoryRoot) ||
|
|
319
|
+
pathContains(packageRoot, outputDirectory) ||
|
|
320
|
+
pathContains(outputDirectory, packageRoot)
|
|
321
|
+
) {
|
|
322
|
+
throw new Error("output directory overlaps the repository or package");
|
|
323
|
+
}
|
|
324
|
+
return { repositoryRoot, packageRoot, outputDirectory };
|
|
325
|
+
} catch (error) {
|
|
326
|
+
return failure(
|
|
327
|
+
"artifact-output-invalid",
|
|
328
|
+
error instanceof Error ? error.message : error,
|
|
329
|
+
"An existing empty output directory outside the repository and package",
|
|
330
|
+
"Create a new isolated output directory and retry.",
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function validArchivePath(entryPath: string): boolean {
|
|
336
|
+
if (
|
|
337
|
+
entryPath.includes("\0") ||
|
|
338
|
+
entryPath.includes("\\") ||
|
|
339
|
+
entryPath.startsWith("/") ||
|
|
340
|
+
!entryPath.startsWith("package/")
|
|
341
|
+
) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
const canonicalPath = entryPath.endsWith("/")
|
|
345
|
+
? entryPath.slice(0, -1)
|
|
346
|
+
: entryPath;
|
|
347
|
+
const segments = canonicalPath.split("/");
|
|
348
|
+
return !segments.some(
|
|
349
|
+
(segment) => segment === "" || segment === "." || segment === "..",
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function archivePathIdentity(entryPath: string): string {
|
|
354
|
+
return entryPath.endsWith("/") ? entryPath.slice(0, -1) : entryPath;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** @internal Pure archive seam used by the verifier's focused negative tests. */
|
|
358
|
+
export async function inspectPublicationArchive(
|
|
359
|
+
archivePath: string,
|
|
360
|
+
): Promise<ArchiveInspection | PublicationArtifactFailure> {
|
|
361
|
+
const files: ArchiveFile[] = [];
|
|
362
|
+
const paths = new Set<string>();
|
|
363
|
+
let unsafe: string | undefined;
|
|
364
|
+
const bodyReads: Promise<void>[] = [];
|
|
365
|
+
try {
|
|
366
|
+
await list({
|
|
367
|
+
file: archivePath,
|
|
368
|
+
onentry(entry) {
|
|
369
|
+
const entryPath = entry.path;
|
|
370
|
+
const pathIdentity = archivePathIdentity(entryPath);
|
|
371
|
+
if (!validArchivePath(entryPath) || paths.has(pathIdentity)) {
|
|
372
|
+
unsafe ??= `unsafe or duplicate path ${JSON.stringify(entryPath)}`;
|
|
373
|
+
entry.resume();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
paths.add(pathIdentity);
|
|
377
|
+
const entryType = entry.type;
|
|
378
|
+
if (entryType === "Directory") {
|
|
379
|
+
const directory = archivePathIdentity(entryPath);
|
|
380
|
+
if (
|
|
381
|
+
!expectedFiles.some((filePath) =>
|
|
382
|
+
filePath.startsWith(`${directory}/`),
|
|
383
|
+
)
|
|
384
|
+
) {
|
|
385
|
+
unsafe ??= `unexpected directory ${JSON.stringify(entryPath)}`;
|
|
386
|
+
}
|
|
387
|
+
entry.resume();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (entryType !== "File") {
|
|
391
|
+
unsafe ??= `unsupported ${entryType} entry ${JSON.stringify(entryPath)}`;
|
|
392
|
+
entry.resume();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const chunks: Buffer[] = [];
|
|
396
|
+
const capture =
|
|
397
|
+
entryPath === "package/package.json" ||
|
|
398
|
+
entryPath === "package/dist/cli.js";
|
|
399
|
+
entry.on("data", (chunk: Buffer | string) => {
|
|
400
|
+
if (capture)
|
|
401
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
402
|
+
});
|
|
403
|
+
bodyReads.push(
|
|
404
|
+
new Promise((resolve, reject) => {
|
|
405
|
+
entry.once("end", () => {
|
|
406
|
+
files.push({
|
|
407
|
+
path: entryPath,
|
|
408
|
+
mode: entry.mode ?? 0,
|
|
409
|
+
size: entry.size,
|
|
410
|
+
...(capture ? { body: Buffer.concat(chunks) } : {}),
|
|
411
|
+
});
|
|
412
|
+
resolve();
|
|
413
|
+
});
|
|
414
|
+
entry.once("error", reject);
|
|
415
|
+
}),
|
|
416
|
+
);
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
await Promise.all(bodyReads);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
return failure(
|
|
422
|
+
"artifact-archive-unsafe",
|
|
423
|
+
error instanceof Error ? error.message : error,
|
|
424
|
+
"A readable gzip tar archive containing only safe package entries",
|
|
425
|
+
"Correct the package contents and run artifact verification again.",
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
if (unsafe !== undefined) {
|
|
429
|
+
return failure(
|
|
430
|
+
"artifact-archive-unsafe",
|
|
431
|
+
unsafe,
|
|
432
|
+
"Canonical package/ paths with only regular files and ancestor directories",
|
|
433
|
+
"Remove unsafe archive entries and retry.",
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
const sortedFiles = files.toSorted((left, right) =>
|
|
437
|
+
left.path < right.path ? -1 : left.path > right.path ? 1 : 0,
|
|
438
|
+
);
|
|
439
|
+
const actualPaths = sortedFiles.map((item) => item.path);
|
|
440
|
+
if (JSON.stringify(actualPaths) !== JSON.stringify(expectedFiles)) {
|
|
441
|
+
return failure(
|
|
442
|
+
"artifact-files-contract",
|
|
443
|
+
JSON.stringify(actualPaths),
|
|
444
|
+
`Exactly ${JSON.stringify(expectedFiles)}`,
|
|
445
|
+
"Correct the package files allow-list and compiled runtime outputs.",
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
const manifestSource = sortedFiles.find(
|
|
449
|
+
(item) => item.path === "package/package.json",
|
|
450
|
+
)?.body;
|
|
451
|
+
if (manifestSource === undefined) {
|
|
452
|
+
return failure(
|
|
453
|
+
"artifact-manifest-invalid",
|
|
454
|
+
"package/package.json is missing or unreadable",
|
|
455
|
+
"One UTF-8 JSON object at package/package.json",
|
|
456
|
+
"Restore the package manifest and retry.",
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
let manifest: unknown;
|
|
460
|
+
try {
|
|
461
|
+
manifest = JSON.parse(
|
|
462
|
+
new TextDecoder("utf-8", { fatal: true }).decode(manifestSource),
|
|
463
|
+
);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
return failure(
|
|
466
|
+
"artifact-manifest-invalid",
|
|
467
|
+
error instanceof Error ? error.message : error,
|
|
468
|
+
"A valid UTF-8 JSON object at package/package.json",
|
|
469
|
+
"Correct the packed package manifest and retry.",
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
if (!isObject(manifest)) {
|
|
473
|
+
return failure(
|
|
474
|
+
"artifact-manifest-invalid",
|
|
475
|
+
JSON.stringify(manifest),
|
|
476
|
+
"A JSON object at package/package.json",
|
|
477
|
+
"Correct the packed package manifest and retry.",
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
const cliSource = sortedFiles.find(
|
|
481
|
+
(item) => item.path === "package/dist/cli.js",
|
|
482
|
+
)?.body;
|
|
483
|
+
if (cliSource === undefined) {
|
|
484
|
+
return failure(
|
|
485
|
+
"artifact-bin-contract",
|
|
486
|
+
"package/dist/cli.js is missing",
|
|
487
|
+
"A compiled JavaScript bin in the archive",
|
|
488
|
+
"Restore the CLI build output and retry.",
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
return { files: sortedFiles, manifest, cliSource };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function canonicalJson(value: unknown): unknown {
|
|
495
|
+
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
496
|
+
if (!isObject(value)) return value;
|
|
497
|
+
return Object.fromEntries(
|
|
498
|
+
Object.entries(value)
|
|
499
|
+
.toSorted(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
500
|
+
.map(([key, item]) => [key, canonicalJson(item)]),
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function sameJson(left: unknown, right: unknown): boolean {
|
|
505
|
+
return (
|
|
506
|
+
JSON.stringify(canonicalJson(left)) === JSON.stringify(canonicalJson(right))
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** @internal Pure packed-manifest seam used by focused contract tests. */
|
|
511
|
+
export function inspectPackedManifestContract(options: {
|
|
512
|
+
readonly manifest: JsonObject;
|
|
513
|
+
readonly sourceManifest: JsonObject;
|
|
514
|
+
readonly readiness: Extract<
|
|
515
|
+
NpmPublicationReadiness,
|
|
516
|
+
{ readonly kind: "ready" }
|
|
517
|
+
>;
|
|
518
|
+
}): PublicationArtifactFailure | undefined {
|
|
519
|
+
const { manifest, sourceManifest, readiness } = options;
|
|
520
|
+
const bin = manifest.bin;
|
|
521
|
+
const dependencies = manifest.dependencies;
|
|
522
|
+
const files = manifest.files;
|
|
523
|
+
const publishConfig = manifest.publishConfig;
|
|
524
|
+
const unexpectedFields = Object.keys(manifest).filter(
|
|
525
|
+
(key) => !allowedPackedManifestFields.has(key),
|
|
526
|
+
);
|
|
527
|
+
const exactIdentity =
|
|
528
|
+
manifest.name === readiness.publication.packageName &&
|
|
529
|
+
manifest.version === readiness.publication.version &&
|
|
530
|
+
manifest.description === sourceManifest.description &&
|
|
531
|
+
manifest.license === sourceManifest.license &&
|
|
532
|
+
sameJson(manifest.repository, sourceManifest.repository) &&
|
|
533
|
+
manifest.homepage === sourceManifest.homepage &&
|
|
534
|
+
sameJson(manifest.bugs, sourceManifest.bugs) &&
|
|
535
|
+
manifest.type === "module" &&
|
|
536
|
+
sameJson(manifest.engines, sourceManifest.engines);
|
|
537
|
+
const exactFiles =
|
|
538
|
+
Array.isArray(files) &&
|
|
539
|
+
files.length === 4 &&
|
|
540
|
+
["dist", "README.md", "LICENSE", "CHANGELOG.md"].every((item) =>
|
|
541
|
+
files.includes(item),
|
|
542
|
+
);
|
|
543
|
+
const exactBin =
|
|
544
|
+
isObject(bin) &&
|
|
545
|
+
Object.keys(bin).length === 1 &&
|
|
546
|
+
bin[readiness.publication.commandName] === "./dist/cli.js";
|
|
547
|
+
const exactDependencies =
|
|
548
|
+
isObject(dependencies) &&
|
|
549
|
+
Object.keys(dependencies).length === 1 &&
|
|
550
|
+
typeof dependencies.commander === "string" &&
|
|
551
|
+
dependencies.commander.length > 0 &&
|
|
552
|
+
validRange(dependencies.commander) !== null;
|
|
553
|
+
const validPublishConfig =
|
|
554
|
+
publishConfig === undefined ||
|
|
555
|
+
(isObject(publishConfig) &&
|
|
556
|
+
Object.keys(publishConfig).length === 2 &&
|
|
557
|
+
publishConfig.access === "public" &&
|
|
558
|
+
publishConfig.registry === "https://registry.npmjs.org/");
|
|
559
|
+
if (
|
|
560
|
+
!exactIdentity ||
|
|
561
|
+
!exactFiles ||
|
|
562
|
+
!exactBin ||
|
|
563
|
+
!exactDependencies ||
|
|
564
|
+
!validPublishConfig ||
|
|
565
|
+
unexpectedFields.length > 0
|
|
566
|
+
) {
|
|
567
|
+
return failure(
|
|
568
|
+
"artifact-manifest-contract",
|
|
569
|
+
JSON.stringify({
|
|
570
|
+
exactIdentity,
|
|
571
|
+
exactFiles,
|
|
572
|
+
exactBin,
|
|
573
|
+
exactDependencies,
|
|
574
|
+
validPublishConfig,
|
|
575
|
+
unexpectedFields,
|
|
576
|
+
}),
|
|
577
|
+
"The closed ready public CLI packed-manifest contract",
|
|
578
|
+
"Correct the public CLI manifest or packing hook and retry.",
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function isolatedChildEnvironment(repositoryRoot: string): NodeJS.ProcessEnv {
|
|
585
|
+
const env: NodeJS.ProcessEnv = {};
|
|
586
|
+
for (const key of inheritedChildEnvironmentKeys) {
|
|
587
|
+
const value = process.env[key];
|
|
588
|
+
if (value !== undefined) env[key] = value;
|
|
589
|
+
}
|
|
590
|
+
env.PATH = `${path.join(repositoryRoot, "node_modules", ".bin")}${path.delimiter}${env.PATH ?? ""}`;
|
|
591
|
+
return env;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function quotedWindowsCommandArgument(value: string): string {
|
|
595
|
+
if (/["\0\r\n]/u.test(value)) {
|
|
596
|
+
throw new Error("Windows command argument is not command-safe");
|
|
597
|
+
}
|
|
598
|
+
return `"${value}"`;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function windowsEnvironmentReference(key: string): string {
|
|
602
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) {
|
|
603
|
+
throw new Error("Windows command environment key is not safe");
|
|
604
|
+
}
|
|
605
|
+
return `"%${key}%"`;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function resolveWindowsExecutable(options: {
|
|
609
|
+
readonly executable: string;
|
|
610
|
+
readonly windowsExecutable?: string;
|
|
611
|
+
readonly windowsExecutableEnvironmentKey?: string;
|
|
612
|
+
}): string {
|
|
613
|
+
if (
|
|
614
|
+
options.windowsExecutable !== undefined &&
|
|
615
|
+
options.windowsExecutableEnvironmentKey !== undefined
|
|
616
|
+
) {
|
|
617
|
+
throw new Error("Windows command executable plan is ambiguous");
|
|
618
|
+
}
|
|
619
|
+
if (options.windowsExecutableEnvironmentKey !== undefined) {
|
|
620
|
+
return windowsEnvironmentReference(options.windowsExecutableEnvironmentKey);
|
|
621
|
+
}
|
|
622
|
+
return quotedWindowsCommandArgument(
|
|
623
|
+
options.windowsExecutable ?? options.executable,
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function planCommandProcessorInvocation(options: {
|
|
628
|
+
readonly platform: NodeJS.Platform;
|
|
629
|
+
readonly executable: string;
|
|
630
|
+
readonly args: readonly string[];
|
|
631
|
+
readonly environment: NodeJS.ProcessEnv;
|
|
632
|
+
readonly windowsExecutable?: string;
|
|
633
|
+
readonly windowsExecutableEnvironmentKey?: string;
|
|
634
|
+
readonly windowsArgumentEnvironment?: {
|
|
635
|
+
readonly index: number;
|
|
636
|
+
readonly key: string;
|
|
637
|
+
readonly value: string;
|
|
638
|
+
};
|
|
639
|
+
}): CommandPlan {
|
|
640
|
+
if (options.platform !== "win32") {
|
|
641
|
+
return {
|
|
642
|
+
executable: options.executable,
|
|
643
|
+
args: options.args,
|
|
644
|
+
environment: options.environment,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
const commandProcessor = options.environment.ComSpec;
|
|
648
|
+
if (
|
|
649
|
+
commandProcessor === undefined ||
|
|
650
|
+
!path.win32.isAbsolute(commandProcessor)
|
|
651
|
+
) {
|
|
652
|
+
throw new Error("ComSpec must name the absolute Windows command processor");
|
|
653
|
+
}
|
|
654
|
+
const windowsExecutable = resolveWindowsExecutable(options);
|
|
655
|
+
const commandEnvironment = { ...options.environment };
|
|
656
|
+
const windowsArguments = options.args.map((argument, index) => {
|
|
657
|
+
const replacement = options.windowsArgumentEnvironment;
|
|
658
|
+
if (replacement?.index !== index) {
|
|
659
|
+
return quotedWindowsCommandArgument(argument);
|
|
660
|
+
}
|
|
661
|
+
if (/["\0\r\n]/u.test(replacement.value)) {
|
|
662
|
+
throw new Error("Windows command environment value is not command-safe");
|
|
663
|
+
}
|
|
664
|
+
commandEnvironment[replacement.key] = replacement.value;
|
|
665
|
+
return windowsEnvironmentReference(replacement.key);
|
|
666
|
+
});
|
|
667
|
+
if (
|
|
668
|
+
options.windowsArgumentEnvironment !== undefined &&
|
|
669
|
+
(options.windowsArgumentEnvironment.index < 0 ||
|
|
670
|
+
options.windowsArgumentEnvironment.index >= options.args.length)
|
|
671
|
+
) {
|
|
672
|
+
throw new Error("Windows command environment argument index is invalid");
|
|
673
|
+
}
|
|
674
|
+
const command = [windowsExecutable, ...windowsArguments].join(" ");
|
|
675
|
+
return {
|
|
676
|
+
executable: commandProcessor,
|
|
677
|
+
args: ["/d", "/v:off", "/s", "/c", `"${command}"`],
|
|
678
|
+
environment: commandEnvironment,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** @internal 用于聚焦平台计划测试的纯 installed-bin 命令 seam。 */
|
|
683
|
+
export function planInstalledBinCommand(options: {
|
|
684
|
+
readonly platform: NodeJS.Platform;
|
|
685
|
+
readonly binPath: string;
|
|
686
|
+
readonly args: readonly string[];
|
|
687
|
+
readonly environment: NodeJS.ProcessEnv;
|
|
688
|
+
}): CommandPlan {
|
|
689
|
+
if (options.platform !== "win32") {
|
|
690
|
+
return {
|
|
691
|
+
executable: options.binPath,
|
|
692
|
+
args: options.args,
|
|
693
|
+
environment: options.environment,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
if (/["\0\r\n]/u.test(options.binPath)) {
|
|
697
|
+
throw new Error("installed Windows bin path is not command-safe");
|
|
698
|
+
}
|
|
699
|
+
return planCommandProcessorInvocation({
|
|
700
|
+
platform: options.platform,
|
|
701
|
+
executable: options.binPath,
|
|
702
|
+
args: options.args,
|
|
703
|
+
environment: {
|
|
704
|
+
...options.environment,
|
|
705
|
+
[installedBinEnvironmentKey]: options.binPath,
|
|
706
|
+
},
|
|
707
|
+
windowsExecutableEnvironmentKey: installedBinEnvironmentKey,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** @internal 用于聚焦平台计划测试的纯 pnpm pack 命令 seam。 */
|
|
712
|
+
export function planPnpmPackCommand(options: {
|
|
713
|
+
readonly platform: NodeJS.Platform;
|
|
714
|
+
readonly packDirectory: string;
|
|
715
|
+
readonly environment: NodeJS.ProcessEnv;
|
|
716
|
+
}): CommandPlan {
|
|
717
|
+
const args = ["pack", "--pack-destination", options.packDirectory] as const;
|
|
718
|
+
return planCommandProcessorInvocation({
|
|
719
|
+
platform: options.platform,
|
|
720
|
+
executable: "pnpm",
|
|
721
|
+
args,
|
|
722
|
+
environment: options.environment,
|
|
723
|
+
windowsExecutable: "pnpm.cmd",
|
|
724
|
+
windowsArgumentEnvironment: {
|
|
725
|
+
index: 2,
|
|
726
|
+
key: "TEMPLATE_VERIFIED_PUBLICATION_PACK_DIRECTORY",
|
|
727
|
+
value: options.packDirectory,
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function smokeCommand(options: {
|
|
733
|
+
readonly executable: string;
|
|
734
|
+
readonly args: readonly string[];
|
|
735
|
+
readonly cwd: string;
|
|
736
|
+
readonly env: NodeJS.ProcessEnv;
|
|
737
|
+
}): Promise<CommandResult | PublicationArtifactFailure> {
|
|
738
|
+
const result = await runCommand(options);
|
|
739
|
+
return result.exitCode === 0
|
|
740
|
+
? result
|
|
741
|
+
: commandFailure(
|
|
742
|
+
"consumer-smoke-failed",
|
|
743
|
+
options.executable,
|
|
744
|
+
options.args,
|
|
745
|
+
result,
|
|
746
|
+
"A successful installed CLI smoke command",
|
|
747
|
+
"Correct the packed runtime or command behavior and retry.",
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
export async function verifyNpmPublicationArtifact(options: {
|
|
752
|
+
readonly repositoryRoot: string;
|
|
753
|
+
readonly packagePath: string;
|
|
754
|
+
readonly outputDirectory: string;
|
|
755
|
+
}): Promise<VerifiedPublicationArtifactResult> {
|
|
756
|
+
const requestedRepositoryRoot = path.resolve(options.repositoryRoot);
|
|
757
|
+
const requestedPackageRoot = path.resolve(
|
|
758
|
+
requestedRepositoryRoot,
|
|
759
|
+
options.packagePath,
|
|
760
|
+
);
|
|
761
|
+
const requestedOutputDirectory = path.resolve(options.outputDirectory);
|
|
762
|
+
const validatedPaths = await validateOutputDirectory({
|
|
763
|
+
repositoryRoot: requestedRepositoryRoot,
|
|
764
|
+
packageRoot: requestedPackageRoot,
|
|
765
|
+
outputDirectory: requestedOutputDirectory,
|
|
766
|
+
});
|
|
767
|
+
if ("code" in validatedPaths) {
|
|
768
|
+
return { kind: "failed", failure: validatedPaths };
|
|
769
|
+
}
|
|
770
|
+
const { repositoryRoot, packageRoot, outputDirectory } = validatedPaths;
|
|
771
|
+
const packagePath = path
|
|
772
|
+
.relative(repositoryRoot, packageRoot)
|
|
773
|
+
.split(path.sep)
|
|
774
|
+
.join("/");
|
|
775
|
+
|
|
776
|
+
const readiness = await inspectNpmPublicationReadiness({
|
|
777
|
+
repositoryRoot,
|
|
778
|
+
packagePath,
|
|
779
|
+
});
|
|
780
|
+
if (readiness.kind === "blocked") return { kind: "blocked", readiness };
|
|
781
|
+
|
|
782
|
+
let sourceManifestValue: unknown;
|
|
783
|
+
try {
|
|
784
|
+
sourceManifestValue = JSON.parse(
|
|
785
|
+
await readFile(path.join(packageRoot, "package.json"), "utf8"),
|
|
786
|
+
);
|
|
787
|
+
} catch (error) {
|
|
788
|
+
return {
|
|
789
|
+
kind: "failed",
|
|
790
|
+
failure: failure(
|
|
791
|
+
"artifact-manifest-invalid",
|
|
792
|
+
error instanceof Error ? error.message : error,
|
|
793
|
+
"The readiness-approved source package manifest snapshot",
|
|
794
|
+
"Restore the package manifest and retry.",
|
|
795
|
+
),
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
if (!isObject(sourceManifestValue)) {
|
|
799
|
+
return {
|
|
800
|
+
kind: "failed",
|
|
801
|
+
failure: failure(
|
|
802
|
+
"artifact-manifest-invalid",
|
|
803
|
+
"source package.json is not an object",
|
|
804
|
+
"The readiness-approved source package manifest snapshot",
|
|
805
|
+
"Restore the package manifest and retry.",
|
|
806
|
+
),
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
const childEnvironment = isolatedChildEnvironment(repositoryRoot);
|
|
810
|
+
|
|
811
|
+
const ownedTemporaryPaths: string[] = [];
|
|
812
|
+
const ownedFinalPaths: string[] = [];
|
|
813
|
+
let result: VerifiedPublicationArtifactResult | undefined;
|
|
814
|
+
try {
|
|
815
|
+
let packDirectory: string;
|
|
816
|
+
let consumerDirectory: string;
|
|
817
|
+
try {
|
|
818
|
+
packDirectory = await mkdtemp(path.join(outputDirectory, "pack-"));
|
|
819
|
+
ownedTemporaryPaths.push(packDirectory);
|
|
820
|
+
consumerDirectory = await mkdtemp(
|
|
821
|
+
path.join(outputDirectory, "consumer-"),
|
|
822
|
+
);
|
|
823
|
+
ownedTemporaryPaths.push(consumerDirectory);
|
|
824
|
+
} catch (error) {
|
|
825
|
+
result = {
|
|
826
|
+
kind: "failed",
|
|
827
|
+
failure: failure(
|
|
828
|
+
"artifact-temporary-output-failed",
|
|
829
|
+
error instanceof Error ? error.message : error,
|
|
830
|
+
"Owned pack and consumer temporary directories",
|
|
831
|
+
"Correct output directory permissions and retry.",
|
|
832
|
+
),
|
|
833
|
+
};
|
|
834
|
+
return result;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const packCommand = planPnpmPackCommand({
|
|
838
|
+
platform: process.platform,
|
|
839
|
+
packDirectory,
|
|
840
|
+
environment: childEnvironment,
|
|
841
|
+
});
|
|
842
|
+
const pack = await runCommand({
|
|
843
|
+
executable: packCommand.executable,
|
|
844
|
+
args: packCommand.args,
|
|
845
|
+
cwd: packageRoot,
|
|
846
|
+
env: packCommand.environment,
|
|
847
|
+
});
|
|
848
|
+
if (pack.exitCode !== 0) {
|
|
849
|
+
result = {
|
|
850
|
+
kind: "failed",
|
|
851
|
+
failure: commandFailure(
|
|
852
|
+
"artifact-pack-failed",
|
|
853
|
+
packCommand.executable,
|
|
854
|
+
packCommand.args,
|
|
855
|
+
pack,
|
|
856
|
+
"The single pnpm pack lifecycle to succeed",
|
|
857
|
+
"Correct the prepack build or package manifest and retry.",
|
|
858
|
+
),
|
|
859
|
+
};
|
|
860
|
+
return result;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const packEntries = await readdir(packDirectory, { withFileTypes: true });
|
|
864
|
+
if (
|
|
865
|
+
packEntries.length !== 1 ||
|
|
866
|
+
!packEntries[0]!.isFile() ||
|
|
867
|
+
!packEntries[0]!.name.endsWith(".tgz")
|
|
868
|
+
) {
|
|
869
|
+
result = {
|
|
870
|
+
kind: "failed",
|
|
871
|
+
failure: failure(
|
|
872
|
+
"artifact-pack-output-invalid",
|
|
873
|
+
JSON.stringify(
|
|
874
|
+
packEntries.map((entry) => ({
|
|
875
|
+
name: entry.name,
|
|
876
|
+
kind: entry.isFile() ? "file" : "other",
|
|
877
|
+
})),
|
|
878
|
+
),
|
|
879
|
+
"Exactly one ordinary .tgz in the owned pack directory",
|
|
880
|
+
"Correct pack lifecycle outputs and retry.",
|
|
881
|
+
),
|
|
882
|
+
};
|
|
883
|
+
return result;
|
|
884
|
+
}
|
|
885
|
+
const artifactBasename = packEntries[0]!.name;
|
|
886
|
+
if (!/^[A-Za-z0-9._-]+\.tgz$/u.test(artifactBasename)) {
|
|
887
|
+
result = {
|
|
888
|
+
kind: "failed",
|
|
889
|
+
failure: failure(
|
|
890
|
+
"artifact-pack-output-invalid",
|
|
891
|
+
artifactBasename,
|
|
892
|
+
"A safe portable tgz basename",
|
|
893
|
+
"Correct the public package identity and retry.",
|
|
894
|
+
),
|
|
895
|
+
};
|
|
896
|
+
return result;
|
|
897
|
+
}
|
|
898
|
+
const packedArtifactPath = path.join(packDirectory, artifactBasename);
|
|
899
|
+
const archive = await inspectPublicationArchive(packedArtifactPath);
|
|
900
|
+
if ("code" in archive) {
|
|
901
|
+
result = { kind: "failed", failure: archive };
|
|
902
|
+
return result;
|
|
903
|
+
}
|
|
904
|
+
const manifestFailure = inspectPackedManifestContract({
|
|
905
|
+
manifest: archive.manifest,
|
|
906
|
+
sourceManifest: sourceManifestValue,
|
|
907
|
+
readiness,
|
|
908
|
+
});
|
|
909
|
+
if (manifestFailure !== undefined) {
|
|
910
|
+
result = { kind: "failed", failure: manifestFailure };
|
|
911
|
+
return result;
|
|
912
|
+
}
|
|
913
|
+
const binFile = archive.files.find(
|
|
914
|
+
(item) => item.path === "package/dist/cli.js",
|
|
915
|
+
)!;
|
|
916
|
+
const shebang = archive.cliSource.toString("utf8").split(/\r?\n/u)[0];
|
|
917
|
+
if (
|
|
918
|
+
shebang !== "#!/usr/bin/env node" ||
|
|
919
|
+
(process.platform !== "win32" && (binFile.mode & 0o111) === 0)
|
|
920
|
+
) {
|
|
921
|
+
result = {
|
|
922
|
+
kind: "failed",
|
|
923
|
+
failure: failure(
|
|
924
|
+
"artifact-bin-contract",
|
|
925
|
+
JSON.stringify({ shebang, mode: binFile.mode }),
|
|
926
|
+
"A Node shebang and executable tar mode on POSIX",
|
|
927
|
+
"Correct the compiled CLI entrypoint and retry.",
|
|
928
|
+
),
|
|
929
|
+
};
|
|
930
|
+
return result;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const cacheDirectory = path.join(consumerDirectory, "npm-cache");
|
|
934
|
+
const userConfigPath = path.join(consumerDirectory, "user.npmrc");
|
|
935
|
+
const globalConfigPath = path.join(consumerDirectory, "global.npmrc");
|
|
936
|
+
await writeFile(
|
|
937
|
+
path.join(consumerDirectory, "package.json"),
|
|
938
|
+
'{"private":true}\n',
|
|
939
|
+
);
|
|
940
|
+
await writeFile(userConfigPath, "");
|
|
941
|
+
await writeFile(globalConfigPath, "");
|
|
942
|
+
const npmArgs = [
|
|
943
|
+
"install",
|
|
944
|
+
packedArtifactPath,
|
|
945
|
+
`--cache=${cacheDirectory}`,
|
|
946
|
+
`--userconfig=${userConfigPath}`,
|
|
947
|
+
`--globalconfig=${globalConfigPath}`,
|
|
948
|
+
"--registry=https://registry.npmjs.org/",
|
|
949
|
+
"--ignore-scripts=true",
|
|
950
|
+
"--bin-links=true",
|
|
951
|
+
"--package-lock=false",
|
|
952
|
+
"--audit=false",
|
|
953
|
+
"--fund=false",
|
|
954
|
+
] as const;
|
|
955
|
+
const npmCliPath = path.join(
|
|
956
|
+
repositoryRoot,
|
|
957
|
+
"node_modules/npm/bin/npm-cli.js",
|
|
958
|
+
);
|
|
959
|
+
const npmCommandArgs = [npmCliPath, ...npmArgs] as const;
|
|
960
|
+
const install = await runCommand({
|
|
961
|
+
executable: process.execPath,
|
|
962
|
+
args: npmCommandArgs,
|
|
963
|
+
cwd: consumerDirectory,
|
|
964
|
+
env: childEnvironment,
|
|
965
|
+
});
|
|
966
|
+
if (install.exitCode !== 0) {
|
|
967
|
+
result = {
|
|
968
|
+
kind: "failed",
|
|
969
|
+
failure: commandFailure(
|
|
970
|
+
"consumer-install-failed",
|
|
971
|
+
process.execPath,
|
|
972
|
+
npmCommandArgs,
|
|
973
|
+
install,
|
|
974
|
+
"The locked npm client to install the local tgz with public runtime dependencies",
|
|
975
|
+
"Restore registry access or correct the packed runtime dependency and retry.",
|
|
976
|
+
),
|
|
977
|
+
};
|
|
978
|
+
return result;
|
|
979
|
+
}
|
|
980
|
+
const installedPackageRoot = path.join(
|
|
981
|
+
consumerDirectory,
|
|
982
|
+
"node_modules",
|
|
983
|
+
readiness.publication.packageName,
|
|
984
|
+
);
|
|
985
|
+
const runtimeImportArgs = [
|
|
986
|
+
"--input-type=module",
|
|
987
|
+
"--eval",
|
|
988
|
+
"await import(process.argv[1])",
|
|
989
|
+
pathToFileURL(path.join(installedPackageRoot, "dist/main.js")).href,
|
|
990
|
+
] as const;
|
|
991
|
+
const runtimeImport = await smokeCommand({
|
|
992
|
+
executable: process.execPath,
|
|
993
|
+
args: runtimeImportArgs,
|
|
994
|
+
cwd: consumerDirectory,
|
|
995
|
+
env: childEnvironment,
|
|
996
|
+
});
|
|
997
|
+
if ("code" in runtimeImport) {
|
|
998
|
+
result = { kind: "failed", failure: runtimeImport };
|
|
999
|
+
return result;
|
|
1000
|
+
}
|
|
1001
|
+
if (runtimeImport.stdout !== "") {
|
|
1002
|
+
result = {
|
|
1003
|
+
kind: "failed",
|
|
1004
|
+
failure: failure(
|
|
1005
|
+
"consumer-smoke-failed",
|
|
1006
|
+
runtimeImport.stdout,
|
|
1007
|
+
"The installed internal runtime module to import without output",
|
|
1008
|
+
"Remove import-time output and retry.",
|
|
1009
|
+
),
|
|
1010
|
+
};
|
|
1011
|
+
return result;
|
|
1012
|
+
}
|
|
1013
|
+
const binPath = path.join(
|
|
1014
|
+
consumerDirectory,
|
|
1015
|
+
"node_modules/.bin",
|
|
1016
|
+
`${readiness.publication.commandName}${process.platform === "win32" ? ".cmd" : ""}`,
|
|
1017
|
+
);
|
|
1018
|
+
if (
|
|
1019
|
+
process.platform !== "win32" &&
|
|
1020
|
+
((await lstat(binPath)).mode & 0o111) === 0
|
|
1021
|
+
) {
|
|
1022
|
+
result = {
|
|
1023
|
+
kind: "failed",
|
|
1024
|
+
failure: failure(
|
|
1025
|
+
"artifact-bin-contract",
|
|
1026
|
+
"installed bin is not executable",
|
|
1027
|
+
"An executable npm-created package bin link",
|
|
1028
|
+
"Correct the bin manifest and retry.",
|
|
1029
|
+
),
|
|
1030
|
+
};
|
|
1031
|
+
return result;
|
|
1032
|
+
}
|
|
1033
|
+
const helpArgs = ["--help"] as const;
|
|
1034
|
+
const versionArgs = ["--version"] as const;
|
|
1035
|
+
const greetArgs = ["greet", " Ada Lovelace "] as const;
|
|
1036
|
+
const invalidGreetArgs = ["greet", " "] as const;
|
|
1037
|
+
const helpCommand = planInstalledBinCommand({
|
|
1038
|
+
platform: process.platform,
|
|
1039
|
+
binPath,
|
|
1040
|
+
args: helpArgs,
|
|
1041
|
+
environment: childEnvironment,
|
|
1042
|
+
});
|
|
1043
|
+
const help = await smokeCommand({
|
|
1044
|
+
executable: helpCommand.executable,
|
|
1045
|
+
args: helpCommand.args,
|
|
1046
|
+
cwd: consumerDirectory,
|
|
1047
|
+
env: helpCommand.environment,
|
|
1048
|
+
});
|
|
1049
|
+
if ("code" in help) {
|
|
1050
|
+
result = { kind: "failed", failure: help };
|
|
1051
|
+
return result;
|
|
1052
|
+
}
|
|
1053
|
+
const versionCommand = planInstalledBinCommand({
|
|
1054
|
+
platform: process.platform,
|
|
1055
|
+
binPath,
|
|
1056
|
+
args: versionArgs,
|
|
1057
|
+
environment: childEnvironment,
|
|
1058
|
+
});
|
|
1059
|
+
const version = await smokeCommand({
|
|
1060
|
+
executable: versionCommand.executable,
|
|
1061
|
+
args: versionCommand.args,
|
|
1062
|
+
cwd: consumerDirectory,
|
|
1063
|
+
env: versionCommand.environment,
|
|
1064
|
+
});
|
|
1065
|
+
if ("code" in version) {
|
|
1066
|
+
result = { kind: "failed", failure: version };
|
|
1067
|
+
return result;
|
|
1068
|
+
}
|
|
1069
|
+
const greetCommand = planInstalledBinCommand({
|
|
1070
|
+
platform: process.platform,
|
|
1071
|
+
binPath,
|
|
1072
|
+
args: greetArgs,
|
|
1073
|
+
environment: childEnvironment,
|
|
1074
|
+
});
|
|
1075
|
+
const greet = await smokeCommand({
|
|
1076
|
+
executable: greetCommand.executable,
|
|
1077
|
+
args: greetCommand.args,
|
|
1078
|
+
cwd: consumerDirectory,
|
|
1079
|
+
env: greetCommand.environment,
|
|
1080
|
+
});
|
|
1081
|
+
if ("code" in greet) {
|
|
1082
|
+
result = { kind: "failed", failure: greet };
|
|
1083
|
+
return result;
|
|
1084
|
+
}
|
|
1085
|
+
if (
|
|
1086
|
+
!help.stdout.includes(`Usage: ${readiness.publication.commandName}`) ||
|
|
1087
|
+
!help.stdout.includes("greet") ||
|
|
1088
|
+
version.stdout !== `${readiness.publication.version}\n` ||
|
|
1089
|
+
greet.stdout !== "Hello, Ada Lovelace\n"
|
|
1090
|
+
) {
|
|
1091
|
+
result = {
|
|
1092
|
+
kind: "failed",
|
|
1093
|
+
failure: failure(
|
|
1094
|
+
"consumer-smoke-failed",
|
|
1095
|
+
JSON.stringify({
|
|
1096
|
+
help: help.stdout,
|
|
1097
|
+
version: version.stdout,
|
|
1098
|
+
greet: greet.stdout,
|
|
1099
|
+
}),
|
|
1100
|
+
"Installed help, exact version, and greet behavior",
|
|
1101
|
+
"Correct the packed command identity or behavior and retry.",
|
|
1102
|
+
),
|
|
1103
|
+
};
|
|
1104
|
+
return result;
|
|
1105
|
+
}
|
|
1106
|
+
const invalidGreetCommand = planInstalledBinCommand({
|
|
1107
|
+
platform: process.platform,
|
|
1108
|
+
binPath,
|
|
1109
|
+
args: invalidGreetArgs,
|
|
1110
|
+
environment: childEnvironment,
|
|
1111
|
+
});
|
|
1112
|
+
const invalidGreet = await runCommand({
|
|
1113
|
+
executable: invalidGreetCommand.executable,
|
|
1114
|
+
args: invalidGreetCommand.args,
|
|
1115
|
+
cwd: consumerDirectory,
|
|
1116
|
+
env: invalidGreetCommand.environment,
|
|
1117
|
+
});
|
|
1118
|
+
if (
|
|
1119
|
+
invalidGreet.exitCode !== 1 ||
|
|
1120
|
+
invalidGreet.stdout !== "" ||
|
|
1121
|
+
!invalidGreet.stderr.startsWith("error: Name must not be empty\n")
|
|
1122
|
+
) {
|
|
1123
|
+
result = {
|
|
1124
|
+
kind: "failed",
|
|
1125
|
+
failure: failure(
|
|
1126
|
+
"consumer-smoke-failed",
|
|
1127
|
+
JSON.stringify({
|
|
1128
|
+
args: invalidGreetArgs,
|
|
1129
|
+
exitCode: invalidGreet.exitCode,
|
|
1130
|
+
stdout: invalidGreet.stdout,
|
|
1131
|
+
stderr: invalidGreet.stderr,
|
|
1132
|
+
}),
|
|
1133
|
+
"Installed greet with a blank name to exit 1 with the stable validation error and no stdout",
|
|
1134
|
+
"Correct the packed command validation behavior and retry.",
|
|
1135
|
+
{
|
|
1136
|
+
executable: invalidGreetCommand.executable,
|
|
1137
|
+
args: invalidGreetCommand.args,
|
|
1138
|
+
exitCode: invalidGreet.exitCode,
|
|
1139
|
+
},
|
|
1140
|
+
),
|
|
1141
|
+
};
|
|
1142
|
+
return result;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const artifactBytes = await readFile(packedArtifactPath);
|
|
1146
|
+
const digest = createHash("sha512").update(artifactBytes).digest();
|
|
1147
|
+
const artifactPath = path.join(outputDirectory, artifactBasename);
|
|
1148
|
+
const checksumPath = path.join(outputDirectory, "SHA512SUMS");
|
|
1149
|
+
const receiptPath = path.join(
|
|
1150
|
+
outputDirectory,
|
|
1151
|
+
"verified-publication-artifact.json",
|
|
1152
|
+
);
|
|
1153
|
+
const checksumTemporaryPath = path.join(outputDirectory, ".SHA512SUMS.tmp");
|
|
1154
|
+
const receiptTemporaryPath = path.join(
|
|
1155
|
+
outputDirectory,
|
|
1156
|
+
".verified-publication-artifact.json.tmp",
|
|
1157
|
+
);
|
|
1158
|
+
ownedTemporaryPaths.push(checksumTemporaryPath, receiptTemporaryPath);
|
|
1159
|
+
const receipt: VerifiedPublicationArtifactReceipt = {
|
|
1160
|
+
schemaVersion: 1,
|
|
1161
|
+
artifact: {
|
|
1162
|
+
file: artifactBasename,
|
|
1163
|
+
checksumFile: "SHA512SUMS",
|
|
1164
|
+
size: artifactBytes.byteLength,
|
|
1165
|
+
integrity: `sha512-${digest.toString("base64")}`,
|
|
1166
|
+
},
|
|
1167
|
+
publication: readiness.publication,
|
|
1168
|
+
packedManifest: archive.manifest,
|
|
1169
|
+
files: archive.files.map(({ path: filePath, mode, size }) => ({
|
|
1170
|
+
path: filePath,
|
|
1171
|
+
mode,
|
|
1172
|
+
size,
|
|
1173
|
+
})),
|
|
1174
|
+
bin: {
|
|
1175
|
+
path: "package/dist/cli.js",
|
|
1176
|
+
shebang: "#!/usr/bin/env node",
|
|
1177
|
+
mode: binFile.mode,
|
|
1178
|
+
posixExecutableChecked: process.platform !== "win32",
|
|
1179
|
+
},
|
|
1180
|
+
smokes: [
|
|
1181
|
+
{ name: "runtime-import", args: [], stdout: "" },
|
|
1182
|
+
{ name: "help", args: helpArgs, stdout: help.stdout },
|
|
1183
|
+
{ name: "version", args: versionArgs, stdout: version.stdout },
|
|
1184
|
+
{ name: "greet", args: greetArgs, stdout: greet.stdout },
|
|
1185
|
+
],
|
|
1186
|
+
};
|
|
1187
|
+
try {
|
|
1188
|
+
await writeFile(
|
|
1189
|
+
checksumTemporaryPath,
|
|
1190
|
+
`${digest.toString("hex")} ${artifactBasename}\n`,
|
|
1191
|
+
);
|
|
1192
|
+
await writeFile(
|
|
1193
|
+
receiptTemporaryPath,
|
|
1194
|
+
`${JSON.stringify(receipt, null, 2)}\n`,
|
|
1195
|
+
);
|
|
1196
|
+
await rename(packedArtifactPath, artifactPath);
|
|
1197
|
+
ownedFinalPaths.push(artifactPath);
|
|
1198
|
+
await rename(checksumTemporaryPath, checksumPath);
|
|
1199
|
+
ownedFinalPaths.push(checksumPath);
|
|
1200
|
+
await rename(receiptTemporaryPath, receiptPath);
|
|
1201
|
+
ownedFinalPaths.push(receiptPath);
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
result = {
|
|
1204
|
+
kind: "failed",
|
|
1205
|
+
failure: failure(
|
|
1206
|
+
"artifact-receipt-failed",
|
|
1207
|
+
error instanceof Error ? error.message : error,
|
|
1208
|
+
"Atomic tgz, SHA512SUMS, and receipt finalization",
|
|
1209
|
+
"Correct output permissions and retry with a new empty directory.",
|
|
1210
|
+
),
|
|
1211
|
+
};
|
|
1212
|
+
return result;
|
|
1213
|
+
}
|
|
1214
|
+
result = {
|
|
1215
|
+
kind: "verified",
|
|
1216
|
+
artifactPath,
|
|
1217
|
+
checksumPath,
|
|
1218
|
+
receiptPath,
|
|
1219
|
+
receipt,
|
|
1220
|
+
};
|
|
1221
|
+
return result;
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
result = {
|
|
1224
|
+
kind: "failed",
|
|
1225
|
+
failure: failure(
|
|
1226
|
+
"artifact-receipt-failed",
|
|
1227
|
+
error instanceof Error ? error.message : error,
|
|
1228
|
+
"A completely verified and recorded publication artifact",
|
|
1229
|
+
"Correct the reported local failure and retry.",
|
|
1230
|
+
),
|
|
1231
|
+
};
|
|
1232
|
+
return result;
|
|
1233
|
+
} finally {
|
|
1234
|
+
const cleanupFailures: string[] = [];
|
|
1235
|
+
const cleanupTargets = [
|
|
1236
|
+
...ownedTemporaryPaths.toReversed(),
|
|
1237
|
+
...(result?.kind === "verified" ? [] : ownedFinalPaths.toReversed()),
|
|
1238
|
+
];
|
|
1239
|
+
for (const target of cleanupTargets) {
|
|
1240
|
+
try {
|
|
1241
|
+
await rm(target, { recursive: true, force: true });
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
cleanupFailures.push(
|
|
1244
|
+
`${target}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
if (cleanupFailures.length > 0 && result?.kind === "verified") {
|
|
1249
|
+
for (const target of ownedFinalPaths.toReversed()) {
|
|
1250
|
+
try {
|
|
1251
|
+
await rm(target, { recursive: true, force: true });
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
cleanupFailures.push(
|
|
1254
|
+
`${target}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
if (cleanupFailures.length > 0 && result !== undefined) {
|
|
1260
|
+
Object.assign(result, {
|
|
1261
|
+
kind: "failed",
|
|
1262
|
+
failure: failure(
|
|
1263
|
+
"artifact-cleanup-failed",
|
|
1264
|
+
`${result.kind === "failed" ? `${result.failure.code}; ` : ""}${cleanupFailures.join("; ")}`,
|
|
1265
|
+
"All exact invocation-owned temporary and partial final paths removed",
|
|
1266
|
+
"Remove the reported invocation-owned paths before retrying.",
|
|
1267
|
+
),
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|