Skip to content

feat(issue): add resolve, unresolve (reopen), and merge commands#778

Merged
BYK merged 10 commits intomainfrom
byk/feat-issue-resolve-merge
Apr 19, 2026
Merged

feat(issue): add resolve, unresolve (reopen), and merge commands#778
BYK merged 10 commits intomainfrom
byk/feat-issue-resolve-merge

Conversation

@BYK
Copy link
Copy Markdown
Member

@BYK BYK commented Apr 19, 2026

Summary

Three new issue commands, so users don't reach for sentry api for common triage ops:

Command Purpose
sentry issue resolve <issue> [--in <spec>] Mark resolved, with optional release / next-release / commit tracking
sentry issue unresolve <issue> (alias: reopen) Reopen a resolved issue
sentry issue merge <issue> <issue>... Consolidate fragmented issues into one group

--in grammar

Form Meaning
omitted Resolve immediately, no regression tracking
<version> Resolve in release. Monorepo-style (pkg@1.2.3) works — no special parsing
@next Resolve in next release (tied to HEAD). Case-insensitive.
@commit Auto-detect: HEAD SHA + origin remote → match against org's Sentry-registered repos (cached 7 days). Hard-errors if any step fails.
@commit:<repo>@<sha> Explicit: skip git, use provided repo + SHA. Repo must still be registered in Sentry.

Any unrecognized @-prefixed token is rejected with a clear error instead of silently falling through to inRelease — guards against typos like --in @netx.

sentry issue resolve CLI-12Z --in 0.26.1
sentry issue resolve CLI-196 --in @next
sentry issue resolve CLI-XX --in @commit
sentry issue resolve CLI-XX --in @commit:getsentry/cli@abc123
sentry issue resolve CLI-XX --in spotlight@1.2.3    # monorepo release

Repo cache

New repo_cache SQLite table (schema v14) + listRepositoriesCached API helper. First @commit call per org populates it by walking every page of listRepositoriesPaginated; subsequent calls hit the 7-day offline cache. Cache write is guarded with try/catch so a read-only DB doesn't crash the command after a successful API fetch (project's established resilience pattern).

Merge --into accepts all positional formats

--into uses the same resolution pipeline as positional args: bare short ID, numeric group ID, org-qualified short ID (my-org/CLI-K9), or project-alias suffix (f-g, fr-a3). Direct-match fast path is case-insensitive, avoiding an extra API call when the user types cli-k9 against canonical CLI-K9.

Duplicate IDs across forms (CLI-A + my-org/CLI-A + 100, all resolving to the same group) are caught client-side with a clear error instead of silently hitting the API.

Design commitments

  • No silent fallbacks for @commit. Every failure mode — not in a git repo, no HEAD, no origin, repo not registered — raises a ValidationError with a concrete remediation hint. Per user directive: a half-correct resolution is worse than a clear error.
  • No silent sentinel typos. @netx, @commmit, @release all reject with a clear message instead of creating a release named @netx.
  • Errors propagate. --into fallback only swallows clean not-found (ResolutionError / ApiError 404). Auth errors, 5xx, network failures all propagate so the user sees a proper diagnostic.
  • No confirmation prompts. Matches the web UI bulk actions.
  • @commit: is the unambiguous anchor. Distinguishes commit specs from release strings like spotlight@1.2.3 (which lack the prefix and are treated as inRelease).

Review rounds

6 rounds of Bugbot + Seer findings addressed:

  1. inCommit shape bug (high) — required {commit, repository} not bare SHA
  2. Doubled command prefix + 204 crash (medium × 2)
  3. Unsafe null as T + null in updateIssueStatus (low + medium)
  4. Org-qualified --into + override warning + URL in output (medium × 3)
  5. Incomplete single-page repo cache + missing try/catch + orphaned JSDoc (medium × 2, low)
  6. Cross-org undefined-org bypass + fragile fallthrough in describeSpec (low × 2)

Final self-review via subagent surfaced 3 more medium findings — all fixed in this PR (not deferred):

  1. Broad catch {} in orderForMerge fallback masked auth/5xx errors — now only clean not-found is swallowed
  2. Case-sensitive sentinel matching with silent fallthrough@Next, @NEXT, @Commit all normalize; unknown @-prefixed tokens reject
  3. Duplicate issue IDs accepted — dedupe after resolution with targeted error

Plus two polish fixes and a happy-path test the reviewer noted missing.

All 22 CI checks passing, 0 unresolved review comments.

Tests

  • 19 API tests: parseResolveSpec grammar (case-insensitive sentinels, @-typo rejection, monorepo-release disambiguation, scoped repo names), mergeIssues (including 204 handling), pagination walking in listAllRepositories
  • 7 resolveCommitSpec tests: every failure path + happy-path (work-tree check → HEAD read → parseRemoteUrl → repo match)
  • 9 repo-cache tests: hit / miss / stale / corruption / multi-org isolation
  • 3 listRepositoriesCached tests: including the resilience path (read-only DB crash guard)
  • 16 command-level tests: every --in variant, --into handling (direct, alias, org-qualified, case-insensitive fast path, warning emission, undefined-org rejection, duplicate rejection, error propagation for auth/5xx, clean not-found swallowing), cross-org rejection, JSON output shape, reopen alias

All 5157 unit tests pass.

@linear-code
Copy link
Copy Markdown

linear-code bot commented Apr 19, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 19, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

  • (issue) Add resolve, unresolve (reopen), and merge commands by BYK in #778

Bug Fixes 🐛

  • (error-reporting) Fall back to message prefix for ValidationError without field by BYK in #776
  • (hex-id) Auto-recover malformed hex IDs in view commands (CLI-16G) by BYK in #777

Internal Changes 🔧

  • Regenerate docs by github-actions[bot] in 58a84035

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 19, 2026

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://cli.sentry.dev/_preview/pr-778/

Built to branch gh-pages at 2026-04-19 18:53 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 19, 2026

Codecov Results 📊

138 passed | Total: 138 | Pass Rate: 100% | Execution Time: 0ms

📊 Comparison with Base Branch

Metric Change
Total Tests
Passed Tests
Failed Tests
Skipped Tests

✨ No test changes detected

All tests are passing successfully.

✅ Patch coverage is 97.19%. Project has 1716 uncovered lines.
✅ Project coverage is 95.5%. Comparing base (base) to head (head).

Files with missing lines (2)
File Patch % Lines
src/lib/api/issues.ts 87.23% ⚠️ 12 Missing
src/lib/api/repositories.ts 87.18% ⚠️ 5 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
+ Coverage    95.50%    95.50%        —%
==========================================
  Files          257       262        +5
  Lines        37569     38170      +601
  Branches         0         0         —
==========================================
+ Hits         35881     36454      +573
- Misses        1688      1716       +28
- Partials         0         0         —

Generated by Codecov Action

Comment thread src/lib/api/issues.ts
Comment thread src/commands/issue/merge.ts Outdated
Comment thread src/commands/issue/merge.ts Outdated
Comment thread src/commands/issue/merge.ts Outdated
Comment thread src/lib/api/issues.ts Outdated
Comment thread src/commands/issue/resolve.ts Outdated
Comment thread src/lib/api/issues.ts
Comment thread src/lib/api/infrastructure.ts
Comment thread src/commands/issue/unresolve.ts
Comment thread src/lib/api/repositories.ts Outdated
Comment thread src/lib/api/repositories.ts Outdated
Comment thread src/lib/api/repositories.ts Outdated
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b842b93. Configure here.

Comment thread src/commands/issue/merge.ts Outdated
Comment thread src/commands/issue/resolve.ts
BYK added 10 commits April 19, 2026 18:52
Three new commands on `sentry issue` so users don't need to reach for
`sentry api` for common triage operations. Matches the Sentry web UI's
Resolve dropdown and bulk-merge action.

  sentry issue resolve <issue> [--in <spec>]

  --in / -i accepts:
    <version>           Resolve in a specific release (e.g. 0.26.1)
    @next               Resolve in the next release (tied to HEAD)
    commit:<sha>        Resolve tied to a commit SHA
    (omitted)           Resolve immediately

  sentry issue unresolve <issue>    # or: sentry issue reopen <issue>

  sentry issue merge <issue> <issue> [...] [--into <issue>]

    Variadic (2+ required). All issues must share an org. --into pins
    the canonical parent — otherwise Sentry auto-picks by event count.

Implementation:

- Extended updateIssueStatus(issueId, status, options) with an optional
  ResolveStatusDetails and orgSlug (for region-aware routing).
- Added mergeIssues(orgSlug, groupIds) that hits the bulk mutate
  endpoint with repeated ?id=... params and {merge: 1} body.
- Added parseResolveSpec() that translates --in strings into the
  ResolveStatusDetails shape. Clean grammar, no escape sequences.
- Commands use the existing resolveIssue() helper, so they accept
  every issue-ID format that `issue view` accepts (short ID, numeric,
  @latest/@most_frequent, org/SHORT-ID, suffix, etc).

UX notes:

- No confirmation prompts — these operations mirror UI bulk actions
  where the web UI also doesn't prompt. Merge and resolve are both
  reversible via the corresponding inverse command.
- `reopen` is a friendlier synonym for `unresolve`, wired in as a
  route alias.
- All three commands support --json with a shape that carries the
  operation metadata (resolved_in, parent/children short IDs, etc).

Tests: 13 new API-layer tests + 14 new command-level tests covering
every branch of --in, --into, cross-org rejection, validation errors,
and JSON output shape.
Bugbot caught (high severity) that the commit: path was non-functional:
Sentry's inCommit resolution requires an object of {commit, repository}
not just a SHA string. My impl sent {inCommit: sha} which the API
rejects.

Rather than expose a more complex grammar like 'commit:<sha>@<repo>',
drop the feature — it's rarely used vs @next, and callers who really
need it can still use 'sentry api' directly.

Removes RESOLVE_COMMIT_PREFIX and associated tests. Keeps the ability
to add it back cleanly later if demand surfaces.
Two bot findings addressed:

1. Bugbot flagged that merge output showed 'See: 100' (bare numeric
   group ID) instead of a clickable URL. Fixed by using buildIssueUrl
   from sentry-urls.ts. JSON output also now includes parent.url.

2. Seer flagged that --into is only a preference — the Sentry API
   picks the parent by event count regardless of input order, so the
   user's choice can be silently overridden. Fixed by:
   - Documenting --into as a preference, not a guarantee (help text
     and docstring)
   - Printing a stderr warning when Sentry picks a different parent
     than the one requested via --into
   - Adding two tests (warning emitted / not emitted when honored)
Bugbot caught that --into compared user input literally against
issue.shortId — so 'my-org/CLI-K9' wouldn't match 'CLI-K9' from the
API response, incorrectly throwing 'did not match any of the provided
issues' even when the issue was passed as a positional arg.

Fix: strip the prefix before the last '/' when present, accepting all
three forms users naturally type:
  --into CLI-K9           (bare short ID)
  --into 100              (numeric group ID)
  --into my-org/CLI-K9    (org-qualified short ID)
Two valid findings from round 3:

1. Seer (medium): resolveIssue() builds error hints as
   '${base} ${command} ...' where base defaults to 'sentry issue'.
   I was passing command='issue resolve' and commandBase='issue',
   producing hints like 'issue issue resolve <id>'. Fixed by passing
   just the subcommand name ('resolve', 'unresolve', 'merge') and
   relying on the default commandBase.

2. Bugbot (medium): apiRequestToRegion unconditionally called
   response.json(), which throws SyntaxError on 204 No Content
   responses. Sentry's bulk-mutate endpoint returns 204 when no
   matching issues are found. Fixed at the infrastructure level —
   204/205 now return {data: null} so all callers benefit. mergeIssues
   translates the null response into a friendly ApiError.
Bugbot and Seer both flagged that the previous 204-handling was unsafe:
apiRequestToRegion<T> returned 'null as T' which bypassed TypeScript
and silently exposed callers (updateIssueStatus, getProject, etc.) to
NPEs if any endpoint ever returned 204 unexpectedly.

New approach: 204/205 throws ApiError(status=204, ...) at the
infrastructure level. The return type stays T (not T | null), so
every existing caller gets a clear error instead of a stealth null.
mergeIssues is the only caller that legitimately expects 204 (the
Sentry bulk-mutate endpoint returns it for no-match) — it catches
the ApiError and re-throws with a user-friendly message.
…xplicit form

Extends `sentry issue resolve --in` with commit-level resolution:

  --in @commit
      Auto-detect: read HEAD + origin from the current git repo, then
      match the origin owner/repo against the org's Sentry-registered
      repositories (cached offline, 7-day TTL).

  --in @commit:<repo>@<sha>
      Explicit: skip git, use the provided repo + SHA directly.
      Repo must still be registered in Sentry (API validator requires it).

Monorepo-style release strings like `spotlight@1.2.3` are unambiguously
treated as `inRelease` — the `@commit:` prefix is the mandatory anchor
that distinguishes commit specs from releases.

Every failure mode in `@commit` auto-detection raises a ValidationError
with an actionable message (not in a git repo, no HEAD, no origin, repo
not registered, etc.) — no silent fallback to `inRelease` or another
resolution mode. Per design: a half-correct resolution is worse than a
clear error the user can fix.

Infrastructure:
- New `repo_cache` SQLite table (schema v14) + `src/lib/db/repo-cache.ts`
  with getCachedRepos / setCachedRepos / clearCachedRepos.
- `listRepositoriesCached(org)` in `src/lib/api/repositories.ts` wraps
  `listRepositories` with the offline cache. First call per org per week
  populates the cache; subsequent calls avoid the round trip.
- New `resolveCommitSpec` in `src/commands/issue/resolve-commit-spec.ts`
  handles git inspection + repo matching with the strict-failure policy.
- `parseResolveSpec` now returns a `ParsedResolveSpec` discriminated
  union (`static` vs `commit`) so the command layer can distinguish
  statically-resolvable specs from those needing async resolution.

Also:
- `issue merge --into` now accepts project-alias suffixes (`f-g`,
  `fr-a3`, etc). Direct-match fast path stays in place; alias fallback
  runs the value through `resolveIssue` and matches by numeric ID.
- Docs fragments updated to show the new grammar and the hard-error
  behavior of @commit.

Tests: 25 new tests covering parseResolveSpec grammar (wrapping, @commit
variants, monorepo release disambiguation), resolveCommitSpec (each
failure path, externalSlug fallback, error-message quality), repo-cache
(hit / miss / stale / corruption / multi-org isolation), and the merge
alias fallback. All 5143 unit tests pass.
1. listRepositoriesCached cached an incomplete single-page list
   (medium). Added listAllRepositories() that walks every page via
   listRepositoriesPaginated + MAX_PAGINATION_PAGES safety cap.
   Cache now stores the complete repo set — @commit lookups find
   repos beyond page 1 for large orgs.

2. Cache write was not guarded with try/catch (medium). On a
   read-only DB (macOS 'sudo brew install' scenario), the command
   would crash despite the primary API fetch succeeding. Now wrapped
   and logged at debug level, following the established project
   pattern for non-essential cache writes.

3. Orphaned JSDoc displaced listRepositoriesPaginated's documentation
   (low). listRepositoriesCached was inserted between the JSDoc and
   the function declaration it described. Moved the new function
   below the paginated helper so the file reads cleanly.

Tests: 1 new pagination test + 3 new repositories.ts tests covering
the resilience path + happy path for listRepositoriesCached.
1. merge (low): resolveAllIssues filtered out undefined orgs before
   the cross-org check, so a mix of 'known org' + 'unknown org' issues
   would silently proceed to the known org's endpoint and fail with a
   confusing API error. Now we reject the request up-front with a
   ValidationError naming the issues whose org couldn't be determined,
   pointing the user at the <org>/<issue> form.

2. resolve (low): describeSpec handled ResolveStatusDetails variants
   via if-chains with an implicit fall-through for inNextRelease —
   adding a new union variant in the future would silently be labeled
   'in the next release'. Added explicit 'inNextRelease' check plus a
   _exhaustive: never sentinel that triggers a TypeScript error if a
   new variant is introduced without a matching branch.

Test added for finding 1 (undefined-org rejection path).
…typo guard

Final self-review (via subagent) surfaced three medium-severity gaps
that would degrade UX at the edges. All fixed inline rather than as
follow-up tickets since they're small and related:

1. orderForMerge --into fallback: broad 'catch {}' swallowed auth /
   5xx / network errors and masked them as the misleading 'did not
   match any of the provided issues'. Now only ResolutionError and
   ApiError(404) are treated as clean not-found; everything else
   propagates so the user sees a real diagnostic. Covered by two new
   tests (auth error + 5xx propagate; ResolutionError still swallowed).

2. parseResolveSpec: sentinel matches were case-sensitive with silent
   fallthrough, so '--in @Next' would quietly create a release literally
   named '@Next'. Made sentinel detection case-insensitive, and any
   unrecognized @-prefixed token now throws ValidationError with a
   clear 'expected @next / @commit / @commit:<repo>@<sha>' message.
   Payload after @commit: keeps original case since repos and SHAs are
   case-sensitive.

3. merge: duplicate issue IDs (e.g. 'CLI-A' + 'my-org/CLI-A' + '100',
   all resolving to the same group) were sent to the API as repeated
   ?id=100 params, which Sentry deduped server-side and returned 204
   → rethrown as a confusing 'no matching issues' error. Now caught
   client-side after resolution with a targeted ValidationError.

Polish (L3, L6):
- Stray literal 'merge' replaced with the COMMAND constant (avoids
  future drift if the command ever renames).
- orderForMerge fast-path now matches short IDs case-insensitively
  (user-typed 'cli-b' against API's 'CLI-B'), avoiding an unnecessary
  round-trip through the alias-resolution fallback.

Happy-path coverage:
- New resolveCommitSpec auto-detect success test exercises the full
  pipeline (work-tree check → HEAD read → parseRemoteUrl → repo match).
  Previous tests covered only failure modes.
@BYK BYK force-pushed the byk/feat-issue-resolve-merge branch from 3d5823b to 2424d2c Compare April 19, 2026 18:53
@BYK BYK merged commit 67ea8e9 into main Apr 19, 2026
27 checks passed
@BYK BYK deleted the byk/feat-issue-resolve-merge branch April 19, 2026 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant