feat(issue): add resolve, unresolve (reopen), and merge commands#778
feat(issue): add resolve, unresolve (reopen), and merge commands#778
Conversation
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. New Features ✨
Bug Fixes 🐛
Internal Changes 🔧
🤖 This preview updates automatically when you update the PR. |
|
Codecov Results 📊✅ 138 passed | Total: 138 | Pass Rate: 100% | Execution Time: 0ms 📊 Comparison with Base Branch
✨ No test changes detected All tests are passing successfully. ✅ Patch coverage is 97.19%. Project has 1716 uncovered lines. Files with missing lines (2)
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 |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ 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.
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.
3d5823b to
2424d2c
Compare

Summary
Three new issue commands, so users don't reach for
sentry apifor common triage ops:sentry issue resolve <issue> [--in <spec>]sentry issue unresolve <issue>(alias:reopen)sentry issue merge <issue> <issue>...--ingrammar<version>pkg@1.2.3) works — no special parsing@next@commit@commit:<repo>@<sha>Any unrecognized
@-prefixed token is rejected with a clear error instead of silently falling through toinRelease— 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 releaseRepo cache
New
repo_cacheSQLite table (schema v14) +listRepositoriesCachedAPI helper. First@commitcall per org populates it by walking every page oflistRepositoriesPaginated; 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
--intoaccepts all positional formats--intouses 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 typescli-k9against canonicalCLI-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
@commit. Every failure mode — not in a git repo, no HEAD, no origin, repo not registered — raises aValidationErrorwith a concrete remediation hint. Per user directive: a half-correct resolution is worse than a clear error.@netx,@commmit,@releaseall reject with a clear message instead of creating a release named@netx.--intofallback only swallows clean not-found (ResolutionError/ApiError 404). Auth errors, 5xx, network failures all propagate so the user sees a proper diagnostic.@commit:is the unambiguous anchor. Distinguishes commit specs from release strings likespotlight@1.2.3(which lack the prefix and are treated asinRelease).Review rounds
6 rounds of Bugbot + Seer findings addressed:
{commit, repository}not bare SHAnull as T+ null inupdateIssueStatus(low + medium)--into+ override warning + URL in output (medium × 3)Final self-review via subagent surfaced 3 more medium findings — all fixed in this PR (not deferred):
catch {}inorderForMergefallback masked auth/5xx errors — now only clean not-found is swallowed@Next,@NEXT,@Commitall normalize; unknown@-prefixed tokens rejectPlus two polish fixes and a happy-path test the reviewer noted missing.
All 22 CI checks passing, 0 unresolved review comments.
Tests
@-typo rejection, monorepo-release disambiguation, scoped repo names), mergeIssues (including 204 handling), pagination walking in listAllRepositories--invariant,--intohandling (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,reopenaliasAll 5157 unit tests pass.