-
-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathreleases.ts
More file actions
491 lines (455 loc) · 13.5 KB
/
releases.ts
File metadata and controls
491 lines (455 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
/**
* Release API functions
*
* Functions for listing, creating, updating, deleting, and deploying
* Sentry releases in an organization.
*/
import type { DeployResponse, OrgReleaseResponse } from "@sentry/api";
import {
createADeploy,
createANewReleaseForAnOrganization,
deleteAnOrganization_sRelease,
listAnOrganization_sReleases,
listARelease_sDeploys,
retrieveAnOrganization_sRelease,
updateAnOrganization_sRelease,
} from "@sentry/api";
import { ApiError, ValidationError } from "../errors.js";
import { getHeadCommit, getRepositoryName } from "../git.js";
import { resolveOrgRegion } from "../region.js";
import {
API_MAX_PER_PAGE,
apiRequestToRegion,
getOrgSdkConfig,
MAX_PAGINATION_PAGES,
type PaginatedResponse,
unwrapPaginatedResult,
unwrapResult,
} from "./infrastructure.js";
import { listRepositoriesPaginated } from "./repositories.js";
// We cast through `unknown` to bridge the gap between the SDK's internal
// return types and the public response types — the shapes are compatible
// at runtime.
/**
* List releases in an organization with pagination control.
* Returns a single page of results with cursor metadata.
*
* When `health` is true, each release's `projects[].healthData` is populated
* with adoption percentages, crash-free rates, and session/user counts.
*
* @param orgSlug - Organization slug
* @param options - Pagination, query, sort, and health options
* @returns Single page of releases with cursor metadata
*/
export async function listReleasesPaginated(
orgSlug: string,
options: {
cursor?: string;
perPage?: number;
query?: string;
sort?: string;
/** Include per-project health/adoption data in the response. */
health?: boolean;
} = {}
): Promise<PaginatedResponse<OrgReleaseResponse[]>> {
const config = await getOrgSdkConfig(orgSlug);
const result = await listAnOrganization_sReleases({
...config,
path: { organization_id_or_slug: orgSlug },
// per_page, sort, and health are supported at runtime but not in the OpenAPI spec
query: {
cursor: options.cursor,
per_page: options.perPage ?? 25,
query: options.query,
sort: options.sort,
health: options.health ? 1 : undefined,
} as { cursor?: string },
});
return unwrapPaginatedResult<OrgReleaseResponse[]>(
result as
| { data: OrgReleaseResponse[]; error: undefined }
| { data: undefined; error: unknown },
"Failed to list releases"
);
}
/** Sort options for the release list endpoint. */
export type ReleaseSortValue =
| "date"
| "sessions"
| "users"
| "crash_free_sessions"
| "crash_free_users";
/**
* Get a single release by version.
* Version is URL-encoded by the SDK.
*
* When `health` is true, each project in the response includes a
* `healthData` object with adoption percentages, crash-free rates,
* and session/user counts for the requested period.
*
* @param orgSlug - Organization slug
* @param version - Release version string (e.g., "1.0.0", "sentry-cli@0.24.0")
* @param options - Optional health and adoption query parameters
* @returns Full release detail
*/
export async function getRelease(
orgSlug: string,
version: string,
options?: {
/** Include per-project health/adoption data. */
health?: boolean;
/** Include adoption stage info (e.g., "adopted", "low_adoption"). */
adoptionStages?: boolean;
/** Period for health stats: "24h", "7d", "14d", etc. Defaults to "24h". */
healthStatsPeriod?: string;
}
): Promise<OrgReleaseResponse> {
const config = await getOrgSdkConfig(orgSlug);
const result = await retrieveAnOrganization_sRelease({
...config,
path: {
organization_id_or_slug: orgSlug,
version,
},
query: {
health: options?.health,
adoptionStages: options?.adoptionStages,
healthStatsPeriod: options?.healthStatsPeriod as
| "24h"
| "7d"
| "14d"
| "30d"
| "1h"
| "1d"
| "2d"
| "48h"
| "90d"
| undefined,
},
});
const data = unwrapResult(result, `Failed to get release '${version}'`);
return data as unknown as OrgReleaseResponse;
}
/**
* Create a new release.
*
* @param orgSlug - Organization slug
* @param body - Release creation payload
* @returns Created release detail
*/
export async function createRelease(
orgSlug: string,
body: {
version: string;
projects?: string[];
ref?: string;
url?: string;
dateReleased?: string;
commits?: Array<{
id: string;
repository?: string;
message?: string;
author_name?: string;
author_email?: string;
timestamp?: string;
}>;
}
): Promise<OrgReleaseResponse> {
const config = await getOrgSdkConfig(orgSlug);
// Cast body through unknown — the SDK's body type requires `projects: string[]`
// as non-optional, but the API accepts it as optional at runtime.
const result = await createANewReleaseForAnOrganization({
...config,
path: { organization_id_or_slug: orgSlug },
body: body as unknown as Parameters<
typeof createANewReleaseForAnOrganization
>[0]["body"],
});
// 208 = release already exists (idempotent) — treat as success
if (result.data) {
return result.data as unknown as OrgReleaseResponse;
}
const data = unwrapResult(result, "Failed to create release");
return data as unknown as OrgReleaseResponse;
}
/**
* Update a release. Used for finalization, setting refs, etc.
*
* @param orgSlug - Organization slug
* @param version - Release version (URL-encoded by SDK)
* @param body - Fields to update
* @returns Updated release detail
*/
export async function updateRelease(
orgSlug: string,
version: string,
body: {
ref?: string;
url?: string;
dateReleased?: string;
commits?: Array<{
id: string;
repository?: string;
message?: string;
author_name?: string;
author_email?: string;
timestamp?: string;
}>;
}
): Promise<OrgReleaseResponse> {
const config = await getOrgSdkConfig(orgSlug);
const result = await updateAnOrganization_sRelease({
...config,
path: {
organization_id_or_slug: orgSlug,
version,
},
body: body as unknown as Parameters<
typeof updateAnOrganization_sRelease
>[0]["body"],
});
const data = unwrapResult(result, `Failed to update release '${version}'`);
return data as unknown as OrgReleaseResponse;
}
/**
* Delete a release.
*
* @param orgSlug - Organization slug
* @param version - Release version
*/
export async function deleteRelease(
orgSlug: string,
version: string
): Promise<void> {
const config = await getOrgSdkConfig(orgSlug);
const result = await deleteAnOrganization_sRelease({
...config,
path: {
organization_id_or_slug: orgSlug,
version,
},
});
unwrapResult(result, `Failed to delete release '${version}'`);
}
/**
* List deploys for a release.
*
* @param orgSlug - Organization slug
* @param version - Release version
* @returns Array of deploy details
*/
export async function listReleaseDeploys(
orgSlug: string,
version: string
): Promise<DeployResponse[]> {
const config = await getOrgSdkConfig(orgSlug);
const result = await listARelease_sDeploys({
...config,
path: {
organization_id_or_slug: orgSlug,
version,
},
});
const data = unwrapResult(
result,
`Failed to list deploys for release '${version}'`
);
return data as unknown as DeployResponse[];
}
/**
* Create a deploy for a release.
*
* @param orgSlug - Organization slug
* @param version - Release version
* @param body - Deploy creation payload
* @returns Created deploy detail
*/
export async function createReleaseDeploy(
orgSlug: string,
version: string,
body: {
environment: string;
name?: string;
url?: string;
dateStarted?: string;
dateFinished?: string;
}
): Promise<DeployResponse> {
const config = await getOrgSdkConfig(orgSlug);
const result = await createADeploy({
...config,
path: {
organization_id_or_slug: orgSlug,
version,
},
body: body as unknown as Parameters<typeof createADeploy>[0]["body"],
});
const data = unwrapResult(result, "Failed to create deploy");
return data as unknown as DeployResponse;
}
/**
* Get the last commit SHA from the previous release that has commits.
*
* Uses the undocumented `/previous-with-commits/` endpoint (same as the
* reference sentry-cli) to determine the commit baseline for range-based
* commit association. Without this, Sentry can't compute which commits
* are new in the current release and reports 0 commits.
*
* @param orgSlug - Organization slug
* @param version - Current release version
* @returns Previous release's last commit SHA, or undefined if no previous release
*/
async function getPreviousReleaseCommit(
orgSlug: string,
version: string
): Promise<string | undefined> {
try {
const regionUrl = await resolveOrgRegion(orgSlug);
const encodedVersion = encodeURIComponent(version);
const { data } = await apiRequestToRegion<{
lastCommit?: { id: string } | null;
}>(
regionUrl,
`organizations/${orgSlug}/releases/${encodedVersion}/previous-with-commits/`,
{ method: "GET" }
);
return data?.lastCommit?.id;
} catch {
// Not critical — if we can't get the previous commit, we still send
// refs without previousCommit. Sentry will try to determine the range
// from its own data (may result in 0 commits for first releases).
return;
}
}
/**
* Set commits on a release using auto-discovery mode.
*
* Lists the org's repositories from the Sentry API, matches against the
* local git remote URL to find the corresponding Sentry repo, then sends
* a refs payload with the HEAD commit SHA. This is the equivalent of the
* reference sentry-cli's `--auto` mode.
*
* Requires a GitHub/GitLab/Bitbucket integration configured in Sentry
* AND a local git repository whose origin remote matches a Sentry repo.
*
* @param orgSlug - Organization slug
* @param version - Release version
* @param cwd - Working directory to discover git remote and HEAD from
* @returns Updated release detail with commit count
* @throws {ApiError} When the org has no repository integrations (400)
* @throws {ValidationError} When local git remote is missing or doesn't match any Sentry repo
*/
export async function setCommitsAuto(
orgSlug: string,
version: string,
cwd?: string
): Promise<OrgReleaseResponse> {
const localRepo = getRepositoryName(cwd);
if (!localRepo) {
throw new ValidationError(
"Could not determine repository name from local git remote.",
"repository"
);
}
// Paginate through org repos to find one matching the local git remote.
// Stops as soon as a match is found to avoid unnecessary API calls.
const localRepoLower = localRepo.toLowerCase();
let cursor: string | undefined;
let foundAnyRepos = false;
for (let page = 0; page < MAX_PAGINATION_PAGES; page++) {
const result = await listRepositoriesPaginated(orgSlug, {
cursor,
perPage: API_MAX_PER_PAGE,
});
if (result.data.length > 0) {
foundAnyRepos = true;
}
const match = result.data.find(
(r) => r.name.toLowerCase() === localRepoLower
);
if (match) {
const headCommit = getHeadCommit(cwd);
const previousCommit = await getPreviousReleaseCommit(orgSlug, version);
const ref: {
repository: string;
commit: string;
previousCommit?: string;
} = { repository: match.name, commit: headCommit };
if (previousCommit) {
ref.previousCommit = previousCommit;
}
return setCommitsWithRefs(orgSlug, version, [ref]);
}
if (!result.nextCursor) {
break;
}
cursor = result.nextCursor;
}
if (!foundAnyRepos) {
const endpoint = `organizations/${orgSlug}/releases/${encodeURIComponent(version)}/`;
throw new ApiError(
"No repository integrations configured for this organization.",
400,
undefined,
endpoint
);
}
throw new ValidationError(
`No Sentry repository matching '${localRepo}'.`,
"repository"
);
}
/**
* Set commits on a release using explicit refs (repository + commit range).
*
* Sends the refs format which supports previous commit for range-based
* commit association (matching the reference sentry-cli's `--commit REPO@PREV..SHA`).
*
* @param orgSlug - Organization slug
* @param version - Release version
* @param refs - Array of ref objects
* @returns Updated release detail
*/
export async function setCommitsWithRefs(
orgSlug: string,
version: string,
refs: Array<{
repository: string;
commit: string;
previousCommit?: string;
}>
): Promise<OrgReleaseResponse> {
const regionUrl = await resolveOrgRegion(orgSlug);
const encodedVersion = encodeURIComponent(version);
const { data } = await apiRequestToRegion<OrgReleaseResponse>(
regionUrl,
`organizations/${orgSlug}/releases/${encodedVersion}/`,
{
method: "PUT",
body: { refs },
}
);
return data;
}
/**
* Set commits on a release using explicit commit data.
*
* @param orgSlug - Organization slug
* @param version - Release version
* @param commits - Array of commit data
* @returns Updated release detail
*/
export function setCommitsLocal(
orgSlug: string,
version: string,
commits: Array<{
id: string;
repository?: string;
message?: string;
author_name?: string;
author_email?: string;
timestamp?: string;
}>
): Promise<OrgReleaseResponse> {
return updateRelease(orgSlug, version, { commits });
}