mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
* 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]>
221 lines
10 KiB
SQL
221 lines
10 KiB
SQL
-- =====================
|
|
-- VCS Connection (Forgejo / Gitea / GitLab)
|
|
-- =====================
|
|
|
|
-- name: ListVCSConnectionsByWorkspace :many
|
|
SELECT * FROM vcs_connection
|
|
WHERE workspace_id = $1
|
|
ORDER BY created_at ASC;
|
|
|
|
-- name: GetVCSConnectionByID :one
|
|
SELECT * FROM vcs_connection
|
|
WHERE id = $1;
|
|
|
|
-- name: UpsertVCSConnection :one
|
|
-- Reconnecting the same instance rotates the stored token/secret, provider,
|
|
-- and identity in place rather than creating a duplicate row.
|
|
INSERT INTO vcs_connection (
|
|
workspace_id, provider, instance_url, account_login,
|
|
access_token_encrypted, webhook_secret_encrypted, connected_by_id
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6, sqlc.narg('connected_by_id')
|
|
)
|
|
ON CONFLICT (workspace_id, instance_url) DO UPDATE SET
|
|
provider = EXCLUDED.provider,
|
|
account_login = EXCLUDED.account_login,
|
|
access_token_encrypted = EXCLUDED.access_token_encrypted,
|
|
webhook_secret_encrypted = EXCLUDED.webhook_secret_encrypted,
|
|
connected_by_id = EXCLUDED.connected_by_id,
|
|
updated_at = now()
|
|
RETURNING *;
|
|
|
|
-- name: DeleteVCSConnection :exec
|
|
-- These tables carry no FKs, so the cascade that once removed the connection's
|
|
-- mirrored PRs, their issue links, and CI statuses is gone (migration 213). Do
|
|
-- that cleanup explicitly here, in one statement so it commits or rolls back
|
|
-- atomically with the connection row. The target CTE also scopes every child
|
|
-- delete to a connection that actually belongs to the workspace, so a wrong
|
|
-- workspace_id is a no-op rather than deleting another tenant's child rows.
|
|
WITH target AS (
|
|
SELECT vcs_connection.id FROM vcs_connection WHERE vcs_connection.id = $1 AND vcs_connection.workspace_id = $2
|
|
),
|
|
cleared_links AS (
|
|
DELETE FROM issue_vcs_pull_request
|
|
WHERE pull_request_id IN (
|
|
SELECT vcs_pull_request.id FROM vcs_pull_request
|
|
WHERE vcs_pull_request.connection_id IN (SELECT target.id FROM target)
|
|
)
|
|
),
|
|
cleared_statuses AS (
|
|
DELETE FROM vcs_commit_status WHERE connection_id IN (SELECT target.id FROM target)
|
|
),
|
|
cleared_prs AS (
|
|
DELETE FROM vcs_pull_request WHERE connection_id IN (SELECT target.id FROM target)
|
|
)
|
|
DELETE FROM vcs_connection WHERE vcs_connection.id = $1 AND vcs_connection.workspace_id = $2;
|
|
|
|
-- name: RotateVCSConnectionWebhookSecret :one
|
|
UPDATE vcs_connection
|
|
SET webhook_secret_encrypted = $3,
|
|
updated_at = now()
|
|
WHERE id = $1 AND workspace_id = $2
|
|
RETURNING *;
|
|
|
|
-- =====================
|
|
-- VCS Pull Request
|
|
-- =====================
|
|
|
|
-- name: UpsertVCSPullRequest :one
|
|
-- pr_updated_at guards against an out-of-order webhook redelivery regressing
|
|
-- the PR state, mirroring the updated_at guard on UpsertVCSCommitStatus. Each
|
|
-- mutable column is applied only when the incoming event is at least as new as
|
|
-- the stored row; a stale event keeps the existing values. The row is still
|
|
-- touched (and thus RETURNED) either way, so callers always get the current PR
|
|
-- — the webhook needs pr.id to link the issue even on a stale redelivery.
|
|
INSERT INTO vcs_pull_request (
|
|
workspace_id, connection_id, provider, 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,
|
|
additions, deletions, changed_files, head_sha
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6,
|
|
$7, $8, $9, sqlc.narg('branch'), sqlc.narg('author_login'), sqlc.narg('author_avatar_url'),
|
|
sqlc.narg('merged_at'), sqlc.narg('closed_at'), $10, $11,
|
|
$12, $13, $14, $15
|
|
)
|
|
ON CONFLICT (connection_id, repo_owner, repo_name, pr_number) DO UPDATE SET
|
|
workspace_id = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.workspace_id ELSE vcs_pull_request.workspace_id END,
|
|
provider = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.provider ELSE vcs_pull_request.provider END,
|
|
title = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.title ELSE vcs_pull_request.title END,
|
|
state = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.state ELSE vcs_pull_request.state END,
|
|
html_url = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.html_url ELSE vcs_pull_request.html_url END,
|
|
branch = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.branch ELSE vcs_pull_request.branch END,
|
|
author_login = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.author_login ELSE vcs_pull_request.author_login END,
|
|
author_avatar_url = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.author_avatar_url ELSE vcs_pull_request.author_avatar_url END,
|
|
merged_at = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.merged_at ELSE vcs_pull_request.merged_at END,
|
|
closed_at = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.closed_at ELSE vcs_pull_request.closed_at END,
|
|
pr_updated_at = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.pr_updated_at ELSE vcs_pull_request.pr_updated_at END,
|
|
additions = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.additions ELSE vcs_pull_request.additions END,
|
|
deletions = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.deletions ELSE vcs_pull_request.deletions END,
|
|
changed_files = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.changed_files ELSE vcs_pull_request.changed_files END,
|
|
head_sha = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.head_sha ELSE vcs_pull_request.head_sha END,
|
|
updated_at = now()
|
|
RETURNING *;
|
|
|
|
-- name: ListVCSPullRequestsByIssue :many
|
|
-- Aggregates each PR's commit statuses for its CURRENT head sha into
|
|
-- passed/failed/pending counts. vcs_commit_status holds one row per
|
|
-- (connection, sha, context) with a normalized state, so a count by state is
|
|
-- correct. Statuses for an old head sha stay stored but are excluded by the
|
|
-- head_sha join, so a stale run can't pollute the bar.
|
|
WITH checks AS (
|
|
SELECT
|
|
pr.id AS pr_id,
|
|
COUNT(*)::bigint AS total,
|
|
SUM(CASE WHEN cs.state = 'failed' THEN 1 ELSE 0 END)::bigint AS failed,
|
|
SUM(CASE WHEN cs.state = 'passed' THEN 1 ELSE 0 END)::bigint AS passed,
|
|
SUM(CASE WHEN cs.state = 'pending' THEN 1 ELSE 0 END)::bigint AS pending
|
|
FROM vcs_pull_request pr
|
|
JOIN issue_vcs_pull_request ipr ON ipr.pull_request_id = pr.id
|
|
JOIN vcs_commit_status cs
|
|
ON cs.connection_id = pr.connection_id
|
|
AND cs.sha = pr.head_sha
|
|
AND pr.head_sha <> ''
|
|
WHERE ipr.issue_id = sqlc.arg('issue_id')
|
|
GROUP BY pr.id
|
|
)
|
|
SELECT
|
|
pr.*,
|
|
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.pending, 0)::bigint AS checks_pending
|
|
FROM vcs_pull_request pr
|
|
JOIN issue_vcs_pull_request ipr ON ipr.pull_request_id = pr.id
|
|
LEFT JOIN checks c ON c.pr_id = pr.id
|
|
WHERE ipr.issue_id = sqlc.arg('issue_id')
|
|
ORDER BY pr.pr_created_at DESC;
|
|
|
|
-- =====================
|
|
-- VCS commit status (CI)
|
|
-- =====================
|
|
|
|
-- name: UpsertVCSCommitStatus :exec
|
|
-- One row per (connection, sha, context); a redelivery or state transition
|
|
-- overwrites in place. updated_at guards against an older event overwriting a
|
|
-- newer one for the same context. state is normalized (passed/failed/pending).
|
|
INSERT INTO vcs_commit_status (
|
|
connection_id, sha, context, state, target_url, description, updated_at
|
|
) VALUES (
|
|
$1, $2, $3, $4, sqlc.narg('target_url'), sqlc.narg('description'), $5
|
|
)
|
|
ON CONFLICT (connection_id, sha, context) DO UPDATE SET
|
|
state = EXCLUDED.state,
|
|
target_url = EXCLUDED.target_url,
|
|
description = EXCLUDED.description,
|
|
updated_at = EXCLUDED.updated_at
|
|
WHERE EXCLUDED.updated_at >= vcs_commit_status.updated_at;
|
|
|
|
-- name: ListIssueIDsForVCSPRHead :many
|
|
-- Issues linked to any PR whose head sha matches the given status, so a
|
|
-- commit-status event can fan out a PR-card refresh to the right issues.
|
|
SELECT DISTINCT ipr.issue_id
|
|
FROM vcs_pull_request pr
|
|
JOIN issue_vcs_pull_request ipr ON ipr.pull_request_id = pr.id
|
|
WHERE pr.connection_id = $1 AND pr.head_sha = $2 AND pr.head_sha <> '';
|
|
|
|
-- =====================
|
|
-- Issue ↔ VCS PR link
|
|
-- =====================
|
|
|
|
-- name: LinkIssueToVCSPullRequest :execrows
|
|
-- Mirrors LinkIssueToPullRequest: automatic link, 1 only when new.
|
|
INSERT INTO issue_vcs_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;
|
|
|
|
-- name: LinkIssueToVCSPullRequestManually :execrows
|
|
INSERT INTO issue_vcs_pull_request (
|
|
issue_id, pull_request_id, linked_by_type, linked_by_id
|
|
) VALUES (
|
|
$1, $2, 'member', sqlc.narg('linked_by_id')
|
|
)
|
|
ON CONFLICT (issue_id, pull_request_id) DO UPDATE SET
|
|
linked_by_type = 'member',
|
|
linked_by_id = EXCLUDED.linked_by_id
|
|
WHERE issue_vcs_pull_request.linked_by_type IS DISTINCT FROM 'member';
|
|
|
|
-- name: ListIssueIDsForVCSPullRequest :many
|
|
SELECT issue_id FROM issue_vcs_pull_request
|
|
WHERE pull_request_id = $1;
|
|
|
|
-- name: ListAutoLinkedIssueIDsForVCSPullRequest :many
|
|
SELECT issue_id FROM issue_vcs_pull_request
|
|
WHERE pull_request_id = $1
|
|
AND COALESCE(linked_by_type, 'system') <> 'member';
|
|
|
|
-- name: UnlinkIssueFromVCSPullRequest :execrows
|
|
DELETE FROM issue_vcs_pull_request
|
|
WHERE issue_id = $1 AND pull_request_id = $2;
|
|
|
|
-- name: GetVCSPullRequestByKey :one
|
|
-- The stored row before a webhook upsert, so the handler can tell a merge
|
|
-- transition apart from a later event on an already-merged PR.
|
|
SELECT * FROM vcs_pull_request
|
|
WHERE connection_id = $1 AND repo_owner = $2 AND repo_name = $3 AND pr_number = $4;
|
|
|
|
-- name: GetVCSPullRequestInWorkspace :one
|
|
SELECT * FROM vcs_pull_request
|
|
WHERE id = $1 AND workspace_id = $2;
|
|
|
|
-- name: FindVCSPullRequestByURL :one
|
|
SELECT * FROM vcs_pull_request
|
|
WHERE workspace_id = $1
|
|
AND lower(rtrim(html_url, '/')) = lower(sqlc.arg('html_url')::text)
|
|
ORDER BY pr_updated_at DESC
|
|
LIMIT 1;
|