Files
multica/server/pkg/db/generated/github.sql.go
T
fc4a615700 MUL-7726: let the workspace choose the status merged PRs move an issue to (#8862)
* feat(pr): move issues to a workspace-chosen status when their PRs merge (MUL-7726)

The merge automation completed an issue only when a PR said "Closes MUL-1",
so the workspace switch mostly did nothing: most PRs are linked by title or
branch and never completed anything. The workspace now picks what a merge
does, in settings.pr_merge_status: "none", or the key of a started or done
status (custom ones included, Blocked excluded). Absent means Done. When
every linked PR is merged, the issue moves to that status. A closing keyword
only links.

- Decision: terminal, triage, "none", the per-issue switch and a new
  at_target state (the issue already has the target) come before the PR
  states, so the issue page only speaks when a merge would move the issue.
  The response adds target_status.
- MoveIssueFromPullRequests replaces CompleteIssueFromPullRequests and
  writes the target status. The target is resolved through the delivery's
  shared catalog read (Resolver.WritableCategory). An archived or unknown
  choice, or a failed read, fails closed to no write.
- Close intent is no longer synced or read. The column stays.
- Migration 551 pins "none" on existing workspaces whose history shows a
  merge never moved every issue: auto-complete switched off, or a linked PR
  whose merges were not all keyword merges. Workspaces with no links, or with
  only keyword merges, keep the Done default. Replays are harmless.
- CLI: `multica issue pr-automation <id> on|off` lets an agent keep one issue
  where it is. The multica-platform skill describes the new rule.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): choose the status merged PRs move an issue to on the GitHub page (MUL-7726)

- Settings → Integrations → GitHub → Features: "After PRs merge, move the
  issue to" picks Don't change, or a started or done status, custom ones
  included and Blocked excluded. It replaces the read-only auto-complete row.
  The self-hosted Git page shows the same setting. The statuses page drops
  its switch, and a badge on the target status now links to the setting.
- Issue sidebar: the line under the PR list names the target status ("Moves
  to In Review when #19 merges"). It says nothing when the workspace leaves
  status alone, when the issue already has the target, or on an older
  backend's no_close_intent. The per-issue switch reads "Keep status when PRs
  merge" and is hidden when it would change nothing. The menu opens the
  GitHub setting. The link hint only shows while auto-link is on.
- core: derivePRMergeStatus replaces derivePRAutoCompleteEnabled; the
  auto_complete schema adds target_status and the at_target state.
- Copy in 5 languages.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* docs: describe the workspace-chosen status for merged PRs (MUL-7726)

GitHub integration, self-hosted Git, issues and environment variable pages
(4-5 languages) now say that the workspace picks what a merge does: Done by
default, another started or done status, or no change. They also say that a
closing keyword only links, and how to keep one issue's status. The
troubleshooting entries explain what a missing line under the PR list means.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* refactor(pr): keep the merge automation out of agents' instructions (MUL-7726)

Agents don't act on what a merge does to an issue, so they don't need a
command for it or a rule to learn. Drop `multica issue pr-automation`, and
cut the multica-platform skill down to how PRs get linked: no merge rule, no
auto_complete field guide, no "the server moves it to done" note. People
still keep one issue's status from its Pull requests menu, and the docs now
point there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(pr): keep the retired auto-complete switch working for old desktop clients (MUL-7726)

Review of #8862: an admin on a desktop client from before this change can
still flip "Complete issues when their PRs merge". The client saved
pr_auto_complete_enabled = false, and the save looked successful, but the
server only read pr_merge_status, so merges kept moving issues to Done.

- Settings writes: a flip of the retired switch becomes a choice (off →
  none, on → Done). An old client that echoes a chosen target without
  flipping the switch leaves the target alone. Every write mirrors the switch
  from the effective choice (false for none, absent otherwise), so old
  clients show the right state.
- Reads: without pr_merge_status, the retired switch decides (off → none,
  else Done), on the server and in derivePRMergeStatus. This also covers
  settings written by a pod that predates the change during rollout.
- Migration 551 sets the switch off on the workspaces it pins to none.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-27 02:26:24 +08:00

940 lines
32 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: github.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createGitHubInstallation = `-- name: CreateGitHubInstallation :one
INSERT INTO github_installation (
workspace_id, installation_id, account_login, account_type, account_avatar_url, connected_by_id
) VALUES (
$1, $2, $3, $4, $5, $6
)
ON CONFLICT (workspace_id, installation_id) DO UPDATE SET
account_login = EXCLUDED.account_login,
account_type = EXCLUDED.account_type,
account_avatar_url = EXCLUDED.account_avatar_url,
connected_by_id = EXCLUDED.connected_by_id,
updated_at = now()
RETURNING id, workspace_id, installation_id, account_login, account_type, account_avatar_url, connected_by_id, created_at, updated_at
`
type CreateGitHubInstallationParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
InstallationID int64 `json:"installation_id"`
AccountLogin string `json:"account_login"`
AccountType string `json:"account_type"`
AccountAvatarUrl pgtype.Text `json:"account_avatar_url"`
ConnectedByID pgtype.UUID `json:"connected_by_id"`
}
func (q *Queries) CreateGitHubInstallation(ctx context.Context, arg CreateGitHubInstallationParams) (GithubInstallation, error) {
row := q.db.QueryRow(ctx, createGitHubInstallation,
arg.WorkspaceID,
arg.InstallationID,
arg.AccountLogin,
arg.AccountType,
arg.AccountAvatarUrl,
arg.ConnectedByID,
)
var i GithubInstallation
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.InstallationID,
&i.AccountLogin,
&i.AccountType,
&i.AccountAvatarUrl,
&i.ConnectedByID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteGitHubInstallation = `-- name: DeleteGitHubInstallation :exec
DELETE FROM github_installation WHERE id = $1 AND workspace_id = $2
`
type DeleteGitHubInstallationParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
func (q *Queries) DeleteGitHubInstallation(ctx context.Context, arg DeleteGitHubInstallationParams) error {
_, err := q.db.Exec(ctx, deleteGitHubInstallation, arg.ID, arg.WorkspaceID)
return err
}
const deleteGitHubInstallationByInstallationID = `-- name: DeleteGitHubInstallationByInstallationID :many
DELETE FROM github_installation WHERE installation_id = $1
RETURNING id, workspace_id
`
type DeleteGitHubInstallationByInstallationIDRow struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
// GitHub-side uninstall/suspend removes trust in the installation entirely, so
// drop every workspace binding. Returns one row per deleted binding so the
// handler can broadcast to each affected workspace.
func (q *Queries) DeleteGitHubInstallationByInstallationID(ctx context.Context, installationID int64) ([]DeleteGitHubInstallationByInstallationIDRow, error) {
rows, err := q.db.Query(ctx, deleteGitHubInstallationByInstallationID, installationID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []DeleteGitHubInstallationByInstallationIDRow{}
for rows.Next() {
var i DeleteGitHubInstallationByInstallationIDRow
if err := rows.Scan(&i.ID, &i.WorkspaceID); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deletePendingGitHubInstallation = `-- name: DeletePendingGitHubInstallation :exec
DELETE FROM github_pending_installation WHERE installation_id = $1
`
func (q *Queries) DeletePendingGitHubInstallation(ctx context.Context, installationID int64) error {
_, err := q.db.Exec(ctx, deletePendingGitHubInstallation, installationID)
return err
}
const findGitHubPullRequestByURL = `-- name: FindGitHubPullRequestByURL :one
SELECT id, workspace_id, installation_id, repo_owner, repo_name, pr_number, title, state, html_url, branch, author_login, author_avatar_url, merged_at, closed_at, pr_created_at, pr_updated_at, created_at, updated_at, head_sha, mergeable_state, additions, deletions, changed_files, api_mergeable, api_merge_state_status, checks_rollup_state, snapshot_head_sha, snapshot_fetched_at FROM github_pull_request
WHERE workspace_id = $1
AND lower(rtrim(html_url, '/')) = lower($2::text)
ORDER BY pr_updated_at DESC
LIMIT 1
`
type FindGitHubPullRequestByURLParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
HtmlUrl string `json:"html_url"`
}
// Resolves a pasted PR URL to a mirrored PR. The caller normalizes the URL to
// scheme://host/owner/repo/pull/N; html_url is stored in that shape.
func (q *Queries) FindGitHubPullRequestByURL(ctx context.Context, arg FindGitHubPullRequestByURLParams) (GithubPullRequest, error) {
row := q.db.QueryRow(ctx, findGitHubPullRequestByURL, arg.WorkspaceID, arg.HtmlUrl)
var i GithubPullRequest
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.InstallationID,
&i.RepoOwner,
&i.RepoName,
&i.PrNumber,
&i.Title,
&i.State,
&i.HtmlUrl,
&i.Branch,
&i.AuthorLogin,
&i.AuthorAvatarUrl,
&i.MergedAt,
&i.ClosedAt,
&i.PrCreatedAt,
&i.PrUpdatedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.HeadSha,
&i.MergeableState,
&i.Additions,
&i.Deletions,
&i.ChangedFiles,
&i.ApiMergeable,
&i.ApiMergeStateStatus,
&i.ChecksRollupState,
&i.SnapshotHeadSha,
&i.SnapshotFetchedAt,
)
return i, err
}
const getGitHubInstallationByID = `-- name: GetGitHubInstallationByID :one
SELECT id, workspace_id, installation_id, account_login, account_type, account_avatar_url, connected_by_id, created_at, updated_at FROM github_installation
WHERE id = $1
`
func (q *Queries) GetGitHubInstallationByID(ctx context.Context, id pgtype.UUID) (GithubInstallation, error) {
row := q.db.QueryRow(ctx, getGitHubInstallationByID, id)
var i GithubInstallation
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.InstallationID,
&i.AccountLogin,
&i.AccountType,
&i.AccountAvatarUrl,
&i.ConnectedByID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getGitHubPullRequest = `-- name: GetGitHubPullRequest :one
SELECT id, workspace_id, installation_id, repo_owner, repo_name, pr_number, title, state, html_url, branch, author_login, author_avatar_url, merged_at, closed_at, pr_created_at, pr_updated_at, created_at, updated_at, head_sha, mergeable_state, additions, deletions, changed_files, api_mergeable, api_merge_state_status, checks_rollup_state, snapshot_head_sha, snapshot_fetched_at FROM github_pull_request
WHERE workspace_id = $1 AND repo_owner = $2 AND repo_name = $3 AND pr_number = $4
`
type GetGitHubPullRequestParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
RepoOwner string `json:"repo_owner"`
RepoName string `json:"repo_name"`
PrNumber int32 `json:"pr_number"`
}
func (q *Queries) GetGitHubPullRequest(ctx context.Context, arg GetGitHubPullRequestParams) (GithubPullRequest, error) {
row := q.db.QueryRow(ctx, getGitHubPullRequest,
arg.WorkspaceID,
arg.RepoOwner,
arg.RepoName,
arg.PrNumber,
)
var i GithubPullRequest
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.InstallationID,
&i.RepoOwner,
&i.RepoName,
&i.PrNumber,
&i.Title,
&i.State,
&i.HtmlUrl,
&i.Branch,
&i.AuthorLogin,
&i.AuthorAvatarUrl,
&i.MergedAt,
&i.ClosedAt,
&i.PrCreatedAt,
&i.PrUpdatedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.HeadSha,
&i.MergeableState,
&i.Additions,
&i.Deletions,
&i.ChangedFiles,
&i.ApiMergeable,
&i.ApiMergeStateStatus,
&i.ChecksRollupState,
&i.SnapshotHeadSha,
&i.SnapshotFetchedAt,
)
return i, err
}
const getGitHubPullRequestInWorkspace = `-- name: GetGitHubPullRequestInWorkspace :one
SELECT id, workspace_id, installation_id, repo_owner, repo_name, pr_number, title, state, html_url, branch, author_login, author_avatar_url, merged_at, closed_at, pr_created_at, pr_updated_at, created_at, updated_at, head_sha, mergeable_state, additions, deletions, changed_files, api_mergeable, api_merge_state_status, checks_rollup_state, snapshot_head_sha, snapshot_fetched_at FROM github_pull_request
WHERE id = $1 AND workspace_id = $2
`
type GetGitHubPullRequestInWorkspaceParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
func (q *Queries) GetGitHubPullRequestInWorkspace(ctx context.Context, arg GetGitHubPullRequestInWorkspaceParams) (GithubPullRequest, error) {
row := q.db.QueryRow(ctx, getGitHubPullRequestInWorkspace, arg.ID, arg.WorkspaceID)
var i GithubPullRequest
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.InstallationID,
&i.RepoOwner,
&i.RepoName,
&i.PrNumber,
&i.Title,
&i.State,
&i.HtmlUrl,
&i.Branch,
&i.AuthorLogin,
&i.AuthorAvatarUrl,
&i.MergedAt,
&i.ClosedAt,
&i.PrCreatedAt,
&i.PrUpdatedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.HeadSha,
&i.MergeableState,
&i.Additions,
&i.Deletions,
&i.ChangedFiles,
&i.ApiMergeable,
&i.ApiMergeStateStatus,
&i.ChecksRollupState,
&i.SnapshotHeadSha,
&i.SnapshotFetchedAt,
)
return i, err
}
const getIssueReviewHeadSha = `-- name: GetIssueReviewHeadSha :one
SELECT head_sha FROM (
SELECT pr.head_sha AS head_sha, pr.state AS state, pr.pr_updated_at AS pr_updated_at
FROM github_pull_request pr
JOIN issue_pull_request ipr ON ipr.pull_request_id = pr.id
WHERE ipr.issue_id = $1 AND pr.head_sha <> ''
UNION ALL
SELECT pr.head_sha AS head_sha, pr.state AS state, pr.pr_updated_at AS pr_updated_at
FROM vcs_pull_request pr
JOIN issue_vcs_pull_request ipr ON ipr.pull_request_id = pr.id
WHERE ipr.issue_id = $1 AND pr.head_sha <> ''
) combined
ORDER BY (state IN ('open', 'draft')) DESC, pr_updated_at DESC
LIMIT 1
`
// Returns the head SHA of the commit currently "under review" for an issue:
// the most-recently-updated linked PR that still has an open/draft state and a
// non-empty head_sha. Used by the reviewer-loop dedup (TEN-356) so a pending
// review task pinned to an old head does not satisfy a request after HEAD
// advanced. Prefers in-flight PRs (open/draft) over merged/closed ones so a
// stale merged sibling can't shadow the live review target; falls back to the
// newest linked PR with a head_sha when none are open. Returns no rows (empty
// string) when the issue has no linked PR — callers treat that as "no SHA key"
// and dedup on (issue_id, agent_id) alone, preserving pre-TEN-356 behavior.
//
// Spans both GitHub and self-hosted VCS PRs: a self-hosted PR pushing a new
// commit must move the dedup head SHA the same way a GitHub PR does, otherwise
// a fresh review round could be merged away against a stale key.
func (q *Queries) GetIssueReviewHeadSha(ctx context.Context, issueID pgtype.UUID) (string, error) {
row := q.db.QueryRow(ctx, getIssueReviewHeadSha, issueID)
var head_sha string
err := row.Scan(&head_sha)
return head_sha, err
}
const getPendingGitHubInstallation = `-- name: GetPendingGitHubInstallation :one
SELECT installation_id, account_login, account_type, account_avatar_url, received_at, updated_at FROM github_pending_installation WHERE installation_id = $1
`
func (q *Queries) GetPendingGitHubInstallation(ctx context.Context, installationID int64) (GithubPendingInstallation, error) {
row := q.db.QueryRow(ctx, getPendingGitHubInstallation, installationID)
var i GithubPendingInstallation
err := row.Scan(
&i.InstallationID,
&i.AccountLogin,
&i.AccountType,
&i.AccountAvatarUrl,
&i.ReceivedAt,
&i.UpdatedAt,
)
return i, err
}
const linkIssueToPullRequest = `-- name: LinkIssueToPullRequest :execrows
INSERT INTO issue_pull_request (
issue_id, pull_request_id, linked_by_type, linked_by_id
) VALUES (
$1, $2, 'system', NULL
)
ON CONFLICT (issue_id, pull_request_id) DO NOTHING
`
type LinkIssueToPullRequestParams struct {
IssueID pgtype.UUID `json:"issue_id"`
PullRequestID pgtype.UUID `json:"pull_request_id"`
}
// =====================
// Issue ↔ Pull Request link
// =====================
// Automatic link from a PR title, branch, or closing keyword. Returns 1 only
// when the link is new, so the webhook evaluates the merge automation on the
// link event and not on every redelivery. An existing link (automatic or
// manual) is left untouched. close_intent is no longer read or written
// (MUL-7726).
func (q *Queries) LinkIssueToPullRequest(ctx context.Context, arg LinkIssueToPullRequestParams) (int64, error) {
result, err := q.db.Exec(ctx, linkIssueToPullRequest, arg.IssueID, arg.PullRequestID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const linkIssueToPullRequestManually = `-- name: LinkIssueToPullRequestManually :execrows
INSERT INTO issue_pull_request (
issue_id, pull_request_id, linked_by_type, linked_by_id
) VALUES (
$1, $2, 'member', $3
)
ON CONFLICT (issue_id, pull_request_id) DO UPDATE SET
linked_by_type = 'member',
linked_by_id = EXCLUDED.linked_by_id
WHERE issue_pull_request.linked_by_type IS DISTINCT FROM 'member'
`
type LinkIssueToPullRequestManuallyParams struct {
IssueID pgtype.UUID `json:"issue_id"`
PullRequestID pgtype.UUID `json:"pull_request_id"`
LinkedByID pgtype.UUID `json:"linked_by_id"`
}
// A member linked this PR by hand. Marking an existing automatic link as
// manual keeps a later title edit from removing it. Returns 1 when the row was
// inserted or converted.
func (q *Queries) LinkIssueToPullRequestManually(ctx context.Context, arg LinkIssueToPullRequestManuallyParams) (int64, error) {
result, err := q.db.Exec(ctx, linkIssueToPullRequestManually, arg.IssueID, arg.PullRequestID, arg.LinkedByID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const listAutoLinkedIssueIDsForPullRequest = `-- name: ListAutoLinkedIssueIDsForPullRequest :many
SELECT issue_id FROM issue_pull_request
WHERE pull_request_id = $1
AND COALESCE(linked_by_type, 'system') <> 'member'
`
// Issues this PR is linked to automatically. Manual links are not listed: the
// webhook reconciles only what it created itself.
func (q *Queries) ListAutoLinkedIssueIDsForPullRequest(ctx context.Context, pullRequestID pgtype.UUID) ([]pgtype.UUID, error) {
rows, err := q.db.Query(ctx, listAutoLinkedIssueIDsForPullRequest, pullRequestID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []pgtype.UUID{}
for rows.Next() {
var issue_id pgtype.UUID
if err := rows.Scan(&issue_id); err != nil {
return nil, err
}
items = append(items, issue_id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listGitHubInstallationsByInstallationID = `-- name: ListGitHubInstallationsByInstallationID :many
SELECT id, workspace_id, installation_id, account_login, account_type, account_avatar_url, connected_by_id, created_at, updated_at FROM github_installation
WHERE installation_id = $1
ORDER BY created_at ASC, id ASC
`
// One installation_id can be bound to several workspaces; webhook routing lists
// every binding and fans the event out to each bound workspace. Ordered oldest
// first so processing is deterministic and replay-stable.
func (q *Queries) ListGitHubInstallationsByInstallationID(ctx context.Context, installationID int64) ([]GithubInstallation, error) {
rows, err := q.db.Query(ctx, listGitHubInstallationsByInstallationID, installationID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GithubInstallation{}
for rows.Next() {
var i GithubInstallation
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.InstallationID,
&i.AccountLogin,
&i.AccountType,
&i.AccountAvatarUrl,
&i.ConnectedByID,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listGitHubInstallationsByWorkspace = `-- name: ListGitHubInstallationsByWorkspace :many
SELECT id, workspace_id, installation_id, account_login, account_type, account_avatar_url, connected_by_id, created_at, updated_at FROM github_installation
WHERE workspace_id = $1
ORDER BY created_at ASC
`
// =====================
// GitHub Installation
// =====================
func (q *Queries) ListGitHubInstallationsByWorkspace(ctx context.Context, workspaceID pgtype.UUID) ([]GithubInstallation, error) {
rows, err := q.db.Query(ctx, listGitHubInstallationsByWorkspace, workspaceID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GithubInstallation{}
for rows.Next() {
var i GithubInstallation
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.InstallationID,
&i.AccountLogin,
&i.AccountType,
&i.AccountAvatarUrl,
&i.ConnectedByID,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listIssueIDsForPullRequest = `-- name: ListIssueIDsForPullRequest :many
SELECT issue_id FROM issue_pull_request
WHERE pull_request_id = $1
`
func (q *Queries) ListIssueIDsForPullRequest(ctx context.Context, pullRequestID pgtype.UUID) ([]pgtype.UUID, error) {
rows, err := q.db.Query(ctx, listIssueIDsForPullRequest, pullRequestID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []pgtype.UUID{}
for rows.Next() {
var issue_id pgtype.UUID
if err := rows.Scan(&issue_id); err != nil {
return nil, err
}
items = append(items, issue_id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listPullRequestsByIssue = `-- name: ListPullRequestsByIssue :many
WITH issue_prs AS (
SELECT pr.id, pr.snapshot_head_sha
FROM github_pull_request pr
JOIN issue_pull_request ipr ON ipr.pull_request_id = pr.id
WHERE ipr.issue_id = $1
),
checks AS (
SELECT
cr.pr_id,
COUNT(*)::bigint AS total,
SUM(CASE WHEN cr.status = 'completed' AND cr.conclusion IN
('failure','cancelled','timed_out','action_required','startup_failure','stale','error')
THEN 1 ELSE 0 END)::bigint AS failed,
SUM(CASE WHEN cr.status = 'completed' AND cr.conclusion IN
('success','neutral','skipped')
THEN 1 ELSE 0 END)::bigint AS passed,
SUM(CASE WHEN cr.status <> 'completed' OR cr.conclusion IS NULL
THEN 1 ELSE 0 END)::bigint AS running,
COALESCE(
array_agg(cr.name) FILTER (WHERE cr.status = 'completed' AND cr.conclusion IN
('failure','cancelled','timed_out','action_required','startup_failure','stale','error')),
'{}'
)::text[] AS failed_names
FROM github_pull_request_check_run cr
JOIN issue_prs ip ON ip.id = cr.pr_id
WHERE cr.head_sha = ip.snapshot_head_sha AND ip.snapshot_head_sha <> ''
GROUP BY cr.pr_id
)
SELECT
pr.id, pr.workspace_id, pr.installation_id, pr.repo_owner, pr.repo_name,
pr.pr_number, pr.title, pr.state, pr.html_url, pr.branch, pr.author_login,
pr.author_avatar_url, pr.merged_at, pr.closed_at, pr.pr_created_at,
pr.pr_updated_at, pr.head_sha, pr.mergeable_state,
pr.additions, pr.deletions, pr.changed_files,
pr.api_mergeable, pr.api_merge_state_status, pr.checks_rollup_state,
pr.snapshot_head_sha, pr.snapshot_fetched_at,
pr.created_at, pr.updated_at,
COALESCE(ipr.linked_by_type, 'system')::text AS linked_by_type,
COALESCE(c.total, 0)::bigint AS checks_total,
COALESCE(c.passed, 0)::bigint AS checks_passed,
COALESCE(c.failed, 0)::bigint AS checks_failed,
COALESCE(c.running, 0)::bigint AS checks_running,
COALESCE(c.failed_names, '{}')::text[] AS failed_check_names
FROM github_pull_request pr
JOIN issue_pull_request ipr ON ipr.pull_request_id = pr.id
LEFT JOIN checks c ON c.pr_id = pr.id
WHERE ipr.issue_id = $1
ORDER BY pr.pr_created_at DESC
`
type ListPullRequestsByIssueRow struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
InstallationID int64 `json:"installation_id"`
RepoOwner string `json:"repo_owner"`
RepoName string `json:"repo_name"`
PrNumber int32 `json:"pr_number"`
Title string `json:"title"`
State string `json:"state"`
HtmlUrl string `json:"html_url"`
Branch pgtype.Text `json:"branch"`
AuthorLogin pgtype.Text `json:"author_login"`
AuthorAvatarUrl pgtype.Text `json:"author_avatar_url"`
MergedAt pgtype.Timestamptz `json:"merged_at"`
ClosedAt pgtype.Timestamptz `json:"closed_at"`
PrCreatedAt pgtype.Timestamptz `json:"pr_created_at"`
PrUpdatedAt pgtype.Timestamptz `json:"pr_updated_at"`
HeadSha string `json:"head_sha"`
MergeableState pgtype.Text `json:"mergeable_state"`
Additions int32 `json:"additions"`
Deletions int32 `json:"deletions"`
ChangedFiles int32 `json:"changed_files"`
ApiMergeable pgtype.Text `json:"api_mergeable"`
ApiMergeStateStatus pgtype.Text `json:"api_merge_state_status"`
ChecksRollupState pgtype.Text `json:"checks_rollup_state"`
SnapshotHeadSha string `json:"snapshot_head_sha"`
SnapshotFetchedAt pgtype.Timestamptz `json:"snapshot_fetched_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
LinkedByType string `json:"linked_by_type"`
ChecksTotal int64 `json:"checks_total"`
ChecksPassed int64 `json:"checks_passed"`
ChecksFailed int64 `json:"checks_failed"`
ChecksRunning int64 `json:"checks_running"`
FailedCheckNames []string `json:"failed_check_names"`
}
// Returns the issue's linked PRs with the GitHub API snapshot (MUL-5265): the
// mergeability verdict, the CI rollup, and per-check counts for the PR's
// CURRENT snapshot head SHA. Checks are aggregated from
// github_pull_request_check_run — the run-level snapshot written by the API
// refresh pipeline — NOT the legacy suite-level webhook aggregation, which is
// removed. The `issue_prs` CTE narrows to this issue's PR ids first so the
// aggregation only touches check rows for those PRs. Rows for an OLD head are
// excluded by the snapshot_head_sha filter. Every link row is a delivery PR:
// the webhook links an identifier it read from the PR title or branch name, and
// a member can link one by hand (linked_by_type = 'member'). MUL-7429.
func (q *Queries) ListPullRequestsByIssue(ctx context.Context, issueID pgtype.UUID) ([]ListPullRequestsByIssueRow, error) {
rows, err := q.db.Query(ctx, listPullRequestsByIssue, issueID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListPullRequestsByIssueRow{}
for rows.Next() {
var i ListPullRequestsByIssueRow
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.InstallationID,
&i.RepoOwner,
&i.RepoName,
&i.PrNumber,
&i.Title,
&i.State,
&i.HtmlUrl,
&i.Branch,
&i.AuthorLogin,
&i.AuthorAvatarUrl,
&i.MergedAt,
&i.ClosedAt,
&i.PrCreatedAt,
&i.PrUpdatedAt,
&i.HeadSha,
&i.MergeableState,
&i.Additions,
&i.Deletions,
&i.ChangedFiles,
&i.ApiMergeable,
&i.ApiMergeStateStatus,
&i.ChecksRollupState,
&i.SnapshotHeadSha,
&i.SnapshotFetchedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.LinkedByType,
&i.ChecksTotal,
&i.ChecksPassed,
&i.ChecksFailed,
&i.ChecksRunning,
&i.FailedCheckNames,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const unlinkIssueFromPullRequest = `-- name: UnlinkIssueFromPullRequest :execrows
DELETE FROM issue_pull_request
WHERE issue_id = $1 AND pull_request_id = $2
`
type UnlinkIssueFromPullRequestParams struct {
IssueID pgtype.UUID `json:"issue_id"`
PullRequestID pgtype.UUID `json:"pull_request_id"`
}
func (q *Queries) UnlinkIssueFromPullRequest(ctx context.Context, arg UnlinkIssueFromPullRequestParams) (int64, error) {
result, err := q.db.Exec(ctx, unlinkIssueFromPullRequest, arg.IssueID, arg.PullRequestID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const updateGitHubInstallationAccountByInstallationID = `-- name: UpdateGitHubInstallationAccountByInstallationID :many
UPDATE github_installation
SET account_login = $2,
account_type = $3,
account_avatar_url = $4,
updated_at = now()
WHERE installation_id = $1
RETURNING id, workspace_id, installation_id, account_login, account_type, account_avatar_url, connected_by_id, created_at, updated_at
`
type UpdateGitHubInstallationAccountByInstallationIDParams struct {
InstallationID int64 `json:"installation_id"`
AccountLogin string `json:"account_login"`
AccountType string `json:"account_type"`
AccountAvatarUrl pgtype.Text `json:"account_avatar_url"`
}
// Refresh the GitHub account display metadata across every workspace binding of
// an installation (fired by installation.created/new_permissions_accepted/
// unsuspend). Leaves workspace_id and connected_by_id untouched.
func (q *Queries) UpdateGitHubInstallationAccountByInstallationID(ctx context.Context, arg UpdateGitHubInstallationAccountByInstallationIDParams) ([]GithubInstallation, error) {
rows, err := q.db.Query(ctx, updateGitHubInstallationAccountByInstallationID,
arg.InstallationID,
arg.AccountLogin,
arg.AccountType,
arg.AccountAvatarUrl,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GithubInstallation{}
for rows.Next() {
var i GithubInstallation
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.InstallationID,
&i.AccountLogin,
&i.AccountType,
&i.AccountAvatarUrl,
&i.ConnectedByID,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const upsertGitHubPullRequest = `-- name: UpsertGitHubPullRequest :one
INSERT INTO github_pull_request (
workspace_id, installation_id, repo_owner, repo_name, pr_number,
title, state, html_url, branch, author_login, author_avatar_url,
merged_at, closed_at, pr_created_at, pr_updated_at,
head_sha, mergeable_state,
additions, deletions, changed_files
) VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8, $15, $16, $17,
$18, $19, $9, $10,
$11, $20,
$12, $13, $14
)
ON CONFLICT (workspace_id, repo_owner, repo_name, pr_number) DO UPDATE SET
installation_id = EXCLUDED.installation_id,
title = EXCLUDED.title,
state = EXCLUDED.state,
html_url = EXCLUDED.html_url,
branch = EXCLUDED.branch,
author_login = EXCLUDED.author_login,
author_avatar_url = EXCLUDED.author_avatar_url,
merged_at = EXCLUDED.merged_at,
closed_at = EXCLUDED.closed_at,
pr_updated_at = EXCLUDED.pr_updated_at,
head_sha = EXCLUDED.head_sha,
mergeable_state = CASE
WHEN COALESCE($21::boolean, FALSE) THEN NULL
WHEN EXCLUDED.mergeable_state IS NOT NULL THEN EXCLUDED.mergeable_state
ELSE github_pull_request.mergeable_state
END,
additions = EXCLUDED.additions,
deletions = EXCLUDED.deletions,
changed_files = EXCLUDED.changed_files,
updated_at = now()
WHERE EXCLUDED.pr_updated_at >= github_pull_request.pr_updated_at
RETURNING id, workspace_id, installation_id, repo_owner, repo_name, pr_number, title, state, html_url, branch, author_login, author_avatar_url, merged_at, closed_at, pr_created_at, pr_updated_at, created_at, updated_at, head_sha, mergeable_state, additions, deletions, changed_files, api_mergeable, api_merge_state_status, checks_rollup_state, snapshot_head_sha, snapshot_fetched_at
`
type UpsertGitHubPullRequestParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
InstallationID int64 `json:"installation_id"`
RepoOwner string `json:"repo_owner"`
RepoName string `json:"repo_name"`
PrNumber int32 `json:"pr_number"`
Title string `json:"title"`
State string `json:"state"`
HtmlUrl string `json:"html_url"`
PrCreatedAt pgtype.Timestamptz `json:"pr_created_at"`
PrUpdatedAt pgtype.Timestamptz `json:"pr_updated_at"`
HeadSha string `json:"head_sha"`
Additions int32 `json:"additions"`
Deletions int32 `json:"deletions"`
ChangedFiles int32 `json:"changed_files"`
Branch pgtype.Text `json:"branch"`
AuthorLogin pgtype.Text `json:"author_login"`
AuthorAvatarUrl pgtype.Text `json:"author_avatar_url"`
MergedAt pgtype.Timestamptz `json:"merged_at"`
ClosedAt pgtype.Timestamptz `json:"closed_at"`
MergeableState pgtype.Text `json:"mergeable_state"`
ClearMergeableState pgtype.Bool `json:"clear_mergeable_state"`
}
// =====================
// GitHub Pull Request
// =====================
// mergeable_state has three-state semantics on UPDATE:
// 1. clear_mergeable_state=true → write NULL (state-changing actions like
// opened/synchronize/reopened/edited(base) invalidate the prior verdict).
// 2. clear_mergeable_state=false, mergeable_state non-null → write the value.
// 3. clear_mergeable_state=false, mergeable_state null → preserve existing
// column. Metadata events (labeled/assigned/etc.) ship payloads without
// mergeability, and silently clobbering a known clean/dirty would lose
// information that GitHub only re-computes lazily.
//
// INSERT path always writes the incoming value (NULL acceptable for a new row).
//
// GitHub may deliver events out of order. An event older than the stored row
// (pr_updated_at) updates nothing and returns no row, so a late "opened" can't
// roll a merged PR back to open — the same guard UpsertVCSPullRequest has.
func (q *Queries) UpsertGitHubPullRequest(ctx context.Context, arg UpsertGitHubPullRequestParams) (GithubPullRequest, error) {
row := q.db.QueryRow(ctx, upsertGitHubPullRequest,
arg.WorkspaceID,
arg.InstallationID,
arg.RepoOwner,
arg.RepoName,
arg.PrNumber,
arg.Title,
arg.State,
arg.HtmlUrl,
arg.PrCreatedAt,
arg.PrUpdatedAt,
arg.HeadSha,
arg.Additions,
arg.Deletions,
arg.ChangedFiles,
arg.Branch,
arg.AuthorLogin,
arg.AuthorAvatarUrl,
arg.MergedAt,
arg.ClosedAt,
arg.MergeableState,
arg.ClearMergeableState,
)
var i GithubPullRequest
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.InstallationID,
&i.RepoOwner,
&i.RepoName,
&i.PrNumber,
&i.Title,
&i.State,
&i.HtmlUrl,
&i.Branch,
&i.AuthorLogin,
&i.AuthorAvatarUrl,
&i.MergedAt,
&i.ClosedAt,
&i.PrCreatedAt,
&i.PrUpdatedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.HeadSha,
&i.MergeableState,
&i.Additions,
&i.Deletions,
&i.ChangedFiles,
&i.ApiMergeable,
&i.ApiMergeStateStatus,
&i.ChecksRollupState,
&i.SnapshotHeadSha,
&i.SnapshotFetchedAt,
)
return i, err
}
const upsertPendingGitHubInstallation = `-- name: UpsertPendingGitHubInstallation :one
INSERT INTO github_pending_installation (
installation_id, account_login, account_type, account_avatar_url
) VALUES (
$1, $2, $3, $4
)
ON CONFLICT (installation_id) DO UPDATE SET
account_login = EXCLUDED.account_login,
account_type = EXCLUDED.account_type,
account_avatar_url = EXCLUDED.account_avatar_url,
updated_at = now()
RETURNING installation_id, account_login, account_type, account_avatar_url, received_at, updated_at
`
type UpsertPendingGitHubInstallationParams struct {
InstallationID int64 `json:"installation_id"`
AccountLogin string `json:"account_login"`
AccountType string `json:"account_type"`
AccountAvatarUrl pgtype.Text `json:"account_avatar_url"`
}
func (q *Queries) UpsertPendingGitHubInstallation(ctx context.Context, arg UpsertPendingGitHubInstallationParams) (GithubPendingInstallation, error) {
row := q.db.QueryRow(ctx, upsertPendingGitHubInstallation,
arg.InstallationID,
arg.AccountLogin,
arg.AccountType,
arg.AccountAvatarUrl,
)
var i GithubPendingInstallation
err := row.Scan(
&i.InstallationID,
&i.AccountLogin,
&i.AccountType,
&i.AccountAvatarUrl,
&i.ReceivedAt,
&i.UpdatedAt,
)
return i, err
}