-
- github.com/settings/tokens
+
+ {tokensUrl.replace(/^https?:\/\//, "")}
diff --git a/apps/fabro-web/app/routes/automations-new.test.tsx b/apps/fabro-web/app/routes/automations-new.test.tsx
index 8a5059a28..cc3044af3 100644
--- a/apps/fabro-web/app/routes/automations-new.test.tsx
+++ b/apps/fabro-web/app/routes/automations-new.test.tsx
@@ -15,6 +15,7 @@ let currentRunStateLoading = false;
let currentRunSettings: any = null;
let currentEnvironments: any[] = [];
let currentEnvironmentsError: unknown = null;
+let currentGithubWebUrl = "https://github.com";
const queryCalls: Array<{ hook: string; id: string | undefined }> = [];
const mountedRenderers: TestRenderer.ReactTestRenderer[] = [];
let teardownReactEnv: (() => void) | undefined;
@@ -67,6 +68,11 @@ mock.module("../lib/queries", () => ({
isLoading: false,
};
},
+ useServerSettings: () => ({
+ data: { server: { integrations: { github: { web_url: currentGithubWebUrl } } } },
+ error: null,
+ isLoading: false,
+ }),
useRunState: (id: string | undefined) => {
queryCalls.push({ hook: "useRunState", id });
return {
@@ -277,6 +283,7 @@ beforeEach(() => {
currentRunState = null;
currentRunStateLoading = false;
currentRunSettings = null;
+ currentGithubWebUrl = "https://github.com";
currentEnvironments = [
{ id: "default", provider: "docker" },
{ id: "daytona-smoke", provider: "daytona" },
@@ -416,6 +423,21 @@ describe("AutomationsNew", () => {
);
});
+ test("run origin on the configured GitHub Enterprise host prefills the target repository", async () => {
+ currentGithubWebUrl = "https://ghe.example.com";
+ currentRun = makeRun({
+ repository: {
+ name: "",
+ origin_url: "https://ghe.example.com/acme/widgets.git",
+ provider: "github",
+ },
+ });
+
+ const { renderer } = await renderAutomationsNew("/automations/new?from_run=run_1");
+
+ expect(fieldValue(renderer, "Run target repository")).toBe("acme/widgets");
+ });
+
test("automationFormValuesFromRun kebab-cases the workflow name fallback", () => {
const run = makeRun({
workflow: {
diff --git a/apps/fabro-web/app/routes/automations-new.tsx b/apps/fabro-web/app/routes/automations-new.tsx
index ecc1b465c..a0751cbf0 100644
--- a/apps/fabro-web/app/routes/automations-new.tsx
+++ b/apps/fabro-web/app/routes/automations-new.tsx
@@ -6,7 +6,13 @@ import type { Environment } from "@qltysh/fabro-api-client";
import { ApiError, apiData, automationsApi } from "../lib/api-client";
import { queryKeys } from "../lib/query-keys";
-import { useEnvironments, useRun, useRunSettings, useRunState } from "../lib/queries";
+import {
+ useEnvironments,
+ useRun,
+ useRunSettings,
+ useRunState,
+ useServerSettings,
+} from "../lib/queries";
import {
AutomationFormFields,
EMPTY_AUTOMATION_FORM,
@@ -37,6 +43,7 @@ export default function AutomationsNew() {
const runStateQuery = useRunState(fromRunId);
const settingsQuery = useRunSettings(fromRunId);
const environmentsQuery = useEnvironments();
+ const serverSettingsQuery = useServerSettings();
const environments = environmentsQuery.data?.data;
const environmentsPending = environmentsQuery.isLoading && !environmentsQuery.data;
const environmentsError = Boolean(environmentsQuery.error);
@@ -86,6 +93,7 @@ export default function AutomationsNew() {
runStateQuery.data ?? null,
settingsQuery.data ?? null,
environments,
+ serverSettingsQuery.data?.server.integrations.github.web_url,
);
return (
diff --git a/docs/public/administration/server-configuration.mdx b/docs/public/administration/server-configuration.mdx
index 215f279c9..a31d52ba3 100644
--- a/docs/public/administration/server-configuration.mdx
+++ b/docs/public/administration/server-configuration.mdx
@@ -303,8 +303,8 @@ When a field is not set, Fabro derives it from the run's GitHub credential:
| Credential | Name | Email |
|---|---|---|
-| GitHub App (`strategy = "app"`) | `[bot]` | `+[bot]@users.noreply.github.com` |
-| Token (`strategy = "token"`) | the token's user login | `+@users.noreply.github.com` |
+| GitHub App (`strategy = "app"`) | `[bot]` | `+[bot]@users.noreply.github.com` (`users.noreply.` on an enterprise host) |
+| Token (`strategy = "token"`) | the token's user login | `+@users.noreply.github.com` (`users.noreply.` on an enterprise host) |
| None | `Fabro` | `noreply@fabro.sh` |
Setting both `name` and `email` skips the credential lookup. Setting one field overlays it on the derived identity. A lookup failure for the selected credential fails the run at setup; Fabro never silently switches to another author. The resolved identity is recorded in the run's event stream as `git.identity.resolved` and in the run state as `git_identity`.
@@ -323,6 +323,15 @@ Configure GitHub integration auth. `strategy = "token"` is the default and uses
strategy = "token"
```
+| Key | Description | Default |
+|---|---|---|
+| `strategy` | `token` or `app` | `token` |
+| `app_id`, `client_id`, `slug` | GitHub App identity (App mode) | none |
+| `web_url` | Web origin of the GitHub deployment, such as `https://ghe.example.com`; must be `https` (`http` only for loopback). Override with `GITHUB_WEB_URL`. Read at server startup: changing it needs a restart | `https://github.com` |
+| `api_url` | REST API base URL; may be plain `http` (for a proxy or mock). Override with `GITHUB_BASE_URL` | derived from `web_url` (`https://api.github.com`, `https://api.` for `*.ghe.com`, `/api/v3` otherwise) |
+
+For GitHub Enterprise Server or GitHub Enterprise Cloud, set `web_url` (see [GitHub Enterprise Server and GitHub Enterprise Cloud](/integrations/github#github-enterprise-server-and-github-enterprise-cloud)). The CLI and the run worker read the host from the local `settings.toml` or `GITHUB_WEB_URL`. Put `web_url` in `settings.toml` before `fabro install`; the installer does not persist it from the environment.
+
For GitHub App mode, set `strategy = "app"` and include `app_id`, `client_id`, and `slug`. Webhook delivery is configured under `[server.integrations.github.webhooks]`:
```toml title="settings.toml"
diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml
index a734c4ddc..0092eb082 100644
--- a/docs/public/api-reference/fabro-api.yaml
+++ b/docs/public/api-reference/fabro-api.yaml
@@ -6638,12 +6638,17 @@ components:
required:
- canonical_url
- object_store_local_root
+ - github_web_url
properties:
canonical_url:
type: string
format: uri
object_store_local_root:
type: string
+ github_web_url:
+ type: string
+ description: Web origin of the GitHub deployment the install targets, without a trailing slash.
+ example: https://github.com
InstallLlmValidationResponse:
description: Successful response from install-time LLM credential validation.
@@ -15672,7 +15677,17 @@ components:
- client_id
- slug
- webhooks
+ - web_url
+ - api_url
properties:
+ web_url:
+ type: string
+ description: Web origin of the GitHub deployment, without a trailing slash.
+ example: https://github.com
+ api_url:
+ type: string
+ description: REST API base URL of the GitHub deployment, without a trailing slash.
+ example: https://api.github.com
enabled:
type: boolean
strategy:
diff --git a/docs/public/changelog/2026-10-25.mdx b/docs/public/changelog/2026-10-25.mdx
new file mode 100644
index 000000000..3558d4eb0
--- /dev/null
+++ b/docs/public/changelog/2026-10-25.mdx
@@ -0,0 +1,22 @@
+---
+title: "GitHub Enterprise Server and Enterprise Cloud hosts"
+date: "2026-10-25"
+---
+
+Fabro can now target a GitHub Enterprise Server instance or a GitHub
+Enterprise Cloud tenant with data residency (`https://.ghe.com`)
+instead of github.com. Set the deployment's web origin once and Fabro derives
+the REST, GraphQL and OAuth URLs, clone URLs, pull request links, Git
+credential scoping and the sandbox `gh` configuration from it:
+
+```toml
+[server.integrations.github]
+web_url = "https://ghe.example.com"
+```
+
+`GITHUB_WEB_URL` overrides the setting, and the existing `GITHUB_BASE_URL`
+still overrides the REST API base. Without the new setting, github.com
+behavior is unchanged.
+
+See [GitHub Enterprise Server and GitHub Enterprise Cloud](/integrations/github#github-enterprise-server-and-github-enterprise-cloud)
+for setup, custom certificate authorities and known limitations.
diff --git a/docs/public/docs.json b/docs/public/docs.json
index 214d4ffea..67e28ceba 100644
--- a/docs/public/docs.json
+++ b/docs/public/docs.json
@@ -303,6 +303,13 @@
"tab": "Changelog",
"icon": "clock-rotate-left",
"groups": [
+ {
+ "group": "October 2026",
+ "icon": "clock-rotate-left",
+ "pages": [
+ "changelog/2026-10-25"
+ ]
+ },
{
"group": "September 2026",
"icon": "clock-rotate-left",
diff --git a/docs/public/integrations/github.mdx b/docs/public/integrations/github.mdx
index f11876231..4bf8561d0 100644
--- a/docs/public/integrations/github.mdx
+++ b/docs/public/integrations/github.mdx
@@ -121,6 +121,7 @@ slug = "fabro-a3f2"
| `app_id` | Numeric GitHub App ID |
| `client_id` | OAuth Client ID for the app |
| `slug` | App slug, used for linking to the GitHub App settings page |
+| `web_url`, `api_url` | Optional. Target GitHub Enterprise Server or Enterprise Cloud instead of github.com; see [GitHub Enterprise Server and GitHub Enterprise Cloud](#github-enterprise-server-and-github-enterprise-cloud) |
### Server vault
@@ -174,6 +175,75 @@ fabro install github
This walks through just the GitHub setup steps — choosing a strategy, registering an app, or storing a token — and restarts the server when done. Stale settings and secrets from the previous strategy are cleaned up automatically.
+## GitHub Enterprise Server and GitHub Enterprise Cloud
+
+Fabro talks to github.com by default. To use a GitHub Enterprise Server (GHES) instance, or a GitHub Enterprise Cloud tenant with data residency (`https://.ghe.com`), set the web origin of that deployment. Fabro derives every other URL from it.
+
+```toml title="settings.toml"
+[server.integrations.github]
+web_url = "https://ghe.example.com"
+# api_url = "https://ghe.example.com/api/v3" # optional override
+```
+
+| Setting | Environment override | Description |
+|---|---|---|
+| `server.integrations.github.web_url` | `GITHUB_WEB_URL` | Web origin of the GitHub deployment, for example `https://ghe.example.com`. Defaults to `https://github.com`. Must be `https` and must not contain a path, query or fragment (plain `http` is accepted only for loopback hosts such as `localhost` and `127.0.0.1`). |
+| `server.integrations.github.api_url` | `GITHUB_BASE_URL` | REST API base URL. Optional: when unset it is derived from `web_url` as shown below. May be plain `http` on any host, for example to reach GitHub through a proxy or a mock; `web_url` must still be `https`. |
+
+The environment variables win over `settings.toml`. `GITHUB_WEB_URL` alone re-derives the API URL from the new web origin; `GITHUB_BASE_URL` alone replaces only the API URL.
+
+The server reads the host once at startup, so changing `web_url` or `api_url` takes effect only after a server restart.
+
+### Derived URLs
+
+| | github.com | GitHub Enterprise Cloud (`https://octocorp.ghe.com`) | GitHub Enterprise Server (`https://ghe.example.com`) |
+|---|---|---|---|
+| REST API | `https://api.github.com` | `https://api.octocorp.ghe.com` | `https://ghe.example.com/api/v3` |
+| GraphQL | `https://api.github.com/graphql` | `https://api.octocorp.ghe.com/graphql` | `https://ghe.example.com/api/graphql` |
+| OAuth | `https://github.com/login/oauth/...` | `https://octocorp.ghe.com/login/oauth/...` | `https://ghe.example.com/login/oauth/...` |
+| Repository clone URL | `https://github.com/owner/repo` | `https://octocorp.ghe.com/owner/repo` | `https://ghe.example.com/owner/repo` |
+| Pull request link | `https://github.com/owner/repo/pull/7` | `https://octocorp.ghe.com/owner/repo/pull/7` | `https://ghe.example.com/owner/repo/pull/7` |
+| Commit email domain | `users.noreply.github.com` | `users.noreply.octocorp.ghe.com` | `users.noreply.ghe.example.com` |
+
+A host named exactly `github.com` is treated as github.com, `ghe.com` or `*.ghe.com` as GitHub Enterprise Cloud, and any other host as GitHub Enterprise Server.
+
+### Where the host is read
+
+The server, the run worker and the CLI all need to agree on the host:
+
+- The **server** reads `web_url` and `api_url` from its `settings.toml` and the environment overrides above. The server passes `GITHUB_WEB_URL` and `GITHUB_BASE_URL` on to run workers it starts.
+- The **run worker** reads the same `settings.toml` (through `FABRO_HOME`) and the same environment variables.
+- The **CLI** reads the host from the local `settings.toml` or from `GITHUB_WEB_URL`; it does not ask a remote server. If you run `fabro` against a remote server, set the same `web_url` in your local `settings.toml` (or export `GITHUB_WEB_URL`) so repository URLs, `fabro install` and `fabro doctor` target the enterprise host.
+
+Put `web_url` in `settings.toml` before running `fabro install`. The installer does not persist the host, so `GITHUB_WEB_URL` alone is not enough: a server restarted without that variable falls back to github.com. The installer uses it for `gh auth token --hostname `, for the GitHub App manifest registration and for the links it prints. The `App:` link it prints points at `/github-apps/` on GitHub Enterprise Server and at `/apps/` on github.com and GitHub Enterprise Cloud.
+
+### Authentication
+
+- **Tokens.** Personal access tokens, fine-grained tokens and OAuth tokens use the same `ghp_`, `github_pat_`, `gho_`, `ghu_` and `ghs_` formats on GHES and GHEC. `fabro install` runs `gh auth token --hostname `, so run `gh auth login --hostname ghe.example.com` first.
+- **GitHub App.** `fabro install` registers the App through the manifest flow against `/settings/apps/new` (or the organization equivalent) and exchanges the code on the host's API. This needs a GHES version that supports GitHub App manifests. If your instance does not, register the App by hand and set `app_id`, `client_id` and `slug` under `[server.integrations.github]`, then store the private key and client secret as described in [Server vault](#server-vault).
+- **Browser sign-in.** OAuth login redirects to `/login/oauth/authorize`, and sessions record the enterprise host as the identity issuer.
+
+### Custom certificate authorities
+
+Enterprise hosts often use a certificate from a private CA. Fabro always verifies TLS certificates and has no setting to disable verification.
+
+Fabro's HTTP client uses rustls with the platform certificate verifier:
+
+- **Linux:** `SSL_CERT_FILE` and `SSL_CERT_DIR` are honoured, in addition to the system trust store. The server passes both on to the run workers it starts.
+- **macOS:** the system Keychain is used. **Windows:** the Windows certificate store is used. The variables are not read on either platform, so install the CA in the system store.
+- **Sandboxes:** containers do not inherit the host's trust store. Bake the CA into the sandbox image, or set `SSL_CERT_FILE` and `GIT_SSL_CAINFO` in the sandbox environment so Git and the `gh` CLI trust it.
+
+### The `gh` CLI in sandboxes
+
+The `gh` CLI reads `GITHUB_TOKEN` only for github.com. For any other host Fabro also injects `GH_HOST=` and `GH_ENTERPRISE_TOKEN=` next to `GITHUB_TOKEN`, so `gh pr list` and similar commands in command stages work without `gh auth login`. Git's credential helper and the `git@:` and `ssh://git@/` rewrites are scoped to the configured host.
+
+### Known limitations
+
+- One GitHub host per server. Repositories on any other host (including github.com when `web_url` points at an enterprise host) are rejected.
+- Features that an older GHES release does not provide, such as newer App permissions or auto-merge, surface as the error GitHub returns.
+- Avatars and the Content Security Policy follow the configured host: for a non-github.com host, `img-src` allows the web origin and `https://avatars.` instead of `avatars.githubusercontent.com`. A GHES instance that serves avatars from another origin shows broken avatar images.
+- The Fabro CLI release channel and documentation links stay on github.com.
+
## `token` mode
Choose **GitHub CLI** in `fabro install` to use the default local-user flow. The installer:
diff --git a/lib/apps/fabro-cli/src/commands/doctor.rs b/lib/apps/fabro-cli/src/commands/doctor.rs
index 4765c7da1..0480d9ac9 100644
--- a/lib/apps/fabro-cli/src/commands/doctor.rs
+++ b/lib/apps/fabro-cli/src/commands/doctor.rs
@@ -3,6 +3,7 @@ use std::path::{Path, PathBuf};
use anyhow::Result;
use fabro_api::types as api_types;
use fabro_config::user::active_settings_path;
+use fabro_github::GitHubHost;
use fabro_types::settings::replace_wildcard_host;
pub(crate) use fabro_util::check_report::{
CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus,
@@ -125,10 +126,14 @@ fn wildcard_public_url_details(path: &Path) -> Vec {
.unwrap_or(&bad_urls[0])
.suggestion
.as_str();
+ let github_host = toml_string_at(&doc, &["server", "integrations", "github", "web_url"])
+ .and_then(|web_url| GitHubHost::new(web_url, None).ok())
+ .and_then(|host| fabro_github::with_env_overrides(&host).ok())
+ .unwrap_or_default();
let github_settings_url = toml_string_at(&doc, &["server", "integrations", "github", "slug"])
.map_or_else(
|| "the GitHub App settings page".to_string(),
- |slug| format!("https://github.com/settings/apps/{slug}"),
+ |slug| format!("{}/settings/apps/{slug}", github_host.web_url()),
);
details.push(CheckDetail {
text: format!(
@@ -470,6 +475,40 @@ url = "http://0.0.0.0:32276"
assert!(details.contains("https://github.com/settings/apps/octocat-fabro"));
}
+ #[test]
+ #[expect(
+ clippy::disallowed_methods,
+ reason = "unit test stages a temporary settings.toml fixture with sync std::fs"
+ )]
+ fn check_config_links_the_github_app_on_the_enterprise_host() {
+ let dir = tempfile::tempdir().unwrap();
+ let settings_path = dir.path().join("settings.toml");
+ std::fs::write(
+ &settings_path,
+ r#"
+_version = 1
+
+[server.web]
+url = "http://0.0.0.0:32276"
+
+[server.integrations.github]
+slug = "octocat-fabro"
+web_url = "https://ghe.example.com"
+"#,
+ )
+ .unwrap();
+
+ let result = check_config(Some(settings_path));
+ let details = result
+ .details
+ .iter()
+ .map(|detail| detail.text.as_str())
+ .collect::>()
+ .join("\n");
+
+ assert!(details.contains("https://ghe.example.com/settings/apps/octocat-fabro"));
+ }
+
#[test]
fn check_version_parity_warns_on_mismatch() {
let result = check_version_parity("0.0.0-test");
diff --git a/lib/apps/fabro-cli/src/commands/graph.rs b/lib/apps/fabro-cli/src/commands/graph.rs
index 285c4add3..a2bda5e08 100644
--- a/lib/apps/fabro-cli/src/commands/graph.rs
+++ b/lib/apps/fabro-cli/src/commands/graph.rs
@@ -22,6 +22,7 @@ use crate::args::{GraphArgs, GraphDirection, GraphOutputFormat};
use crate::command_context::CommandContext;
use crate::commands::run::output::api_diagnostics_to_local;
use crate::shared::{absolute_or_current, print_diagnostics, print_json_pretty, relative_path};
+use crate::user_config;
pub(crate) async fn run(
args: &GraphArgs,
@@ -39,6 +40,7 @@ pub(crate) async fn run(
cwd: ctx.cwd().to_path_buf(),
environment_defaults: fabro_environment::seeded_catalog_layer(),
user_settings_path: Some(active_settings_path(None)),
+ github_host: user_config::github_host()?,
..Default::default()
})?;
let client = ctx.server().await?;
diff --git a/lib/apps/fabro-cli/src/commands/install.rs b/lib/apps/fabro-cli/src/commands/install.rs
index 1c61c032c..adf30e7d1 100644
--- a/lib/apps/fabro-cli/src/commands/install.rs
+++ b/lib/apps/fabro-cli/src/commands/install.rs
@@ -27,6 +27,7 @@ use fabro_config::bind::Bind;
use fabro_config::daemon::ServerDaemon;
use fabro_config::user::{SETTINGS_CONFIG_FILENAME, default_storage_dir};
use fabro_config::{Storage, UserSettingsBuilder, envfile};
+use fabro_github::GitHubHost;
use fabro_install::{
GITHUB_APP_VAULT_KEYS, GITHUB_INSTALL_SECRET_KEYS, InstallListenConfig, InstallPersistencePlan,
PendingDevTokenWrite, PendingSettingsWrite, SecretStoreWrite,
@@ -498,7 +499,8 @@ impl InstallInputSource for InteractiveInstallInputSource {
s: &Styles,
_printer: Printer,
) -> Result {
- let gh_available = GhCli::detect().await.is_some();
+ let github_host = user_config::github_host()?;
+ let gh_available = GhCli::detect(&github_host).await.is_some();
let token_label = if gh_available {
"Personal Access Token — use your existing `gh` login"
@@ -518,7 +520,7 @@ impl InstallInputSource for InteractiveInstallInputSource {
match strategy {
0 => {
let token = if gh_available {
- fabro_github::gh_auth_token()
+ fabro_github::gh_auth_token(&github_host)
.await
.context("Run `gh auth login` and rerun `fabro install`.")?
} else {
@@ -528,7 +530,7 @@ impl InstallInputSource for InteractiveInstallInputSource {
Ok(GitHubInstallSelection::Token { token })
}
1 => {
- let (owner, username) = prompt_github_app_owner(s).await?;
+ let (owner, username) = prompt_github_app_owner(s, &github_host).await?;
Ok(GitHubInstallSelection::App { owner, username })
}
_ => unreachable!("prompt_select returned an out-of-range index"),
@@ -694,9 +696,10 @@ impl InstallInputSource for NonInteractiveInstallInputSource {
_s: &Styles,
_printer: Printer,
) -> Result {
+ let github_host = user_config::github_host()?;
match self.args.github_strategy {
Some(InstallGitHubStrategyArg::Token) => {
- let token = fabro_github::gh_auth_token()
+ let token = fabro_github::gh_auth_token(&github_host)
.await
.context("Run `gh auth login` and rerun `fabro install`.")?;
fabro_github::validate_static_github_token(&token)?;
@@ -708,7 +711,7 @@ impl InstallInputSource for NonInteractiveInstallInputSource {
"non-interactive install requires --github-owner for --github-strategy app",
)?,
)?,
- username: best_effort_github_username().await,
+ username: best_effort_github_username(&github_host).await,
}),
None => bail!("non-interactive install requires --github-strategy"),
}
@@ -780,9 +783,10 @@ async fn choose_install_github_selection(
return input.choose_github_install(s, printer).await;
}
+ let github_host = user_config::github_host()?;
match github_args.strategy {
Some(InstallGitHubStrategyArg::Token) => {
- let token = fabro_github::gh_auth_token()
+ let token = fabro_github::gh_auth_token(&github_host)
.await
.context("Run `gh auth login` and rerun `fabro install github`.")?;
fabro_github::validate_static_github_token(&token)?;
@@ -792,7 +796,7 @@ async fn choose_install_github_selection(
owner: GitHubAppOwner::parse_scripted(github_args.owner.as_deref().context(
"install github --non-interactive requires --owner for --strategy app",
)?)?,
- username: best_effort_github_username().await,
+ username: best_effort_github_username(&github_host).await,
}),
None => bail!("install github --non-interactive requires --strategy"),
}
@@ -824,11 +828,12 @@ impl GitHubAppOwner {
Ok(Self::Organization(org.to_string()))
}
- fn manifest_form_action(&self) -> String {
+ fn manifest_form_action(&self, github_host: &GitHubHost) -> String {
+ let web_url = github_host.web_url();
match self {
- Self::Personal => "https://github.com/settings/apps/new".to_string(),
+ Self::Personal => format!("{web_url}/settings/apps/new"),
Self::Organization(org) => {
- format!("https://github.com/organizations/{org}/settings/apps/new")
+ format!("{web_url}/organizations/{org}/settings/apps/new")
}
}
}
@@ -860,8 +865,8 @@ impl GitHubAppOwner {
}
}
-async fn best_effort_github_username() -> Option {
- let gh = GhCli::detect().await?;
+async fn best_effort_github_username(github_host: &GitHubHost) -> Option {
+ let gh = GhCli::detect(github_host).await?;
gh.authenticated_user().await
}
@@ -872,10 +877,13 @@ async fn best_effort_github_username() -> Option {
/// Always offers a manual "Other" option so org app managers can enter a slug.
///
/// Returns `(owner, username)`.
-async fn prompt_github_app_owner(_s: &Styles) -> Result<(GitHubAppOwner, Option)> {
+async fn prompt_github_app_owner(
+ _s: &Styles,
+ github_host: &GitHubHost,
+) -> Result<(GitHubAppOwner, Option)> {
let spinner = cyan_spinner("Checking GitHub CLI...");
- let Some(gh) = GhCli::detect().await else {
+ let Some(gh) = GhCli::detect(github_host).await else {
spinner.finish_and_clear();
return Ok((GitHubAppOwner::Personal, None));
};
@@ -974,6 +982,7 @@ async fn setup_github_app(
printer: Printer,
) -> Result {
let app_name = owner.app_name(username);
+ let github_host = user_config::github_host()?;
// Bind to random port
let listener = TcpListener::bind("127.0.0.1:0")
@@ -999,7 +1008,7 @@ async fn setup_github_app(
let code_tx = std::sync::Arc::new(std::sync::Mutex::new(Some(code_tx)));
let shutdown_tx = std::sync::Arc::new(std::sync::Mutex::new(Some(shutdown_tx)));
- let form_action = owner.manifest_form_action();
+ let form_action = owner.manifest_form_action(&github_host);
let index_html = format!(
r#"
@@ -1117,7 +1126,8 @@ async fn setup_github_app(
let client = fabro_http::http_client()?;
let resp = client
.post(format!(
- "https://api.github.com/app-manifests/{code}/conversions"
+ "{}/app-manifests/{code}/conversions",
+ github_host.api_url()
))
.header("Accept", "application/vnd.github+json")
.header("User-Agent", "fabro-cli")
@@ -1159,7 +1169,7 @@ async fn setup_github_app(
printer,
" {}",
s.dim
- .apply_to(format!("App: https://github.com/apps/{slug}"))
+ .apply_to(format!("App: {}", github_host.app_page_url(&slug)))
);
// Return secret pairs
@@ -2569,16 +2579,29 @@ client_id = "client-id"
fn github_app_owner_personal_url() {
let owner = GitHubAppOwner::Personal;
assert_eq!(
- owner.manifest_form_action(),
+ owner.manifest_form_action(&GitHubHost::default()),
"https://github.com/settings/apps/new"
);
}
+ #[test]
+ fn github_app_owner_urls_follow_the_enterprise_host() {
+ let host = GitHubHost::new("https://ghe.example.com", None).unwrap();
+ assert_eq!(
+ GitHubAppOwner::Personal.manifest_form_action(&host),
+ "https://ghe.example.com/settings/apps/new"
+ );
+ assert_eq!(
+ GitHubAppOwner::Organization("my-org".to_string()).manifest_form_action(&host),
+ "https://ghe.example.com/organizations/my-org/settings/apps/new"
+ );
+ }
+
#[test]
fn github_app_owner_org_url() {
let owner = GitHubAppOwner::Organization("my-org".to_string());
assert_eq!(
- owner.manifest_form_action(),
+ owner.manifest_form_action(&GitHubHost::default()),
"https://github.com/organizations/my-org/settings/apps/new"
);
}
diff --git a/lib/apps/fabro-cli/src/commands/preflight.rs b/lib/apps/fabro-cli/src/commands/preflight.rs
index 2bf94cb08..2f3426e9f 100644
--- a/lib/apps/fabro-cli/src/commands/preflight.rs
+++ b/lib/apps/fabro-cli/src/commands/preflight.rs
@@ -12,6 +12,7 @@ use crate::commands::run::output::{
use crate::commands::run::overrides::preflight_args_overrides;
use crate::manifest_args::preflight_manifest_args;
use crate::shared::{cyan_spinner, print_json_pretty};
+use crate::user_config;
pub(crate) async fn execute(
mut args: PreflightArgs,
@@ -32,6 +33,7 @@ pub(crate) async fn execute(
args: preflight_manifest_args(&args),
environment_defaults: fabro_environment::seeded_catalog_layer(),
user_settings_path: Some(active_settings_path(None)),
+ github_host: user_config::github_host()?,
})?;
let spinner = (!ctx.json_output()).then(|| cyan_spinner("Running checks..."));
diff --git a/lib/apps/fabro-cli/src/commands/repo/init.rs b/lib/apps/fabro-cli/src/commands/repo/init.rs
index 71c644b72..e537d3760 100644
--- a/lib/apps/fabro-cli/src/commands/repo/init.rs
+++ b/lib/apps/fabro-cli/src/commands/repo/init.rs
@@ -11,6 +11,7 @@ use tokio::task::spawn_blocking;
use crate::args::{RepoInitArgs, ServerTargetArgs};
use crate::command_context::CommandContext;
+use crate::user_config;
#[expect(
clippy::disallowed_methods,
@@ -193,8 +194,11 @@ async fn check_github_app_installation(target: &ServerTargetArgs, base_ctx: &Com
};
// Convert SSH URL to HTTPS and parse owner/repo
+ let Ok(host) = user_config::github_host() else {
+ return; // Unreadable server settings — skip silently
+ };
let https_url = fabro_github::ssh_url_to_https(&remote_url);
- let Ok((owner, repo)) = fabro_github::parse_github_owner_repo(&https_url) else {
+ let Ok((owner, repo)) = fabro_github::parse_github_owner_repo(&host, &https_url) else {
return; // Not a GitHub repo — skip silently
};
diff --git a/lib/apps/fabro-cli/src/commands/run/create.rs b/lib/apps/fabro-cli/src/commands/run/create.rs
index 6e07c0a65..9b5fc50c3 100644
--- a/lib/apps/fabro-cli/src/commands/run/create.rs
+++ b/lib/apps/fabro-cli/src/commands/run/create.rs
@@ -14,7 +14,7 @@ use super::{resolution, selection};
use crate::args::RunArgs;
use crate::command_context::CommandContext;
use crate::commands::resolve_run_id;
-use crate::user_config::{RunSettingsKeyPresence, read_project_run_settings_key_presence};
+use crate::user_config::{self, RunSettingsKeyPresence, read_project_run_settings_key_presence};
pub(crate) struct CreatedRun {
pub(crate) run_id: RunId,
@@ -104,7 +104,10 @@ pub(crate) async fn create_run(
&& args.target_repo.is_none()
&& args.target_repo_selector.is_none() =>
{
- fabro_manifest::configured_repo_origin_url_for_location(package.workflow_location())?
+ fabro_manifest::configured_repo_origin_url_for_location(
+ &user_config::github_host()?,
+ package.workflow_location(),
+ )?
}
_ => None,
};
diff --git a/lib/apps/fabro-cli/src/commands/run/petri_worker.rs b/lib/apps/fabro-cli/src/commands/run/petri_worker.rs
index 267c18070..4af4fe9c5 100644
--- a/lib/apps/fabro-cli/src/commands/run/petri_worker.rs
+++ b/lib/apps/fabro-cli/src/commands/run/petri_worker.rs
@@ -109,7 +109,7 @@ use tracing::{info, warn};
use super::publish;
use super::runner::{self, WorkerTitlePhase};
use crate::args::RunWorkerMode;
-use crate::command_context;
+use crate::{command_context, user_config};
/// What the worker holds when it hands a run to Petri.
pub(super) struct PetriWorker<'a> {
@@ -221,10 +221,17 @@ pub(super) async fn execute(worker: PetriWorker<'_>) -> Result<()> {
None
}
};
- let read_tokens = publish::read_token_source(&worker.run_state.spec, github.as_ref());
- runtime.stage_credentials =
- StageCredentials::for_run(&worker.run_state.spec, github.as_ref(), read_tokens.clone())?;
+ let github_host = user_config::github_host()?;
+ let read_tokens =
+ publish::read_token_source(&worker.run_state.spec, github.as_ref(), &github_host);
+ runtime.stage_credentials = StageCredentials::for_run(
+ &worker.run_state.spec,
+ github.as_ref(),
+ &github_host,
+ read_tokens.clone(),
+ )?;
let mut source = RunSource::for_run(
+ &github_host,
worker.run_state.spec.target.as_ref(),
&worker.run_state.spec.settings.run,
None,
@@ -236,6 +243,7 @@ pub(super) async fn execute(worker: PetriWorker<'_>) -> Result<()> {
run_id,
&worker.run_state.spec,
github,
+ github_host,
Arc::new(VaultCredentialSource::new(Arc::clone(&vault))),
Arc::new(catalog),
Arc::clone(&records),
diff --git a/lib/apps/fabro-cli/src/commands/run/publish.rs b/lib/apps/fabro-cli/src/commands/run/publish.rs
index 8555f3161..8ea028c7c 100644
--- a/lib/apps/fabro-cli/src/commands/run/publish.rs
+++ b/lib/apps/fabro-cli/src/commands/run/publish.rs
@@ -39,7 +39,7 @@ use fabro_static::EnvVars;
use fabro_store::platform_records::{PlatformRecord, PullRequestCreatedRecord};
use fabro_types::settings::run::{PullRequestSettings, RunMode};
use fabro_types::settings::server::GithubIntegrationStrategy;
-use fabro_types::{GitHubRepositorySlug, RunId, RunSpec, RunTarget};
+use fabro_types::{GitHubHost, GitHubRepositorySlug, RunId, RunSpec, RunTarget};
use fabro_vault::Vault;
use fabro_workflow::pull_request::{self, AutoMergeOptions, OpenPullRequestRequest};
use tokio::time;
@@ -77,11 +77,11 @@ pub(super) fn github_credentials(vault: &Vault) -> Result