@getexception/cli 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/dist/index.js +95 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,11 +16,17 @@ yarn getexception sourcemaps upload --dir .getexception-maps --url https://sentr
|
|
|
16
16
|
|
|
17
17
|
The Owner creates **Source map upload tokens** in the project's settings. Store the token in the CI secret `GETEXCEPTION_UPLOAD_TOKEN`. It expires after 90 days and can be revoked. Never pass it as a command-line argument or include it in the browser environment, repository, logs or public artifacts. Only trusted build jobs may read this secret.
|
|
18
18
|
|
|
19
|
+
For GitLab MR previews use short-lived `GETEXCEPTION_GITLAB_ID_TOKEN` instead, after the operator has configured the pinned GitLab trust policy. Run `getexception ci context --url <HTTPS dashboard origin> --project <UUID>` before building; use the returned release, deployment and asset prefix for the actual JavaScript output. Upload and release registration use that identity automatically. Never set both credential variables. See the [GitLab integration guide](../../docs/gitlab-ci.md) for server configuration, exact build isolation and production restrictions. Version 0.1.7 does not support this flow.
|
|
20
|
+
|
|
21
|
+
The current CI context contract is **version 2**; CLI 0.1.8 must be upgraded to use it. The Owner selects **Production only** or **Production and MR** under project settings and adds trusted fork project IDs/paths. No container restart is needed. A context includes `sourceMaps: {enabled: true}` or, for a trusted preview with maps disabled, `{enabled: false, reason: "preview_disabled"}`. An unlisted preview source receives only `{version: 2, sourceMaps: {enabled: false, reason: "source_not_allowed"}}` with no build context or release-registration permission. Skip responses mean build/deploy without maps, not disable SDK events. Invalid credentials, HTTP failures and unknown contracts remain errors. A trusted context can still register its release while maps are disabled. Production never receives a skip response.
|
|
22
|
+
|
|
23
|
+
Permanent upload tokens cannot be created or used for projects with GitLab authentication configured. They remain available for other trusted CI integrations. Forks may upload only their own preview job's maps; the main repository remains the only production source. Actual map read access is never granted to CI.
|
|
24
|
+
|
|
19
25
|
`prepare` modifies the **final ESM JavaScript**: it adds a Debug ID, shifts mappings by one line, removes sourceMappingURL comments and moves `.js.map` / `.mjs.map` files out of the public build. The private output directory must not exist and must be outside `dist`. Run this once per fresh build, before compression, SRI signing or deployment. Legacy IIFE/CommonJS bundles are unsupported. If assets are served below an additional base path, pass `--url-prefix <base-path>`; paths must match deployed JavaScript URLs.
|
|
20
26
|
|
|
21
|
-
Deploy the resulting `dist` unchanged. Upload failure does not undo preparation:
|
|
27
|
+
Deploy the resulting `dist` unchanged. Upload failure does not undo preparation: retry within the same job using the same prepared directory. **Do not save source maps as GitLab artifacts or cache when access has not been verified.** The GitLab identity flow keeps them only in the isolated job workspace and uploads directly to GetException. A retry after that job ends requires a new build with a new job prefix; it cannot replace maps for the old build. Do not run `prepare` twice or rebuild only the maps. Never publish the private artifact directory alongside the application. Other map formats (for example CSS maps) should also be excluded from the public deploy artifact by your build configuration.
|
|
22
28
|
|
|
23
|
-
`upload` sends an authenticated manifest, uploads files with checksums, then waits for background validation. It supports retries and resuming the same manifest, has a two-minute deadline, and exits nonzero on failure. CI may allow
|
|
29
|
+
`upload` sends an authenticated manifest, uploads files with checksums, then waits for background validation. It supports retries and resuming the same manifest, has a two-minute deadline, and exits nonzero on failure. CI may allow an urgent production deployment without maps with an explicit warning. MR previews with maps enabled require all batches to be ready before deploying. Do not retain maps in GitLab artifacts as a fallback. Raw maps have no download endpoint.
|
|
24
30
|
|
|
25
31
|
Limits: 128 JS files and 128 MiB per upload, 16 MiB per map, 1 GiB per project, 10 GiB per installation. Non-indexed Source Map v3 JSON only; no archives, compression or remote source downloads. Existing events are processed after a late upload. Errors thrown in the browser console have no source file and cannot gain a source snippet from a map.
|
|
26
32
|
|
|
@@ -31,9 +37,11 @@ MIT. See `THIRD-PARTY-NOTICES.md`.
|
|
|
31
37
|
Register the environment after a successful deployment, with the same project token. This works independently of source-map upload:
|
|
32
38
|
|
|
33
39
|
```bash
|
|
34
|
-
yarn getexception releases register --url https://sentry.frontend.sndsy.ru --project "$GETEXCEPTION_PROJECT_ID" --release "account@$CI_COMMIT_SHA" --environment staging --repository-id "$
|
|
40
|
+
yarn getexception releases register --url https://sentry.frontend.sndsy.ru --project "$GETEXCEPTION_PROJECT_ID" --release "account@$CI_COMMIT_SHA" --environment staging --repository-id "$CI_MERGE_REQUEST_TARGET_PROJECT_ID" --merge-request "$CI_MERGE_REQUEST_IID"
|
|
35
41
|
```
|
|
36
42
|
|
|
37
43
|
Use the GitLab target repository's numeric project ID and the MR IID belonging to it. Standard merge-request pipelines provide these [predefined GitLab variables](https://docs.gitlab.com/ci/variables/predefined_variables/); custom preview pipelines must supply the actual pair explicitly. For production, use `--environment production` and omit both MR options. Repeated registration is safe. One SHA may have both production and staging contexts; this records release history, not current deployment status. Old events only establish an environment and cannot reveal an MR number.
|
|
38
44
|
|
|
45
|
+
With GitLab build authentication, use the exact `release` and `deployment` returned by `ci context` instead of constructing registration metadata yourself. `CI_MERGE_REQUEST_PROJECT_ID` is not a substitute for the target project in a fork workflow.
|
|
46
|
+
|
|
39
47
|
Debug IDs are deterministic for identical JS, maps and paths, regardless of the release SHA. The server reuses private bytes within the project and `upload` skips files it already has. Every release still sends its own manifest. Changed JavaScript requires its matching map: there is no fallback to the latest production map. Ready maps older than 30 days are eligible for cleanup only when their release has no retained events or pending inbox events. Shared files survive until all references expire.
|
package/dist/index.js
CHANGED
|
@@ -18985,6 +18985,33 @@ var releaseRegistrationSchema = external_exports.object({
|
|
|
18985
18985
|
deployment: deploymentSchema
|
|
18986
18986
|
}).strict();
|
|
18987
18987
|
|
|
18988
|
+
// packages/protocol/src/ci.ts
|
|
18989
|
+
var buildContextSchema = external_exports.object({
|
|
18990
|
+
release: releaseNameSchema,
|
|
18991
|
+
assetPrefix: external_exports.string().regex(/^assets\/ge-gl-[1-9][0-9]*-[1-9][0-9]*\/$/),
|
|
18992
|
+
deployment: deploymentSchema
|
|
18993
|
+
}).strict();
|
|
18994
|
+
var sourceMaps = external_exports.discriminatedUnion("enabled", [
|
|
18995
|
+
external_exports.object({ enabled: external_exports.literal(true) }).strict(),
|
|
18996
|
+
external_exports.object({
|
|
18997
|
+
enabled: external_exports.literal(false),
|
|
18998
|
+
reason: external_exports.literal("preview_disabled")
|
|
18999
|
+
}).strict()
|
|
19000
|
+
]);
|
|
19001
|
+
var ciContextSchema = external_exports.union([
|
|
19002
|
+
buildContextSchema.extend({ version: external_exports.literal(2), sourceMaps }).refine(
|
|
19003
|
+
(context) => context.sourceMaps.enabled || context.deployment.environment === "staging",
|
|
19004
|
+
"Only preview builds may skip source maps"
|
|
19005
|
+
),
|
|
19006
|
+
external_exports.object({
|
|
19007
|
+
version: external_exports.literal(2),
|
|
19008
|
+
sourceMaps: external_exports.object({
|
|
19009
|
+
enabled: external_exports.literal(false),
|
|
19010
|
+
reason: external_exports.literal("source_not_allowed")
|
|
19011
|
+
}).strict()
|
|
19012
|
+
}).strict()
|
|
19013
|
+
]);
|
|
19014
|
+
|
|
18988
19015
|
// packages/protocol/src/index.ts
|
|
18989
19016
|
var safeFrameSchema = external_exports.object({
|
|
18990
19017
|
filename: external_exports.string().max(512),
|
|
@@ -19163,25 +19190,54 @@ import { setTimeout as delay2 } from "timers/promises";
|
|
|
19163
19190
|
|
|
19164
19191
|
// packages/cli/src/api.ts
|
|
19165
19192
|
import { setTimeout as delay } from "timers/promises";
|
|
19193
|
+
|
|
19194
|
+
// packages/cli/src/credentials.ts
|
|
19195
|
+
function ciCredential(env = process.env) {
|
|
19196
|
+
if (env.GETEXCEPTION_GITLAB_ID_TOKEN && env.GETEXCEPTION_UPLOAD_TOKEN) {
|
|
19197
|
+
throw new Error("Choose one CI authentication method");
|
|
19198
|
+
}
|
|
19199
|
+
return env.GETEXCEPTION_GITLAB_ID_TOKEN ? { gitlabIdToken: env.GETEXCEPTION_GITLAB_ID_TOKEN } : env.GETEXCEPTION_UPLOAD_TOKEN ?? "";
|
|
19200
|
+
}
|
|
19201
|
+
function authorizationHeader(credential) {
|
|
19202
|
+
if (typeof credential === "string") {
|
|
19203
|
+
if (!/^[a-f0-9]{64}$/.test(credential)) {
|
|
19204
|
+
throw new Error(
|
|
19205
|
+
"Set GETEXCEPTION_UPLOAD_TOKEN or GETEXCEPTION_GITLAB_ID_TOKEN"
|
|
19206
|
+
);
|
|
19207
|
+
}
|
|
19208
|
+
return `Bearer ${credential}`;
|
|
19209
|
+
}
|
|
19210
|
+
if (credential.gitlabIdToken.length > 16384 || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(
|
|
19211
|
+
credential.gitlabIdToken
|
|
19212
|
+
)) {
|
|
19213
|
+
throw new Error("Invalid GitLab build identity");
|
|
19214
|
+
}
|
|
19215
|
+
return `GitLab ${credential.gitlabIdToken}`;
|
|
19216
|
+
}
|
|
19217
|
+
|
|
19218
|
+
// packages/cli/src/api.ts
|
|
19166
19219
|
function projectApi(address, project, token, transport = fetch) {
|
|
19167
19220
|
const url2 = new URL(address);
|
|
19168
19221
|
if (url2.protocol !== "https:" || url2.username || url2.password || url2.search || url2.hash || url2.pathname !== "/") {
|
|
19169
19222
|
throw new Error("Use the HTTPS dashboard origin");
|
|
19170
19223
|
}
|
|
19171
|
-
if (!external_exports.string().uuid().safeParse(project).success
|
|
19172
|
-
throw new Error("Set a valid project ID
|
|
19224
|
+
if (!external_exports.string().uuid().safeParse(project).success) {
|
|
19225
|
+
throw new Error("Set a valid project ID");
|
|
19173
19226
|
}
|
|
19227
|
+
const authorization = authorizationHeader(token);
|
|
19174
19228
|
const base = `${url2.origin}/api/v1/projects/${project}`;
|
|
19175
19229
|
const deadline = Date.now() + 12e4;
|
|
19176
19230
|
return async function request(path, method = "GET", body) {
|
|
19177
19231
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
19178
19232
|
if (Date.now() >= deadline) {
|
|
19179
|
-
throw new Error(
|
|
19233
|
+
throw new Error(
|
|
19234
|
+
"Upload timed out; retry within the job or run a new build"
|
|
19235
|
+
);
|
|
19180
19236
|
}
|
|
19181
19237
|
const response = await transport(base + path, {
|
|
19182
19238
|
method,
|
|
19183
19239
|
headers: {
|
|
19184
|
-
Authorization:
|
|
19240
|
+
Authorization: authorization,
|
|
19185
19241
|
"Content-Type": "application/json"
|
|
19186
19242
|
},
|
|
19187
19243
|
body,
|
|
@@ -19266,6 +19322,18 @@ async function registerRelease(address, project, token, value, transport = fetch
|
|
|
19266
19322
|
)("/releases", "POST", JSON.stringify(input2));
|
|
19267
19323
|
}
|
|
19268
19324
|
|
|
19325
|
+
// packages/cli/src/ci.ts
|
|
19326
|
+
async function buildContext(address, project, credential, transport = fetch) {
|
|
19327
|
+
return ciContextSchema.parse(
|
|
19328
|
+
await projectApi(
|
|
19329
|
+
address,
|
|
19330
|
+
project,
|
|
19331
|
+
credential,
|
|
19332
|
+
transport
|
|
19333
|
+
)("/ci?version=2", "POST")
|
|
19334
|
+
);
|
|
19335
|
+
}
|
|
19336
|
+
|
|
19269
19337
|
// packages/cli/src/index.ts
|
|
19270
19338
|
async function main() {
|
|
19271
19339
|
const { values, positionals } = parseArgs({
|
|
@@ -19285,30 +19353,33 @@ async function main() {
|
|
|
19285
19353
|
});
|
|
19286
19354
|
if (values.help) {
|
|
19287
19355
|
process.stdout.write(
|
|
19288
|
-
"getexception sourcemaps prepare --dir dist --output ../private-maps --release app@<40-character-SHA> [--url-prefix assets]\ngetexception sourcemaps upload --dir ../private-maps --url https://dashboard.example --project <UUID>\ngetexception releases register --url https://dashboard.example --project <UUID> --release app@<40-character-SHA> --environment <production|staging|development> [--repository-id <GitLab project ID> --merge-request <IID>]\
|
|
19356
|
+
"getexception sourcemaps prepare --dir dist --output ../private-maps --release app@<40-character-SHA> [--url-prefix assets]\ngetexception sourcemaps upload --dir ../private-maps --url https://dashboard.example --project <UUID>\ngetexception releases register --url https://dashboard.example --project <UUID> --release app@<40-character-SHA> --environment <production|staging|development> [--repository-id <GitLab project ID> --merge-request <IID>]\ngetexception ci context --url https://dashboard.example --project <UUID>\nAuthentication: GETEXCEPTION_UPLOAD_TOKEN or GETEXCEPTION_GITLAB_ID_TOKEN, never both. Prepare modifies ESM JavaScript and removes public .js.map files.\n"
|
|
19357
|
+
);
|
|
19358
|
+
return;
|
|
19359
|
+
}
|
|
19360
|
+
if (positionals.length === 2 && positionals[0] === "ci" && positionals[1] === "context" && values.url && values.project) {
|
|
19361
|
+
process.stdout.write(
|
|
19362
|
+
JSON.stringify(
|
|
19363
|
+
await buildContext(values.url, values.project, ciCredential())
|
|
19364
|
+
) + "\n"
|
|
19289
19365
|
);
|
|
19290
19366
|
return;
|
|
19291
19367
|
}
|
|
19292
19368
|
if (positionals.length === 2 && positionals[0] === "releases" && positionals[1] === "register" && values.url && values.project) {
|
|
19293
19369
|
const hasReview = values["repository-id"] !== void 0 || values["merge-request"] !== void 0;
|
|
19294
|
-
await registerRelease(
|
|
19295
|
-
values.
|
|
19296
|
-
|
|
19297
|
-
|
|
19298
|
-
|
|
19299
|
-
|
|
19300
|
-
|
|
19301
|
-
|
|
19302
|
-
|
|
19303
|
-
|
|
19304
|
-
|
|
19305
|
-
repositoryId: Number(values["repository-id"]),
|
|
19306
|
-
number: Number(values["merge-request"])
|
|
19307
|
-
}
|
|
19308
|
-
} : {}
|
|
19309
|
-
}
|
|
19370
|
+
await registerRelease(values.url, values.project, ciCredential(), {
|
|
19371
|
+
release: values.release,
|
|
19372
|
+
deployment: {
|
|
19373
|
+
environment: values.environment,
|
|
19374
|
+
...hasReview ? {
|
|
19375
|
+
review: {
|
|
19376
|
+
provider: "gitlab",
|
|
19377
|
+
repositoryId: Number(values["repository-id"]),
|
|
19378
|
+
number: Number(values["merge-request"])
|
|
19379
|
+
}
|
|
19380
|
+
} : {}
|
|
19310
19381
|
}
|
|
19311
|
-
);
|
|
19382
|
+
});
|
|
19312
19383
|
process.stdout.write("Release environment registered.\n");
|
|
19313
19384
|
return;
|
|
19314
19385
|
}
|
|
@@ -19327,12 +19398,7 @@ async function main() {
|
|
|
19327
19398
|
`
|
|
19328
19399
|
);
|
|
19329
19400
|
} else if (positionals[1] === "upload" && values.url && values.project) {
|
|
19330
|
-
await uploadMaps(
|
|
19331
|
-
values.dir,
|
|
19332
|
-
values.url,
|
|
19333
|
-
values.project,
|
|
19334
|
-
process.env.GETEXCEPTION_UPLOAD_TOKEN ?? ""
|
|
19335
|
-
);
|
|
19401
|
+
await uploadMaps(values.dir, values.url, values.project, ciCredential());
|
|
19336
19402
|
process.stdout.write("Source maps validated and ready.\n");
|
|
19337
19403
|
} else {
|
|
19338
19404
|
throw new Error("Invalid command");
|
|
@@ -19340,7 +19406,7 @@ async function main() {
|
|
|
19340
19406
|
}
|
|
19341
19407
|
void main().catch(() => {
|
|
19342
19408
|
process.stderr.write(
|
|
19343
|
-
"Command failed. Check options (--help), CI
|
|
19409
|
+
"Command failed. Check options (--help), CI authentication, limits and build scope. Never publish maps as CI artifacts.\n"
|
|
19344
19410
|
);
|
|
19345
19411
|
process.exitCode = 1;
|
|
19346
19412
|
});
|