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
1 change: 1 addition & 0 deletions front/assets/js/git_integration/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const App = () => {
<stores.Config.Context.Provider value={{ ...config, redirectToAfterSetup: redirectToAfterSetup }}>
<Routes>
<Route path="/" element={<pages.HomePage/>}/>
<Route path="/github_app/setup" element={<pages.GithubAppSetup/>}/>
<Route path="/:type" element={<pages.IntegrationPage/>}/>
<Route path="*" element={<Navigate to="/"/>}/>
</Routes>
Expand Down
Original file line number Diff line number Diff line change
@@ -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) => {
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

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

The connectButtonUrl prop is no longer used but remains in the interface. Consider removing it from IntegrationStarterProps since the component now navigates to a hardcoded route instead of using the provided URL.

Copilot uses AI. Check for mistakes.
return (
<form
className="d-flex flex-items-center"
onSubmit={submitManifest}
method="post"
<NavLink
className="btn btn-primary btn-small"
to="/github_app/setup"
>
<button
className="btn btn-primary btn-small"
>
Connect
</button>
</form>
Connect
</NavLink>
);
};
150 changes: 150 additions & 0 deletions front/assets/js/git_integration/pages/github_app_setup.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<NavLink className="gray link f6 mb2 dib" to="/">
← Back to Integrations
</NavLink>
<p>GitHub App integration not available</p>
</div>
);
}

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());
}
Comment on lines +33 to +35
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

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

The organization name is only whitespace-trimmed before being sent to the backend. Consider validating the organization name format (e.g., GitHub organization names can only contain alphanumeric characters and hyphens) to prevent potential issues with invalid characters being passed through.

Copilot uses AI. Check for mistakes.

window.location.href = urlWithToken.toString();
};

return (
<div>
<NavLink className="gray link f6 mb2 dib" to="/">
← Back to Integrations
</NavLink>
<h2 className="f3 f2-m mb0">GitHub App</h2>
<p className="measure">
GitHub Cloud integration through installed GitHub App.
</p>

<div className="pv3 bt b--lighter-gray">
<div className="mb1">
<label className="b mr1">Setup Configuration</label>
</div>
<p className="mb3">
Choose where to create the GitHub App and configure its visibility settings.
</p>

<form onSubmit={submitManifest}>
<div className="mv3 br3 shadow-3 bg-white pa3 bb b--black-075">
<div className="flex items-center mb2 pb3 bb bw1 b--black-075">
<span className="material-symbols-outlined mr2">settings</span>
<span className="b f5">GitHub App Configuration</span>
</div>

<div className="mb3">
<label className="b db mb2">Account Type</label>
<div className="mb2">
<label className="flex items-center pointer">
<input
type="radio"
name="accountType"
value="personal"
checked={accountType === "personal"}
onChange={() => setAccountType("personal")}
className="mr2"
/>
<span>Personal Account</span>
</label>
</div>
<div className="mb2">
<label className="flex items-center pointer">
<input
type="radio"
name="accountType"
value="organization"
checked={accountType === "organization"}
onChange={() => setAccountType("organization")}
className="mr2"
/>
<span>Organization Account</span>
</label>
</div>

{accountType === "organization" && (
<div className="mt3">
<label className="db mb2" htmlFor="organizationName">
Organization Name
</label>
<input
id="organizationName"
type="text"
value={organizationName}
onChange={(e) => setOrganizationName((e.target as HTMLInputElement).value)}
placeholder="e.g., my-company"
className="form-control w-100"
required
/>
<p className="f6 gray mt2 mb0">
The exact name of your GitHub organization
Copy link
Contributor

Choose a reason for hiding this comment

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

Please extend this line with info that you need to be an admin on this organization.

</p>
</div>
)}
</div>

<div className="mb3 pt3 bt b--black-075">
<label className="b db mb2">App Visibility</label>
<label className="flex items-start pointer">
<input
type="checkbox"
checked={isPublic}
onChange={(e) => setIsPublic((e.target as HTMLInputElement).checked)}
className="mr2 mt1"
/>
<div>
<span className="db mb1">Make app public</span>
<span className="f6 gray db">
{isPublic
? "Public apps can be installed by anyone and may appear in GitHub's marketplace."
: `Private apps only work with ${
accountType === "organization" ? "the specified organization" : "your personal account"
}. They cannot be installed elsewhere.`}
</span>
</div>
</label>
</div>
</div>

<div className="mt3">
<button type="submit" className="btn btn-primary">
Continue to GitHub
</button>
<NavLink to="/" className="ml3 link gray">
Cancel
</NavLink>
</div>
</form>
</div>
</div>
);
};
4 changes: 3 additions & 1 deletion front/assets/js/git_integration/pages/index.ts
Original file line number Diff line number Diff line change
@@ -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
};
1 change: 1 addition & 0 deletions front/test/support/stubs/feature.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
2 changes: 1 addition & 1 deletion guard/config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
26 changes: 23 additions & 3 deletions guard/lib/guard/instance_config/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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} ->
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions guard/lib/guard/instance_config/github_app.ex
Original file line number Diff line number Diff line change
@@ -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: <<Enum.random('0123456789abcdef')>>)
Expand All @@ -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",
Expand Down