Warn instead of failing when issue fields can't be set - #13899
Conversation
`gh issue create` creates the issue and then applies the deferred fields
(type, parent, blocked-by, blocking) in a second step. When one of those
mutations failed, createRun returned the error. The deferred PreserveInput
saw a non-nil error, wrote a recovery file and printed
X operation failed. To restore: gh issue create --recover <file>
and the issue URL was never printed. The issue did exist, so the message
was misleading and invited a duplicate submission. This is reproducible by
creating an issue on a repository where you cannot set the issue type: the
UpdateIssueIssueType mutation fails with a permissions error.
The issue already exists once api.IssueCreate returns, and
api.DeferredUpdateIssue already joins its failures so one failing mutation
does not abort the rest. Report the failure as a warning on stderr and
still print the issue URL.
Resolving the deferred fields is left alone: it runs before any mutation
and reports invalid input such as `type "Bugz" not found`.
Fixes cli#13804
d0891dd to
c82266c
Compare
BagToad
left a comment
There was a problem hiding this comment.
Thanks @imkp1. I agree with the initial direction, but I want a bit more.
On top of not failing completely when we get an err from a deferred update, I think we shouldn't be showing the type picker at all when a user doesn't have permission. Today, it's gated by whether the repo has types, but clearly that's not enough. We need to gate types showing up based on repo.ViewerCanTriage(), I think. Though a test is in order to ensure I'm correct and that's the right permission.
The interactive type picker is gated only on the repository having issue types. A viewer without triage access is offered the picker, picks a type, and UpdateIssueIssueType then fails after the issue already exists. Gate the picker on ViewerCanTriage. IssueRepoInfo already selects viewerPermission, so this costs no extra request, and it skips the RepositoryIssueTypes fetch for viewers who cannot use the result. ConfirmIssueSubmission already gates the metadata option the same way.
This comment was marked as outdated.
This comment was marked as outdated.
|
Gated the picker on On checking whether TRIAGE is the right permission: against cli/cli with One asymmetry for your call: |
This comment has been minimized.
This comment has been minimized.
|
Back in draft to clear the auto-close timer — not a status change, ready for another look whenever you have time. The one thing I still can't resolve is whether |
|
@imkp1 Triage permission is fine. Custom roles can be out of scope. The warning closes the gap enough IMO, leaving custom roles as an edge case. Apologies I haven't had a chance to re-review, but I'll reopen this and remove it from the auto-close so it remains in my PR review queue. |
This comment was marked as outdated.
This comment was marked as outdated.
There was a problem hiding this comment.
Pull request overview
This PR adjusts gh issue create to treat failures that occur while applying deferred issue fields (type/parent/blocking relationships) as warnings rather than fatal errors, so the command still prints the created issue URL and avoids triggering interactive recovery-file behavior when the issue was already created successfully.
Changes:
- Gate the interactive “Issue type” prompt behind
repo.ViewerCanTriage()to avoid offering a selection that will predictably fail for low-permission viewers. - Convert deferred field-application failures (
api.DeferredUpdateIssue) from a returned error into a stderr warning while continuing to print the issue URL to stdout. - Add table-driven tests covering the new interactive permission gating and the “created but deferred fields couldn’t be set” warning behavior.
Show a summary per file
| File | Description |
|---|---|
| pkg/cmd/issue/create/create.go | Avoid prompting for issue type without triage access; warn (don’t fail) when deferred field mutations fail after issue creation. |
| pkg/cmd/issue/create/create_test.go | Add tests for type prompt gating (READ vs TRIAGE) and for warning behavior when deferred type update is forbidden. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Low
| // The issue exists by now, so failing to apply the deferred fields is | ||
| // not a failure of the whole operation. Returning the error here would | ||
| // make PreserveInput write a recovery file and print "operation | ||
| // failed", implying the issue was never created. | ||
| if updateErr := api.DeferredUpdateIssue(apiClient, updateOpts); updateErr != nil { |
There was a problem hiding this comment.
Confirmed and fixed in 745dc68. Resolution now happens before the create mutation.
The ordering was as described: resolution ran at create.go:418, after IssueCreate at :412, and assigned to the named return, so an unknown --type or an unresolvable ref returned fatally with the issue already created — no URL, and PreserveInput writing a recovery file in interactive mode.
Split as suggested: resolveDeferredUpdateIssueOptions() takes only the client, repo and flags, runs before IssueCreate, and IssueID is attached to the resolved options afterwards. Only DeferredUpdateIssue itself stays downgraded to a warning.
Two tests cover it: the existing "type not found" case now excludes the IssueCreate mutation instead of stubbing it, and a new "unresolvable parent" case does the same for a bad ref.
Resolution of --type/--parent/--blocked-by/--blocking ran after api.IssueCreate, so a bad flag value failed fatally with the issue already created: no URL printed, and a recovery file written in interactive mode. Resolve into DeferredUpdateIssueOptions before the create mutation and attach the issue ID afterwards. Only the post-create mutations stay downgraded to a warning.
|
Thanks @BagToad — settled then, triage gate stays as-is and custom roles are out of scope. No further change for that thread; 8fbdb9b already has the gate plus READ/TRIAGE tests. Also pushed 745dc68 for the Copilot review: deferred field resolution now runs before IssueCreate, so a bad --type or unresolvable ref fails without creating an issue. Only the post-create mutations remain warnings. Ready for re-review. |
Refs resolve in a loop, so cover a later ref failing after an earlier one succeeded. Excludes the IssueCreate mutation to assert no issue is left behind.
Summary
gh issue createcreates the issue, then applies the deferred fields (type,parent, blocked-by, blocking) in a second step. When one of those mutations
fails,
createRunreturns the error. The deferredPreserveInputsees anon-nil error, writes a recovery file and prints:
The issue URL is never printed. But the issue does exist —
api.IssueCreatealready returned successfully. The message is misleading, and the
--recoverprompt invites a duplicate submission of an issue that was just created.
This is reproducible by creating an issue on a repository where you cannot set
the issue type: the
UpdateIssueIssueTypemutation fails with a permissionserror after the issue has been created.
This assigns the deferred-update failure to a separate
updateErrinstead ofthe named return
err, reports it as a warning on stderr, and still prints theissue URL:
Using a separate variable (rather than clearing
err) means the named return isprovably never set on this path, so
PreserveInputcannot fire and no recoveryfile is written.
The issue already exists once
api.IssueCreatereturns, andapi.DeferredUpdateIssuealready joins its failures so that a single failingmutation does not abort the rest — by the time it returns an error, every
mutation has been attempted.
Scope
Resolving the deferred fields is deliberately left alone.
deferredUpdateIssueOptionsruns before any mutation and reports invalid input such as
type "Bugz" not found;that remains a fatal error, and the existing
create with type not foundtest pinsthis behaviour.
Testing
create with type that cannot be set— added to theTest_createRuntable,stubbing
UpdateIssueIssueTypewith aFORBIDDENerror. Asserts the issue URLis printed to stdout, the warning is printed to stderr, and that
createRunreturns no error (which is what stops
PreserveInputfrom writing a recoveryfile). Verified to fail without the change and pass with it.
go test ./pkg/cmd/issue/...,go vet ./pkg/cmd/issue/create/...andgofmtall pass.
Fixes #13804