RetryIssueApproval re-runs approval-template finding for an issue stuck in CHECKING. The synchronous post-create path in `postCreateIssue` swallows errors (e.g. CEL evaluation failure against a malformed workspace approval rule), and there is no event-driven retry for non-DATABASE_CHANGE issue types
(ctx context.Context, req *connect.Request[v1pb.RetryIssueApprovalRequest])
| 891 | // Idempotent: returns the existing issue unchanged when approval-finding |
| 892 | // has already completed. |
| 893 | func (s *IssueService) RetryIssueApproval(ctx context.Context, req *connect.Request[v1pb.RetryIssueApprovalRequest]) (*connect.Response[v1pb.Issue], error) { |
| 894 | issue, err := s.getIssueMessage(ctx, req.Msg.Name) |
| 895 | if err != nil { |
| 896 | return nil, err |
| 897 | } |
| 898 | |
| 899 | user, ok := GetUserFromContext(ctx) |
| 900 | if !ok { |
| 901 | return nil, connect.NewError(connect.CodeInternal, errors.New("user not found")) |
| 902 | } |
| 903 | if !canRequestIssue(issue.CreatorEmail, user) { |
| 904 | return nil, connect.NewError(connect.CodePermissionDenied, errors.New("only the issue creator can retry approval finding")) |
| 905 | } |
| 906 | |
| 907 | // No-op fast path: nothing to retry if the previous attempt completed. |
| 908 | if issue.Payload.GetApproval().GetApprovalFindingDone() { |
| 909 | issueV1, err := s.convertToIssue(issue) |
| 910 | if err != nil { |
| 911 | return nil, connect.NewError(connect.CodeInternal, errors.Wrapf(err, "failed to convert to issue")) |
| 912 | } |
| 913 | return connect.NewResponse(issueV1), nil |
| 914 | } |
| 915 | |
| 916 | if err := approval.FindAndApplyApprovalTemplate(ctx, s.store, s.webhookManager, s.licenseService, issue); err != nil { |
| 917 | // Surface the underlying cause (e.g. CEL error in the workspace |
| 918 | // approval rule) so the operator can fix it. |
| 919 | return nil, connect.NewError(connect.CodeFailedPrecondition, errors.Wrap(err, "approval finding still failing")) |
| 920 | } |
| 921 | |
| 922 | uid := issue.UID |
| 923 | refreshed, err := s.store.GetIssue(ctx, &store.FindIssueMessage{ |
| 924 | Workspace: common.GetWorkspaceIDFromContext(ctx), |
| 925 | ProjectIDs: []string{issue.ProjectID}, |
| 926 | UID: &uid, |
| 927 | }) |
| 928 | if err != nil { |
| 929 | return nil, connect.NewError(connect.CodeInternal, errors.Wrap(err, "failed to refresh issue")) |
| 930 | } |
| 931 | if refreshed == nil { |
| 932 | return nil, connect.NewError(connect.CodeNotFound, errors.New("issue not found after retry")) |
| 933 | } |
| 934 | |
| 935 | // Mirror the post-approval side effects that `postCreateIssue` and the |
| 936 | // approval runner perform when finding completes — without these, an |
| 937 | // auto-approved (template-less / SKIPPED) retry result would leave the |
| 938 | // issue out of CHECKING but skip the actual work: |
| 939 | // * ACCESS_GRANT / ROLE_GRANT → activate the grant via |
| 940 | // `completeAccessRequestIssue`. |
| 941 | // * DATABASE_CHANGE with a plan → enqueue rollout creation. |
| 942 | approved, err := utils.CheckIssueApproved(refreshed) |
| 943 | if err != nil { |
| 944 | return nil, connect.NewError(connect.CodeInternal, errors.Wrap(err, "failed to check approval state")) |
| 945 | } |
| 946 | if approved { |
| 947 | switch refreshed.Type { |
| 948 | case storepb.Issue_ACCESS_GRANT, storepb.Issue_ROLE_GRANT: |
| 949 | if completed, completeErr := completeAccessRequestIssue(ctx, s.store, user.Email, refreshed); completeErr != nil { |
| 950 | slog.Warn("failed to complete access request issue after retry", log.BBError(completeErr)) |
nothing calls this directly
no test coverage detected