Skip to main content

vgi_forge_github/
config.rs

1//! Adapter configuration.
2
3use std::time::Duration;
4
5use url::Url;
6use vgi_forge::{ForgeError, Result};
7
8/// `actions/checkout` pinned to a commit (v7.0.1), the same pin this
9/// repository's own workflows use. The generated workflow references
10/// actions by SHA only, like every workflow here (SEC-4045).
11pub const DEFAULT_CHECKOUT_ACTION: &str =
12    "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1";
13
14/// What the App JWT's `iss` claim carries.
15#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum JwtIssuer {
18    /// The client id — GitHub's recommendation on github.com.
19    #[default]
20    ClientId,
21    /// The numeric App id — for GitHub Enterprise Server releases that do
22    /// not accept a client id as issuer.
23    AppId,
24}
25
26/// How to reach one GitHub (github.com or a GHES instance) as one App.
27#[derive(Debug, Clone)]
28#[non_exhaustive]
29pub struct GitHubConfig {
30    /// Forge host resources on this GitHub start with: `github.com`, or the
31    /// GHES host.
32    pub host: String,
33    /// REST API base: `https://api.github.com`, `https://<ghes>/api/v3`.
34    pub api_base: Url,
35    /// Web base, for install pages and the OAuth device flow:
36    /// `https://github.com`, `https://<ghes>`.
37    pub web_base: Url,
38    /// The App's numeric id.
39    pub app_id: u64,
40    /// The App's client id (`Iv1.…` / `Iv23…`): the JWT issuer and the
41    /// device-flow client.
42    pub client_id: String,
43    /// The App's URL slug, for its install page.
44    pub app_slug: String,
45    /// Which identifier the App JWT is issued under.
46    pub jwt_issuer: JwtIssuer,
47    /// `uses:` reference for checkout in the generated workflow; must be
48    /// pinned to a 40-hex commit.
49    pub checkout_action: String,
50    /// The GitHub Actions App's id, which the required status check is pinned
51    /// to. `None` looks it up (`GET /apps/github-actions`) when first needed.
52    /// Pinning matters: an unpinned required check is satisfied by a status
53    /// *anyone with write access* posts under that name.
54    pub actions_integration_id: Option<u64>,
55    /// Per-request timeout.
56    pub request_timeout: Duration,
57    /// One second of device-flow polling interval, as the adapter waits it.
58    /// Always one second outside tests.
59    pub device_poll_unit: Duration,
60    /// Where no org required workflow is available (personal accounts,
61    /// organisations without org rulesets), the bridge posts the check
62    /// itself and the ruleset requires it from **this App** rather than
63    /// from GitHub Actions (§9, "forged check runs"): a workflow on another
64    /// branch can post a "Verify commit trust" run as the Actions App, but
65    /// not as the community's App. The plan then commits no workflow. Off by
66    /// default, so a caller with no check poster keeps the Actions workflow;
67    /// the bridge turns it on.
68    pub bridge_checks: bool,
69}
70
71impl GitHubConfig {
72    /// github.com.
73    pub fn github_com(
74        app_id: u64,
75        client_id: impl Into<String>,
76        app_slug: impl Into<String>,
77    ) -> Self {
78        GitHubConfig {
79            host: "github.com".into(),
80            api_base: Url::parse("https://api.github.com").expect("static URL"),
81            web_base: Url::parse("https://github.com").expect("static URL"),
82            app_id,
83            client_id: client_id.into(),
84            app_slug: app_slug.into(),
85            jwt_issuer: JwtIssuer::ClientId,
86            checkout_action: DEFAULT_CHECKOUT_ACTION.into(),
87            actions_integration_id: None,
88            request_timeout: Duration::from_secs(30),
89            device_poll_unit: Duration::from_secs(1),
90            bridge_checks: false,
91        }
92    }
93
94    /// A GitHub Enterprise Server instance at `host`.
95    pub fn enterprise(
96        host: &str,
97        app_id: u64,
98        client_id: impl Into<String>,
99        app_slug: impl Into<String>,
100    ) -> Result<Self> {
101        let host = host.to_ascii_lowercase();
102        let web_base = Url::parse(&format!("https://{host}"))
103            .map_err(|e| ForgeError::Config(format!("GHES host `{host}`: {e}")))?;
104        let api_base = web_base
105            .join("api/v3")
106            .map_err(|e| ForgeError::Config(e.to_string()))?;
107        let mut cfg = GitHubConfig::github_com(app_id, client_id, app_slug);
108        cfg.host = host;
109        cfg.api_base = api_base;
110        cfg.web_base = web_base;
111        Ok(cfg)
112    }
113
114    /// Point the API and web bases elsewhere (a proxy, a test server). The
115    /// host that resources must name is unchanged.
116    pub fn with_endpoints(mut self, api_base: Url, web_base: Url) -> Self {
117        self.api_base = api_base;
118        self.web_base = web_base;
119        self
120    }
121
122    /// Issue App JWTs under the numeric App id instead of the client id.
123    pub fn with_app_id_issuer(mut self) -> Self {
124        self.jwt_issuer = JwtIssuer::AppId;
125        self
126    }
127
128    /// Pin the Actions App id instead of looking it up.
129    pub fn with_actions_integration_id(mut self, id: u64) -> Self {
130        self.actions_integration_id = Some(id);
131        self
132    }
133
134    /// Have the bridge post the check itself where there is no org required
135    /// workflow (see [`GitHubConfig::bridge_checks`]).
136    pub fn with_bridge_checks(mut self) -> Self {
137        self.bridge_checks = true;
138        self
139    }
140}