mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-03 11:10:23 +02:00
feat(github): GitHub App integration for PR ↔ issue linking (#1817)
* feat(github): GitHub App backend for PR ↔ issue linking - New tables: github_installation (workspace ↔ App install), github_pull_request (mirrored PR state), issue_pull_request (M:N link). - Webhook handler verifies HMAC-SHA256, upserts PR rows, parses issue identifiers from PR title/body/branch and auto-links them. Merging a linked PR moves the issue to done. - Connect/setup endpoints power the zero-config "Connect GitHub" install flow; state token is HMAC-signed so the setup callback can recover the workspace. - Workspace-scoped admin routes for listing/disconnecting installations, plus a per-issue `pull-requests` list endpoint. Co-authored-by: multica-agent <github@multica.ai> * feat(github): UI for connecting GitHub and viewing linked PRs - Settings → Integrations: new tab with Connect GitHub / installations list / disconnect, gated on the deployment having the App configured. - Issue detail sidebar: Pull requests section showing linked PR title, repo, state (open/draft/merged/closed), and author, with deep link to GitHub. - Real-time refresh: github_installation:* and pull_request:* events invalidate the matching TanStack Query caches. Co-authored-by: multica-agent <github@multica.ai> * fix(github): address review — null actor, role gating, configured guard, scoped uninstall broadcast - listeners: use optionalUUID(e.ActorID) so the system actor on the github-driven issue:updated event no longer panics activity / notification listeners; merged-PR → issue done now produces a status_changed activity and inbox entry. - IntegrationsTab: gate the admin-only installations query on canManage so members no longer hit /github/installations 403; the configured/not-configured copy is also scoped to admins. - backend: introduce isGitHubConfigured() requiring both GITHUB_APP_SLUG and GITHUB_WEBHOOK_SECRET, and surface that single flag from list-installations + connect endpoints so the frontend Connect button stays disabled until both are set. - DeleteGitHubInstallationByInstallationID now RETURNs workspace_id; webhook handler publishes github_installation:deleted scoped to the right workspace so already-open Settings tabs invalidate in real time. ErrNoRows on a re-fired delete short-circuits cleanly. - tests: focused webhook integration coverage (auto-link + merge → done, cancelled preservation, uninstall returns workspace). Co-authored-by: multica-agent <github@multica.ai> * fix(github): i18n the new GitHub UI strings to satisfy lint CI flagged every literal string in the Integrations tab, the Pull requests sidebar section, and the per-PR row label. Move them through useT() and add the matching `integrations.*` block to settings.json (en / zh-Hans) plus `detail.section_pull_requests` / `detail.pull_request_state_*` / loading + empty copy under `issues.json`. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
97
server/pkg/db/queries/github.sql
Normal file
97
server/pkg/db/queries/github.sql
Normal file
@@ -0,0 +1,97 @@
|
||||
-- =====================
|
||||
-- GitHub Installation
|
||||
-- =====================
|
||||
|
||||
-- name: ListGitHubInstallationsByWorkspace :many
|
||||
SELECT * FROM github_installation
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY created_at ASC;
|
||||
|
||||
-- name: GetGitHubInstallationByInstallationID :one
|
||||
SELECT * FROM github_installation
|
||||
WHERE installation_id = $1;
|
||||
|
||||
-- name: GetGitHubInstallationByID :one
|
||||
SELECT * FROM github_installation
|
||||
WHERE id = $1;
|
||||
|
||||
-- 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, sqlc.narg('account_avatar_url'), sqlc.narg('connected_by_id')
|
||||
)
|
||||
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 *;
|
||||
|
||||
-- name: DeleteGitHubInstallation :exec
|
||||
DELETE FROM github_installation WHERE id = $1 AND workspace_id = $2;
|
||||
|
||||
-- name: DeleteGitHubInstallationByInstallationID :one
|
||||
DELETE FROM github_installation WHERE installation_id = $1
|
||||
RETURNING id, workspace_id;
|
||||
|
||||
-- =====================
|
||||
-- GitHub Pull Request
|
||||
-- =====================
|
||||
|
||||
-- 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
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7, $8, sqlc.narg('branch'), sqlc.narg('author_login'), sqlc.narg('author_avatar_url'),
|
||||
sqlc.narg('merged_at'), sqlc.narg('closed_at'), $9, $10
|
||||
)
|
||||
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,
|
||||
updated_at = now()
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetGitHubPullRequest :one
|
||||
SELECT * FROM github_pull_request
|
||||
WHERE workspace_id = $1 AND repo_owner = $2 AND repo_name = $3 AND pr_number = $4;
|
||||
|
||||
-- name: ListPullRequestsByIssue :many
|
||||
SELECT pr.*
|
||||
FROM github_pull_request pr
|
||||
JOIN issue_pull_request ipr ON ipr.pull_request_id = pr.id
|
||||
WHERE ipr.issue_id = $1
|
||||
ORDER BY pr.pr_created_at DESC;
|
||||
|
||||
-- name: ListIssueIDsForPullRequest :many
|
||||
SELECT issue_id FROM issue_pull_request
|
||||
WHERE pull_request_id = $1;
|
||||
|
||||
-- =====================
|
||||
-- Issue ↔ Pull Request link
|
||||
-- =====================
|
||||
|
||||
-- name: LinkIssueToPullRequest :exec
|
||||
INSERT INTO issue_pull_request (
|
||||
issue_id, pull_request_id, linked_by_type, linked_by_id
|
||||
) VALUES (
|
||||
$1, $2, sqlc.narg('linked_by_type'), sqlc.narg('linked_by_id')
|
||||
)
|
||||
ON CONFLICT (issue_id, pull_request_id) DO NOTHING;
|
||||
|
||||
-- name: UnlinkIssueFromPullRequest :exec
|
||||
DELETE FROM issue_pull_request
|
||||
WHERE issue_id = $1 AND pull_request_id = $2;
|
||||
Reference in New Issue
Block a user