Skip to content

Let several LiveReview accounts share one GitHub App installation, plus /gh-login and link-preview fixes - #139

Merged
lovestaco merged 10 commits into
masterfrom
gh-login-v2
Oct 23, 2026
Merged

lovestaco merged 10 commits into
masterfrom
gh-login-v2

Conversation

@lovestaco

@lovestaco lovestaco commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #137 from the launch dry run: several people from the same GitHub organization can now sign up with GitHub on their own, plus fixes to the /gh-login page and to how LiveReview links look when shared.

1. One GitHub App installation, several LiveReview accounts

Before: once someone connected the App on a GitHub organization (e.g. HexmosTech), anyone else from that organization who signed up got "Your GitHub installations are already connected to other LiveReview accounts" and couldn't sign in.

Now each LiveReview org gets its own connection to the same installation, with no invite needed:

  • Repositories: an org gets every repository the installation covers only if the person who connected it can access all of them. Otherwise it gets just the repositories that person can access. GitHub supplies both lists, so editing the callback URL by hand gains nothing. The list is refreshed on every GitHub sign-in.
  • Webhooks fan out:
    • Install, suspend and uninstall events reach every org.
    • PR updates reach every org that has the repository. Only the oldest connection writes the LiveReview section in the PR description, so it's added once.
    • Comment replies come from the org that reviewed that PR most recently, using its AI provider.
  • "Start a review" / "Review history" links in the PR description open the clicking user's own org's copy of the PR.
  • Git Providers → GitHub App and get-started:
    • Disconnect removes the installation from this org only. The App stays installed on GitHub for anyone else using it.
    • Uninstall removes the App from GitHub, and so from every org. The confirmation says how many other accounts use it.
    • "Also used by N other LiveReview accounts" opens a list of those accounts' owners by masked email (at•••@gmail.com; the server masks them).
    • A note shows when the App was installed by someone else.
  • Migration 20261023090000 replaces the one-org-per-installation unique index with one per (installation, org).

2. /gh-login fixes

  • Landing after a failed GitHub sign-in: the page used to save "go to get-started" before leaving for GitHub, so after a failed attempt, signing in with Google landed on get-started. The callback now decides the landing page only when sign-in succeeds.
  • Other sign-in methods: the small "Other ways to sign in" link is now an "Or sign in with" row of Google, LinkedIn and Email tiles. They go straight to hexmos.com/signin?app=livereview, using the same sign-in URL helper as the Cloud login page.
  • Readability: the three points under the headline are 14px instead of 12px.

3. Title, description and link previews

Shared links (e.g. livereview.hexmos.com/gh-login?src=launch-trial) previewed as "A powerful tool for automated code reviews" with the React boilerplate's og:url and a map-collage image.

  • Content: new title and description that match the launch message, and a 1200×630 og-image.png.
  • Tag format: Open Graph tags now use property=, which Facebook and LinkedIn need.
  • Cloud builds: canonical URL https://livereview.hexmos.com/, indexable.
  • Self-hosted builds: noindex, no canonical.

Linked Issue

No linked issue; found while testing the Product Hunt launch flow (#137) with two HexmosTech accounts.

Validation

  • Go: tests for internal/api, internal/githubapp, internal/jobqueue, storage/providers/... and network/providers/github pass. New tests cover:
    • a second org connecting the same installation, with repositories limited to its user's access, including a hand-edited installation_id
    • the installer org staying unrestricted
    • webhook fan-out and repository coverage
    • plain disconnect versus uninstall-for-everyone
    • picking the org that reviewed most recently
    • resolving a PR link to the user's own org's copy
    • masked emails (full addresses are never sent)
    • landing on get-started only after a successful sign-in
  • UI: tsc --noEmit and make check-status-doc. The webpack config loads, and the index.html template renders the new tags.
  • Migration: tried in a rolled-back transaction, then applied locally.
  • Manual: two accounts on HexmosTech through a dev tunnel and a test GitHub App. The second account signed up without an invite, saw the repositories, and reviewed a PR from the Explore pull requests list.

Checklist

  • This PR fulfills an agreed issue.
  • I kept the change narrow and scoped.
  • I ran the most specific relevant validation and described it above.
  • If I changed behavior, I called that out clearly in this PR.
  • If I touched UI, I attached a GIF or video walkthrough. This is required.
  • If this change touches security, disclosure flow, credentials, storage, network behavior, or licensing/entitlement enforcement, I reviewed SECURITY.md and updated related documentation if needed.
  • I have read and accept the Contributor License Agreement.

Notes For Reviewers

Behavior changes worth a look

  • Repository scope (connectGitHubAppInstallation in internal/api/github_app_auth.go): an org that only sees the installation is limited to its user's repositories. Repositories added to the installation later appear for that org after its next GitHub sign-in.
  • Disconnect: it no longer uninstalls from GitHub (that's now Uninstall). A disconnected org isn't reconnected by a plain GitHub sign-in, only by installing or configuring the App again.
  • Link previews change only after a deploy with a rebuilt UI. Slack caches previews for a while.

Before / after deploy

  • Apply migration 20261023090000.
  • Each org syncs repositories and PRs separately, so N orgs on one installation means N× the GitHub API calls. That's fine at launch scale.

🔍 LiveReview

Auto-approval decisions

Review Decision Reason
#13212 ❌ Not approved Risk score 1.00 (P100) is above the cutoff of 0.75 (P20)

⚙️ Customize risk-based approval policy for this repository or your organization.

Two people from the same GitHub organization can now each sign up with
GitHub: the second person's org gets its own connector for the
installation instead of "already connected to other LiveReview
accounts".

- One connector per (installation, org). An org gets every repository
  the installation covers only if the person who connected it can
  access all of them; otherwise just the repositories they can access
  (GitHub reports both lists, so a hand-edited callback URL gains
  nothing). The list is refreshed on every GitHub sign-in.
- Webhooks fan out: installation and repository events reach every org;
  PR updates reach every org that has the repository, and only the
  oldest connector writes the PR description links; comment replies
  come from the org that reviewed the PR most recently.
- "Start a review" and "Review history" links open the user's own
  org's copy of the PR.
- Git Providers / get-started: separate Disconnect (this org only) and
  Uninstall (removes the App from GitHub for every org, with a warning
  saying how many other accounts use it). Notes when the App was
  installed by someone else.
- Fix: a failed /gh-login no longer leaves a get-started redirect
  behind; the callback sets the landing page only on success.
"Also used by N other LiveReview accounts" on the GitHub App card and
get-started is now a link: it opens a popup listing those accounts'
owners by masked email (first two letters and the domain, e.g.
at•••@gmail.com). The server masks them; full addresses are never sent.
Shared LiveReview links (e.g. /gh-login?src=launch-trial) previewed as
"LiveReview - A powerful tool for automated code reviews" with the
React boilerplate's og:url and a map-collage thumbnail.

- Title and description match the launch messaging.
- Open Graph tags use the property attribute (Facebook and LinkedIn
  ignore name="og:..."), plus og:type, og:site_name and image
  size/alt; Twitter card tags too.
- New 1200x630 og-image.png replaces the boilerplate thumbnail.jpg and
  the SVG og:image social sites don't render.
- Cloud builds: og:url and canonical https://livereview.hexmos.com,
  indexable. Self-hosted builds: noindex, no canonical.
The three points under the headline were 12px; they are now 16px with
larger check marks and a little more spacing.
The small "Other ways to sign in" link with 14px icons is now an
"Or sign in with" row of 44px Google, LinkedIn and Email tiles in
their brand colors, each clickable. All of them open the normal Hexmos
sign-in, as before.
The Google, LinkedIn and Email tiles went to the LiveReview login page
first, which then needed another click. They now go straight to
hexmos.com/signin?app=livereview and come back to the app root. The
sign-in URL is shared with the Cloud login page (utils/hexmosSignin).
@lovestaco lovestaco changed the title Gh login v2 Let several LiveReview accounts share one GitHub App installation, plus /gh-login and link-preview fixes Oct 23, 2026
@livereview-agent-local

Copy link
Copy Markdown

Add Multi-Org GitHub App Installations with Repo Scoping

Overview

This change lets many LiveReview orgs share one GitHub App installation. It scopes each org's repository access to the acting GitHub user's access. It adds disconnect vs uninstall paths and routes shared PR events across orgs.

Technical Highlights

  • db/migrations/661-19-055950000_github_app_shared_installations.sql: Replaces the GitHub App unique index with (provider_url, provider_app_id, org_id).
  • storage/providers/github/token_store.go: Keys connectors by BaseURL, InstallationID, and OrgID; adds RepoScope, AllowedRepoIDs, and cross-org queries.
  • network/providers/github/app.go: Adds ListUserInstallationRepositoryIDs with Link-header pagination capped at 100 pages.
  • internal/api/github_app_auth.go: connectGitHubAppInstallation takes userToken and explicit, returns a resync flag, and webhooks fan out PR syncs.
  • internal/api/auto_webhook_installer.go: Filters user-scoped connector repos and prunes stale repos with DeleteRepositoriesByConnectorExcept.
  • internal/jobqueue/repo_sync_worker.go: Adds SkipLinks to PRStateSyncJobArgs so one connector posts PR links.
  • internal/api/pull_request_review_trigger.go: Resolves a sibling PR in the user's org; response adds pull_request_id.
  • ui/src/components/GitHubApp/GitHubAppAccounts.tsx: Adds Disconnect vs Uninstall modes and an other-accounts modal.
  • ui/src/api/githubApp.ts: Adds repo_scope, other_orgs, other_accounts, and an uninstall flag.
  • ui/meta.config.js: Exports buildMetaConfig(isCloud) and adds OG, Twitter, canonical, and robots tags.

Impact

  • Functionality: Multiple orgs can connect one GitHub App installation; each org sees only repos allowed for its acting user.
  • Risk: Empty or misparsed AllowedRepoIDs prunes all connector repos. Down migration fails if an installation is shared. GetAppConnectorByInstallation signature change breaks callers. Uninstall is cross-tenant destructive.

Comment thread internal/api/auto_webhook_installer.go Outdated
return nil, false
}
allowed = map[string]bool{}
ids, _ := connector.Metadata["allowed_repo_ids"].([]interface{})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: critical

Silent type assertion on allowed_repo_ids. If metadata stores ids as []string/[]int64, or the key is absent, allowed stays empty while scoped=true. Every repo is then skipped and pruned, deleting all connector repositories.

Suggestions:

  1. Validate the assertion and return scoped=false (or an error) when allowed_repo_ids is missing or not []interface{}.
  2. Treat an empty allowed set as 'not scoped' rather than 'nothing allowed' to avoid mass deletion.

for id := range allowed {
keep = append(keep, id)
}
if err := store.DeleteRepositoriesByConnectorExcept(int64(connectorID), keep); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: critical

DeleteRepositoriesByConnectorExcept with an empty keep list deletes every repository for the connector. This is destructive and runs whenever allowed parses to empty.

Suggestions:

  1. Skip the prune call when keep is empty, or guard it behind a positive allowed count.
  2. Log the number of pruned repositories for auditability.

log.Printf("[GitHubApp] disconnect connector %d (org %d) failed: %v", connectorID, orgID, err)
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to disconnect"})
}
if err := repos.DeleteRepositoriesByConnector(connectorID); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

DisconnectAppConnector runs before DeleteRepositoriesByConnector. If repo deletion fails, the connector is already marked removed but repositories remain; a retry may 404 and never clean them.

Suggestions:

  1. Delete repositories first, then mark the connector removed, or make the operation idempotent/retryable.
  2. Ensure GetGitHubTokenByConnectorID still returns removed connectors so retries can finish cleanup.

Comment thread internal/api/github_app_auth.go Outdated
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to disconnect"})
yes := true
for _, r := range recs {
if err := store.SetAppConnectorFlags(r.ID, &yes, nil); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

Loop removes connectors one by one after the GitHub uninstall. A mid-loop failure leaves earlier connectors removed and later ones orphaned, pointing at an App that no longer exists.

Suggestions:

  1. Wrap the per-connector cleanup in a transaction or continue-on-error with a summary, so all connectors are attempted.
  2. Log which connectors were left unprocessed so they can be reconciled.

yes := true
if err := store.SetAppConnectorFlags(connectorID, &yes, nil); err != nil {
log.Printf("[GitHubApp] disconnect connector %d (org %d) failed: %v", connectorID, orgID, err)
recs, err := store.ListAppConnectorsByInstallation(cfg.BaseURL, meta.InstallationID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

?uninstall=true lets one org owner uninstall the App from GitHub, removing every other org's connector for that installation. Cross-tenant impact without other orgs' consent.

Suggestions:

  1. Confirm this cross-org blast radius is intended; consider restricting uninstall to a single-org installation or requiring explicit confirmation.
  2. Surface the number of affected orgs to the caller before uninstalling.

Comment thread internal/api/github_app_auth.go Outdated
if err != nil {
log.Printf("[GitHubApp] installation %q event failed for connector %d: %v", env.Action, rec.ID, err)
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal_error"})
if env.Issue != nil && strings.Contains(env.Issue.HTMLURL, "/pull/") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

Detecting a PR via strings.Contains(htmlURL, "/pull/") is fragile. A repo literally named "pull" yields an issue URL like /owner/pull/issues/1 that matches, misrouting the event to the wrong org.

Suggestions:

  1. Prefer the explicit pull_request field when present, and only fall back to the issue URL.
  2. If parsing the URL, match the path segment structure (e.g. /pull/<number>) instead of a substring.

}
// The other orgs only need their copy of the PR list updated.
if event == "pull_request" && len(targets) > 1 {
if err := s.queueSharedPullRequestState(c, body, targets[1:]); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

A failure queuing shared PR state for secondary orgs returns 500 before the primary org's event is processed. One bad secondary connector blocks the main pipeline.

Suggestions:

  1. Process the primary event first, then queue shared state for the others.
  2. Log and continue on secondary-org queue failures instead of aborting the whole webhook.

case "unsuspend":
err = store.SetAppConnectorFlags(rec.ID, nil, &no)
}
if err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

handleGitHubAppInstallationEvent returns 500 on the first connector error, leaving later connectors unprocessed. State becomes partially applied until GitHub retries.

Suggestions:

  1. Attempt all connectors, collect errors, and return 500 only if any failed.
  2. Ensure the operation is idempotent so retries converge cleanly.

if meta.RepoScope == storagegithub.RepoScopeUser {
scope = storagegithub.RepoScopeUser
}
others, err := store.CountOtherOrgsUsingInstallation(cfg.BaseURL, meta.InstallationID, orgID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

Two DB queries per connector (CountOtherOrgsUsingInstallation and OtherOrgOwnerEmailsForInstallation) inside the loop create an N+1 pattern on the demo endpoint.

Suggestions:

  1. Batch the counts and owner emails for all installation IDs in one query.
  2. Or fetch other-org data once per installation rather than per connector.


// One org handles the event through the normal pipeline: the oldest connector, or for comments the org
// that reviewed this PR most recently (so replies come from that org and its AI provider).
target := targets[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

target := targets[0] relies on ListAppConnectorsByInstallation returning oldest-first. If the query does not guarantee ordering, the wrong org handles the event.

Suggestions:

  1. Confirm the storage query orders by created_at/ID ascending.
  2. If ordering is not guaranteed, sort targets explicitly before selecting.

var ids []int64
cursor := ""
for page := 0; ; page++ {
if page == 100 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

The 100-page cap maps to a repo count that depends on the page size used by ListInstallationRepositories; the error text asserts 10000 repos.

Suggestions:

  1. Derive the limit from the actual page size, or reword the error to state the page cap.
  2. Extract the magic number into a named constant.


// maskEmail keeps the first two characters of the name and the domain: "athreyac4@gmail.com" -> "at•••@gmail.com"
// (one character when the name is that short).
func maskEmail(email string) string {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

maskEmail is defined but not referenced anywhere in this diff. Unused helpers become dead code and untested surface.

Suggestions:

  1. Confirm maskEmail is called from the email display/log path; if not, remove it.
  2. If it is used, add a unit test covering short local parts and missing '@'.

if err == sql.ErrNoRows {
// Orgs sharing a GitHub App installation each keep their own copy of the PR, and the link names one of
// them: open the copy in an org the user belongs to.
orgID, resolvedID, err = pullrequests.NewStore(s.db).SiblingPullRequestForUser(prID, user.ID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

This fallback resolves a PR to another org's copy. SiblingPullRequestForUser must verify the user is a member of that org; matching only by shared PR identity would leak another org's PR.

Suggestions:

  1. Confirm SiblingPullRequestForUser filters by user membership/role, not just PR identity.
  2. Add a test where the user is not a member of the sibling org and expect 404.

return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "failed to look up pull request"})
}
return JSONWithEnvelope(c, http.StatusOK, map[string]interface{}{"org_id": orgID})
return JSONWithEnvelope(c, http.StatusOK, map[string]interface{}{"org_id": orgID, "pull_request_id": resolvedID})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

Response now returns pull_request_id. Clients that only read org_id will keep using the original PR id, which may belong to a different org.

Suggestions:

  1. Update the frontend to consume pull_request_id from this response.
  2. Document the new field in the API contract to prevent stale client usage.

}

if action, _ := event.Metadata["action"].(string); action == "opened" || action == "open" {
if action, _ := event.Metadata["action"].(string); !args.SkipLinks && (action == "opened" || action == "open") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

SkipLinks silently drops the PR description links comment. If the caller ever queues all connectors for a shared installation with SkipLinks=true, no links are posted and nothing errors.

Suggestions:

  1. Guarantee the caller always enqueues exactly one non-skipping job per PR (e.g. the lowest-ID/primary connector).
  2. Add a test asserting a shared-installation PR still gets exactly one links comment.

t.Fatalf("first org's connector changed: %+v", rec)
}
recs, err := store.ListAppConnectorsByInstallation("https://ghes.test", inst)
if err != nil || len(recs) != 2 || recs[0].ID != id || recs[1].ID != idB {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

Test asserts recs[0]/recs[1] order from ListAppConnectorsByInstallation. If the query has no deterministic ORDER BY, this becomes flaky.

Suggestions:

  1. Assert the returned set by ID membership rather than positional order, or document/guarantee ORDER BY id in the store method.

Comment thread storage/providers/github/token_store.go Outdated
var id int64
err := s.db.QueryRow(`
SELECT connector_id FROM reviews
WHERE connector_id = ANY($1) AND (pr_mr_url = $2 OR pr_mr_url LIKE $2 || '/%')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

pr_mr_url LIKE $2 || '/%' treats % and _ in prURL as wildcards. A pasted URL containing these characters over-matches unrelated PRs and can select the wrong connector.

Suggestions:

  1. Escape LIKE metacharacters in prURL before building the pattern
  2. Or use LIKE ... ESCAPE '\' with an escaped pattern

err := s.db.QueryRow(`
SELECT COUNT(*) FROM integration_tokens
WHERE token_type = $1 AND provider_url = $2 AND provider_app_id = $3 AND org_id <> $4
AND COALESCE(metadata->>'removed', 'false') <> 'true'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

Counts suspended connectors as active users. HasAppConnectorForOwnerAllRepos excludes suspended; this query only excludes removed, so the two disagree.

Suggestions:

  1. Add AND COALESCE(metadata->>'suspended','false') <> 'true' to match the live-connector definition used elsewhere

// for the installation, sorted and without duplicates. Callers show them masked only.
func (s *TokenStore) OtherOrgOwnerEmailsForInstallation(baseURL string, installationID, orgID int64) ([]string, error) {
rows, err := s.db.Query(`
SELECT DISTINCT LOWER(u.email)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

Returns raw owner emails of other orgs. A caller that forgets to mask leaks PII across org boundaries.

Suggestions:

  1. Mask emails in SQL (e.g. keep only domain or first char) instead of relying on callers
  2. Or return a non-PII identifier and resolve display separately

Disconnected bool `json:"disconnected"`
}
_ = json.Unmarshal(existingMeta, &prev)
if in.KeepDisconnected && prev.Removed && prev.Disconnected {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

KeepDisconnected defaults to false, so any caller that omits it reconnects a disconnected connector. Webhook or other callers may silently resurrect it.

Suggestions:

  1. Invert the flag to an explicit Reconnect so the zero value keeps the disconnected state
  2. Or require every non-install caller to set KeepDisconnected explicitly

// DeleteRepositoriesByConnectorExcept removes the connector's repositories whose provider id isn't in keep (a
// user-scoped GitHub App connector whose user lost access to some).
func (s *Store) DeleteRepositoriesByConnectorExcept(connectorID int64, keep []string) error {
if keep == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

Converting nil keep to an empty slice makes an empty/nil keep delete every repository for the connector. A caller passing an empty slice after a failed fetch silently wipes all repos.

Suggestions:

  1. Guard against an empty keep list (return an error) unless delete-all is explicitly intended.
  2. Document the destructive empty-input behavior on the method and at call sites.

FROM pull_requests pr
JOIN repositories r ON r.id = pr.repository_id
JOIN integration_tokens t ON t.id = r.connector_id
JOIN pull_requests sib ON sib.provider = pr.provider AND sib.provider_pr_id = pr.provider_pr_id AND sib.id <> pr.id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The sibling join does not exclude a sibling in the same org as the source PR. If an org has two connectors syncing the same PR, this returns a same-org copy, violating the 'another org' contract.

Suggestions:

  1. Add AND sib.org_id <> pr.org_id to the join or WHERE clause.
  2. Add a test covering two connectors in the same org syncing the same PR.

Comment thread ui/meta.config.js
*/
'og:title': title,
'og:description': description,
'og:url': homepage,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

module.exports changed from an object to a function. Any caller still doing require('./meta.config.js') and spreading it will get undefined title/meta and emit empty tags.

Suggestions:

  1. Update the webpack config to call buildMetaConfig(isCloud) and pass siteURL into html-webpack-plugin options.
  2. Verify the build emits title, og:*, and canonical tags for both cloud and self-hosted.

Comment thread ui/src/api/githubApp.ts
*/
export const disconnectGitHubInstallation = (connectorId: number): Promise<{ status: string }> =>
apiClient.delete<{ status: string }>(`/api/v1/github-app/installations/${connectorId}`);
export const disconnectGitHubInstallation = (connectorId: number, uninstall = false): Promise<{ status: string }> =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

uninstall=true uninstalls the App from GitHub for every account using the installation, not just the current org. Callers must confirm this cross-tenant destructive action before invoking.

Suggestions:

  1. Require an explicit confirmation dialog before sending uninstall=true.
  2. Surface other_orgs/other_accounts in the confirmation so users see the blast radius.

const OtherAccountsModal: React.FC<{ inst: GitHubAppInstallation; onClose: () => void }> = ({ inst, onClose }) => {
const unnamed = inst.other_orgs - inst.other_accounts.length;
return createPortal(
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

The modal has no dialog semantics or keyboard handling. Screen readers won't announce it and Escape won't close it.

Suggestions:

  1. Add role="dialog" and aria-modal="true" to the container
  2. Close on Escape via a keydown listener
  3. Move focus into the modal on open and restore it on close

</p>
<ul className="mt-4 space-y-2">
{inst.other_accounts.map((email) => (
<li key={email} className="flex items-center gap-2 text-sm text-slate-200">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: warning

Using the masked email as the React key can collide when two accounts share the same masked address, causing reconciliation bugs.

Suggestions:

  1. Use a stable unique id from the API if available
  2. Otherwise combine the email with a discriminator (e.g. index) to guarantee uniqueness

if (!uninstall) {
return `LiveReview stops reviewing ${inst.account_login}'s pull requests for your account and removes its repositories here. ` +
'The app stays installed on GitHub' +
(inst.other_orgs > 0 ? `, so the ${accountsLabel(inst.other_orgs)} using it keep working` : '') +

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: info

Singular case reads 'the 1 other LiveReview account ... keep working'. The verb should agree with the singular subject.

Suggestions:

  1. Branch on inst.other_orgs === 1 to emit 'keeps working' vs 'keep working'

@livereview-agent-local livereview-agent-local Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ LiveReview policy: Not approved (blocked at Risk score) · Review #13212

Stage Result Detail
Eligibility ✅ author level 1 >= required 1
Static rules ✅ no blocking rulesets configured
Risk score ❌ Risk score 1.00 (P100) is above the cutoff of 0.75 (P20)
LLM classification ✅ class "logic-change", confidence 8
Final decision ❌ risk: Risk score 1.00 (P100) is above the cutoff of 0.75 (P20)

- Repo sync: an unreadable allowed_repo_ids list on a user-scoped
  connector stops the sync instead of counting as "no repositories",
  which would have dropped all of them.
- Comment routing: identify the PR from issue.pull_request instead of
  "/pull/" in the URL (a repo named pull matched), and match review
  URLs with starts_with so % and _ aren't wildcards.
- Webhooks: a failure updating other orgs' copy of a PR no longer
  blocks the org handling the event; installation events and Uninstall
  process every org's connector before reporting a failure.
- Other-accounts popup: dialog semantics, Escape closes it, focus on
  Close, index keys (masked emails can repeat); singular wording fix.
@lovestaco
lovestaco merged commit 7de9fa9 into master Oct 23, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant