mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
* fix(server): gate GitHub auto-close on closing keywords (MUL-2680) Closes multica-ai/multica#3264. The PR webhook previously treated any mention of an issue identifier in a PR title/body/branch as a close intent, so a body of "Closes MUL-1. Follow up in MUL-2. Unblocks MUL-3." would advance all three issues to done on merge. The auto-link layer stays generous (mentions still link the PR), but advancing to done now requires an explicit "Closes/Fixes/Resolves MUL-X" keyword adjacent to the identifier in the title or body — bare title prefixes (`MUL-1: ...`) and branch-name references no longer auto-complete. MUL-2680 Co-authored-by: multica-agent <github@multica.ai> * fix(server): persist close_intent on issue↔PR link rows (MUL-2680) The first take of MUL-2680 gated auto-advance on `closingIdents[id]` from the current webhook event. That broke the multi-PR sibling case: a PR declaring `Closes MUL-X` could merge first while a link-only sibling stayed open, leaving the issue in_progress; when the sibling closed later, its webhook carried no closing keyword and the handler skipped re-evaluation, so the issue stayed stuck forever. Move close intent from per-event state to per-link state: - New `close_intent` column on `issue_pull_request` (migration 109), set monotonically — `LinkIssueToPullRequest` ORs the existing flag with the incoming one so a subsequent webhook re-fire without the keyword cannot clear it. - New `GetIssuePullRequestCloseAggregate` query returns open-count and merged-with-close-intent-count for an issue. The auto-advance gate now reads from this persisted aggregate, which is event-agnostic: any terminal linked-PR event re-evaluates and the verdict only depends on accumulated DB state. - Webhook handler links all mentioned identifiers first (writing close_intent for the ones declared with a keyword), then iterates the affected issues in a separate pass to re-evaluate. The 'only fires for keyword-declared identifiers in this event' gate is gone — replaced by `merged_with_close_intent_count > 0` against the link rows. Regression test `TestWebhook_LinkOnlySiblingMergeAfterCloseKeywordPR` walks the full open→merge→open→merge sequence Elon described and asserts the issue advances on the link-only sibling's merge. MUL-2680 Co-authored-by: multica-agent <github@multica.ai> * Fix GitHub close intent updates Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Eve <eve@multica-ai.local>
649 lines
22 KiB
Go
649 lines
22 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 (installation_id) DO UPDATE SET
|
|
workspace_id = EXCLUDED.workspace_id,
|
|
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 :one
|
|
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"`
|
|
}
|
|
|
|
func (q *Queries) DeleteGitHubInstallationByInstallationID(ctx context.Context, installationID int64) (DeleteGitHubInstallationByInstallationIDRow, error) {
|
|
row := q.db.QueryRow(ctx, deleteGitHubInstallationByInstallationID, installationID)
|
|
var i DeleteGitHubInstallationByInstallationIDRow
|
|
err := row.Scan(&i.ID, &i.WorkspaceID)
|
|
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 getGitHubInstallationByInstallationID = `-- name: GetGitHubInstallationByInstallationID :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 installation_id = $1
|
|
`
|
|
|
|
func (q *Queries) GetGitHubInstallationByInstallationID(ctx context.Context, installationID int64) (GithubInstallation, error) {
|
|
row := q.db.QueryRow(ctx, getGitHubInstallationByInstallationID, installationID)
|
|
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 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,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getIssuePullRequestCloseAggregate = `-- name: GetIssuePullRequestCloseAggregate :one
|
|
SELECT
|
|
COALESCE(SUM(CASE WHEN pr.state IN ('open', 'draft') THEN 1 ELSE 0 END), 0)::bigint AS open_count,
|
|
COALESCE(SUM(CASE WHEN pr.state = 'merged' AND ipr.close_intent THEN 1 ELSE 0 END), 0)::bigint AS merged_with_close_intent_count
|
|
FROM github_pull_request pr
|
|
JOIN issue_pull_request ipr ON ipr.pull_request_id = pr.id
|
|
WHERE ipr.issue_id = $1
|
|
`
|
|
|
|
type GetIssuePullRequestCloseAggregateRow struct {
|
|
OpenCount int64 `json:"open_count"`
|
|
MergedWithCloseIntentCount int64 `json:"merged_with_close_intent_count"`
|
|
}
|
|
|
|
// Aggregates the issue's linked PRs into the two counts that gate
|
|
// auto-advance: how many are still in flight (`open` or `draft`) and how
|
|
// many merged PRs declared explicit closing intent on the link row. The
|
|
// webhook auto-advances the issue when open_count = 0 AND
|
|
// merged_with_close_intent_count > 0. Both the PR state and the link row
|
|
// (with close_intent) are persisted before this query runs, so the result
|
|
// is event-agnostic — a link-only sibling closing after a closing-keyword
|
|
// PR has already merged still resolves the issue.
|
|
func (q *Queries) GetIssuePullRequestCloseAggregate(ctx context.Context, issueID pgtype.UUID) (GetIssuePullRequestCloseAggregateRow, error) {
|
|
row := q.db.QueryRow(ctx, getIssuePullRequestCloseAggregate, issueID)
|
|
var i GetIssuePullRequestCloseAggregateRow
|
|
err := row.Scan(&i.OpenCount, &i.MergedWithCloseIntentCount)
|
|
return i, err
|
|
}
|
|
|
|
const linkIssueToPullRequest = `-- name: LinkIssueToPullRequest :exec
|
|
|
|
INSERT INTO issue_pull_request (
|
|
issue_id, pull_request_id, linked_by_type, linked_by_id, close_intent
|
|
) VALUES (
|
|
$1, $2, $4, $5, $3
|
|
)
|
|
ON CONFLICT (issue_id, pull_request_id) DO UPDATE SET
|
|
close_intent = CASE
|
|
WHEN $6 THEN issue_pull_request.close_intent
|
|
ELSE EXCLUDED.close_intent
|
|
END
|
|
`
|
|
|
|
type LinkIssueToPullRequestParams struct {
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
PullRequestID pgtype.UUID `json:"pull_request_id"`
|
|
CloseIntent bool `json:"close_intent"`
|
|
LinkedByType pgtype.Text `json:"linked_by_type"`
|
|
LinkedByID pgtype.UUID `json:"linked_by_id"`
|
|
PreserveCloseIntent bool `json:"preserve_close_intent"`
|
|
}
|
|
|
|
// =====================
|
|
// Issue ↔ Pull Request link
|
|
// =====================
|
|
// close_intent reflects the PR's explicit close declaration at the moment
|
|
// the webhook is allowed to update that intent. Open/edit/merge webhooks use
|
|
// the current title/body parse result so authors can remove a closing keyword
|
|
// before merge. Post-terminal edits can opt into preserving the stored value,
|
|
// keeping the merge-time decision stable.
|
|
func (q *Queries) LinkIssueToPullRequest(ctx context.Context, arg LinkIssueToPullRequestParams) error {
|
|
_, err := q.db.Exec(ctx, linkIssueToPullRequest,
|
|
arg.IssueID,
|
|
arg.PullRequestID,
|
|
arg.CloseIntent,
|
|
arg.LinkedByType,
|
|
arg.LinkedByID,
|
|
arg.PreserveCloseIntent,
|
|
)
|
|
return err
|
|
}
|
|
|
|
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.head_sha
|
|
FROM github_pull_request pr
|
|
JOIN issue_pull_request ipr ON ipr.pull_request_id = pr.id
|
|
WHERE ipr.issue_id = $1
|
|
),
|
|
per_app_latest AS (
|
|
SELECT DISTINCT ON (cs.pr_id, cs.app_id)
|
|
cs.pr_id, cs.app_id, cs.conclusion, cs.status
|
|
FROM github_pull_request_check_suite cs
|
|
JOIN issue_prs ip ON ip.id = cs.pr_id
|
|
WHERE cs.head_sha = ip.head_sha AND ip.head_sha <> ''
|
|
ORDER BY cs.pr_id, cs.app_id, cs.updated_at DESC
|
|
),
|
|
checks AS (
|
|
SELECT
|
|
pr_id,
|
|
COUNT(*)::bigint AS total,
|
|
SUM(CASE WHEN status = 'completed' AND conclusion IN
|
|
('failure','cancelled','timed_out','action_required','startup_failure','stale')
|
|
THEN 1 ELSE 0 END)::bigint AS failed,
|
|
SUM(CASE WHEN status = 'completed' AND conclusion IN
|
|
('success','neutral','skipped')
|
|
THEN 1 ELSE 0 END)::bigint AS passed,
|
|
SUM(CASE WHEN status <> 'completed' OR conclusion IS NULL
|
|
THEN 1 ELSE 0 END)::bigint AS pending
|
|
FROM per_app_latest
|
|
GROUP BY 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.created_at, pr.updated_at,
|
|
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.pending, 0)::bigint AS checks_pending
|
|
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"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
ChecksTotal int64 `json:"checks_total"`
|
|
ChecksPassed int64 `json:"checks_passed"`
|
|
ChecksFailed int64 `json:"checks_failed"`
|
|
ChecksPending int64 `json:"checks_pending"`
|
|
}
|
|
|
|
// Returns the issue's linked PRs with the aggregated check-suite counts for
|
|
// the PR's CURRENT head SHA. The `issue_prs` CTE narrows to this issue's PR
|
|
// ids first so the per-app aggregation only touches suite rows for those
|
|
// PRs — without that scoping the planner has to scan/aggregate every PR's
|
|
// suites in the workspace before joining on issue. Per-app latest suite is
|
|
// selected so a single app firing multiple suites on the same head doesn't
|
|
// get counted N times. Late-arriving suites for an OLD head are stored but
|
|
// excluded by the head_sha filter, so they can't override the new head's
|
|
// pending view.
|
|
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.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ChecksTotal,
|
|
&i.ChecksPassed,
|
|
&i.ChecksFailed,
|
|
&i.ChecksPending,
|
|
); 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 :exec
|
|
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) error {
|
|
_, err := q.db.Exec(ctx, unlinkIssueFromPullRequest, arg.IssueID, arg.PullRequestID)
|
|
return err
|
|
}
|
|
|
|
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()
|
|
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
|
|
`
|
|
|
|
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).
|
|
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,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const upsertPullRequestCheckSuite = `-- name: UpsertPullRequestCheckSuite :exec
|
|
|
|
INSERT INTO github_pull_request_check_suite (
|
|
pr_id, suite_id, head_sha, app_id, conclusion, status, updated_at
|
|
) VALUES (
|
|
$1, $2, $3, $4, $7, $5, $6
|
|
)
|
|
ON CONFLICT (pr_id, suite_id) DO UPDATE SET
|
|
head_sha = EXCLUDED.head_sha,
|
|
app_id = EXCLUDED.app_id,
|
|
conclusion = EXCLUDED.conclusion,
|
|
status = EXCLUDED.status,
|
|
updated_at = EXCLUDED.updated_at
|
|
WHERE EXCLUDED.updated_at >= github_pull_request_check_suite.updated_at
|
|
`
|
|
|
|
type UpsertPullRequestCheckSuiteParams struct {
|
|
PrID pgtype.UUID `json:"pr_id"`
|
|
SuiteID int64 `json:"suite_id"`
|
|
HeadSha string `json:"head_sha"`
|
|
AppID int64 `json:"app_id"`
|
|
Status string `json:"status"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
Conclusion pgtype.Text `json:"conclusion"`
|
|
}
|
|
|
|
// =====================
|
|
// GitHub PR check suite
|
|
// =====================
|
|
// Upserts a single check_suite row keyed by (pr_id, suite_id). The WHERE
|
|
// clause on the DO UPDATE branch prevents a late-arriving older event from
|
|
// overwriting a newer one — same-PR/same-suite ordering protection. Late
|
|
// events targeting an old head still land here (their head_sha is stored
|
|
// on the row); the head_sha filter in ListPullRequestsByIssue keeps them
|
|
// out of the current aggregate.
|
|
func (q *Queries) UpsertPullRequestCheckSuite(ctx context.Context, arg UpsertPullRequestCheckSuiteParams) error {
|
|
_, err := q.db.Exec(ctx, upsertPullRequestCheckSuite,
|
|
arg.PrID,
|
|
arg.SuiteID,
|
|
arg.HeadSha,
|
|
arg.AppID,
|
|
arg.Status,
|
|
arg.UpdatedAt,
|
|
arg.Conclusion,
|
|
)
|
|
return err
|
|
}
|