diff --git a/.github/workflows/code-release.yml b/.github/workflows/code-release.yml index 91e7ffd7c..d3f6b93e8 100644 --- a/.github/workflows/code-release.yml +++ b/.github/workflows/code-release.yml @@ -167,8 +167,10 @@ jobs: - name: Extract version from tag id: version shell: pwsh + env: + GITHUB_REF: ${{ github.ref }} run: | - $tagVersion = "${{ github.ref }}" -replace "refs/tags/v", "" + $tagVersion = "$env:GITHUB_REF" -replace "refs/tags/v", "" echo "Version: $tagVersion" echo "version=$tagVersion" >> $env:GITHUB_OUTPUT diff --git a/apps/code/src/main/services/github-integration/service.ts b/apps/code/src/main/services/github-integration/service.ts index 2361b94ab..500df9b02 100644 --- a/apps/code/src/main/services/github-integration/service.ts +++ b/apps/code/src/main/services/github-integration/service.ts @@ -61,7 +61,7 @@ export class GitHubIntegrationService extends TypedEventEmitter { try { const cloudUrl = getCloudUrlFromRegion(region); - const nextPath = `/account/social-connected?provider=github&project_id=${projectId}&connect_from=posthog_code`; + const nextPath = `/account-connected/github-login?provider=github&project_id=${projectId}&connect_from=posthog_code`; const authorizeUrl = `${cloudUrl}/api/environments/${projectId}/integrations/authorize/?kind=github&next=${encodeURIComponent(nextPath)}`; this.clearFlowTimeout(); diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index a7f6b141b..f660aea4b 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -558,6 +558,40 @@ export class PostHogAPIClient { return data.github_login; } + /** + * `POST .../integrations/github/start/`. Optional `teamId` matches app project when session `current_team` differs. + */ + async startGithubUserIntegrationConnect(teamId?: number): Promise<{ + install_url: string; + connect_flow?: "oauth_authorize" | "app_install"; + }> { + const id = teamId ?? (await this.getTeamId()); + const urlPath = `/api/users/@me/integrations/github/start/`; + const url = new URL(`${this.api.baseUrl}${urlPath}`); + const response = await this.api.fetcher.fetch({ + method: "post", + url, + path: urlPath, + overrides: { + body: JSON.stringify({ team_id: id, connect_from: "posthog_code" }), + }, + }); + if (!response.ok) { + const err = (await response.json().catch(() => ({}))) as { + detail?: unknown; + }; + const detail = + typeof err.detail === "string" + ? err.detail + : "Failed to start GitHub connection"; + throw new Error(detail); + } + return (await response.json()) as { + install_url: string; + connect_flow?: "oauth_authorize" | "app_install"; + }; + } + async switchOrganization(orgId: string): Promise { await this.api.patch("/api/users/{uuid}/", { path: { uuid: "@me" }, diff --git a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx index 522cc81b4..1053cc46a 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx @@ -24,7 +24,10 @@ import { isReportUpForReview, } from "@features/inbox/utils/filterReports"; import { INBOX_REFETCH_INTERVAL_MS } from "@features/inbox/utils/inboxConstants"; -import { useRepositoryIntegration } from "@hooks/useIntegrations"; +import { + useIntegrations, + useRepositoryIntegration, +} from "@hooks/useIntegrations"; import { Box, Flex, ScrollArea } from "@radix-ui/themes"; import type { SignalReportsQueryParams } from "@shared/types"; import { useNavigationStore } from "@stores/navigationStore"; @@ -55,7 +58,15 @@ export function InboxSignalsTab() { const { hasGithubIntegration } = useRepositoryIntegration(); // ── Signal source configs ─────────────────────────────────────────────── - const { data: signalSourceConfigs } = useSignalSourceConfigs(); + const { data: signalSourceConfigs, isPending: signalSourceConfigsPending } = + useSignalSourceConfigs(); + const { isPending: integrationsPending, data: integrationsData } = + useIntegrations(); + /** Matches store-backed `hasGithubIntegration`, but uses query data so there is no lag behind the `useIntegrations` → Zustand sync effect. */ + const hasGithubIntegrationFromQuery = useMemo( + () => integrationsData?.some((i) => i.kind === "github") ?? false, + [integrationsData], + ); const hasSignalSources = signalSourceConfigs?.some((c) => c.enabled) ?? false; const enabledProducts = useMemo(() => { const seen = new Set(); @@ -78,6 +89,9 @@ export function InboxSignalsTab() { const isInboxView = useNavigationStore((s) => s.view.type === "inbox"); const inboxPollingActive = windowFocused && isInboxView; + const inboxSourcesPrerequisitesLoaded = + !integrationsPending && !signalSourceConfigsPending; + // ── Data fetching ─────────────────────────────────────────────────────── useInboxAvailableSuggestedReviewers({ enabled: isInboxView, @@ -121,6 +135,40 @@ export function InboxSignalsTab() { staleTime: inboxPollingActive ? INBOX_REFETCH_INTERVAL_MS : 12_000, }); + const didAutoOpenSourcesDialogThisInboxVisitRef = useRef(false); + + useEffect(() => { + if (!isInboxView) { + didAutoOpenSourcesDialogThisInboxVisitRef.current = false; + return; + } + if (!inboxSourcesPrerequisitesLoaded || isLoading || error != null) { + return; + } + if (totalCount <= 0) { + return; + } + const needsSourcesOrGithubSetup = + !hasSignalSources || !hasGithubIntegrationFromQuery; + if (!needsSourcesOrGithubSetup) { + return; + } + if (didAutoOpenSourcesDialogThisInboxVisitRef.current) { + return; + } + didAutoOpenSourcesDialogThisInboxVisitRef.current = true; + setSourcesDialogOpen(true); + }, [ + isInboxView, + inboxSourcesPrerequisitesLoaded, + isLoading, + error, + totalCount, + hasSignalSources, + hasGithubIntegrationFromQuery, + setSourcesDialogOpen, + ]); + const reports = useMemo( () => filterReportsBySearch(allReports, searchQuery), [allReports, searchQuery], diff --git a/apps/code/src/renderer/features/inbox/components/InboxSourcesDialog.tsx b/apps/code/src/renderer/features/inbox/components/InboxSourcesDialog.tsx index 1f1bd1bad..ac740a383 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxSourcesDialog.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxSourcesDialog.tsx @@ -17,10 +17,10 @@ export function InboxSourcesDialog({ }: InboxSourcesDialogProps) { return ( - + - Signal sources + Inbox configuration ); diff --git a/apps/code/src/renderer/features/settings/components/sections/GitHubIntegrationSection.tsx b/apps/code/src/renderer/features/settings/components/sections/GitHubIntegrationSection.tsx index 21fcd1b62..7b498abbd 100644 --- a/apps/code/src/renderer/features/settings/components/sections/GitHubIntegrationSection.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/GitHubIntegrationSection.tsx @@ -56,6 +56,18 @@ export function GitHubIntegrationSection({ } }, [hasGithubIntegration, connecting, stopPolling]); + // Fallback for when the `posthog-code://integration` deep link from PostHog Cloud + // never makes it back to the app (browser blocked the protocol prompt, focus didn't + // return cleanly, etc.). The integrations query has a 5-minute staleTime so the + // global `refetchOnWindowFocus: true` won't refetch it on its own — invalidate + // explicitly while a connect flow is in flight. + useEffect(() => { + if (!connecting) return; + const handleFocus = () => invalidateIntegrations(); + window.addEventListener("focus", handleFocus); + return () => window.removeEventListener("focus", handleFocus); + }, [connecting, invalidateIntegrations]); + useGitHubIntegrationCallback({ onSuccess: () => { stopPolling(); @@ -111,7 +123,7 @@ export function GitHubIntegrationSection({ - Code access + Project-level code access {hasGithubIntegration && !isLoadingRepos &&