Skip to content

ref(cli): Ratchet hand-parsed positionals and test argument conventions - #1739

Merged
BYK merged 1 commit into
mainfrom
byk/cli-argument-conventions
Oct 29, 2026
Merged

BYK merged 1 commit into
mainfrom
byk/cli-argument-conventions

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Part of #1564.

Every command that still parses a positional inside func is now named on a lint allow-list with its follow-up, so the rest can be migrated one by one and no new one can appear. Release commands, the small straggler, move to a bound positional. A new table-driven test checks that commands taking IDs read the same inputs the same way.

Lint

  • no-ad-hoc-id-arguments now also rejects parseIssueTarget and parseDashboardTarget in commands. These parsers only tag a positional for completion and leave the parsing to func.
  • require-target-resolution now also rejects parseOrgTarget, parseOrgProjectTarget, parseOrgProjectArg, and the resolve{OrgOnly,OrgOptional,ProjectBound}FromArg helpers in commands.
  • Both rules use allow-lists (AD_HOC_ID_PARSING_ALLOWED, UNBOUND_TARGET_PARSING_ALLOWED), and each entry names its follow-up. The lists only shrink: an allow-listed file that stops using the names gets "Remove this file from the allow-list".
  • event view is no longer exempt, since ref(cli): Parse event view arguments with the shared ID grammar #1733 merged.
  • ReleaseTarget was added to BOUND_TARGET_TYPES.
  • New rule tests are in scripts/cli-lint-rules.test.mjs.

Release positional

releaseParameter(USAGE_HINT, brief) in release/parse.ts returns a bound ReleaseTarget. func reads { version, org, detectedFrom } from await target.resolved. A blank version now fails the parse (exit 252) instead of failing inside func. Migrated commands: archive, create, delete, deploy, deploys, finalize, restore, set-commits, view.

Cross-command conventions test

test/lib/argument-conventions.test.ts runs the same inputs through each command's own positional with applyBoundPositionals, the way Stricli does. The inputs are:

  • a bare ID
  • <x>/<id>
  • <org>/<project>/<id>
  • <org>/<project> <id>
  • reversed arguments (expects a warning)
  • extra or multiple IDs
  • a missing ID (expects ArgumentParseError)
  • a pasted URL (its server must be applied only when .resolved runs)

Commands covered: trace view/logs, span view/list, log view/list, event view, replay view/download, the single-issue commands (issue and feedback), and all release commands. Where a command legitimately differs, its row says how:

  • slashTarget: "project" for span view, log view and event view
  • multiple for span view, log view and event view
  • optional for span list and log list
  • separateTarget: false for span view
  • no URL for log view

Audit: positional parsing in func

Excludes event view (#1733, merged) and dashboard/* (#1730).

Already on bound parsers (verified)

Allow-listed, still parsing in func

Not target parsing, and not covered by these rules

  • monitor run: args[0] is a monitor slug followed by a passthrough command. This is arity handling, not target parsing.
  • Paths, endpoints, versions, selectors and doc parts: api, auth login/switch, build/debug-files/snapshots upload, debug-files find (debug IDs), docs *, help, local *, schema, cli upgrade, org list, react-native xcode, wasm-split.

Checks

pnpm run tsc and pnpm run lint pass, and the lint-rule tests pass (70/70). The new conventions test and the release and issue tests pass. The only full-suite failures locally are sandbox noise: tests that need to run without root, e2e and init-eval tests that need a build or keys, and timing-sensitive tests.

via BYK.

--

View Junior Session in Sentry

Commands that still parse a positional inside `func` are now on an
allow-list in the lint plugin, each with its follow-up, so the remaining
ones are named and no new one can appear.

- `require-target-resolution` also rejects the completion-only target
  parsers (`parseOrgTarget`, `parseOrgProjectTarget`), `parseOrgProjectArg`,
  and the `resolve*FromArg` helpers in commands.
- `no-ad-hoc-id-arguments` also rejects the completion-only
  `parseIssueTarget` and `parseDashboardTarget`.
- Both allow-lists are ratchets: an entry whose file stopped parsing by
  hand fails until it is removed.

Release commands take `[<org>/]<version>` through `releaseParameter`, a
bound positional: a blank version fails in the argument scanner (exit
252), and `func` awaits `target.resolved` for the org.

`test/lib/argument-conventions.test.ts` feeds the same inputs through
every command that takes trace, span, log, replay, issue, or release
arguments, using the command's own positional, and encodes where a
command legitimately differs.

Part of #1564.

Co-Authored-By: BYK <burak.kaya@sentry.io>
@vercel

vercel Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
sentry-local Ready Ready Preview Oct 29, 2026 8:35pm UTC

Request Review

@BYK
BYK merged commit cfb568c into main Oct 29, 2026
35 checks passed
@BYK
BYK deleted the byk/cli-argument-conventions branch October 29, 2026 21:22
sentry-junior Bot added a commit that referenced this pull request Oct 29, 2026
…get-parser

Conflicts: packages/cli/AGENTS.md (kept both this branch's dashboard note and main's argument-conventions note; dropped parseDashboardTarget, which this branch removes, from main's list of completion-only parsers). Also removed the dashboard entries from the #1739 allow-lists in packages/cli/lint-rules/cli-oxlint-plugin.js, since these commands now parse before func.
Co-Authored-By: BYK <burak.kaya@sentry.io>
BYK added a commit that referenced this pull request Oct 29, 2026
…mar (#1730)

Part of #1564. Dashboard commands parsed their positionals by hand
inside `func`, and issue commands used their own grammar (#1729). This
moves both onto the shared ID grammar (`grammar/id-arguments.pegjs`,
#1714), parsed before `func` runs. Event view's issue checks (#1733,
#1738) use the new issue kind too.

**Shared grammar: kinds with names.** `IdKind.names` adds one idea to
`id-arguments.pegjs`: a kind may name an item without its ID, and
anything that is not, and may not be, an ID is a name. Kinds without
names keep their grammar paths exactly as before. Kinds with names keep
each line whole (no splitting at spaces), so `Top 10` stays one title
and never becomes title `Top` + ID `10`.
- `ID_KINDS.dashboard`: a numeric ID or a title, which may hold spaces
and slashes (`Latency/p95`). `<org>/[<project>/]<digits>` reads as a
path only when slugs come before the digits, so `Errors.v2/123` and
`/123` are titles. After a target, the title takes the rest of the
arguments (`my-org/ My Dashboard`). A bare first word names an org
(`orgScoped`).
- `ID_KINDS.issue`: an identifier (numeric ID, short ID, selector, issue
URL) is one whole token (`wholeIds`). `src/lib/issue-identifier.ts`
takes it apart with the #1729 grammar, which is unchanged. Text with
spaces is a name, meaning a search.
- `dashboard-arguments.pegjs` is gone. `dashboard list` keeps its thin
`parseDashboardListArgs`, because a bare word there is a title filter
(see #1711).

**Bare first word: org or title?** For `dashboard view My Dashboard`,
the parser makes no decision. It returns the structural reading (org
`My`, title `Dashboard`) and the whole input as one title
(`nameWithTarget`, surfaced as `orgOrTitle`). `resolveDashboardInput`
decides after auth:
- First it checks whether `My` is an org: the cached org list and region
cache, then `GET /organizations/My/` only on a cache miss.
- If the org exists, the title lookup in that org is final, even when it
misses.
- On a 404 or 403 it warns ``No org 'My'; treating "My Dashboard" as the
dashboard title. Use my-org/ to name an org.`` and looks up "My
Dashboard" in the auto-detected org.
- If that misses too, one error names both readings (no org `My` to find
`Dashboard` in, and no `My Dashboard` in `<detected-org>`) and hints at
`<org>/ Dashboard` and at quoting.
- A 5xx or network error from the org check propagates, with no
fallback.
- It never falls back for `my-org/ …` (explicit org), a numeric ref, or
a URL.

**Issue search text.** `issue view "login timeout"` resolves through
`GET /organizations/{org}/issues/?query=…` in the detected org. One hit
is the issue. Zero or several hits raise a `ResolutionError` that lists
up to 5 candidates as runnable commands and points at `sentry issue list
<org>/ --query '…'`. I checked the endpoint against getsentry/sentry
(`organization_group_index.py`, `group_index/index.py`): `query` takes
free text, an exact event ID or short ID gets a fast lookup, and an
explicit query replaces the default `is:unresolved`.

**Search hints.** Text where a trace, span, log, or replay ID belongs
(`trace view login`) now fails with ``Not a trace ID: "login". To
search, try `sentry trace list --query 'login'`.``. The hint comes from
`validate()`, so commands that recover malformed IDs (`trace view`,
`span view`, `log view`) recover first and use the hint only as their
final error. Event IDs get no hint, because `event list` lists one
issue's events.

**event view.** `ID_KINDS.issue.distinctForm` (uppercase short ID or
numeric) replaces `looksLikeIssueShortId` + `isAllDigits` and keeps
#1738's order: `<org>/<short-id>`, then `<short-id>/<event-id>` (even an
all-digit event ID), then `<org>/<numeric-id>`. I added a regression
test for the all-digit event ID and one for that order.

**Behavior changes:**
- `dashboard view my-org My Dashboard` (unquoted title) looks up "My
Dashboard". Before, it used `My` and dropped `Dashboard`. The same goes
for `my-org/ My Dashboard` and `https://acme.sentry.io/ My Dashboard`.
- `dashboard view My Dashboard`: when there is no org `My` (404/403), it
falls back to the title "My Dashboard" in the detected org, with the
warning above. If neither reading finds a dashboard, one error names
both. Dashboard commands with one dashboard ref get this (`view`,
`restore`, `revisions`, `widget edit/delete`, and `widget add` after it
splits off its widget title).
- A numeric ref first is the dashboard, as with every ID kind:
`dashboard view 42 X`, `my-org/42 X`. Before, the first of two arguments
was always the target. After a target, a numeric ID keeps the rest as
extra (`my-org 123 extra` → ID 123, ignored `extra`) instead of joining
them into a title. Arguments after the ID go through the shared rules:
extra, reversed (`42 my-org/`), or the shared "reference different
projects" error when they name another target.
- A lone non-Sentry URL is no longer taken as a dashboard title.
- `issue view "login timeout"` (text with spaces) searches instead of
failing as a malformed identifier. Control characters and URLs are still
rejected.
- Trace/span/log/replay IDs that are plain text get a search hint
instead of "Invalid trace ID".
- Parse errors (no dashboard, an org URL without a dashboard, a
dashboard URL passed to `list`, an invalid target slug) fail before the
auth guard, as in the earlier commits on this branch. They print `Failed
to parse "…" for <placeholder>: …` and exit 252.
- `widget add/edit/delete`: an argument error comes before flag errors
like "Specify --index or --title". `widget add`'s positional completes
cached dashboards. `parseDashboardTarget` had no callers left and is
removed.

**Lint ratchet (#1739):** the dashboard entries are off both
allow-lists, since these commands now parse before `func`. The issue
entries (`issue/utils.ts`, `issue/merge.ts`, which need an array issue
positional) stay.

**Syncing:** this branch merges `main` (no rebase). 54ebfa2 and 7f52f19
merge #1729, #1732–#1734, #1736, #1738 and #1739.

Verification:
- `tsc`, `pnpm run lint` (oxlint, format, ast-grep) and
`scripts/cli-lint-rules.test.mjs` (70/70) pass.
- vitest on `test/lib`, `test/grammar` and the dashboard, issue, event,
trace, span, log, replay and feedback command tests: 10,158 passed. The
6 failures are the sandbox-only permission tests (completions
EACCES/EPERM, code-scanner, file-changes, telemetry readonly, upgrade
lock), which also fail on `main`. The full CLI suite shows the same 6,
plus e2e/eval files that need a build or credentials.
- New tests:
- The bare-org fallback: the org from cache and from the API (no
fallback), org missing then fallback hit with the warning, a 403, org
missing then fallback miss with the combined error, a 5xx (propagates,
no fallback), `my-org/ My Dashboard`, and a numeric ref.
  - Issue search with 0, 1 and several hits.
  - Search hints, including `validate: false` deferral.
  - #1738's event view order.
- Differential runs against this PR's previous parsers:
- Issue arguments: 8,575 cases. 1,388 former errors are now searches
(text with spaces), with 0 other differences.
- Dashboard view: 32,552 cases over 0–3 arguments. Every difference is
in one of the categories above: a numeric ID first (2,600), arguments
after a dashboard URL that name another target (688), a numeric ID after
a target with more words (475), and a lone non-Sentry URL (1).

<!-- junior-request-attribution:start -->
via **BYK**.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3A5a5e7e4543124325a44d731c08dd6089 -->

--

[View Junior Session in
Sentry](https://sentry.sentry.io/explore/conversations/agent%3A5a5e7e4543124325a44d731c08dd6089/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <burak.kaya@sentry.io>
BYK pushed a commit that referenced this pull request Oct 29, 2026
…#1742)

Part of #1564. Follow-up to #1730 and #1739.

#1739 added `argument-conventions.test.ts`, which checks that commands
sharing a parser read the same input the same way. #1730 then moved
dashboards and issues onto the shared ID grammar, and dashboards and
issues weren't in that test yet. This adds them. It only changes
`packages/cli/test/lib/argument-conventions.test.ts`.

**Dashboard commands:** `view`, `restore`, `revisions`, and `widget
add/edit/delete` must all read every `DASHBOARD_INPUTS` entry the same
way `dashboard view` does:
- Inputs: a bare ID or title, `<org>/<id>`, `<org>/<project>/<id>`, a
separate target, `<target> <title words…>`, the bare-org fallback (`My
Dashboard`), reversed arguments, extra arguments, and dashboard and org
URLs.
- Usage-hint text in warnings is normalized before comparing, and
`widget add`'s own widget title is removed first. `widget add` only gets
inputs of up to 2 arguments, because its last argument is the widget
title.
- Per command: a missing dashboard fails at parse time, a pasted URL's
server is applied on resolve and not on parse, `widget add` splits off
its title, and `widget add` rejects more than 3 arguments.

**`dashboard list`:** its own block, because it reads a target plus a
title filter, not a dashboard. A dashboard URL fails with a suggestion
to use `dashboard view`.

**Issue commands:** `ISSUE_INPUTS` gains `login timeout`, so every issue
command must read search text the same way `issue resolve` does. A
separate test checks that it parses as `{ type: "search" }`.

No lint allow-list changes: #1730 already removed the dashboard entries.
The `issue/utils.ts` and `issue/merge.ts` entries are still in use and
get migrated separately.

Verification:
- `argument-conventions.test.ts`: 336 passed, 19 skipped.
- Dashboard, issue and event command suites, plus the id-arguments and
dashboard-arguments lib tests: 31 files pass.
- `pnpm run lint` in packages/cli is clean, `tsc --noEmit` is clean, and
`scripts/cli-lint-rules.test.mjs` passes 70/70.

<!-- junior-request-attribution:start -->
via **BYK**.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:local%3Aweb%3Aa3773d7739de1b873375a6f1 -->

--

[View Junior Session in
Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3Aa3773d7739de1b873375a6f1/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview — 988fa7c7 Deployed Oct 29, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant