Skip to content

Commit c27f041

Browse files
committed
feat(release-list): add --sort flag for health-based ordering
Support sorting releases by: date (default), sessions, users, crash_free_sessions, crash_free_users. Switches from buildOrgListCommand to buildListCommand to support the custom flag (-s alias).
1 parent 49c74de commit c27f041

File tree

2 files changed

+163
-50
lines changed

2 files changed

+163
-50
lines changed

plugins/sentry-cli/skills/sentry-cli/references/release.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ List releases with adoption and health metrics
1717

1818
**Flags:**
1919
- `-n, --limit <value> - Maximum number of releases to list - (default: "25")`
20+
- `-s, --sort <value> - Sort order: date, sessions, users, crash_free_sessions, crash_free_users - (default: "date")`
2021
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
2122
- `-c, --cursor <value> - Navigate pages: "next", "prev", "first" (or raw cursor string)`
2223

src/commands/release/list.ts

Lines changed: 162 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,59 @@
66
*/
77

88
import type { OrgReleaseResponse } from "@sentry/api";
9-
import { listReleasesPaginated } from "../../lib/api-client.js";
9+
import type { SentryContext } from "../../context.js";
10+
import {
11+
listReleasesPaginated,
12+
type ReleaseSortValue,
13+
} from "../../lib/api-client.js";
14+
import { parseOrgProjectArg } from "../../lib/arg-parsing.js";
1015
import { escapeMarkdownCell } from "../../lib/formatters/markdown.js";
16+
import { CommandOutput } from "../../lib/formatters/output.js";
1117
import { type Column, formatTable } from "../../lib/formatters/table.js";
1218
import { formatRelativeTime } from "../../lib/formatters/time-utils.js";
1319
import {
14-
buildOrgListCommand,
15-
type OrgListCommandDocs,
20+
buildListCommand,
21+
buildListLimitFlag,
22+
LIST_BASE_ALIASES,
23+
LIST_TARGET_POSITIONAL,
1624
} from "../../lib/list-command.js";
17-
import type { OrgListConfig } from "../../lib/org-list.js";
25+
import {
26+
dispatchOrgScopedList,
27+
jsonTransformListResult,
28+
type ListResult,
29+
type OrgListConfig,
30+
} from "../../lib/org-list.js";
1831
import { fmtPct } from "./format.js";
1932

2033
export const PAGINATION_KEY = "release-list";
2134

2235
type ReleaseWithOrg = OrgReleaseResponse & { orgSlug?: string };
2336

37+
/** Valid values for the `--sort` flag. */
38+
const VALID_SORT_VALUES: ReleaseSortValue[] = [
39+
"date",
40+
"sessions",
41+
"users",
42+
"crash_free_sessions",
43+
"crash_free_users",
44+
];
45+
46+
const DEFAULT_SORT: ReleaseSortValue = "date";
47+
48+
/**
49+
* Parse and validate the `--sort` flag value.
50+
*
51+
* @throws Error when value is not one of the accepted sort keys
52+
*/
53+
function parseSortFlag(value: string): ReleaseSortValue {
54+
if (VALID_SORT_VALUES.includes(value as ReleaseSortValue)) {
55+
return value as ReleaseSortValue;
56+
}
57+
throw new Error(
58+
`Invalid sort value. Must be one of: ${VALID_SORT_VALUES.join(", ")}`
59+
);
60+
}
61+
2462
/**
2563
* Extract health data from the first project that has it.
2664
*
@@ -60,52 +98,126 @@ const RELEASE_COLUMNS: Column<ReleaseWithOrg>[] = [
6098
{ header: "DEPLOYS", value: (r) => String(r.deployCount ?? 0) },
6199
];
62100

63-
const releaseListConfig: OrgListConfig<OrgReleaseResponse, ReleaseWithOrg> = {
64-
paginationKey: PAGINATION_KEY,
65-
entityName: "release",
66-
entityPlural: "releases",
67-
commandPrefix: "sentry release list",
68-
// listForOrg fetches a buffer page for multi-org fan-out.
69-
// The framework truncates results to --limit after aggregation.
70-
// health=true to populate per-project adoption/crash-free metrics.
71-
listForOrg: async (org) => {
72-
const { data } = await listReleasesPaginated(org, {
73-
perPage: 100,
74-
health: true,
75-
});
76-
return data;
77-
},
78-
listPaginated: (org, opts) =>
79-
listReleasesPaginated(org, { ...opts, health: true }),
80-
withOrg: (release, orgSlug) => ({ ...release, orgSlug }),
81-
displayTable: (releases: ReleaseWithOrg[]) =>
82-
formatTable(releases, RELEASE_COLUMNS),
83-
};
101+
/**
102+
* Build the OrgListConfig with the given sort value baked into API calls.
103+
*
104+
* We build this per-invocation so the `--sort` flag value flows into
105+
* `listForOrg` and `listPaginated` closures.
106+
*/
107+
function buildReleaseListConfig(
108+
sort: ReleaseSortValue
109+
): OrgListConfig<OrgReleaseResponse, ReleaseWithOrg> {
110+
return {
111+
paginationKey: PAGINATION_KEY,
112+
entityName: "release",
113+
entityPlural: "releases",
114+
commandPrefix: "sentry release list",
115+
listForOrg: async (org) => {
116+
const { data } = await listReleasesPaginated(org, {
117+
perPage: 100,
118+
health: true,
119+
sort,
120+
});
121+
return data;
122+
},
123+
listPaginated: (org, opts) =>
124+
listReleasesPaginated(org, { ...opts, health: true, sort }),
125+
withOrg: (release, orgSlug) => ({ ...release, orgSlug }),
126+
displayTable: (releases: ReleaseWithOrg[]) =>
127+
formatTable(releases, RELEASE_COLUMNS),
128+
};
129+
}
130+
131+
/** Format a ListResult as human-readable output. */
132+
function formatListHuman(result: ListResult<ReleaseWithOrg>): string {
133+
const parts: string[] = [];
134+
135+
if (result.items.length === 0) {
136+
if (result.hint) {
137+
parts.push(result.hint);
138+
}
139+
return parts.join("\n");
140+
}
141+
142+
parts.push(formatTable(result.items, RELEASE_COLUMNS));
143+
144+
if (result.header) {
145+
parts.push(`\n${result.header}`);
146+
}
84147

85-
const docs: OrgListCommandDocs = {
86-
brief: "List releases with adoption and health metrics",
87-
fullDescription:
88-
"List releases in an organization with adoption and crash-free metrics.\n\n" +
89-
"Health data (adoption %, crash-free session rate) is shown per-release\n" +
90-
"from the first project that has session data.\n\n" +
91-
"Target specification:\n" +
92-
" sentry release list # auto-detect from DSN or config\n" +
93-
" sentry release list <org>/ # list all releases in org (paginated)\n" +
94-
" sentry release list <org>/<proj> # list releases in org (project context)\n" +
95-
" sentry release list <org> # list releases in org\n\n" +
96-
"Pagination:\n" +
97-
" sentry release list <org>/ -c next # fetch next page\n" +
98-
" sentry release list <org>/ -c prev # fetch previous page\n\n" +
99-
"Examples:\n" +
100-
" sentry release list # auto-detect or list all\n" +
101-
" sentry release list my-org/ # list releases in my-org (paginated)\n" +
102-
" sentry release list --limit 10\n" +
103-
" sentry release list --json\n\n" +
104-
"Alias: `sentry releases` → `sentry release list`",
148+
return parts.join("");
149+
}
150+
151+
type ListFlags = {
152+
readonly limit: number;
153+
readonly sort: ReleaseSortValue;
154+
readonly json: boolean;
155+
readonly cursor?: string;
156+
readonly fresh: boolean;
157+
readonly fields?: string[];
105158
};
106159

107-
export const listCommand = buildOrgListCommand(
108-
releaseListConfig,
109-
docs,
110-
"release"
111-
);
160+
export const listCommand = buildListCommand("release", {
161+
docs: {
162+
brief: "List releases with adoption and health metrics",
163+
fullDescription:
164+
"List releases in an organization with adoption and crash-free metrics.\n\n" +
165+
"Health data (adoption %, crash-free session rate) is shown per-release\n" +
166+
"from the first project that has session data.\n\n" +
167+
"Sort options:\n" +
168+
" date # by creation date (default)\n" +
169+
" sessions # by total sessions\n" +
170+
" users # by total users\n" +
171+
" crash_free_sessions # by crash-free session rate\n" +
172+
" crash_free_users # by crash-free user rate\n\n" +
173+
"Target specification:\n" +
174+
" sentry release list # auto-detect from DSN or config\n" +
175+
" sentry release list <org>/ # list all releases in org (paginated)\n" +
176+
" sentry release list <org>/<proj> # list releases in org (project context)\n" +
177+
" sentry release list <org> # list releases in org\n\n" +
178+
"Pagination:\n" +
179+
" sentry release list <org>/ -c next # fetch next page\n" +
180+
" sentry release list <org>/ -c prev # fetch previous page\n\n" +
181+
"Examples:\n" +
182+
" sentry release list # auto-detect or list all\n" +
183+
" sentry release list my-org/ # list releases in my-org (paginated)\n" +
184+
" sentry release list --sort crash_free_sessions\n" +
185+
" sentry release list --limit 10\n" +
186+
" sentry release list --json\n\n" +
187+
"Alias: `sentry releases` → `sentry release list`",
188+
},
189+
output: {
190+
human: formatListHuman,
191+
jsonTransform: (result: ListResult<ReleaseWithOrg>, fields?: string[]) =>
192+
jsonTransformListResult(result, fields),
193+
},
194+
parameters: {
195+
positional: LIST_TARGET_POSITIONAL,
196+
flags: {
197+
limit: buildListLimitFlag("releases"),
198+
sort: {
199+
kind: "parsed" as const,
200+
parse: parseSortFlag,
201+
brief:
202+
"Sort order: date, sessions, users, crash_free_sessions, crash_free_users",
203+
default: DEFAULT_SORT,
204+
},
205+
},
206+
aliases: { ...LIST_BASE_ALIASES, s: "sort" },
207+
},
208+
async *func(this: SentryContext, flags: ListFlags, target?: string) {
209+
const { cwd } = this;
210+
const parsed = parseOrgProjectArg(target);
211+
const config = buildReleaseListConfig(flags.sort);
212+
const result = await dispatchOrgScopedList({
213+
config,
214+
cwd,
215+
flags,
216+
parsed,
217+
orgSlugMatchBehavior: "redirect",
218+
});
219+
yield new CommandOutput(result);
220+
const hint = result.items.length > 0 ? result.hint : undefined;
221+
return { hint };
222+
},
223+
});

0 commit comments

Comments
 (0)