Skip to content

Warn instead of failing when issue fields can't be set - #13899

Open
imkp1 wants to merge 6 commits into
cli:trunkfrom
imkp1:fix/13804-gh-issue-create-errors-after-successful
Open

Warn instead of failing when issue fields can't be set#13899
imkp1 wants to merge 6 commits into
cli:trunkfrom
imkp1:fix/13804-gh-issue-create-errors-after-successful

Conversation

@imkp1

@imkp1 imkp1 commented Jul 16, 2026

Copy link
Copy Markdown

Summary

gh issue create creates the issue, then applies the deferred fields (type,
parent, blocked-by, blocking) in a second step. When one of those mutations
fails, createRun returns the error. The deferred PreserveInput sees a
non-nil error, writes a recovery file and prints:

X operation failed. To restore: gh issue create --recover /var/folders/.../gh2543392225.json

GraphQL: monalisa does not have the correct permissions to execute `UpdateIssueIssueType` (updateIssueIssueType)

The issue URL is never printed. But the issue does existapi.IssueCreate
already returned successfully. The message is misleading, and the --recover
prompt 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 UpdateIssueIssueType mutation fails with a permissions
error after the issue has been created.

This assigns the deferred-update failure to a separate updateErr instead of
the named return err, reports it as a warning on stderr, and still prints the
issue URL:

$ gh issue create --title "Bug" --type "Bug"
! Issue created, but not all fields could be set: GraphQL: monalisa does not have
  the correct permissions to execute `UpdateIssueIssueType` (updateIssueIssueType)
https://github.com/OWNER/REPO/issues/123

Using a separate variable (rather than clearing err) means the named return is
provably never set on this path, so PreserveInput cannot fire and no recovery
file is written.

The issue already exists once api.IssueCreate returns, and
api.DeferredUpdateIssue already joins its failures so that a single failing
mutation 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. deferredUpdateIssueOptions
runs 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 found test pins
this behaviour.

Testing

  • create with type that cannot be set — added to the Test_createRun table,
    stubbing UpdateIssueIssueType with a FORBIDDEN error. Asserts the issue URL
    is printed to stdout, the warning is printed to stderr, and that createRun
    returns no error (which is what stops PreserveInput from writing a recovery
    file). Verified to fail without the change and pass with it.
  • go test ./pkg/cmd/issue/..., go vet ./pkg/cmd/issue/create/... and gofmt
    all pass.

Fixes #13804

@github-actions github-actions Bot added external pull request originating outside of the CLI core team needs-triage needs to be reviewed labels Jul 16, 2026
`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

@BagToad BagToad 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.

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.

@BagToad BagToad added gh-issue relating to the gh issue command and removed needs-triage needs to be reviewed labels Jul 17, 2026
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.
@imkp1
imkp1 marked this pull request as ready for review July 17, 2026 05:21
@imkp1
imkp1 requested a review from a team as a code owner July 17, 2026 05:21
@imkp1
imkp1 requested a review from BagToad July 17, 2026 05:21
@github-actions

This comment was marked as outdated.

@imkp1

imkp1 commented Jul 17, 2026

Copy link
Copy Markdown
Author

Gated the picker on repo.ViewerCanTriage() in 8fbdb9b, kept the warn behavior. Costs nothing extra — IssueRepoInfo already selects viewerPermission, and ConfirmIssueSubmission gates the metadata option the same way. Tests cover READ (no picker, types never fetched) and TRIAGE (picker shown).

On checking whether TRIAGE is the right permission: against cli/cli with viewerPermission: READ, viewerCanType comes back false — so the gate does fix the reported case. But I couldn't pin the exact boundary. GitHub has a purpose-built Issue.viewerCanType separate from the triage-level viewerCanUpdateMetadata, which suggests types aren't precisely triage-gated. It's on Issue, not Repository, so it doesn't exist before creation and can't drive the picker.

One asymmetry for your call: ViewerCanTriage reads the base role, which ignores custom roles. If TRIAGE is insufficient, the gate is too loose and the warn path catches it. If a custom role grants type-setting on a base-READ repo, the picker silently disappears with no fallback. Happy to adjust if you know the server-side rule.

@taralove0518-netizen

This comment has been minimized.

@imkp1
imkp1 marked this pull request as draft July 20, 2026 13:49
@imkp1

imkp1 commented Jul 20, 2026

Copy link
Copy Markdown
Author

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 ViewerCanTriage() is the right boundary. It reads the base role and ignores custom roles, so a custom role granting type-setting on a base-READ repo would lose the picker with no fallback. If you know the server-side rule for Issue.viewerCanType, I'll match it; otherwise I'd lean toward keeping the triage gate and letting the warn path catch the gap.

@BagToad

BagToad commented Jul 20, 2026

Copy link
Copy Markdown
Member

@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.

@BagToad
BagToad marked this pull request as ready for review July 20, 2026 20:45
Copilot AI review requested due to automatic review settings July 20, 2026 20:45
@github-actions

This comment was marked as outdated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment on lines +422 to +426
// 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 {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.
@imkp1

imkp1 commented Jul 21, 2026

Copy link
Copy Markdown
Author

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.

imkp1 added 3 commits July 21, 2026 07:55
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external pull request originating outside of the CLI core team gh-issue relating to the gh issue command ready-for-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gh issue create errors after successful creation when issue type can't be set

4 participants