fabro/lib/crates/fabro-cli/src/shared/github.rs
Bryan Helmkamp 6cb185b858
feat(github): point install errors at the configured app and require creds for docker
GitHubAppCredentials now carries the configured app slug, so the "not
installed" error from the installation lookup links to the specific
app's install page (https://github.com/organizations/{owner}/settings/apps/{slug}/installations)
when known, instead of the generic org installations page. Threaded
through the server, workflow pipeline, and CLI runner.

Also treat docker like daytona for GitHub credential gating: both are
clone-based providers that need an installation token to fetch the repo,
so a docker run now requires credentials when daytona would.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 10:43:00 -04:00

46 lines
1.6 KiB
Rust

use anyhow::anyhow;
use fabro_github::GitHubCredentials;
use fabro_static::EnvVars;
use fabro_types::settings::server::GithubIntegrationStrategy;
use fabro_vault::Vault;
pub(crate) fn build_github_credentials(
strategy: GithubIntegrationStrategy,
app_id: Option<&str>,
app_slug: Option<&str>,
vault: Option<&Vault>,
) -> anyhow::Result<Option<GitHubCredentials>> {
match strategy {
GithubIntegrationStrategy::App => {
GitHubCredentials::from_env_with_slug(app_id, app_slug).map_err(|err| anyhow!(err))
}
GithubIntegrationStrategy::Token => {
let token = lookup_github_token(vault);
match token {
Some(t) => Ok(Some(GitHubCredentials::Token(t))),
None => Err(anyhow!(
"GITHUB_TOKEN not configured — run fabro install or set GITHUB_TOKEN"
)),
}
}
}
}
/// Look up GitHub token: GITHUB_TOKEN env -> vault GITHUB_TOKEN -> GH_TOKEN env
/// -> vault GH_TOKEN
fn lookup_github_token(vault: Option<&Vault>) -> Option<String> {
lookup_env_or_vault(EnvVars::GITHUB_TOKEN, vault)
.or_else(|| lookup_env_or_vault(EnvVars::GH_TOKEN, vault))
}
#[expect(
clippy::disallowed_methods,
reason = "GitHub credential resolution intentionally falls back from vault to documented process-env names."
)]
fn lookup_env_or_vault(name: &str, vault: Option<&Vault>) -> Option<String> {
std::env::var(name)
.ok()
.or_else(|| vault.and_then(|v| v.get(name).map(str::to_string)))
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
}