Skip to content

ref(cli): Type-check command yields against their output config - #1854

Merged
BYK merged 2 commits into
mainfrom
byk/ref/cli-type-check-command-output
Nov 3, 2026
Merged

BYK merged 2 commits into
mainfrom
byk/ref/cli-type-check-command-output

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1852, the follow-up from #1830.

buildCommand took output?: OutputConfig<any> and typed func as yielding unknown, so nothing tied what a command yields to what its human formatter and jsonTransform expect. #1830's derived types only helped where the yield or the formatter was annotated.

buildCommand, buildListCommand, and buildDeleteCommand now take an OUTPUT type parameter, inferred from output. func must yield CommandYield<OUTPUT> (CommandOutput<OUTPUT> | ClearScreen). func's side is wrapped in NoInfer, so a mismatched yield is an error instead of widening OUTPUT. Commands with no output config get OUTPUT = never, so a CommandOutput yielded there (which the wrapper would silently drop) is now an error too. The list and delete builders reuse the exported SentryCommandFunction instead of their own copies.

Since T is now real instead of any, jsonExclude is checked as well. It needed JsonExcludeKey<T>, which handles the two shapes applyJsonExclude already supports: array data (trial list strips displayName from each item) and union data (help, project create).

trial start's handlePlanTrial yielded unknown. It now yields CommandYield<TrialStartOutput>, using the type its formatter already took.

output.schema is checked too, when there's no jsonTransform

The builders also infer the schema's type. Without jsonTransform, the data (each item, for array data) must satisfy InferOutput<typeof schema>. So a field the schema documents but the command doesn't produce, or documents with another type, is a compile error. Extra fields on the data are fine, which covers jsonExclude'd human-only fields.

With jsonTransform, the schema describes the transform's item (what --fields works on), not T, and every transform returns unknown, so there is nothing to check against. These commands stay unchecked; #1860 tracks covering them.

The check found these mismatches:

  • alert issues edit / alert metrics edit: the API helpers returned Record<string, unknown>, so they couldn't satisfy schemas built from @sentry/api/valibot. They now return UpdateOrganizationWorkflowResponse / UpdateOrganizationDetectorResponse. That showed both endpoints return enabled, not status. Because of this, alert metrics edit always reported status: "active", even for a disabled detector. Both commands now derive status from enabled. A regression test is added.
  • feedback resolve / spam / unresolve: these yielded SentryIssue against SentryFeedbackSchema. A shared updateFeedbackStatus now returns SentryFeedback.
  • release create: the schema now documents projects as either full release projects or the --dry-run { slug } objects.
  • trial list: the schema was the raw API ProductTrialSchema, which documented reasonCode (never emitted) and missed name, status, daysRemaining. TrialListEntrySchema now describes what the command emits.

The generated skill references (release.md, trial.md) are regenerated.

Yield annotations

The 69 new CommandOutput<X>(…) annotations the builders made redundant are removed. Four remain in react-native/xcode.ts and schema.ts: they build discriminated-union literals (kind/mode), and without the annotation the discriminant widens to string and the check fails. packages/cli/AGENTS.md now says when an annotation is still needed.

Checks

I built throwaway commands for each kind of drift (missing field, wrong field type, list and delete builders, a yield with no output config, a bad jsonExclude key, a disagreeing jsonTransform, a factory renderer, and a schema field the data lacks), and each one failed to compile. The repo has no type-test harness (tsc only covers src/), so these were not added as permanent tests.

via @BYK.

--

View Session in Sentry

buildCommand, buildListCommand and buildDeleteCommand now infer the
output type from `output` and require every CommandOutput that `func`
yields to carry it. `NoInfer` keeps the yields from widening the type,
so an untyped `yield new CommandOutput(data)` that doesn't match the
formatter is a compile error. Commands without an output config may
only yield ClearScreen.

Fixes #1852

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
@vercel

vercel Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
sentry-local Skipped Skipped Nov 3, 2026 4:48pm UTC

Request Review

@github-actions github-actions Bot added the risk: medium PR risk score: medium label Nov 3, 2026

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not covered: output.schema is still GenericSchema, not tied to T. I tried requiring GenericSchema<unknown, T | item of T> and got 36 errors: list commands whose schema describes the item while T is the ListResult envelope, and generated @sentry/api/valibot schemas whose optionality differs from the @sentry/api types. Tying the schema to T needs its own decision about the envelope and exclude cases.

Well, what's the decision we need to make here or how can we cover this?

The existing yield new CommandOutput(…) annotations aren't needed anymore, but I left them in place.

Why, isn't this just noise?

Without jsonTransform, the yielded data (each item for array data) must
now satisfy output.schema, so a field the schema documents but the
command doesn't produce, or documents with another type, is a compile
error. With jsonTransform the schema describes the transform's item,
which the types can't see, so it stays unchecked.

The check found these mismatches, now fixed:

- alert issues/metrics edit: type the update responses with their
  @sentry/api types. The endpoints return `enabled`, not `status`, so
  `alert metrics edit` always reported "active"; derive status from
  `enabled` in both commands.
- feedback resolve/spam/unresolve: return SentryFeedback from a shared
  updateFeedbackStatus helper.
- release create: document `projects` as either full projects or the
  dry run's `{ slug }` objects.
- trial list: document the fields the command emits (name, status,
  daysRemaining) instead of the raw API trial, which listed reasonCode.

Also drop the CommandOutput<T> yield annotations the builders made
redundant, keeping the ones that stop discriminant literals widening to
string.

Co-Authored-By: BYK <burak.kaya@sentry.io>
@BYK
BYK enabled auto-merge (squash) November 3, 2026 16:55
@BYK
BYK merged commit 47dcdf1 into main Nov 3, 2026
33 checks passed
@BYK
BYK deleted the byk/ref/cli-type-check-command-output branch November 3, 2026 17:03
BYK added a commit that referenced this pull request Nov 3, 2026
Fixes #1860, the follow-up from #1854.

#1854 checked `output.schema` against the yielded data, but only for
commands without `jsonTransform`. With a transform, the schema describes
the transform's item rather than `T`, and every transform returned
`unknown`, so there was nothing to check against.

The builders now infer the transform's return type. The item is each
element of a `{ data: [...] }` envelope or an array, or else the result
itself. Every field the schema documents must exist on that item with a
matching type. Optionality isn't compared, since `--fields` makes items
`Partial`. A transform that returns `unknown` no longer matches a
schema. On a mismatch, the error lists only the mismatched fields under
`~outputMismatch` (e.g. `{ "~outputMismatch": { b: number } }`), not the
whole schema.

To make the results typed:

- `jsonTransformListResult` returns `ListJson<T>` (`ListEnvelope<T> |
Partial<T>[]`).
- The hand-built list envelopes (agent-conversation, dashboard,
dashboard revisions, events, replay, span, trace) are typed as
`ListEnvelope<Item>`.
- `log list` returns `ListEnvelope<LogLike> | Partial<LogLike>`.
- The single-object views (event, issue, feedback, project, replay) drop
their `Record<string, unknown>` annotations and let the spread's type
through, using `filterFieldsIfSet`.
- `OrgListConfig` takes the schema's type as a third parameter and
checks the entity against it, so team, repo, monitor, and integration
list are covered. `buildOrgListCommand` itself only sees a generic
`TWithOrg` and can't check anything, so it erases the schema type there.
- `issue list` and `project list` used `OutputConfig<T>` as an
annotation or with `satisfies`. That erased the schema's type, so the
check passed without comparing anything. Both are now inferred.

The check found these mismatches:

- **`span list`**: the schema documented the API's field names (`id`,
`span.op`, `span.duration`, `timestamp`, `project`, `trace`). The
command emits the names `spanListItemToFlatSpan` gives them (`span_id`,
`op`, `duration_ms`, `start_timestamp`, `project_slug`, `trace_id`). As
a result, `--help`, `--fields` completion, and the skill reference
listed fields that never appear in the output. A new `FlatSpanSchema`
documents the emitted fields, and `FlatSpan` is now derived from it. A
regression test checks that every key in the JSON output is a documented
field. It fails against the old schema.
- **`IssueEvent`**: the hand-written type said `metadata` could be
`null`. The schema, built from `@sentry/api`, says it can't.
`IssueEvent` is now the `GroupEventsResponseDict` item type.
- **`ReplayListItem` / `ReplayDetails`**: these typed `project_id` as
`string | number`, but `lib/api/replays.ts` normalizes it to a string.
`normalizeReplayProjectId` now returns that type.

The regenerated `span.md` skill reference is included.
`packages/cli/AGENTS.md` describes the check and how to type a
transform.

Transforms without a schema (`api`, `issue merge`, `trace view`, `span
view`, and others) are unchanged.

To confirm the check catches drift, I built throwaway commands that each
break one way:

- a transform's envelope item is missing a field;
- an envelope item has a field of the wrong type;
- a transform returns `unknown`;
- a single-object transform is missing a field;
- an array transform is missing a field;
- an `OrgListConfig` entity is missing a field.

All six failed to compile. The matching versions, and a transform
without a schema, compiled. These weren't kept as tests, because `tsc`
only covers `src/`.

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

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

--

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

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

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

This branch was previously deployed

1 inactive deployment
Preview — da08ab6b Deployed Nov 3, 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.

Type-check command output against OutputConfig in buildCommand

1 participant