Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions apps/code/src/renderer/api/posthogClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,33 @@ export class PostHogAPIClient {
};
}

/** Seed team GitHub setup callback state before opening github.com installation settings. */
async prepareGithubTeamIntegrationCallback(
teamId: number,
next: string,
): Promise<void> {
const urlPath = `/api/environments/${teamId}/integrations/github/prepare_callback/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
const response = await this.api.fetcher.fetch({
method: "post",
url,
path: urlPath,
overrides: {
body: JSON.stringify({ next }),
},
});
if (!response.ok) {
const err = (await response.json().catch(() => ({}))) as {
detail?: unknown;
};
const detail =
typeof err.detail === "string"
? err.detail
: "Failed to prepare GitHub callback";
throw new Error(detail);
}
}

async getGithubUserIntegrations(): Promise<UserGitHubIntegration[]> {
const urlPath = `/api/users/@me/integrations/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface IntegrationAccount {

export interface IntegrationConfig {
account?: IntegrationAccount;
installation_id?: string | number | null;
[key: string]: unknown;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import {
githubInstallationSettingsUrl,
resolveGithubInstallationId,
} from "./githubInstallationSettingsUrl";

describe("githubInstallationSettingsUrl", () => {
it("uses org settings for organization accounts", () => {
expect(
githubInstallationSettingsUrl("99", {
type: "Organization",
name: "posthog",
}),
).toBe(
"https://github.com/organizations/posthog/settings/installations/99",
);
});

it("uses user settings for personal accounts", () => {
expect(
githubInstallationSettingsUrl("42", { type: "User", name: "octocat" }),
).toBe("https://github.com/settings/installations/42");
});
});
describe("resolveGithubInstallationId", () => {
it.each([
[
"prefers top-level installation_id over integration_id and config",
{
id: 99,
kind: "github",
installation_id: "a",
config: { installation_id: "c" },
},
"a",
],
[
"falls back to integration_id when installation_id is absent",
{ id: 1, kind: "github", integration_id: 12345 },
"12345",
],
[
"falls back to config.installation_id as last resort",
{ id: 1, kind: "github", config: { installation_id: "c" } },
"c",
],
])("%s", (_label, input, expected) => {
expect(
resolveGithubInstallationId(
input as Parameters<typeof resolveGithubInstallationId>[0],
),
).toBe(expected);
});
});
Comment thread
Twixes marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Integration } from "../stores/integrationStore";

interface GithubInstallationAccount {
type?: string | null;
name?: string | null;
}

export function githubInstallationSettingsUrl(
installationId: string,
account?: GithubInstallationAccount | null,
): string {
const accountType = account?.type;
const accountName = account?.name;
if (
typeof accountType === "string" &&
accountType.toLowerCase() === "organization" &&
typeof accountName === "string" &&
accountName
) {
return `https://github.com/organizations/${accountName}/settings/installations/${installationId}`;
}
return `https://github.com/settings/installations/${installationId}`;
}

/** Resolves a GitHub App installation id from team or user integration payloads. */
export function resolveGithubInstallationId(
integration: Integration,
): string | null {
const legacy = integration as {
installation_id?: string | null;
integration_id?: string | number | null;
};
const candidates = [
legacy.installation_id,
legacy.integration_id,
integration.config?.installation_id,
];
for (const value of candidates) {
if (value === null || value === undefined) continue;
const id = String(value).trim();
if (id) return id;
}
return null;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient";
import { useAuthStateValue } from "@features/auth/hooks/authQueries";
import {
describeGithubConnectError,
useGithubConnect,
} from "@features/integrations/hooks/useGithubUserConnect";
import { useIntegrationSelectors } from "@features/integrations/stores/integrationStore";
import {
githubInstallationSettingsUrl,
resolveGithubInstallationId,
} from "@features/integrations/utils/githubInstallationSettingsUrl";
import { useRepositoryIntegration } from "@hooks/useIntegrations";
import {
ArrowSquareOutIcon,
Expand All @@ -12,7 +18,9 @@ import {
} from "@phosphor-icons/react";
import { Button } from "@posthog/quill";
import { Box, Flex, Spinner, Text, Tooltip } from "@radix-ui/themes";
import { openUrlInBrowser } from "@utils/browser";
import { useMemo } from "react";
import { toast } from "sonner";

/**
* Past this count, the tooltip would become an unreadable wall of `owner/name`
Expand Down Expand Up @@ -48,6 +56,8 @@ export function GitHubIntegrationSection({
: null,
[repositories],
);
const { githubIntegrations } = useIntegrationSelectors();
const client = useOptionalAuthenticatedClient();
const projectId = useAuthStateValue((state) => state.projectId);
const {
error: connectError,
Expand All @@ -60,6 +70,31 @@ export function GitHubIntegrationSection({
projectHasTeamIntegration: hasGithubIntegration,
});

const handleUpdateInGitHub = async () => {
const integration = githubIntegrations[0];
if (!integration || projectId === null || !client) return;
const installationId = resolveGithubInstallationId(integration);
if (!installationId) {
toast.error("Couldn't find GitHub installation details");
return;
}
const nextPath = `/account-connected/github-integration?provider=github&project_id=${projectId}&connect_from=posthog_code`;
try {
await client.prepareGithubTeamIntegrationCallback(projectId, nextPath);
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Failed to open GitHub settings",
);
return;
}
Comment thread
charlesvien marked this conversation as resolved.
void openUrlInBrowser(
githubInstallationSettingsUrl(
installationId,
integration.config?.account,
),
);
};

if (isLoading) {
return (
<Flex
Expand Down Expand Up @@ -168,7 +203,11 @@ export function GitHubIntegrationSection({
type="button"
variant="outline"
size="sm"
onClick={() => void handleConnect()}
onClick={() =>
hasGithubIntegration
? void handleUpdateInGitHub()
: void handleConnect()
}
>
{hasGithubIntegration
? "Update in GitHub"
Expand Down
Loading