From f8d058281499d81ebcfa412331564a65c348ed3f Mon Sep 17 00:00:00 2001 From: Lince Mathew Date: Sat, 17 Oct 2026 15:32:51 +0530 Subject: [PATCH 1/4] Enable auto-approve by default and fix connector delete Every org gets a default auto-approve policy (backfill plus an orgs insert trigger). Repo override policies now cascade when a repo is deleted, so deleting a connector no longer fails. LiveReview Pre-Commit Check: vouched (iter:1, coverage:0%) --- ...approve_policies_repository_fk_cascade.sql | 13 +++++++++ ...1017100000_default_auto_approve_policy.sql | 26 ++++++++++++++++++ db/schema.sql | 27 +++++++++++++++++-- internal/policyengine/policy_test.go | 14 ++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 db/migrations/20261017090000_auto_approve_policies_repository_fk_cascade.sql create mode 100644 db/migrations/20261017100000_default_auto_approve_policy.sql diff --git a/db/migrations/20261017090000_auto_approve_policies_repository_fk_cascade.sql b/db/migrations/20261017090000_auto_approve_policies_repository_fk_cascade.sql new file mode 100644 index 00000000..e75db1fa --- /dev/null +++ b/db/migrations/20261017090000_auto_approve_policies_repository_fk_cascade.sql @@ -0,0 +1,13 @@ +-- migrate:up + +-- Deleting a connector cascades to its repositories; repo override policies blocked that. +-- A repo's override policy is meaningless without the repo, so delete it too. +ALTER TABLE auto_approve_policies DROP CONSTRAINT auto_approve_policies_repository_id_fkey; +ALTER TABLE auto_approve_policies ADD CONSTRAINT auto_approve_policies_repository_id_fkey + FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE; + +-- migrate:down + +ALTER TABLE auto_approve_policies DROP CONSTRAINT auto_approve_policies_repository_id_fkey; +ALTER TABLE auto_approve_policies ADD CONSTRAINT auto_approve_policies_repository_id_fkey + FOREIGN KEY (repository_id) REFERENCES repositories(id); diff --git a/db/migrations/20261017100000_default_auto_approve_policy.sql b/db/migrations/20261017100000_default_auto_approve_policy.sql new file mode 100644 index 00000000..006c4655 --- /dev/null +++ b/db/migrations/20261017100000_default_auto_approve_policy.sql @@ -0,0 +1,26 @@ +-- migrate:up + +-- Auto-approve is on by default: every org gets an org-level policy with the column defaults. +INSERT INTO auto_approve_policies (org_id) +SELECT o.id FROM orgs o +WHERE NOT EXISTS ( + SELECT 1 FROM auto_approve_policies p WHERE p.org_id = o.id AND p.repository_id IS NULL +); + +CREATE FUNCTION create_default_auto_approve_policy() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + INSERT INTO auto_approve_policies (org_id) VALUES (NEW.id); + RETURN NEW; +END; +$$; + +CREATE TRIGGER orgs_default_auto_approve_policy + AFTER INSERT ON orgs + FOR EACH ROW EXECUTE FUNCTION create_default_auto_approve_policy(); + +-- migrate:down + +DROP TRIGGER IF EXISTS orgs_default_auto_approve_policy ON orgs; +DROP FUNCTION IF EXISTS create_default_auto_approve_policy(); diff --git a/db/schema.sql b/db/schema.sql index 30e65c76..c7c79636 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -113,6 +113,20 @@ END $$; +-- +-- Name: create_default_auto_approve_policy(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.create_default_auto_approve_policy() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + INSERT INTO auto_approve_policies (org_id) VALUES (NEW.id); + RETURN NEW; +END; +$$; + + -- -- Name: license_seat_assignments_set_updated_at(); Type: FUNCTION; Schema: public; Owner: - -- @@ -6120,6 +6134,13 @@ CREATE TRIGGER trg_org_billing_state_audit AFTER UPDATE ON public.org_billing_st CREATE TRIGGER trg_org_billing_state_updated_at BEFORE UPDATE ON public.org_billing_state FOR EACH ROW EXECUTE FUNCTION public.org_billing_state_set_updated_at(); +-- +-- Name: orgs orgs_default_auto_approve_policy; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER orgs_default_auto_approve_policy AFTER INSERT ON public.orgs FOR EACH ROW EXECUTE FUNCTION public.create_default_auto_approve_policy(); + + -- -- Name: plan_catalog trg_plan_catalog_updated_at; Type: TRIGGER; Schema: public; Owner: - -- @@ -6216,7 +6237,7 @@ ALTER TABLE ONLY public.auto_approve_policies -- ALTER TABLE ONLY public.auto_approve_policies - ADD CONSTRAINT auto_approve_policies_repository_id_fkey FOREIGN KEY (repository_id) REFERENCES public.repositories(id); + ADD CONSTRAINT auto_approve_policies_repository_id_fkey FOREIGN KEY (repository_id) REFERENCES public.repositories(id) ON DELETE CASCADE; -- @@ -7258,4 +7279,6 @@ INSERT INTO public.schema_migrations (version) VALUES ('20260905090000'), ('20261012090000'), ('20261012100000'), - ('20261013090000'); + ('20261013090000'), + ('20261017090000'), + ('20261017100000'); diff --git a/internal/policyengine/policy_test.go b/internal/policyengine/policy_test.go index 55cb838c..022daa1d 100644 --- a/internal/policyengine/policy_test.go +++ b/internal/policyengine/policy_test.go @@ -38,6 +38,7 @@ func seedOrg(t *testing.T, db *sql.DB, name string) int64 { // already inserted a row for this org by the time the test ends; its FK // has no cascade, so it must be cleared first or the org delete leaks. _, _ = db.Exec(`DELETE FROM dashboard_cache WHERE org_id = $1`, orgID) + _, _ = db.Exec(`DELETE FROM auto_approve_policies WHERE org_id = $1`, orgID) _, _ = db.Exec(`DELETE FROM orgs WHERE id = $1`, orgID) }) return orgID @@ -45,6 +46,15 @@ func seedOrg(t *testing.T, db *sql.DB, name string) int64 { func insertPolicy(t *testing.T, db *sql.DB, orgID int64, repoID *int64, mode string, minLevel int) int64 { var id int64 + if repoID == nil { + // Every new org already gets a default org-level row from a trigger; reuse it. + if err := db.QueryRow(` + UPDATE auto_approve_policies SET mode = $2, min_author_level = $3 + WHERE org_id = $1 AND repository_id IS NULL RETURNING id`, orgID, mode, minLevel).Scan(&id); err != nil { + t.Fatalf("update default policy: %v", err) + } + return id + } if err := db.QueryRow(` INSERT INTO auto_approve_policies (org_id, repository_id, mode, min_author_level) VALUES ($1, $2, $3, $4) RETURNING id`, orgID, repoID, mode, minLevel).Scan(&id); err != nil { @@ -62,6 +72,10 @@ func insertPolicy(t *testing.T, db *sql.DB, orgID int64, repoID *int64, mode str func TestResolvePolicyNoneConfigured(t *testing.T) { db := testDB(t) orgID := seedOrg(t, db, "policy-resolve-none") + // Drop the default row the org-insert trigger created. + if _, err := db.Exec(`DELETE FROM auto_approve_policies WHERE org_id = $1`, orgID); err != nil { + t.Fatalf("delete default policy: %v", err) + } _, found, err := ResolvePolicy(context.Background(), db, orgID, 0) if err != nil { From 388fe840362eede9f806605309e70d0ae25c615f Mon Sep 17 00:00:00 2001 From: Lince Mathew Date: Sat, 17 Oct 2026 15:32:58 +0530 Subject: [PATCH 2/4] Run auto-approve on manual PR reviews and group PR policy links Reviews started from New Review or the Explore PR table now get the policy check and Auto Approve tab. Trigger Review in Explore opens the new review; the PR description links to repo and org policy under the decisions table. LiveReview Pre-Commit Check: vouched (iter:1, coverage:0%) --- internal/api/pull_request_review_trigger.go | 22 +++++++++++-------- internal/api/review_service.go | 1 + .../routes_guide/explore/merge-requests.md | 3 ++- .../routes_guide/reviews/pr-review-link.md | 14 +++++++++--- internal/jobqueue/repo_sync_worker.go | 14 +++++++----- ui/src/pages/Explore/MergeRequests.tsx | 22 ++----------------- 6 files changed, 37 insertions(+), 39 deletions(-) diff --git a/internal/api/pull_request_review_trigger.go b/internal/api/pull_request_review_trigger.go index a1015647..07f80397 100644 --- a/internal/api/pull_request_review_trigger.go +++ b/internal/api/pull_request_review_trigger.go @@ -98,15 +98,7 @@ func (s *Server) createReview(c echo.Context) error { return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()}) } s.enrichMetadata(ctx) - if req.FromPRLink { - // Marks the review so the worker runs the approval policy check once it completes. - if _, err := s.db.Exec( - `UPDATE reviews SET metadata = COALESCE(metadata, '{}'::jsonb) || '{"pr_link_policy": true}'::jsonb WHERE id = $1 AND org_id = $2`, - ctx.review.ID, orgID, - ); err != nil { - log.Printf("[WARN] createReview: failed to mark review %d for policy check: %v", ctx.review.ID, err) - } - } + s.markForPolicyCheck(ctx.review.ID, orgID) s.trackActivity(ctx) if err := s.launchBackgroundProcessing(ctx); err != nil { log.Printf("[ERROR] createReview: Failed to queue manual review: %v", err) @@ -152,3 +144,15 @@ func (s *Server) getPullRequestOrg(c echo.Context) error { } return JSONWithEnvelope(c, http.StatusOK, map[string]interface{}{"org_id": orgID}) } + +// markForPolicyCheck flags a manual PR review so the worker runs the approval policy check once it completes. +// Reviews not linked to a known PR are left alone. +func (s *Server) markForPolicyCheck(reviewID, orgID int64) { + if _, err := s.db.Exec( + `UPDATE reviews SET metadata = COALESCE(metadata, '{}'::jsonb) || '{"pr_link_policy": true}'::jsonb + WHERE id = $1 AND org_id = $2 AND pull_request_id IS NOT NULL`, + reviewID, orgID, + ); err != nil { + log.Printf("[WARN] failed to mark review %d for policy check: %v", reviewID, err) + } +} diff --git a/internal/api/review_service.go b/internal/api/review_service.go index fd6066e1..2be4c56f 100644 --- a/internal/api/review_service.go +++ b/internal/api/review_service.go @@ -138,6 +138,7 @@ func (s *Server) TriggerReviewV2(c echo.Context) error { // Phase 4: Enrich metadata (fetch MR details, update DB) s.enrichMetadata(ctx) + s.markForPolicyCheck(ctx.review.ID, ctx.orgID) // Phase 5: Track activity (log the trigger event) s.trackActivity(ctx) diff --git a/internal/docindex/docs/routes_guide/explore/merge-requests.md b/internal/docindex/docs/routes_guide/explore/merge-requests.md index 14604ff9..88fec3c7 100644 --- a/internal/docindex/docs/routes_guide/explore/merge-requests.md +++ b/internal/docindex/docs/routes_guide/explore/merge-requests.md @@ -16,7 +16,8 @@ Any authenticated org member. - Search/filter/sort PRs by provider, repo, state, etc. - Trigger a review directly for a specific pull request - (`triggerReviewForPullRequest`). + (`triggerReviewForPullRequest`). It opens the new review's page right away, + and the review runs the auto-approve policy check when it finishes. ## Related pages diff --git a/internal/docindex/docs/routes_guide/reviews/pr-review-link.md b/internal/docindex/docs/routes_guide/reviews/pr-review-link.md index cc84d313..147c26c5 100644 --- a/internal/docindex/docs/routes_guide/reviews/pr-review-link.md +++ b/internal/docindex/docs/routes_guide/reviews/pr-review-link.md @@ -6,12 +6,19 @@ ## Purpose When a new pull/merge request is opened on GitHub, GitLab, Bitbucket, Gitea, or Azure DevOps in a repo with the -LiveReview webhook installed, LiveReview adds a section with two links to the end of its description: +LiveReview webhook installed, LiveReview adds a section to the end of its description: - **Start a review** → this page. It starts a LiveReview review of that PR and opens the review's detail page. - **Review history** → the [Reviews list](reviews-list.md) filtered to that PR (`/reviews?pull_request_id=`). +- A line under the auto-approval decisions (or under the two links before any + decision): "Customize risk-based approval policy for **this repository** or + **your organization**". The repository link opens the repo's settings page, + Auto Approve section (`/explore/repositories//settings#auto-approve`); + the organization link opens [Settings → Auto Approve Policies](../settings/auto-approve.md) + (`/settings#auto-approve`). Both pages are for org owners and admins, and + open in the user's current org. `:id` is LiveReview's internal pull request id, not the GitHub/GitLab PR number. The section is added once per PR, only when it's opened (not for PRs that @@ -49,8 +56,9 @@ aren't members of that org see "pull request wasn't found". - Azure DevOps: a comment thread, then the bot votes Approved or "Waiting for author". - The decisions table shows the last 10 policy-checked reviews. -- Reviews started any other way (reviewer assignment, @mentions, Explore) don't - run the policy check. +- Reviews started by hand for a known PR also run the policy check and get the + Auto Approve tab: pasting the PR URL on New Review, or Start review in the + Explore PR table. Reviewer assignment and @mention reviews don't. ## Related pages diff --git a/internal/jobqueue/repo_sync_worker.go b/internal/jobqueue/repo_sync_worker.go index f44d698c..09f5fb4c 100644 --- a/internal/jobqueue/repo_sync_worker.go +++ b/internal/jobqueue/repo_sync_worker.go @@ -278,7 +278,7 @@ const ( legacySectionEnd = "" ) -// postLinksComment adds the "Start a review / Review history" links to the end of a new PR/MR's description. +// postLinksComment adds the LiveReview links (review, history, repo/org policy) to the end of a new PR/MR's description. // The links_comment_posted_at claim stops duplicate webhook deliveries from adding it twice. func (w *PRStateSyncWorker) postLinksComment(ctx context.Context, repositoryID, pullRequestID int64, number int) error { host, err := w.webhookStore.GetWebhookPublicEndpoint(ctx) @@ -299,7 +299,7 @@ func (w *PRStateSyncWorker) postLinksComment(ctx context.Context, repositoryID, info, err := loadRepositoryConnectorInfo(w.db, repositoryID) if err == nil { - err = writePRSection(ctx, info, number, buildPRLinksSection(host, pullRequestID, "")) + err = writePRSection(ctx, info, number, buildPRLinksSection(host, pullRequestID, repositoryID, "")) } if err != nil { // Release the claim so River's retry can add it. @@ -312,7 +312,7 @@ func (w *PRStateSyncWorker) postLinksComment(ctx context.Context, repositoryID, } // buildPRLinksSection renders the LiveReview section; history is the review history table, or "". -func buildPRLinksSection(host string, pullRequestID int64, history string) string { +func buildPRLinksSection(host string, pullRequestID, repositoryID int64, history string) string { host = strings.TrimSuffix(host, "/") return fmt.Sprintf(`%s @@ -322,8 +322,10 @@ func buildPRLinksSection(host string, pullRequestID int64, history string) strin - ▶️ **[Start a review](%s/#/pr/%d/review)**: reviews all changes in this PR against its target branch - 📜 **[Review history](%s/#/reviews?pull_request_id=%d)**: all past reviews of this PR -%s%s -`, prSectionStart, host, pullRequestID, host, pullRequestID, history, prSectionEnd) +%s⚙️ Customize risk-based approval policy for [this repository](%s/#/explore/repositories/%d/settings#auto-approve) or [your organization](%s/#/settings#auto-approve). + +%s +`, prSectionStart, host, pullRequestID, host, pullRequestID, history, host, repositoryID, host, prSectionEnd) } // refreshPRSection rewrites the LiveReview section with one history row per policy-checked review of this PR. @@ -369,7 +371,7 @@ func refreshPRSection(ctx context.Context, db *sql.DB, info *repositoryConnector return err } b.WriteString("\n") - return writePRSection(ctx, info, number, buildPRLinksSection(host, pullRequestID, b.String())) + return writePRSection(ctx, info, number, buildPRLinksSection(host, pullRequestID, info.RepositoryID, b.String())) } // decisionReason is the readable "why" for one decision row: each failing stage's reason, or that all passed. diff --git a/ui/src/pages/Explore/MergeRequests.tsx b/ui/src/pages/Explore/MergeRequests.tsx index be95c8b7..ad929a15 100644 --- a/ui/src/pages/Explore/MergeRequests.tsx +++ b/ui/src/pages/Explore/MergeRequests.tsx @@ -96,7 +96,6 @@ const MergeRequests: React.FC = () => { // In-memory only (not persisted) - reverts to "Trigger Review" on a page // refresh or re-visit, since we have no reliable way to know a triggered // review's live status from here. - const [triggeredReviewIds, setTriggeredReviewIds] = useState>({}); const [repositoryFilterName, setRepositoryFilterName] = useState(null); // repository_id stays a server-side fetch scope (driven by the URL, e.g. @@ -140,8 +139,7 @@ const MergeRequests: React.FC = () => { setTriggeringId(pr.id); try { const response = await triggerReviewForPullRequest(pr.id); - setTriggeredReviewIds((prev) => ({ ...prev, [pr.id]: response.reviewId })); - notify.success(`Review triggered for #${pr.number}`); + navigate(`/reviews/${response.reviewId}`); } catch (err) { console.error('Failed to trigger review:', err); notify.error(err instanceof Error ? err.message : 'Failed to trigger review'); @@ -316,22 +314,6 @@ const MergeRequests: React.FC = () => { header: () => Actions, cell: ({ row }) => { const pr = row.original; - const triggeredReviewId = triggeredReviewIds[pr.id]; - if (triggeredReviewId) { - return ( - - ); - } return ( diff --git a/ui/src/components/Connector/BitbucketConnector.tsx b/ui/src/components/Connector/BitbucketConnector.tsx index c35f3360..197bb78d 100644 --- a/ui/src/components/Connector/BitbucketConnector.tsx +++ b/ui/src/components/Connector/BitbucketConnector.tsx @@ -33,7 +33,7 @@ const BitbucketConnector: React.FC = () => { return (
-
diff --git a/ui/src/components/Connector/GitHubConnector.tsx b/ui/src/components/Connector/GitHubConnector.tsx index 607d2340..129a314a 100644 --- a/ui/src/components/Connector/GitHubConnector.tsx +++ b/ui/src/components/Connector/GitHubConnector.tsx @@ -40,7 +40,7 @@ const GitHubConnector: React.FC = () => { return (
-
diff --git a/ui/src/components/Connector/GitLabComConnector.tsx b/ui/src/components/Connector/GitLabComConnector.tsx index 60528f0a..07865c57 100644 --- a/ui/src/components/Connector/GitLabComConnector.tsx +++ b/ui/src/components/Connector/GitLabComConnector.tsx @@ -42,7 +42,7 @@ const GitLabComConnector: React.FC = () => { return (
-
diff --git a/ui/src/components/Connector/GitLabSelfHostedConnector.tsx b/ui/src/components/Connector/GitLabSelfHostedConnector.tsx index 1a51101d..f679e0a1 100644 --- a/ui/src/components/Connector/GitLabSelfHostedConnector.tsx +++ b/ui/src/components/Connector/GitLabSelfHostedConnector.tsx @@ -42,7 +42,7 @@ const GitLabSelfHostedConnector: React.FC = () => { return (
-
diff --git a/ui/src/components/Connector/GiteaConnector.tsx b/ui/src/components/Connector/GiteaConnector.tsx index 8f29a215..24253479 100644 --- a/ui/src/components/Connector/GiteaConnector.tsx +++ b/ui/src/components/Connector/GiteaConnector.tsx @@ -9,7 +9,7 @@ const GiteaConnector: React.FC = () => { return (
-
diff --git a/ui/src/components/UIPrimitives.tsx b/ui/src/components/UIPrimitives.tsx index 7560b1c4..5cbf6a5b 100644 --- a/ui/src/components/UIPrimitives.tsx +++ b/ui/src/components/UIPrimitives.tsx @@ -867,6 +867,12 @@ export const Icons = { ), + Back: () => ( + + + + ), + // Tree view icons ChevronRight: () => ( diff --git a/ui/src/pages/Checkout/TeamCheckout.tsx b/ui/src/pages/Checkout/TeamCheckout.tsx index f687109e..c7c2de44 100644 --- a/ui/src/pages/Checkout/TeamCheckout.tsx +++ b/ui/src/pages/Checkout/TeamCheckout.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { Icons } from '../../components/UIPrimitives'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useOrgContext } from '../../hooks/useOrgContext'; import apiClient from '../../api/apiClient'; @@ -831,9 +832,7 @@ const TeamCheckout: React.FC = () => { onClick={() => navigate('/subscribe')} className="inline-flex items-center text-slate-400 hover:text-white mb-4 transition-colors" > - - - + Back to pricing

diff --git a/ui/src/pages/CiRulesets/CiRulesetIntegration.tsx b/ui/src/pages/CiRulesets/CiRulesetIntegration.tsx index 0a112b12..c6595804 100644 --- a/ui/src/pages/CiRulesets/CiRulesetIntegration.tsx +++ b/ui/src/pages/CiRulesets/CiRulesetIntegration.tsx @@ -424,6 +424,7 @@ const CiRulesetIntegration: React.FC = () => {
); if (error) { diff --git a/ui/src/pages/GitProviders/ConnectorDetails.tsx b/ui/src/pages/GitProviders/ConnectorDetails.tsx index 34b4218c..4065f2d5 100644 --- a/ui/src/pages/GitProviders/ConnectorDetails.tsx +++ b/ui/src/pages/GitProviders/ConnectorDetails.tsx @@ -667,10 +667,10 @@ const ConnectorDetails: React.FC = () => {
@@ -691,10 +691,10 @@ const ConnectorDetails: React.FC = () => {
diff --git a/ui/src/pages/Licenses/LicenseAssignment.tsx b/ui/src/pages/Licenses/LicenseAssignment.tsx index 2798c363..ad1170fd 100644 --- a/ui/src/pages/Licenses/LicenseAssignment.tsx +++ b/ui/src/pages/Licenses/LicenseAssignment.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { Icons } from '../../components/UIPrimitives'; import { useNavigate, useParams } from 'react-router-dom'; import moment from 'moment-timezone'; import { useOrgContext } from '../../hooks/useOrgContext'; @@ -315,9 +316,7 @@ const LicenseAssignment: React.FC = () => { onClick={() => navigate('/subscribe/manage')} className="inline-flex items-center text-slate-400 hover:text-white mb-4 transition-colors" > - - - + Back to Subscriptions
diff --git a/ui/src/pages/Reviews/CreateReviewCLI.tsx b/ui/src/pages/Reviews/CreateReviewCLI.tsx index 20a3498a..36283d18 100644 --- a/ui/src/pages/Reviews/CreateReviewCLI.tsx +++ b/ui/src/pages/Reviews/CreateReviewCLI.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Card, PageHeader, Spinner } from '../../components/UIPrimitives'; +import { Button, Card, Icons, PageHeader, Spinner } from '../../components/UIPrimitives'; import { useDashboardQuery } from '../../api/dashboard'; import { OnboardingSteps } from '../../components/Dashboard/OnboardingSteps'; import { ErrorBoundary } from '../../components/ErrorBoundary'; @@ -37,6 +37,9 @@ const CreateReviewCLI: React.FC = () => { return (
+ { + const navigate = useNavigate(); return (
+ { return Math.max(1, Math.ceil(diffMs / (24 * 60 * 60 * 1000))); }; +// URL formats shown as provider chips. +const URL_EXAMPLES = [ + { provider: 'github', label: 'GitHub', url: 'https://github.com/owner/repo/pull/123' }, + { provider: 'gitlab', label: 'GitLab', url: 'https://gitlab.com/group/project/-/merge_requests/123' }, + { provider: 'bitbucket', label: 'Bitbucket', url: 'https://bitbucket.org/workspace/repo/pull-requests/123' }, + { provider: 'gitea', label: 'Gitea', url: 'https://gitea.example.com/owner/repo/pulls/123' }, + { provider: 'azuredevops', label: 'Azure DevOps', url: 'https://dev.azure.com/org/project/_git/repo/pullrequest/123' }, +]; + +const isUrlLike = (value: string) => /^https?:\/\//i.test(value.trim()); + const NewReview: React.FC = () => { const navigate = useNavigate(); const { isFreePlan, isSuperAdmin } = useOrgContext(); @@ -78,7 +92,11 @@ const NewReview: React.FC = () => { const [loading, setLoading] = useState(false); const [loadingConnectors, setLoadingConnectors] = useState(true); const [error, setError] = useState(null); - const [success, setSuccess] = useState(null); + const [suggestions, setSuggestions] = useState([]); + const [suggestOpen, setSuggestOpen] = useState(false); + const [activeIndex, setActiveIndex] = useState(0); + const [showOnPRSteps, setShowOnPRSteps] = useState(false); + const suggestRequestRef = useRef(0); const [showUpgradeModal, setShowUpgradeModal] = useState(false); const [upgradeReason, setUpgradeReason] = useState<'DAILY_LIMIT' | 'NOT_ORG_CREATOR'>('DAILY_LIMIT'); const [limitInfo, setLimitInfo] = useState<{ used: number; limit: number }>({ used: 3, limit: 3 }); @@ -131,38 +149,41 @@ const NewReview: React.FC = () => { ); }; - // Handle form submission - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (isReadOnly) { - setShowPlanUpgradeDialog(true); - return; - } - setError(null); - setSuccess(null); - - // Validate URL - if (!url) { - setError('Please enter a URL'); - return; - } + // Provider of the connector matching the typed URL, for the input icon. + const detectedProvider = (() => { + const baseUrl = extractBaseUrl(url); + if (!baseUrl) return ''; + return connectors.find((c) => c.provider_url && c.provider_url.includes(baseUrl))?.provider || ''; + })(); - try { - // Validate URL format - new URL(url); - } catch (err) { - setError('Please enter a valid URL'); + // Open PRs matching the typed text (repo, title or author). + useEffect(() => { + if (isUrlLike(url) || !url.trim()) { + setSuggestions([]); return; } - - // Check if URL is from a supported provider - if (!isUrlSupported(url)) { - setError( - 'This URL is not from a connected Git provider. Please connect the appropriate Git provider first.' - ); + const requestId = ++suggestRequestRef.current; + const timer = setTimeout(() => { + getPullRequests({ state: 'open', search: url.trim(), perPage: 8 }) + .then((res) => { + if (requestId !== suggestRequestRef.current) return; + setSuggestions(res.pull_requests || []); + setActiveIndex(0); + }) + .catch(() => { + if (requestId === suggestRequestRef.current) setSuggestions([]); + }); + }, 300); + return () => clearTimeout(timer); + }, [url]); + + // Shared plan/quota gating and error handling for both ways of starting a review. + const startReview = async (run: () => Promise<{ reviewId: string }>) => { + if (isReadOnly) { + setShowPlanUpgradeDialog(true); return; } - + setError(null); if (quotaStatus?.envelope?.trial_readonly) { setError('Trial is read-only for this organization. Upgrade your plan to resume review creation.'); return; @@ -173,19 +194,9 @@ const NewReview: React.FC = () => { } setLoading(true); - try { - const request: TriggerReviewRequest = { url }; - const response = await triggerReview(request); - - setSuccess(`Review successfully triggered! Review ID: ${response.reviewId}`); - setUrl(''); - - // Navigate directly to the new review's detail page - setTimeout(() => { - navigate(`/reviews/${response.reviewId}`); - }, 1500); - + const response = await run(); + navigate(`/reviews/${response.reviewId}`); } catch (err: any) { console.error('Error triggering review:', err); @@ -226,26 +237,78 @@ const NewReview: React.FC = () => { } }; + const startForPullRequest = (pr: PullRequestWithRepo) => { + setSuggestOpen(false); + startReview(() => triggerReviewForPullRequest(pr.id)); + }; + + // Handle form submission: a pasted URL, or the highlighted suggestion. + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (!url.trim()) { + setError('Paste a pull request URL, or type a repository or PR title.'); + return; + } + if (!isUrlLike(url)) { + const pick = suggestions[activeIndex]; + if (pick) startForPullRequest(pick); + else setError('No open pull requests match. Paste the PR URL instead.'); + return; + } + + try { + new URL(url); + } catch (err) { + setError('Please enter a valid URL'); + return; + } + + // Check if URL is from a supported provider + if (!isUrlSupported(url)) { + setError( + 'This URL is not from a connected Git provider. Please connect the appropriate Git provider first.' + ); + return; + } + + startReview(() => triggerReview({ url })); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!suggestOpen || suggestions.length === 0) return; + if (e.key === 'ArrowDown') { + e.preventDefault(); + setActiveIndex((i) => (i + 1) % suggestions.length); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setActiveIndex((i) => (i - 1 + suggestions.length) % suggestions.length); + } else if (e.key === 'Escape') { + setSuggestOpen(false); + } + }; + + const submitBlocked = + loading || + !url.trim() || + Boolean(quotaStatus?.envelope?.blocked) || + Boolean(quotaStatus?.envelope?.trial_readonly) || + (quotaStatus?.envelope?.usage_percent ?? 0) >= 100 || + quotaStatus?.can_trigger_reviews === false; + return ( -
+
+ navigate(-1)} - icon={ - - } - > - Back - - } + description="Review any pull or merge request." /> - + {loadingConnectors ? (
@@ -267,11 +330,6 @@ const NewReview: React.FC = () => { /> ) : (
- {/* Safety Banner */} - - -

Enter Merge/Pull Request URL

- {/* LOC 90% Warning Banner */} {/* LOC 100% Exhausted Banner */} {!quotaStatus?.envelope?.blocked && !quotaStatus?.envelope?.trial_readonly && (quotaStatus?.envelope?.usage_percent ?? 0) >= 100 && ( @@ -343,83 +401,141 @@ const NewReview: React.FC = () => { )} {error && ( - } - > + }> {error} )} - {success && ( - } - > -
-
{success}
-
Redirecting to reviews list...
-
-
- )} - -
- setUrl(e.target.value)} - disabled={loading} - icon={ - - } - helperText="Enter the URL of the merge/pull request to start a review" - /> -
- - {/* URL Examples */} -
-

Supported URL Examples:

-
-
• GitLab: https://gitlab.com/group/project/-/merge_requests/123
-
• GitHub: https://github.com/owner/repo/pull/123
-
• Bitbucket: https://bitbucket.org/workspace/repo/pull-requests/123
-
• Azure DevOps: https://dev.azure.com/org/project/_git/repo/pullrequest/123
+ +
+
+ 0} + aria-controls="pr-suggestions" + aria-activedescendant={suggestOpen && suggestions[activeIndex] ? `pr-suggestion-${suggestions[activeIndex].id}` : undefined} + placeholder="Paste a PR URL, or type a repo or PR title" + value={url} + onChange={(e) => { setUrl(e.target.value); setSuggestOpen(e.target.value.trim() !== ''); }} + onFocus={() => setSuggestOpen(url.trim() !== '')} + onBlur={() => setTimeout(() => setSuggestOpen(false), 150)} + onKeyDown={handleKeyDown} + disabled={loading} + autoFocus + icon={detectedProvider ? : } + /> + {suggestOpen && suggestions.length > 0 && ( +
    +
  • + Matching open pull requests +
  • + {suggestions.map((pr, i) => ( +
  • { e.preventDefault(); startForPullRequest(pr); }} + onMouseEnter={() => setActiveIndex(i)} + className={`flex items-start gap-3 px-3 py-2 cursor-pointer ${i === activeIndex ? 'bg-slate-700' : ''}`} + > + + + + {pr.repository_full_name.split('/').pop()} + #{pr.number} + {' '}{pr.title} + + + {pr.author_name || pr.author_username || 'unknown'} · {formatRelativeTime(pr.provider_updated_at || pr.last_synced_at)} + + +
  • + ))} +
  • + +
  • +
+ )}
- +
-
- - +
+ Supported: + {URL_EXAMPLES.map((ex) => ( + + + + {ex.label} + + + ))}
)} +

Other ways to review

+
+
+
+ + On the PR +
+

Start a review from the pull request itself.

+ + {showOnPRSteps && ( +
    +
  1. Assign the LiveReview bot as a reviewer.
  2. +
  3. Or @mention the bot in a PR comment.
  4. +
  5. Or click Start a review in the PR description.
  6. +
+ )} +
+ {[ + { icon: , title: 'Pre-commit CLI', text: 'Review staged changes before you commit with git-lrc.', link: 'Install the CLI', to: '/reviews/create-cli' }, + { icon: , title: 'IDE / MCP', text: 'Ask your AI coding agent to review changes through LiveReview\'s MCP server.', link: 'See setup details', to: '/reviews/create-mcp' }, + { icon: , title: 'CI/CD & API', text: 'Gate merges in CI, or start reviews from your own scripts with an API key.', link: 'Set up CI rulesets', to: '/ci-rulesets' }, + { icon: , title: 'Chatbot', text: 'Ask Livi to review a pull request in plain language.', link: 'Open the chat', to: '/chat?prefill=' + encodeURIComponent('Trigger a review for ') }, + { icon: , title: 'Scheduled', text: 'Review whole repositories on a regular schedule.', link: 'Set up a schedule', to: '/reviews/scheduled' }, + ].map((way) => ( +
+
+ {way.icon} + {way.title} +
+

{way.text}

+ +
+ ))} +
+ {/* Upgrade Modal */} { as={Link} to="/reviews" variant="outline" + icon={} > Back to Reviews @@ -519,17 +520,17 @@ const ReviewDetail: React.FC = () => { return (
{/* Header */} +
-

{review.repository.split('/').pop() || review.repository} diff --git a/ui/src/pages/Reviews/ScheduledReviewRuns.tsx b/ui/src/pages/Reviews/ScheduledReviewRuns.tsx index cdbb9a8c..bf723dc5 100644 --- a/ui/src/pages/Reviews/ScheduledReviewRuns.tsx +++ b/ui/src/pages/Reviews/ScheduledReviewRuns.tsx @@ -328,8 +328,8 @@ const ScheduledReviewRuns: React.FC = () => { return (
-
diff --git a/ui/src/pages/Reviews/ScheduledReviews.tsx b/ui/src/pages/Reviews/ScheduledReviews.tsx index 6ac6f54c..587b385e 100644 --- a/ui/src/pages/Reviews/ScheduledReviews.tsx +++ b/ui/src/pages/Reviews/ScheduledReviews.tsx @@ -564,6 +564,9 @@ const ScheduledReviews: React.FC = () => { return (
+ Date: Sat, 17 Oct 2026 15:52:38 +0530 Subject: [PATCH 4/4] Bump kin-openapi to v0.151.0 for OSV advisories Clears six kin-openapi advisories published on 15-16 Oct that fail the OSV scan. LiveReview Pre-Commit Check: vouched (iter:1, coverage:0%) --- go.mod | 6 +++--- go.sum | 12 ++++-------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 3300291b..cf626a11 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 github.com/bwmarrin/discordgo v0.29.0 github.com/d1vbyz3r0/typed v0.2.3 - github.com/getkin/kin-openapi v0.145.0 + github.com/getkin/kin-openapi v0.151.0 github.com/go-co-op/gocron/v2 v2.22.0 github.com/go-swiss/fonts v0.0.0-20230807175105-90067c2f5042 github.com/golang-jwt/jwt/v5 v5.3.1 @@ -69,7 +69,7 @@ require ( google.golang.org/api v0.272.0 ) -require github.com/cdipaolo/goml v0.0.0-20220715001353-00e0c845ae1c // indirect +require github.com/cdipaolo/goml v0.0.0-20220715001353-00e0c845ae1c require ( cel.dev/expr v0.25.2 // indirect @@ -212,7 +212,7 @@ require ( github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sahilm/fuzzy v0.1.3 // indirect - github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/sorairolake/lzip-go v0.3.8 // indirect diff --git a/go.sum b/go.sum index 7fc425f8..70e7eed5 100644 --- a/go.sum +++ b/go.sum @@ -75,10 +75,6 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/HexmosTech/deidentify v1.0.0 h1:yC+CMoFdkyGNgSU4gEyB/AAJbJ3KLjqNuNe3Km/LCDA= github.com/HexmosTech/deidentify v1.0.0/go.mod h1:1+fAOPPy8g/7KoF+OOY673j+3BxAs8xTzjlkNKgMWjU= -github.com/HexmosTech/git-lrc/blastradius v0.0.0-20261009104557-0cc6cb4e1a6c h1:ARY/iCSKMhvEWX3nG5OJVKQEsKc97l/rAsHUICmbozQ= -github.com/HexmosTech/git-lrc/blastradius v0.0.0-20261009104557-0cc6cb4e1a6c/go.mod h1:uAX2wgGb22UPGzmXVs6b8rKRzZnEkfFlnhxiv/ewp9M= -github.com/HexmosTech/git-lrc/blastradius v0.0.0-20261009151730-cb9fce8104d0 h1:4RTFP63IZ6cJJ+ibkAQ9m9sQbCa4Kxu0lkuiKw2++bQ= -github.com/HexmosTech/git-lrc/blastradius v0.0.0-20261009151730-cb9fce8104d0/go.mod h1:uAX2wgGb22UPGzmXVs6b8rKRzZnEkfFlnhxiv/ewp9M= github.com/HexmosTech/git-lrc/blastradius v0.1.0 h1:fiykl9PI7plYe6cnKYT6uojVlzr8/EJb3sZ5Om+W4L8= github.com/HexmosTech/git-lrc/blastradius v0.1.0/go.mod h1:uAX2wgGb22UPGzmXVs6b8rKRzZnEkfFlnhxiv/ewp9M= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= @@ -227,8 +223,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= -github.com/getkin/kin-openapi v0.145.0 h1:htBX+Q7SevVaCUqymFegUKzH2WCbewl9tsmyn2FMGWY= -github.com/getkin/kin-openapi v0.145.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY= +github.com/getkin/kin-openapi v0.151.0 h1:p8NIstL53h620DnlO9R4pR4QRkEzJ3jbw9flb4kc1hg= +github.com/getkin/kin-openapi v0.151.0/go.mod h1:1+BHDzstro+P5CKtPy1X4PfofnFgmRe6uvMy9+r9fKY= github.com/gitleaks/go-gitdiff v0.9.1 h1:ni6z6/3i9ODT685OLCTf+s/ERlWUNWQF4x1pvoNICw0= github.com/gitleaks/go-gitdiff v0.9.1/go.mod h1:pKz0X4YzCKZs30BL+weqBIG7mx0jl4tF1uXV9ZyNvrA= github.com/go-co-op/gocron/v2 v2.22.0 h1:uEuH2F7k7VoESb1BYSaffuuV+T0kkpzsC0aXk7/z79I= @@ -510,8 +506,8 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sahilm/fuzzy v0.1.3 h1:juByESSS32nVD81vr6tHmKmA/8zde7gE+x5CLxrzXPU= github.com/sahilm/fuzzy v0.1.3/go.mod h1:au6//VbVSqu6DFrkL2CfjlJ5iURpNCPeE+1GwY3XsT8= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shrsv/AgentLaws v0.2.1 h1:00pN3dEhsFFtz+qrcqBwVy8ieLzaivHbpyyuAk3+Lsk=