Skip to content

Commit 1dd29a1

Browse files
committed
fix: reject @-selectors in parseOrgProjectArg with helpful redirect
Fixes CLI-MH: 'Project @latest not found' when running 'sentry issues @latest'. The @latest and @most_frequent magic selectors are only valid for parseIssueArg (issue view/explain/plan), not for parseOrgProjectArg (list commands). Previously, @ was not in RESOURCE_ID_FORBIDDEN so '@latest' passed validation and entered project-search mode, leading to a confusing ResolutionError. Add rejectAtSelector() guard in: - parseOrgProjectArg() bare-slug branch - parseSlashOrgProject() both project slug branches (/@latest, org/@latest) Known selectors get a redirect: 'Use: sentry issue view @latest' Unknown @-prefixed values get: 'starts with @, slugs use letters/numbers/hyphens'
1 parent 45cdd06 commit 1dd29a1

File tree

2 files changed

+83
-0
lines changed

2 files changed

+83
-0
lines changed

src/lib/arg-parsing.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,38 @@ function issueArgFromUrl(parsed: ParsedSentryUrl): ParsedIssueArg | null {
418418
};
419419
}
420420

421+
/**
422+
* Reject `@`-prefixed values in org/project positions.
423+
*
424+
* `@latest` and `@most_frequent` are issue selectors supported by
425+
* `parseIssueArg()` (for `issue view`, `explain`, `plan`). They are not
426+
* valid project slugs. This guard provides a helpful redirect instead of
427+
* the confusing "Project '@latest' not found" resolution error.
428+
*
429+
* Unknown `@`-prefixed values are also rejected — `@` is never valid in
430+
* Sentry slugs.
431+
*/
432+
function rejectAtSelector(value: string, label: string): void {
433+
if (!value.startsWith("@")) {
434+
return;
435+
}
436+
437+
const selector = parseSelector(value);
438+
if (selector) {
439+
throw new ValidationError(
440+
`'${value}' is an issue selector, not a ${label}.\n` +
441+
` Use: sentry issue view ${value}`,
442+
label
443+
);
444+
}
445+
446+
throw new ValidationError(
447+
`Invalid ${label}: '${value}' starts with '@'.\n` +
448+
" Slugs contain only letters, numbers, hyphens, and underscores.",
449+
label
450+
);
451+
}
452+
421453
/**
422454
* Parse a slash-delimited `org/project` string into a {@link ParsedOrgProject}.
423455
* Applies {@link normalizeSlug} to both components and validates against
@@ -435,6 +467,7 @@ function parseSlashOrgProject(input: string): ParsedOrgProject {
435467
'Invalid format: "/" requires a project slug (e.g., "/cli")'
436468
);
437469
}
470+
rejectAtSelector(rawProject, "project slug");
438471
validateResourceId(rawProject, "project slug");
439472
const np = normalizeSlug(rawProject);
440473
return {
@@ -457,6 +490,7 @@ function parseSlashOrgProject(input: string): ParsedOrgProject {
457490
}
458491

459492
// "sentry/cli" → explicit org and project
493+
rejectAtSelector(rawProject, "project slug");
460494
validateResourceId(rawProject, "project slug");
461495
const np = normalizeSlug(rawProject);
462496
const normalized = no.normalized || np.normalized;
@@ -508,6 +542,7 @@ export function parseOrgProjectArg(arg: string | undefined): ParsedOrgProject {
508542
parsed = parseSlashOrgProject(trimmed);
509543
} else {
510544
// No slash → search for project across all orgs
545+
rejectAtSelector(trimmed, "project slug");
511546
validateResourceId(trimmed, "project slug");
512547
const np = normalizeSlug(trimmed);
513548
parsed = {

test/lib/arg-parsing.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,54 @@ describe("parseOrgProjectArg", () => {
251251
expect(stderrOutput).not.toContain("Normalized slug");
252252
});
253253
});
254+
255+
describe("@-selector rejection", () => {
256+
test("@latest throws with redirect to issue view", () => {
257+
expect(() => parseOrgProjectArg("@latest")).toThrow(
258+
"is an issue selector, not a project slug"
259+
);
260+
expect(() => parseOrgProjectArg("@latest")).toThrow(
261+
"sentry issue view @latest"
262+
);
263+
});
264+
265+
test("@most_frequent throws with redirect to issue view", () => {
266+
expect(() => parseOrgProjectArg("@most_frequent")).toThrow(
267+
"is an issue selector, not a project slug"
268+
);
269+
expect(() => parseOrgProjectArg("@most_frequent")).toThrow(
270+
"sentry issue view @most_frequent"
271+
);
272+
});
273+
274+
test("case-insensitive selector variants are rejected", () => {
275+
expect(() => parseOrgProjectArg("@Latest")).toThrow(
276+
"is an issue selector"
277+
);
278+
expect(() => parseOrgProjectArg("@LATEST")).toThrow(
279+
"is an issue selector"
280+
);
281+
expect(() => parseOrgProjectArg("@mostFrequent")).toThrow(
282+
"is an issue selector"
283+
);
284+
});
285+
286+
test("unknown @-prefixed value throws as invalid slug", () => {
287+
expect(() => parseOrgProjectArg("@unknown")).toThrow("starts with '@'");
288+
});
289+
290+
test("/@latest (leading slash) throws with redirect", () => {
291+
expect(() => parseOrgProjectArg("/@latest")).toThrow(
292+
"is an issue selector"
293+
);
294+
});
295+
296+
test("sentry/@latest (org/selector) throws with redirect", () => {
297+
expect(() => parseOrgProjectArg("sentry/@latest")).toThrow(
298+
"is an issue selector"
299+
);
300+
});
301+
});
254302
});
255303

256304
describe("parseIssueArg", () => {

0 commit comments

Comments
 (0)