mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
* feat(pr): only a closing keyword completes an issue on merge (MUL-7672) #8758 made every linked PR count toward auto-complete, so a title-only "MUL-123: ..." PR moved its issue to Done on merge. Restore the earlier contract: a PR completes its issue only when its title or body puts a closing keyword (Closes/Fixes/Resolves) right before the identifier. - Body closing keywords link again; a bare body mention still links nothing. - close_intent is recorded on the link and follows the PR text until the merge/close event, then freezes; a link first made after merge carries none. - The decision requires every linked PR merged and at least one with close intent; a new no_close_intent state explains why a merge won't complete. - Everything else from #8758 stays: manual link/remove, per-issue and workspace switches, edge-triggered evaluation, the result line. - Docs (4 languages), UI copy (5 languages) and the multica-platform skill describe the keyword rule again. Co-authored-by: multica-agent <[email protected]> * fix(pr): sync close intent on every link of the PR (MUL-7672) Review of #8794 found two ways a withdrawn closing keyword still completed the issue: - close_intent was refreshed only for identifiers the PR still claimed, so a manual link, or an automatic link whose merge event (carrying the final text) arrived before the edit event, kept a stale true. - the refresh sat behind the auto-link toggle, so turning auto-link off froze whatever intent was recorded. Replace the per-link update with one PR-wide sync: until the merge/close event, every link of the PR (automatic or manual) gets close_intent exactly when the current title/body closes its issue. It runs whenever GitHub features are on, independent of auto-link, which now only decides which links are created. Co-authored-by: multica-agent <[email protected]> * fix(pr): decide close intent apart from auto-link ownership (MUL-7672) Re-review of #8794: the PR-wide close intent sync reused the auto-link verdict, and resolvePRLinkPolicy skips workspaces with auto-link off. With an installation bound to several workspaces, turning auto-link off in one of them therefore cleared a closing keyword only that workspace resolves, and the merge no longer completed the issue. The policy now reads every bound workspace with GitHub on, and records the identifier's sole resolver next to the auto-linking owner. Links still follow the owner (auto-link off workspaces are not competing claimants); close intent is permitted for the owner or the sole resolver, so an identifier that resolves in more than one workspace stays withheld. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]>
960 lines
33 KiB
Go
960 lines
33 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 auto-complete on the link
|
|
// event and not on every redelivery. An existing link (automatic or manual) is
|
|
// left untouched; close_intent is set by SyncPullRequestCloseIntent.
|
|
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 syncPullRequestCloseIntent = `-- name: SyncPullRequestCloseIntent :exec
|
|
UPDATE issue_pull_request
|
|
SET close_intent = (issue_id = ANY($1::uuid[]))
|
|
WHERE pull_request_id = $2
|
|
AND close_intent <> (issue_id = ANY($1::uuid[]))
|
|
`
|
|
|
|
type SyncPullRequestCloseIntentParams struct {
|
|
ClosingIssueIds []pgtype.UUID `json:"closing_issue_ids"`
|
|
PullRequestID pgtype.UUID `json:"pull_request_id"`
|
|
}
|
|
|
|
// Sets close_intent on every link of the PR, automatic or manual, to whether
|
|
// the PR text closes that issue with a keyword ("Closes MUL-1" in its title or
|
|
// body), so a keyword removed before the merge stops counting. The webhook
|
|
// calls it until the PR's merge/close event, which fixes the decision.
|
|
func (q *Queries) SyncPullRequestCloseIntent(ctx context.Context, arg SyncPullRequestCloseIntentParams) error {
|
|
_, err := q.db.Exec(ctx, syncPullRequestCloseIntent, arg.ClosingIssueIds, arg.PullRequestID)
|
|
return err
|
|
}
|
|
|
|
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
|
|
}
|