@logbrew/sdk 0.1.38 → 0.1.39
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/automatic-delivery.cjs +271 -0
- package/batch-sender.cjs +164 -0
- package/bounded-number.cjs +34 -0
- package/browser-breadcrumbs.cjs +346 -136
- package/capture-support.cjs +100 -0
- package/client-options.cjs +292 -0
- package/client-report.cjs +232 -0
- package/client.cjs +447 -0
- package/console-arguments.cjs +151 -0
- package/console-capture.cjs +194 -0
- package/core.cjs +30 -2810
- package/delivery-health.cjs +133 -0
- package/error-causes.cjs +130 -0
- package/error-details.cjs +124 -0
- package/error-issue.cjs +187 -0
- package/event-clone.cjs +55 -0
- package/event-metadata.cjs +40 -0
- package/event-pipeline.cjs +325 -0
- package/event-queue.cjs +306 -0
- package/event-store.cjs +237 -0
- package/event-validation.cjs +159 -0
- package/examples/agent-timeline.cjs +57 -46
- package/examples/agent-timeline.mjs +41 -35
- package/examples/index.mjs +122 -94
- package/examples/readme-example.cjs +36 -27
- package/examples/readme-example.mjs +21 -17
- package/examples/real-user-smoke.cjs +41 -32
- package/examples/real-user-smoke.mjs +26 -22
- package/exception-chain.cjs +205 -0
- package/index.d.ts +144 -1076
- package/index.js +1 -1
- package/issue-exception.cjs +13 -8
- package/metric-validation.cjs +95 -0
- package/network-milestone.cjs +155 -0
- package/null-value.cjs +4 -0
- package/opentelemetry-bridge.cjs +330 -0
- package/opentelemetry-context.cjs +245 -0
- package/opentelemetry-exceptions.cjs +69 -0
- package/opentelemetry-keys.cjs +183 -0
- package/opentelemetry-span.cjs +286 -0
- package/opentelemetry-summary.cjs +213 -0
- package/opentelemetry-time.cjs +131 -0
- package/opentelemetry.cjs +99 -1068
- package/package.json +55 -1
- package/pino-destination.cjs +108 -0
- package/pino-record.cjs +237 -0
- package/product-action.cjs +163 -0
- package/provided-breadcrumbs.cjs +117 -0
- package/random-hex.cjs +29 -0
- package/react-native.js +1 -1
- package/recording-transport.cjs +68 -0
- package/release-artifacts-build.cjs +263 -84
- package/release-artifacts-common.js +347 -66
- package/release-artifacts-debug-id.js +178 -0
- package/release-artifacts-inspect.js +213 -0
- package/release-artifacts-issue-event.js +99 -0
- package/release-artifacts-manifest-artifact.js +107 -0
- package/release-artifacts-manifest.js +77 -0
- package/release-artifacts-mappings.js +265 -0
- package/release-artifacts-prepare.js +297 -0
- package/release-artifacts-source-context.js +210 -0
- package/release-artifacts-symbolication.js +272 -421
- package/release-artifacts-upload-files.js +122 -0
- package/release-artifacts-upload-request.js +246 -0
- package/release-artifacts-upload.js +155 -300
- package/release-artifacts-urls.js +49 -0
- package/release-artifacts.js +189 -662
- package/retry-delay.cjs +32 -0
- package/span-validation.cjs +191 -0
- package/stored-events.cjs +243 -0
- package/types/client.d.ts +67 -0
- package/types/delivery.d.ts +125 -0
- package/types/issues.d.ts +249 -0
- package/types/logging.d.ts +175 -0
- package/types/opentelemetry.d.ts +205 -0
- package/types/product.d.ts +128 -0
- package/types/recording-transport.d.ts +15 -0
- package/types/sdk-error.d.ts +9 -0
- package/types/telemetry.d.ts +171 -0
- package/types/trace-context.d.ts +110 -0
- package/types/transport-error.d.ts +10 -0
- package/utf8.cjs +37 -0
- package/validation.cjs +49 -1
- package/vite-release-artifacts-config.cjs +120 -0
- package/vite-release-artifacts.cjs +199 -99
- package/vite-release-artifacts.d.cts +65 -40
- package/vite-release-artifacts.d.ts +65 -40
- package/vite-release-artifacts.js +2 -2
- package/winston-info.cjs +205 -0
- package/winston.cjs +189 -289
|
@@ -1,337 +1,192 @@
|
|
|
1
|
-
/* global AbortController, clearTimeout */
|
|
2
|
-
|
|
3
|
-
import { Buffer } from "node:buffer";
|
|
4
|
-
import crypto from "node:crypto";
|
|
5
|
-
import fs from "node:fs";
|
|
6
|
-
import net from "node:net";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
|
|
9
1
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
encodeMultipart,
|
|
3
|
+
endpointWithoutQuery,
|
|
4
|
+
requireUploadEndpoint,
|
|
5
|
+
uploadWithRetries
|
|
6
|
+
} from "./release-artifacts-upload-request.js";
|
|
7
|
+
import {
|
|
8
|
+
errorMessage,
|
|
12
9
|
parseOptions,
|
|
10
|
+
pathExists,
|
|
13
11
|
printJson,
|
|
14
12
|
readJsonObject,
|
|
15
|
-
requireOption,
|
|
16
13
|
requireBuildDir,
|
|
17
|
-
|
|
18
|
-
sha256File,
|
|
19
|
-
stableJson
|
|
14
|
+
requireOption
|
|
20
15
|
} from "./release-artifacts-common.js";
|
|
16
|
+
import { collectUploadFiles } from "./release-artifacts-upload-files.js";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @import { CliOptions, JsonObject } from "./release-artifacts-common.js"
|
|
21
|
+
* @import { UploadFile } from "./release-artifacts-upload-request.js"
|
|
22
|
+
* @typedef {{ readonly endpoint: string, readonly files: readonly UploadFile[], readonly manifest: JsonObject, readonly report: JsonObject }} PreparedUpload
|
|
23
|
+
*/
|
|
21
24
|
|
|
22
25
|
const DEFAULT_UPLOAD_TOKEN_ENV = "LOGBREW_RELEASE_ARTIFACT_TOKEN";
|
|
23
|
-
const HOSTED_UPLOAD_ENDPOINT = "https://api.logbrew.co/api/release-artifacts";
|
|
24
|
-
const NON_RETRYABLE_UPLOAD_STATUSES = new Set([400, 401, 403, 413]);
|
|
25
|
-
const RETRYABLE_UPLOAD_STATUSES = new Set([408, 429]);
|
|
26
26
|
const SCRIPT_VERSION = "0.1.0";
|
|
27
|
-
|
|
27
|
+
const NO_RETRIES = 0;
|
|
28
|
+
const DECIMAL = 10;
|
|
29
|
+
const NON_NEGATIVE_INTEGER_RE = /^\d+$/u;
|
|
30
|
+
const UPLOADER = { name: "logbrew-js-release-artifact-upload-verifier", version: SCRIPT_VERSION };
|
|
31
|
+
const EXIT_OK = 0;
|
|
32
|
+
const EXIT_AUTH_MISSING = 2;
|
|
33
|
+
const EXIT_AUTH_FAILED = 3;
|
|
34
|
+
const EXIT_VALIDATION_FAILED = 4;
|
|
35
|
+
const EXIT_CODES = new Map([
|
|
36
|
+
["auth_failed", EXIT_AUTH_FAILED],
|
|
37
|
+
["auth_missing", EXIT_AUTH_MISSING],
|
|
38
|
+
["dry_run", EXIT_OK],
|
|
39
|
+
["uploaded", EXIT_OK],
|
|
40
|
+
["validation_failed", EXIT_VALIDATION_FAILED]
|
|
41
|
+
]);
|
|
42
|
+
const UNKNOWN_FAILURE_EXIT_CODE = 5;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} value
|
|
46
|
+
* @param {string} label
|
|
47
|
+
* @returns {number}
|
|
48
|
+
*/
|
|
28
49
|
function parseNonNegativeInteger(value, label) {
|
|
29
50
|
const trimmed = value.trim();
|
|
30
|
-
if (
|
|
51
|
+
if (!NON_NEGATIVE_INTEGER_RE.test(trimmed)) {
|
|
31
52
|
throw new Error(`${label} must be a non-negative integer`);
|
|
32
53
|
}
|
|
33
|
-
return Number.parseInt(trimmed,
|
|
54
|
+
return Number.parseInt(trimmed, DECIMAL);
|
|
34
55
|
}
|
|
35
56
|
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} value
|
|
59
|
+
* @param {string} label
|
|
60
|
+
* @returns {number}
|
|
61
|
+
*/
|
|
36
62
|
function parseNonNegativeNumber(value, label) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
63
|
+
const parsed = Number(value.trim());
|
|
64
|
+
if (!Number.isFinite(parsed) || parsed < NO_RETRIES) {
|
|
40
65
|
throw new Error(`${label} must be a non-negative number`);
|
|
41
66
|
}
|
|
42
67
|
return parsed;
|
|
43
68
|
}
|
|
44
69
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
if (!allowHosted) {
|
|
79
|
-
throw new Error("release artifact hosted upload requires explicit --allow-hosted; use loopback endpoints for local proof");
|
|
80
|
-
}
|
|
81
|
-
if (endpoint !== HOSTED_UPLOAD_ENDPOINT) {
|
|
82
|
-
throw new Error(`hosted release artifact uploads must use ${HOSTED_UPLOAD_ENDPOINT}`);
|
|
83
|
-
}
|
|
84
|
-
return parsed;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function quoteMultipartValue(value) {
|
|
88
|
-
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function encodeMultipart(manifest, files) {
|
|
92
|
-
const boundary = `logbrew-${crypto.randomUUID().replaceAll("-", "")}`;
|
|
93
|
-
const chunks = [];
|
|
94
|
-
const appendPart = (name, filename, contentType, bytes) => {
|
|
95
|
-
chunks.push(Buffer.from(`--${boundary}\r\n`, "ascii"));
|
|
96
|
-
chunks.push(
|
|
97
|
-
Buffer.from(
|
|
98
|
-
`Content-Disposition: form-data; name="${quoteMultipartValue(name)}"; filename="${quoteMultipartValue(filename)}"\r\n`,
|
|
99
|
-
"utf8"
|
|
100
|
-
)
|
|
101
|
-
);
|
|
102
|
-
chunks.push(Buffer.from(`Content-Type: ${contentType}\r\n\r\n`, "ascii"));
|
|
103
|
-
chunks.push(Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes));
|
|
104
|
-
chunks.push(Buffer.from("\r\n", "ascii"));
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
appendPart("manifest", "manifest.json", "application/json", Buffer.from(stableJson(manifest), "utf8"));
|
|
108
|
-
for (const [name, filePath] of files) {
|
|
109
|
-
appendPart(name, path.basename(filePath), "application/octet-stream", fs.readFileSync(filePath));
|
|
110
|
-
}
|
|
111
|
-
chunks.push(Buffer.from(`--${boundary}--\r\n`, "ascii"));
|
|
112
|
-
return { body: Buffer.concat(chunks), boundary };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function classifyUploadStatus(status) {
|
|
116
|
-
if (status >= 200 && status < 300) {
|
|
117
|
-
return "uploaded";
|
|
118
|
-
}
|
|
119
|
-
if (status === 401 || status === 403) {
|
|
120
|
-
return "auth_failed";
|
|
121
|
-
}
|
|
122
|
-
if (NON_RETRYABLE_UPLOAD_STATUSES.has(status)) {
|
|
123
|
-
return "validation_failed";
|
|
124
|
-
}
|
|
125
|
-
if (RETRYABLE_UPLOAD_STATUSES.has(status) || status >= 500) {
|
|
126
|
-
return "retryable_error";
|
|
127
|
-
}
|
|
128
|
-
return "upload_failed";
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function sleep(seconds) {
|
|
132
|
-
return new Promise((resolve) => {
|
|
133
|
-
setTimeout(resolve, seconds * 1000);
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function postMultipart(endpoint, token, body, boundary, timeoutSeconds) {
|
|
138
|
-
const controller = new AbortController();
|
|
139
|
-
const timeout = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
|
|
140
|
-
try {
|
|
141
|
-
const response = await fetch(endpoint, {
|
|
142
|
-
method: "POST",
|
|
143
|
-
headers: {
|
|
144
|
-
Authorization: `Bearer ${token}`,
|
|
145
|
-
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
146
|
-
"User-Agent": `logbrew-release-artifact-verifier/${SCRIPT_VERSION}`
|
|
147
|
-
},
|
|
148
|
-
body,
|
|
149
|
-
signal: controller.signal
|
|
150
|
-
});
|
|
151
|
-
await response.arrayBuffer();
|
|
152
|
-
return response.status;
|
|
153
|
-
} finally {
|
|
154
|
-
clearTimeout(timeout);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async function uploadWithRetries({ endpoint, token, body, boundary, maxRetries, retryDelaySeconds, timeoutSeconds }) {
|
|
159
|
-
const attempts = [];
|
|
160
|
-
for (let attempt = 1; attempt <= maxRetries + 1; attempt += 1) {
|
|
161
|
-
let result;
|
|
162
|
-
try {
|
|
163
|
-
const httpStatus = await postMultipart(endpoint, token, body, boundary, timeoutSeconds);
|
|
164
|
-
result = classifyUploadStatus(httpStatus);
|
|
165
|
-
attempts.push({ attempt, httpStatus, result });
|
|
166
|
-
} catch {
|
|
167
|
-
result = "retryable_error";
|
|
168
|
-
attempts.push({ attempt, result });
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (result === "uploaded") {
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
if (result !== "retryable_error" || attempt > maxRetries) {
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
if (retryDelaySeconds > 0) {
|
|
178
|
-
await sleep(retryDelaySeconds);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
const finalResult = attempts.at(-1)?.result ?? "upload_failed";
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} status
|
|
72
|
+
* @returns {number}
|
|
73
|
+
*/
|
|
74
|
+
function exitCodeForUploadStatus(status) {
|
|
75
|
+
return EXIT_CODES.get(status) ?? UNKNOWN_FAILURE_EXIT_CODE;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {CliOptions} options
|
|
80
|
+
* @param {string} name
|
|
81
|
+
* @param {string} fallback
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
function optionOrDefault(options, name, fallback) {
|
|
85
|
+
const value = options[name];
|
|
86
|
+
return typeof value === "string" ? value : fallback;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {CliOptions} options
|
|
91
|
+
* @returns {Promise<PreparedUpload>}
|
|
92
|
+
*/
|
|
93
|
+
async function prepareUpload(options) {
|
|
94
|
+
const endpoint = requireOption(options, "endpoint");
|
|
95
|
+
const loopback = requireUploadEndpoint(endpoint, options["allow-hosted"] === true);
|
|
96
|
+
const buildDir = await requireBuildDir(requireOption(options, "build-dir"));
|
|
97
|
+
const manifestPath = path.resolve(requireOption(options, "manifest"));
|
|
98
|
+
if (!(await pathExists(manifestPath))) {
|
|
99
|
+
throw new Error(`manifest file does not exist: ${String(options["manifest"])}`);
|
|
100
|
+
}
|
|
101
|
+
const manifest = await readJsonObject(manifestPath, "manifest");
|
|
102
|
+
const { artifactCount, files } = await collectUploadFiles(manifest, buildDir, !loopback);
|
|
182
103
|
return {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (!Array.isArray(manifest.artifacts) || manifest.artifacts.length === 0) {
|
|
197
|
-
throw new Error("manifest must contain at least one JavaScript release artifact");
|
|
198
|
-
}
|
|
199
|
-
normalizeProjectId(
|
|
200
|
-
manifest.projectId,
|
|
201
|
-
requireProjectId
|
|
202
|
-
? "hosted release artifact uploads require manifest projectId as a UUID"
|
|
203
|
-
: "manifest projectId must be a UUID when provided",
|
|
204
|
-
requireProjectId,
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function requireArtifactFile(artifact, buildDir, section, requiredFields) {
|
|
209
|
-
const payload = artifact[section];
|
|
210
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
211
|
-
throw new Error(`artifact is missing ${section}`);
|
|
212
|
-
}
|
|
213
|
-
for (const field of requiredFields) {
|
|
214
|
-
if (payload[field] === undefined || payload[field] === "") {
|
|
215
|
-
throw new Error(`${section} is missing ${field}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
const filePath = safeResolve(path.join(buildDir, String(payload.path)), buildDir);
|
|
219
|
-
if (!filePath || !fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
220
|
-
throw new Error(`${section} file is missing: ${payload.path}`);
|
|
221
|
-
}
|
|
222
|
-
if (byteSize(filePath) !== Number(payload.byteSize)) {
|
|
223
|
-
throw new Error(`${section} byte size changed after manifest creation: ${payload.path}`);
|
|
224
|
-
}
|
|
225
|
-
if (sha256File(filePath) !== String(payload.artifactSha256)) {
|
|
226
|
-
throw new Error(`${section} sha256 changed after manifest creation: ${payload.path}`);
|
|
227
|
-
}
|
|
228
|
-
return filePath;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function collectUploadFiles(manifest, buildDir, requireProjectId) {
|
|
232
|
-
requireReadyJavaScriptManifest(manifest, requireProjectId);
|
|
233
|
-
const files = [];
|
|
234
|
-
for (const [index, artifact] of manifest.artifacts.entries()) {
|
|
235
|
-
if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) {
|
|
236
|
-
throw new Error("artifact entries must be JSON objects");
|
|
104
|
+
endpoint,
|
|
105
|
+
files,
|
|
106
|
+
manifest,
|
|
107
|
+
report: {
|
|
108
|
+
artifactCount,
|
|
109
|
+
artifactType: manifest["artifactType"],
|
|
110
|
+
dryRun: options["dry-run"] === true,
|
|
111
|
+
endpoint: endpointWithoutQuery(endpoint),
|
|
112
|
+
environment: manifest["environment"],
|
|
113
|
+
filePartCount: files.length,
|
|
114
|
+
release: manifest["release"],
|
|
115
|
+
service: manifest["service"],
|
|
116
|
+
uploader: UPLOADER
|
|
237
117
|
}
|
|
238
|
-
files.push([
|
|
239
|
-
`minified_source_${index}`,
|
|
240
|
-
requireArtifactFile(artifact, buildDir, "minifiedSource", ["path", "artifactSha256", "byteSize"])
|
|
241
|
-
]);
|
|
242
|
-
files.push([
|
|
243
|
-
`source_map_${index}`,
|
|
244
|
-
requireArtifactFile(artifact, buildDir, "sourceMap", ["path", "artifactSha256", "byteSize"])
|
|
245
|
-
]);
|
|
246
|
-
}
|
|
247
|
-
return files;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function buildUploadReport({ endpoint, manifest, files, dryRun }) {
|
|
251
|
-
return {
|
|
252
|
-
uploader: { name: "logbrew-js-release-artifact-upload-verifier", version: SCRIPT_VERSION },
|
|
253
|
-
endpoint: endpointWithoutQuery(endpoint),
|
|
254
|
-
dryRun,
|
|
255
|
-
release: manifest.release,
|
|
256
|
-
environment: manifest.environment,
|
|
257
|
-
service: manifest.service,
|
|
258
|
-
artifactType: manifest.artifactType,
|
|
259
|
-
artifactCount: manifest.artifacts.length,
|
|
260
|
-
filePartCount: files.length
|
|
261
118
|
};
|
|
262
119
|
}
|
|
263
120
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
121
|
+
/**
|
|
122
|
+
* @param {CliOptions} options
|
|
123
|
+
* @param {PreparedUpload} prepared
|
|
124
|
+
* @param {string} token
|
|
125
|
+
* @returns {Promise<number>}
|
|
126
|
+
*/
|
|
127
|
+
async function sendUpload(options, { endpoint, files, manifest, report }, token) {
|
|
128
|
+
const maxRetries = parseNonNegativeInteger(optionOrDefault(options, "max-retries", "2"), "--max-retries");
|
|
129
|
+
const retryDelaySeconds = parseNonNegativeNumber(optionOrDefault(options, "retry-delay", "0.25"), "--retry-delay");
|
|
130
|
+
const timeoutSeconds = parseNonNegativeNumber(optionOrDefault(options, "timeout", "5"), "--timeout");
|
|
131
|
+
const { body, boundary } = await encodeMultipart(manifest, files);
|
|
132
|
+
const uploadReport = await uploadWithRetries({ body, boundary, endpoint, maxRetries, retryDelaySeconds, timeoutSeconds, token });
|
|
133
|
+
printJson({ ...report, ...uploadReport });
|
|
134
|
+
return exitCodeForUploadStatus(uploadReport.status);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {CliOptions} options
|
|
139
|
+
* @returns {string}
|
|
140
|
+
*/
|
|
141
|
+
function requireTokenEnv(options) {
|
|
142
|
+
const tokenEnv = optionOrDefault(options, "token-env", DEFAULT_UPLOAD_TOKEN_ENV).trim();
|
|
143
|
+
if (tokenEnv === "") {
|
|
144
|
+
throw new Error("--token-env must not be empty");
|
|
145
|
+
}
|
|
146
|
+
return tokenEnv;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {CliOptions} options
|
|
151
|
+
* @returns {Promise<number>}
|
|
152
|
+
*/
|
|
153
|
+
async function uploadFromOptions(options) {
|
|
154
|
+
const prepared = await prepareUpload(options);
|
|
155
|
+
if (options["dry-run"] === true) {
|
|
156
|
+
printJson({ ...prepared.report, attempts: [], retryCount: NO_RETRIES, status: "dry_run" });
|
|
157
|
+
return exitCodeForUploadStatus("dry_run");
|
|
158
|
+
}
|
|
159
|
+
const tokenEnv = requireTokenEnv(options);
|
|
160
|
+
const token = (process.env[tokenEnv] ?? "").trim();
|
|
161
|
+
if (token === "") {
|
|
162
|
+
printJson({ ...prepared.report, attempts: [], auth: { tokenEnv }, retryCount: NO_RETRIES, status: "auth_missing" });
|
|
163
|
+
return exitCodeForUploadStatus("auth_missing");
|
|
164
|
+
}
|
|
165
|
+
return sendUpload(options, prepared, token);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {readonly string[]} args
|
|
170
|
+
* @returns {Promise<number>}
|
|
171
|
+
*/
|
|
172
|
+
async function runUploadJs(args) {
|
|
275
173
|
const options = parseOptions(args, {
|
|
174
|
+
"allow-hosted": "boolean",
|
|
276
175
|
"build-dir": "string",
|
|
277
|
-
manifest: "string",
|
|
278
|
-
endpoint: "string",
|
|
279
|
-
"token-env": "string",
|
|
280
176
|
"dry-run": "boolean",
|
|
281
|
-
|
|
177
|
+
endpoint: "string",
|
|
178
|
+
manifest: "string",
|
|
282
179
|
"max-retries": "string",
|
|
283
180
|
"retry-delay": "string",
|
|
284
|
-
timeout: "string"
|
|
181
|
+
timeout: "string",
|
|
182
|
+
"token-env": "string"
|
|
285
183
|
});
|
|
286
184
|
try {
|
|
287
|
-
|
|
288
|
-
const parsedEndpoint = requireUploadEndpoint(endpoint, Boolean(options["allow-hosted"]));
|
|
289
|
-
const buildDir = requireBuildDir(requireOption(options, "build-dir"));
|
|
290
|
-
const manifestPath = path.resolve(requireOption(options, "manifest"));
|
|
291
|
-
if (!fs.existsSync(manifestPath)) {
|
|
292
|
-
throw new Error(`manifest file does not exist: ${options.manifest}`);
|
|
293
|
-
}
|
|
294
|
-
const manifest = readJsonObject(manifestPath, "manifest");
|
|
295
|
-
const files = collectUploadFiles(manifest, buildDir, !isLoopbackEndpoint(parsedEndpoint));
|
|
296
|
-
const dryRun = Boolean(options["dry-run"]);
|
|
297
|
-
const report = buildUploadReport({ endpoint, manifest, files, dryRun });
|
|
298
|
-
|
|
299
|
-
if (dryRun) {
|
|
300
|
-
printJson({ ...report, status: "dry_run", attempts: [], retryCount: 0 });
|
|
301
|
-
return 0;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const tokenEnv = (options["token-env"] ?? DEFAULT_UPLOAD_TOKEN_ENV).trim();
|
|
305
|
-
if (tokenEnv === "") {
|
|
306
|
-
throw new Error("--token-env must not be empty");
|
|
307
|
-
}
|
|
308
|
-
const token = (process.env[tokenEnv] ?? "").trim();
|
|
309
|
-
if (!token) {
|
|
310
|
-
printJson({ ...report, status: "auth_missing", attempts: [], retryCount: 0, auth: { tokenEnv } });
|
|
311
|
-
return exitCodeForUploadStatus("auth_missing");
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const maxRetries = parseNonNegativeInteger(options["max-retries"] ?? "2", "--max-retries");
|
|
315
|
-
const retryDelaySeconds = parseNonNegativeNumber(options["retry-delay"] ?? "0.25", "--retry-delay");
|
|
316
|
-
const timeoutSeconds = parseNonNegativeNumber(options.timeout ?? "5", "--timeout");
|
|
317
|
-
const { body, boundary } = encodeMultipart(manifest, files);
|
|
318
|
-
const uploadReport = await uploadWithRetries({
|
|
319
|
-
endpoint,
|
|
320
|
-
token,
|
|
321
|
-
body,
|
|
322
|
-
boundary,
|
|
323
|
-
maxRetries,
|
|
324
|
-
retryDelaySeconds,
|
|
325
|
-
timeoutSeconds
|
|
326
|
-
});
|
|
327
|
-
printJson({ ...report, ...uploadReport });
|
|
328
|
-
return exitCodeForUploadStatus(uploadReport.status);
|
|
185
|
+
return await uploadFromOptions(options);
|
|
329
186
|
} catch (error) {
|
|
330
|
-
printJson({
|
|
331
|
-
status: "validation_failed",
|
|
332
|
-
uploader: { name: "logbrew-js-release-artifact-upload-verifier", version: SCRIPT_VERSION },
|
|
333
|
-
validation: { errors: [error.message] }
|
|
334
|
-
});
|
|
187
|
+
printJson({ status: "validation_failed", uploader: UPLOADER, validation: { errors: [errorMessage(error)] } });
|
|
335
188
|
return exitCodeForUploadStatus("validation_failed");
|
|
336
189
|
}
|
|
337
190
|
}
|
|
191
|
+
|
|
192
|
+
export { runUploadJs };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { fileReference } from "./release-artifacts-common.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const TRAILING_SLASHES_RE = /\/+$/u;
|
|
5
|
+
const LEADING_SLASHES_RE = /^\/+/u;
|
|
6
|
+
const URL_WITH_SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//u;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {Readonly<{ host: string, protocol: string }>} parsed
|
|
10
|
+
* @param {string} pathname
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
function urlWithPath({ host, protocol }, pathname) {
|
|
14
|
+
return host === "" ? `${protocol}//${pathname}` : `${protocol}//${host}${pathname}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} value
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
function normalizeUrlOrPath(value) {
|
|
22
|
+
const trimmed = value.trim();
|
|
23
|
+
if (URL_WITH_SCHEME_RE.test(trimmed)) {
|
|
24
|
+
const { host, pathname, protocol } = new URL(trimmed);
|
|
25
|
+
const normalizedPath = path.posix.normalize(pathname === "" ? "/" : pathname).replace(TRAILING_SLASHES_RE, "");
|
|
26
|
+
return urlWithPath({ host, protocol }, normalizedPath === "" ? "/" : normalizedPath);
|
|
27
|
+
}
|
|
28
|
+
return fileReference(trimmed).trim().replace(TRAILING_SLASHES_RE, "");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} prefix
|
|
33
|
+
* @param {string} relativePath
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
function joinUrlOrPath(prefix, relativePath) {
|
|
37
|
+
const normalizedPrefix = normalizeUrlOrPath(prefix);
|
|
38
|
+
const normalizedRelative = relativePath.replaceAll("\\", "/").replace(LEADING_SLASHES_RE, "");
|
|
39
|
+
if (URL_WITH_SCHEME_RE.test(normalizedPrefix)) {
|
|
40
|
+
const { host, pathname, protocol } = new URL(normalizedPrefix);
|
|
41
|
+
const joinedPath = path.posix.join(pathname.replace(TRAILING_SLASHES_RE, ""), normalizedRelative);
|
|
42
|
+
return urlWithPath({ host, protocol }, joinedPath.startsWith("/") ? joinedPath : `/${joinedPath}`);
|
|
43
|
+
}
|
|
44
|
+
return normalizedPrefix === ""
|
|
45
|
+
? normalizedRelative
|
|
46
|
+
: `${normalizedPrefix.replace(TRAILING_SLASHES_RE, "")}/${normalizedRelative}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { joinUrlOrPath, normalizeUrlOrPath };
|