@@ -1063,6 +1063,7 @@ pub fn is_gateway_mintable_strategy(strategy: ProviderCredentialRefreshStrategy)
10631063 | ProviderCredentialRefreshStrategy :: Oauth2ClientCredentials
10641064 | ProviderCredentialRefreshStrategy :: GoogleServiceAccountJwt
10651065 | ProviderCredentialRefreshStrategy :: AwsStsAssumeRole
1066+ | ProviderCredentialRefreshStrategy :: GithubAppInstallation
10661067 )
10671068}
10681069
@@ -1313,6 +1314,7 @@ pub fn provider_refresh_strategy_from_yaml(raw: &str) -> Option<ProviderCredenti
13131314 Some ( ProviderCredentialRefreshStrategy :: GoogleServiceAccountJwt )
13141315 }
13151316 "aws_sts_assume_role" => Some ( ProviderCredentialRefreshStrategy :: AwsStsAssumeRole ) ,
1317+ "github_app_installation" => Some ( ProviderCredentialRefreshStrategy :: GithubAppInstallation ) ,
13161318 _ => None ,
13171319 }
13181320}
@@ -1328,6 +1330,7 @@ pub fn provider_refresh_strategy_to_yaml(
13281330 ProviderCredentialRefreshStrategy :: Oauth2ClientCredentials => "oauth2_client_credentials" ,
13291331 ProviderCredentialRefreshStrategy :: GoogleServiceAccountJwt => "google_service_account_jwt" ,
13301332 ProviderCredentialRefreshStrategy :: AwsStsAssumeRole => "aws_sts_assume_role" ,
1333+ ProviderCredentialRefreshStrategy :: GithubAppInstallation => "github_app_installation" ,
13311334 ProviderCredentialRefreshStrategy :: Unspecified => "unspecified" ,
13321335 }
13331336}
@@ -2198,6 +2201,20 @@ pub fn validate_profile_set(
21982201 }
21992202
22002203 if let Some ( refresh) = credential. refresh . as_ref ( ) {
2204+ if refresh. strategy == ProviderCredentialRefreshStrategy :: GithubAppInstallation
2205+ && ( !refresh
2206+ . token_url
2207+ . ends_with ( "/app/installations/{installation_id}/access_tokens" )
2208+ || refresh. token_url . matches ( "{installation_id}" ) . count ( ) != 1
2209+ || !refresh. scopes . is_empty ( ) )
2210+ {
2211+ diagnostics. push ( ProfileValidationDiagnostic :: error (
2212+ source,
2213+ profile_id,
2214+ "credentials.refresh" ,
2215+ "github_app_installation requires a token_url ending in /app/installations/{installation_id}/access_tokens and uses permissions material instead of scopes" ,
2216+ ) ) ;
2217+ }
22012218 if refresh. strategy == ProviderCredentialRefreshStrategy :: Unspecified {
22022219 diagnostics. push ( ProfileValidationDiagnostic :: error (
22032220 source,
@@ -5896,6 +5913,25 @@ binaries:
58965913 ) ;
58975914 }
58985915
5916+ #[ test]
5917+ fn github_app_profile_roundtrip_and_runtime_credentials ( ) {
5918+ let profile = example_profile ( "github-app" ) ;
5919+ assert ! ( profile. required_static_credentials( ) . is_empty( ) ) ;
5920+ assert ! ( validate_profile_set( & [ ( "github-app.yaml" . into( ) , profile. clone( ) ) ] ) . is_empty( ) ) ;
5921+ let proto = profile. to_proto ( ) ;
5922+ assert_eq ! (
5923+ proto. credentials[ 0 ] . refresh. as_ref( ) . unwrap( ) . strategy,
5924+ openshell_core:: proto:: ProviderCredentialRefreshStrategy :: GithubAppInstallation as i32
5925+ ) ;
5926+ let restored = ProviderTypeProfile :: from_proto ( & proto) ;
5927+ let yaml = serde_yml:: to_string ( & restored) . unwrap ( ) ;
5928+ assert ! ( yaml. contains( "github_app_installation" ) ) ;
5929+ let mut invalid = profile. clone ( ) ;
5930+ invalid. credentials [ 0 ] . refresh . as_mut ( ) . unwrap ( ) . token_url =
5931+ "https://api.github.com/token" . into ( ) ;
5932+ assert ! ( !validate_profile_set( & [ ( "invalid.yaml" . into( ) , invalid) ] ) . is_empty( ) ) ;
5933+ }
5934+
58995935 #[ test]
59005936 fn aws_sts_strategy_serde_roundtrip ( ) {
59015937 use openshell_core:: proto:: ProviderCredentialRefreshStrategy ;
0 commit comments