diff --git a/front/assets/js/git_integration/app.tsx b/front/assets/js/git_integration/app.tsx index 7757d83a2..14c854402 100644 --- a/front/assets/js/git_integration/app.tsx +++ b/front/assets/js/git_integration/app.tsx @@ -23,6 +23,7 @@ export const App = () => { }/> + }/> }/> }/> diff --git a/front/assets/js/git_integration/components/integration_starter.tsx b/front/assets/js/git_integration/components/integration_starter.tsx index cc1b6bae4..7252982b7 100644 --- a/front/assets/js/git_integration/components/integration_starter.tsx +++ b/front/assets/js/git_integration/components/integration_starter.tsx @@ -1,32 +1,17 @@ -import { useContext } from "preact/hooks"; -import * as stores from "../stores"; +import { NavLink } from "react-router-dom"; interface IntegrationStarterProps { connectButtonUrl: string; } -export const IntegrationStarter = (props: IntegrationStarterProps) => { - const config = useContext(stores.Config.Context); - const submitManifest = (e: Event) => { - e.preventDefault(); - - const urlWithToken = new URL(props.connectButtonUrl); - urlWithToken.searchParams.append(`org_id`, config.orgId); - - window.location.href = urlWithToken.toString(); - }; +export const IntegrationStarter = (_props: IntegrationStarterProps) => { return ( -
- -
+ Connect + ); }; diff --git a/front/assets/js/git_integration/pages/github_app_setup.tsx b/front/assets/js/git_integration/pages/github_app_setup.tsx new file mode 100644 index 000000000..536a627d5 --- /dev/null +++ b/front/assets/js/git_integration/pages/github_app_setup.tsx @@ -0,0 +1,150 @@ +import { useContext, useState } from "preact/hooks"; +import { NavLink } from "react-router-dom"; +import * as stores from "../stores"; + +export const GithubAppSetup = () => { + const config = useContext(stores.Config.Context); + const [accountType, setAccountType] = useState<"personal" | "organization">("personal"); + const [isPublic, setIsPublic] = useState(false); + const [organizationName, setOrganizationName] = useState(""); + + const githubAppIntegration = config.newIntegrations?.find( + (integration) => integration.type === "github_app" + ); + + if (!githubAppIntegration) { + return ( +
+ + ← Back to Integrations + +

GitHub App integration not available

+
+ ); + } + + const submitManifest = (e: Event) => { + e.preventDefault(); + + const urlWithToken = new URL(githubAppIntegration.connectUrl); + urlWithToken.searchParams.append(`org_id`, config.orgId); + urlWithToken.searchParams.append(`is_public`, isPublic ? "true" : "false"); + + if (accountType === "organization" && organizationName.trim()) { + urlWithToken.searchParams.append(`organization`, organizationName.trim()); + } + + window.location.href = urlWithToken.toString(); + }; + + return ( +
+ + ← Back to Integrations + +

GitHub App

+

+ GitHub Cloud integration through installed GitHub App. +

+ +
+
+ +
+

+ Choose where to create the GitHub App and configure its visibility settings. +

+ +
+
+
+ settings + GitHub App Configuration +
+ +
+ +
+ +
+
+ +
+ + {accountType === "organization" && ( +
+ + setOrganizationName((e.target as HTMLInputElement).value)} + placeholder="e.g., my-company" + className="form-control w-100" + required + /> +

+ The exact name of your GitHub organization +

+
+ )} +
+ +
+ + +
+
+ +
+ + + Cancel + +
+
+
+
+ ); +}; diff --git a/front/assets/js/git_integration/pages/index.ts b/front/assets/js/git_integration/pages/index.ts index d120b7156..fbf4bf7fc 100644 --- a/front/assets/js/git_integration/pages/index.ts +++ b/front/assets/js/git_integration/pages/index.ts @@ -1,7 +1,9 @@ import { IntegrationPage } from './integration_page'; import { HomePage } from './home_page'; +import { GithubAppSetup } from './github_app_setup'; export { HomePage, - IntegrationPage + IntegrationPage, + GithubAppSetup }; diff --git a/front/test/support/stubs/feature.ex b/front/test/support/stubs/feature.ex index 16fe246ae..3dafbe092 100644 --- a/front/test/support/stubs/feature.ex +++ b/front/test/support/stubs/feature.ex @@ -105,6 +105,7 @@ defmodule Support.Stubs.Feature do {"test_results", state: :HIDDEN, quantity: 0}, {"ip_allow_list", state: :HIDDEN, quantity: 0}, {"superjerry_tests", state: :HIDDEN, quantity: 1}, + {"rbac__groups", state: :HIDDEN, quantity: 0}, # features enabled only in CE {"instance_git_integration", state: :ENABLED, quantity: 1} diff --git a/guard/config/prod.exs b/guard/config/prod.exs index d1b7733a7..20c714667 100644 --- a/guard/config/prod.exs +++ b/guard/config/prod.exs @@ -6,4 +6,4 @@ config :oauth2, adapter: Tesla.Adapter.Hackney config :tesla, adapter: Tesla.Adapter.Hackney -config :guard, :github_app_install_url, "https://github.com/settings/apps/new" +config :guard, :github_app_base_url, "https://github.com" diff --git a/guard/lib/guard/instance_config/api.ex b/guard/lib/guard/instance_config/api.ex index eeb45b218..5635830aa 100644 --- a/guard/lib/guard/instance_config/api.ex +++ b/guard/lib/guard/instance_config/api.ex @@ -111,17 +111,29 @@ defmodule Guard.InstanceConfig.Api do get "/github_app_manifest" do org_id = conn.assigns[:org_id] - manifest = Guard.InstanceConfig.GithubApp.manifest(conn.assigns[:org_username]) + # Extract is_public parameter (defaults to false) + is_public = + case conn.query_params["is_public"] do + "true" -> true + _ -> false + end + + # Extract organization parameter for GitHub URL + organization = conn.query_params["organization"] + + manifest = Guard.InstanceConfig.GithubApp.manifest(conn.assigns[:org_username], is_public) with integration <- Guard.InstanceConfig.Store.get(:CONFIG_TYPE_GITHUB_APP), {:does_not_exist, true} <- {:does_not_exist, is_nil(integration)}, token <- Guard.InstanceConfig.Token.encode(org_id), {:ok, manifest_json} <- Jason.encode(manifest) do + github_url = github_app_install_url(organization) + conn |> put_resp_cookie(@state_cookie_key, token) |> render_manifest_page( manifest: manifest_json |> html_escape(), - url: github_app_install_url() <> "?state=#{token}" + url: github_url <> "?state=#{token}" ) else {:does_not_exist, false} -> @@ -213,7 +225,15 @@ defmodule Guard.InstanceConfig.Api do defp dynamic_plug_session(conn, opts), do: Guard.Session.setup(conn, opts) - defp github_app_install_url, do: Application.get_env(:guard, :github_app_install_url) + defp github_app_install_url(org) when is_binary(org) and byte_size(org) > 0 do + base_url = Application.get_env(:guard, :github_app_base_url, "https://github.com") + "#{base_url}/organizations/#{URI.encode(org)}/settings/apps/new" + end + + defp github_app_install_url(_) do + base_url = Application.get_env(:guard, :github_app_base_url, "https://github.com") + "#{base_url}/settings/apps/new" + end defp plug_fetch_query_params(conn, _opts) do conn |> fetch_query_params() diff --git a/guard/lib/guard/instance_config/github_app.ex b/guard/lib/guard/instance_config/github_app.ex index b189eb1ac..5a881c9fe 100644 --- a/guard/lib/guard/instance_config/github_app.ex +++ b/guard/lib/guard/instance_config/github_app.ex @@ -1,7 +1,7 @@ defmodule Guard.InstanceConfig.GithubApp do require Logger - def manifest(org_username \\ "") do + def manifest(org_username \\ "", is_public \\ false) do default_rand_name = "#{org_username}-" <> for(_ <- 1..10, into: "", do: <>) @@ -26,7 +26,7 @@ defmodule Guard.InstanceConfig.GithubApp do callback_urls: callback_urls, setup_url: setup_url, hook_attributes: %{url: webhook_url, active: true}, - public: false, + public: is_public, redirect_url: redirect_url, default_events: [ "create",