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}