Files
kandev/apps/backend/internal/github/client.go
T
João Salavisa 3d860290da feat(github): add merge button when PR is ready to merge (#979)
* feat(github): add merge button when PR is ready to merge

Surfaces a one-click merge action in the PR detail panel header and as a
compact green pill in the PR topbar hover popover, gated by the existing
isPRReadyToMerge predicate (open, CI success, mergeable_state=clean, and
either approved or no review process).

Backend adds MergePR to the Client interface, implements it across
PAT/gh/Noop/Mock clients, exposes PUT /api/v1/github/prs/:owner/:repo/:number/merge,
and maps GitHub 405/409 to HTTP 409 so the UI can show a clear error.

* fix(github): address PR review feedback on merge endpoint

- Map gh CLI 404/403/405/409 stderr to *GitHubAPIError via a new
  ghMergeStatusCode helper, so merge conflicts surface as HTTP 409 for
  gh CLI users (previously only PAT users got the right status).
- Service.MergePR returns ErrNoClient when unconfigured; httpMergePR
  translates that to HTTP 503 with github_not_configured, matching
  httpListRepoBranches.
- httpMergePR distinguishes empty body (io.EOF, fall through to default
  merge method) from malformed JSON (HTTP 400 invalid payload).
- PRMergeButton tracks a local merged flag so the button stays hidden
  across the async refresh window after a successful merge.

* test(github): cover merge helpers and extract PRMergeButton

- TestGhMergeStatusCode locks in the gh CLI stderr → status-code parser
  for nil, unrelated errors, the HTTP / status: / bare-phrase forms of
  404/403/405/409, and a 500 non-match guard.
- Broaden ghMergeStatusCode to also match "405 Method Not Allowed" /
  "409 Conflict" phrase forms so the merge endpoint's 409 mapping does
  not depend on the gh CLI keeping its current "HTTP <code>" prefix.
- TestHttpMergePR_MalformedJSON covers the new non-EOF bind-error branch
  (truncated body → 400). TestHttpMergePR_NoClient_Returns503 covers the
  ErrNoClient → 503 path on Service.MergePR.
- Document the post/put error-type divergence on PATClient.post so a
  future caller does not silently lose per-status mapping.
- Move PRMergeButton out of pr-shared.tsx into its own file, and reset
  the local "merged" flag when taskPR.id changes so an instance that
  re-renders for a different PR does not stay hidden.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-21 03:05:28 +01:00

91 lines
4.5 KiB
Go

package github
import (
"context"
"time"
)
// Client defines the interface for interacting with the GitHub API.
type Client interface {
// IsAuthenticated checks if the client is authenticated with GitHub.
IsAuthenticated(ctx context.Context) (bool, error)
// GetAuthenticatedUser returns the username of the authenticated user.
GetAuthenticatedUser(ctx context.Context) (string, error)
// GetPR retrieves a single pull request by number.
GetPR(ctx context.Context, owner, repo string, number int) (*PR, error)
// FindPRByBranch finds an open PR for the given head branch.
FindPRByBranch(ctx context.Context, owner, repo, branch string) (*PR, error)
// ListAuthoredPRs lists open PRs authored by the authenticated user for a repo.
ListAuthoredPRs(ctx context.Context, owner, repo string) ([]*PR, error)
// ListReviewRequestedPRs lists open PRs where the user's review is requested.
// scope controls the search qualifier: ReviewScopeUser uses user-review-requested:@me,
// ReviewScopeUserAndTeams (or empty) uses review-requested:@me.
// filter is an optional additional search qualifier (e.g. "repo:owner/name" or "org:myorg").
// customQuery, when non-empty, replaces the entire generated query.
ListReviewRequestedPRs(ctx context.Context, scope, filter, customQuery string) ([]*PR, error)
// ListUserOrgs returns the GitHub organizations the authenticated user belongs to.
ListUserOrgs(ctx context.Context) ([]GitHubOrg, error)
// SearchOrgRepos searches repositories in an organization, optionally filtered by a query string.
SearchOrgRepos(ctx context.Context, org, query string, limit int) ([]GitHubRepo, error)
// ListPRReviews lists reviews on a pull request.
ListPRReviews(ctx context.Context, owner, repo string, number int) ([]PRReview, error)
// ListPRComments lists review comments on a pull request.
// If since is non-nil, only comments updated after that time are returned.
ListPRComments(ctx context.Context, owner, repo string, number int, since *time.Time) ([]PRComment, error)
// ListCheckRuns lists CI check runs for a given git ref (branch or SHA).
ListCheckRuns(ctx context.Context, owner, repo, ref string) ([]CheckRun, error)
// GetPRFeedback fetches aggregated feedback (reviews, comments, checks) for a PR.
GetPRFeedback(ctx context.Context, owner, repo string, number int) (*PRFeedback, error)
// GetPRStatus fetches lightweight PR status (state, review summary, checks summary).
// Unlike GetPRFeedback, it skips comments for efficiency.
GetPRStatus(ctx context.Context, owner, repo string, number int) (*PRStatus, error)
// ListPRFiles lists files changed in a pull request.
ListPRFiles(ctx context.Context, owner, repo string, number int) ([]PRFile, error)
// ListPRCommits lists commits in a pull request.
ListPRCommits(ctx context.Context, owner, repo string, number int) ([]PRCommitInfo, error)
// SubmitReview submits a review on a pull request.
// event is one of "APPROVE", "COMMENT", "REQUEST_CHANGES".
SubmitReview(ctx context.Context, owner, repo string, number int, event, body string) error
// MergePR merges a pull request. mergeMethod is one of "merge", "squash", "rebase".
MergePR(ctx context.Context, owner, repo string, number int, mergeMethod string) error
// ListRepoBranches lists branches for a repository.
ListRepoBranches(ctx context.Context, owner, repo string) ([]RepoBranch, error)
// ListIssues searches for open issues (not PRs) matching the given query.
// filter is an optional additional search qualifier (e.g. "repo:owner/name" or "label:bug").
// customQuery, when non-empty, replaces the entire generated query.
ListIssues(ctx context.Context, filter, customQuery string) ([]*Issue, error)
// SearchPRs searches for PRs matching the given query.
// filter is an optional additional search qualifier (e.g. "author:@me" or "repo:owner/name").
// customQuery, when non-empty, replaces the entire generated query.
SearchPRs(ctx context.Context, filter, customQuery string) ([]*PR, error)
// SearchPRsPaged is the paginated variant of SearchPRs. page is 1-indexed;
// perPage is clamped to GitHub's 1..100 range by the implementation.
SearchPRsPaged(ctx context.Context, filter, customQuery string, page, perPage int) (*PRSearchPage, error)
// ListIssuesPaged is the paginated variant of ListIssues.
ListIssuesPaged(ctx context.Context, filter, customQuery string, page, perPage int) (*IssueSearchPage, error)
// GetIssueState returns the state of a single issue ("open" or "closed").
GetIssueState(ctx context.Context, owner, repo string, number int) (string, error)
}