1+ use async_trait:: async_trait;
12use git2:: Repository ;
23use lazy_static:: lazy_static;
34use regex:: Regex ;
5+ use serde:: Deserialize ;
46use serde_json:: Value ;
57use simplelog:: SharedLogger ;
68use std:: collections:: BTreeMap ;
79use std:: { env, fs} ;
810
911use crate :: prelude:: * ;
12+ use crate :: request_client:: OIDC_CLIENT ;
1013use crate :: run:: run_environment:: { RunEnvironment , RunPart } ;
1114use crate :: run:: {
1215 config:: Config ,
@@ -30,6 +33,12 @@ pub struct GitHubActionsProvider {
3033 pub gh_data : GhData ,
3134 pub event : RunEvent ,
3235 pub repository_root_path : String ,
36+
37+ /// Indicates whether the head repository is a fork of the base repository.
38+ is_head_repo_fork : bool ,
39+
40+ /// Indicates whether the repository is private.
41+ is_repository_private : bool ,
3342}
3443
3544impl GitHubActionsProvider {
@@ -42,6 +51,11 @@ impl GitHubActionsProvider {
4251 }
4352}
4453
54+ #[ derive( Deserialize ) ]
55+ struct OIDCResponse {
56+ value : Option < String > ,
57+ }
58+
4559lazy_static ! {
4660 static ref PR_REF_REGEX : Regex = Regex :: new( r"^refs/pull/(?P<pr_number>\d+)/merge$" ) . unwrap( ) ;
4761}
@@ -53,13 +67,18 @@ impl TryFrom<&Config> for GitHubActionsProvider {
5367 bail ! ( "Specifying owner and repository from CLI is not supported for Github Actions" ) ;
5468 }
5569 let ( owner, repository) = Self :: get_owner_and_repository ( ) ?;
70+
71+ let github_event_path = get_env_variable ( "GITHUB_EVENT_PATH" ) ?;
72+ let github_event = fs:: read_to_string ( github_event_path) ?;
73+ let github_event: Value =
74+ serde_json:: from_str ( & github_event) . expect ( "GITHUB_EVENT_PATH file could not be read" ) ;
75+
5676 let ref_ = get_env_variable ( "GITHUB_REF" ) ?;
5777 let is_pr = PR_REF_REGEX . is_match ( & ref_) ;
58- let head_ref = if is_pr {
59- let github_event_path = get_env_variable ( "GITHUB_EVENT_PATH" ) ?;
60- let github_event = fs:: read_to_string ( github_event_path) ?;
61- let github_event: Value = serde_json:: from_str ( & github_event)
62- . expect ( "GITHUB_EVENT_PATH file could not be read" ) ;
78+
79+ let is_repository_private = github_event[ "repository" ] [ "private" ] . as_bool ( ) . unwrap ( ) ;
80+
81+ let ( head_ref, is_head_repo_fork) = if is_pr {
6382 let pull_request = github_event[ "pull_request" ] . as_object ( ) . unwrap ( ) ;
6483
6584 let head_repo = pull_request[ "head" ] [ "repo" ] . as_object ( ) . unwrap ( ) ;
@@ -76,9 +95,9 @@ impl TryFrom<&Config> for GitHubActionsProvider {
7695 } else {
7796 pull_request[ "head" ] [ "ref" ] . as_str ( ) . unwrap ( ) . to_owned ( )
7897 } ;
79- Some ( head_ref)
98+ ( Some ( head_ref) , is_head_repo_fork )
8099 } else {
81- None
100+ ( None , false )
82101 } ;
83102
84103 let github_event_name = get_env_variable ( "GITHUB_EVENT_NAME" ) ?;
@@ -118,6 +137,8 @@ impl TryFrom<&Config> for GitHubActionsProvider {
118137 } ) ,
119138 base_ref : get_env_variable ( "GITHUB_BASE_REF" ) . ok ( ) ,
120139 repository_root_path,
140+ is_head_repo_fork,
141+ is_repository_private,
121142 } )
122143 }
123144}
@@ -129,6 +150,7 @@ impl RunEnvironmentDetector for GitHubActionsProvider {
129150 }
130151}
131152
153+ #[ async_trait( ?Send ) ]
132154impl RunEnvironmentProvider for GitHubActionsProvider {
133155 fn get_repository_provider ( & self ) -> RepositoryProvider {
134156 RepositoryProvider :: GitHub
@@ -236,6 +258,99 @@ impl RunEnvironmentProvider for GitHubActionsProvider {
236258 . to_string ( ) ;
237259 Ok ( commit_hash)
238260 }
261+
262+ /// Set the OIDC token for GitHub Actions if necessary
263+ ///
264+ /// ## Logic
265+ /// - If the user has explicitly set a token in the configuration (i.e. "static token"), do not override it, but display an info message.
266+ /// - Otherwise, check if the necessary environment variables are set to use OIDC.
267+ /// - Then attempt to request an OIDC token.
268+ ///
269+ /// If environment variables are not set, this could be because:
270+ /// - The user has misconfigured the workflow (missing `id-token` permission)
271+ /// - The run is from a public fork, in which case GitHub Actions does not provide these environment variables for security reasons.
272+ ///
273+ ///
274+ /// ## Notes
275+ /// Retrieving the token requires that the workflow has the `id-token` permission enabled.
276+ ///
277+ /// Docs:
278+ /// - https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-with-reusable-workflows
279+ /// - https://docs.github.com/en/actions/concepts/security/openid-connect
280+ /// - https://docs.github.com/en/actions/reference/security/oidc#methods-for-requesting-the-oidc-token
281+ async fn set_oidc_token ( & self , config : & mut Config ) -> Result < ( ) > {
282+ // Check if a static token is already set
283+ if config. token . is_some ( ) {
284+ info ! (
285+ "CodSpeed now supports OIDC tokens for authentication.\n
286+ Benefit from enhanced security by adding the `id-token: write` permission to your workflow and removing the static token from your configuration.\n
287+ Learn more at https://codspeed.io/docs/integrations/ci/github-actions#openid-connect-oidc-authentication"
288+ ) ;
289+
290+ return Ok ( ( ) ) ;
291+ }
292+
293+ // The `ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variable is set when the `id-token` permission is granted, which is necessary to authenticate with OIDC.
294+ let request_token = get_env_variable ( "ACTIONS_ID_TOKEN_REQUEST_TOKEN" ) . ok ( ) ;
295+ let request_url = get_env_variable ( "ACTIONS_ID_TOKEN_REQUEST_URL" ) . ok ( ) ;
296+
297+ if request_token. is_none ( ) || request_url. is_none ( ) {
298+ // If the run is from a fork, it is expected that these environment variables are not set.
299+ // We will fall back to tokenless authentication in this case.
300+ if self . is_head_repo_fork {
301+ return Ok ( ( ) ) ;
302+ }
303+
304+ if self . is_repository_private {
305+ bail ! (
306+ "Unable to retrieve OIDC token for authentication. \n
307+ Make sure your workflow has the `id-token: write` permission set. \n
308+ See https://codspeed.io/docs/integrations/ci/github-actions#openid-connect-oidc-authentication"
309+ )
310+ }
311+
312+ info ! (
313+ "CodSpeed now supports OIDC tokens for authentication.\n
314+ Benefit from enhanced security and faster processing times by adding the `id-token: write` permission to your workflow.\n
315+ Learn more at https://codspeed.io/docs/integrations/ci/github-actions#openid-connect-oidc-authentication"
316+ ) ;
317+
318+ return Ok ( ( ) ) ;
319+ }
320+
321+ let request_url = request_url. unwrap ( ) ;
322+ let request_url = format ! ( "{request_url}&audience={}" , self . get_oidc_audience( ) ) ;
323+ let request_token = request_token. unwrap ( ) ;
324+
325+ let token = match OIDC_CLIENT
326+ . get ( request_url)
327+ . header ( "Accept" , "application/json" )
328+ . header ( "Authorization" , format ! ( "Bearer {request_token}" ) )
329+ . send ( )
330+ . await
331+ {
332+ Ok ( response) => match response. json :: < OIDCResponse > ( ) . await {
333+ Ok ( oidc_response) => oidc_response. value ,
334+ Err ( _) => None ,
335+ } ,
336+ Err ( _) => None ,
337+ } ;
338+
339+ if token. is_some ( ) {
340+ debug ! ( "Successfully retrieved OIDC token for authentication." ) ;
341+ config. set_token ( token) ;
342+ } else if self . is_repository_private {
343+ bail ! (
344+ "Unable to retrieve OIDC token for authentication. \n
345+ Make sure your workflow has the `id-token: write` permission set. \n
346+ See https://codspeed.io/docs/integrations/ci/github-actions#openid-connect-oidc-authentication"
347+ )
348+ } else {
349+ warn ! ( "Failed to retrieve OIDC token for authentication." ) ;
350+ }
351+
352+ Ok ( ( ) )
353+ }
239354}
240355
241356#[ cfg( test) ]
@@ -271,6 +386,16 @@ mod tests {
271386 ( "GITHUB_ACTOR" , Some ( "actor" ) ) ,
272387 ( "GITHUB_BASE_REF" , Some ( "main" ) ) ,
273388 ( "GITHUB_EVENT_NAME" , Some ( "push" ) ) ,
389+ (
390+ "GITHUB_EVENT_PATH" ,
391+ Some (
392+ format ! (
393+ "{}/src/run/run_environment/github_actions/samples/push-event.json" ,
394+ env!( "CARGO_MANIFEST_DIR" )
395+ )
396+ . as_str ( ) ,
397+ ) ,
398+ ) ,
274399 ( "GITHUB_JOB" , Some ( "job" ) ) ,
275400 ( "GITHUB_REF" , Some ( "refs/heads/main" ) ) ,
276401 ( "GITHUB_REPOSITORY" , Some ( "owner/repository" ) ) ,
@@ -298,6 +423,8 @@ mod tests {
298423 github_actions_provider. sender. as_ref( ) . unwrap( ) . id,
299424 "1234567890"
300425 ) ;
426+ assert ! ( !github_actions_provider. is_head_repo_fork) ;
427+ assert ! ( !github_actions_provider. is_repository_private) ;
301428 } ,
302429 )
303430 }
@@ -338,6 +465,9 @@ mod tests {
338465 ..Config :: test ( )
339466 } ;
340467 let github_actions_provider = GitHubActionsProvider :: try_from ( & config) . unwrap ( ) ;
468+ assert ! ( !github_actions_provider. is_head_repo_fork) ;
469+ assert ! ( github_actions_provider. is_repository_private) ;
470+
341471 let run_environment_metadata = github_actions_provider
342472 . get_run_environment_metadata ( )
343473 . unwrap ( ) ;
@@ -391,6 +521,9 @@ mod tests {
391521 ..Config :: test ( )
392522 } ;
393523 let github_actions_provider = GitHubActionsProvider :: try_from ( & config) . unwrap ( ) ;
524+ assert ! ( github_actions_provider. is_head_repo_fork) ;
525+ assert ! ( !github_actions_provider. is_repository_private) ;
526+
394527 let run_environment_metadata = github_actions_provider
395528 . get_run_environment_metadata ( )
396529 . unwrap ( ) ;
@@ -471,6 +604,9 @@ mod tests {
471604 ..Config :: test ( )
472605 } ;
473606 let github_actions_provider = GitHubActionsProvider :: try_from ( & config) . unwrap ( ) ;
607+ assert ! ( !github_actions_provider. is_head_repo_fork) ;
608+ assert ! ( github_actions_provider. is_repository_private) ;
609+
474610 let run_environment_metadata = github_actions_provider
475611 . get_run_environment_metadata ( )
476612 . unwrap ( ) ;
@@ -503,6 +639,8 @@ mod tests {
503639 } ,
504640 event : RunEvent :: Push ,
505641 repository_root_path : "/home/work/my-repo" . into ( ) ,
642+ is_head_repo_fork : false ,
643+ is_repository_private : false ,
506644 } ;
507645
508646 let run_part = github_actions_provider. get_run_provider_run_part ( ) . unwrap ( ) ;
@@ -545,6 +683,8 @@ mod tests {
545683 } ,
546684 event : RunEvent :: Push ,
547685 repository_root_path : "/home/work/my-repo" . into ( ) ,
686+ is_head_repo_fork : false ,
687+ is_repository_private : false ,
548688 } ;
549689
550690 let run_part = github_actions_provider. get_run_provider_run_part ( ) . unwrap ( ) ;
@@ -596,6 +736,8 @@ mod tests {
596736 } ,
597737 event : RunEvent :: Push ,
598738 repository_root_path : "/home/work/my-repo" . into ( ) ,
739+ is_head_repo_fork : false ,
740+ is_repository_private : false ,
599741 } ;
600742
601743 let run_part = github_actions_provider. get_run_provider_run_part ( ) . unwrap ( ) ;
@@ -645,6 +787,8 @@ mod tests {
645787 } ,
646788 event : RunEvent :: Push ,
647789 repository_root_path : "/home/work/my-repo" . into ( ) ,
790+ is_head_repo_fork : false ,
791+ is_repository_private : false ,
648792 } ;
649793
650794 let run_part = github_actions_provider. get_run_provider_run_part ( ) . unwrap ( ) ;
0 commit comments