@@ -2,10 +2,11 @@ use lazy_static::lazy_static;
22use regex:: Regex ;
33use serde_json:: Value ;
44use simplelog:: SharedLogger ;
5+ use std:: collections:: BTreeMap ;
56use std:: { env, fs} ;
67
78use crate :: prelude:: * ;
8- use crate :: run:: ci_provider:: interfaces:: Platform ;
9+ use crate :: run:: ci_provider:: interfaces:: { Platform , RunPart } ;
910use crate :: run:: {
1011 ci_provider:: {
1112 interfaces:: { CIProviderMetadata , GhData , RepositoryProvider , RunEvent , Sender } ,
@@ -133,6 +134,64 @@ impl CIProvider for GitHubActionsProvider {
133134 Platform :: GithubActions
134135 }
135136
137+ /// For Github, the platform run part is the most complicated
138+ /// since we support matrix jobs.
139+ ///
140+ /// Computing the `run_part_id`:
141+ /// - not in a matrix:
142+ /// - simply take the job name
143+ /// - in a matrix:
144+ /// - take the job name
145+ /// - concatenate it with key-values from `matrix` and `strategy`
146+ ///
147+ /// `GH_MATRIX` and `GH_STRATEGY` are environment variables computed by
148+ /// https://github.com/CodSpeedHQ/action:
149+ /// - `GH_MATRIX`: ${{ toJson(matrix) }}
150+ /// - `GH_STRATEGY`: ${{ toJson(strategy) }}
151+ ///
152+ /// A note on parsing:
153+ ///
154+ /// The issue is these variables from Github Actions are multiline.
155+ /// As we need to use them compute an identifier, we need them as a single line.
156+ /// Plus we are interested in the content of these objects,
157+ /// so it makes sense to parse and re-serialize them.
158+ fn get_platform_run_part ( & self ) -> Option < RunPart > {
159+ let job_name = self . gh_data . job . clone ( ) ;
160+
161+ let mut metadata = BTreeMap :: new ( ) ;
162+
163+ let gh_matrix = get_env_variable ( "GH_MATRIX" )
164+ . ok ( )
165+ . and_then ( |v| serde_json:: from_str :: < Value > ( & v) . ok ( ) ) ;
166+
167+ let gh_strategy = get_env_variable ( "GH_STRATEGY" )
168+ . ok ( )
169+ . and_then ( |v| serde_json:: from_str :: < Value > ( & v) . ok ( ) ) ;
170+
171+ let run_part_id = if let ( Some ( Value :: Object ( matrix) ) , Some ( Value :: Object ( strategy) ) ) =
172+ ( gh_matrix, gh_strategy)
173+ {
174+ // The re-serialization is on purpose here. We want to serialize it as a single line.
175+ let matrix_str = serde_json:: to_string ( & matrix) . expect ( "Unable to re-serialize matrix" ) ;
176+ let strategy_str =
177+ serde_json:: to_string ( & strategy) . expect ( "Unable to re-serialize strategy" ) ;
178+
179+ metadata. extend ( matrix) ;
180+ metadata. extend ( strategy) ;
181+
182+ format ! ( "{job_name}-{matrix_str}-{strategy_str}" )
183+ } else {
184+ job_name
185+ } ;
186+
187+ Some ( RunPart {
188+ run_id : self . gh_data . run_id . clone ( ) ,
189+ run_part_id,
190+ job_name : self . gh_data . job . clone ( ) ,
191+ metadata,
192+ } )
193+ }
194+
136195 fn get_ci_provider_metadata ( & self ) -> Result < CIProviderMetadata > {
137196 Ok ( CIProviderMetadata {
138197 base_ref : self . base_ref . clone ( ) ,
@@ -246,13 +305,15 @@ mod tests {
246305 } ;
247306 let github_actions_provider = GitHubActionsProvider :: try_from ( & config) . unwrap ( ) ;
248307 let provider_metadata = github_actions_provider. get_ci_provider_metadata ( ) . unwrap ( ) ;
308+ let run_part = github_actions_provider. get_platform_run_part ( ) . unwrap ( ) ;
249309
250310 assert_json_snapshot ! ( provider_metadata, {
251311 ".runner.version" => insta:: dynamic_redaction( |value, _path| {
252312 assert_eq!( value. as_str( ) . unwrap( ) , VERSION . to_string( ) ) ;
253313 "[version]"
254314 } ) ,
255315 } ) ;
316+ assert_json_snapshot ! ( run_part) ;
256317 } ,
257318 ) ;
258319 }
@@ -282,6 +343,7 @@ mod tests {
282343 ( "GITHUB_REPOSITORY" , Some ( "my-org/adrien-python-test" ) ) ,
283344 ( "GITHUB_RUN_ID" , Some ( "6957110437" ) ) ,
284345 ( "VERSION" , Some ( "0.1.0" ) ) ,
346+ ( "GH_MATRIX" , Some ( "null" ) ) ,
285347 ] ,
286348 || {
287349 let config = Config {
@@ -290,6 +352,7 @@ mod tests {
290352 } ;
291353 let github_actions_provider = GitHubActionsProvider :: try_from ( & config) . unwrap ( ) ;
292354 let provider_metadata = github_actions_provider. get_ci_provider_metadata ( ) . unwrap ( ) ;
355+ let run_part = github_actions_provider. get_platform_run_part ( ) . unwrap ( ) ;
293356
294357 assert_eq ! ( provider_metadata. owner, "my-org" ) ;
295358 assert_eq ! ( provider_metadata. repository, "adrien-python-test" ) ;
@@ -298,13 +361,259 @@ mod tests {
298361 provider_metadata. head_ref,
299362 Some ( "fork-owner:feat/codspeed-runner" . into( ) )
300363 ) ;
364+
301365 assert_json_snapshot ! ( provider_metadata, {
302366 ".runner.version" => insta:: dynamic_redaction( |value, _path| {
303367 assert_eq!( value. as_str( ) . unwrap( ) , VERSION . to_string( ) ) ;
304368 "[version]"
305369 } ) ,
306370 } ) ;
371+ assert_json_snapshot ! ( run_part) ;
307372 } ,
308373 ) ;
309374 }
375+
376+ #[ test]
377+ fn test_matrix_job_provider_metadata ( ) {
378+ with_vars (
379+ [
380+ ( "GITHUB_ACTIONS" , Some ( "true" ) ) ,
381+ ( "GITHUB_ACTOR_ID" , Some ( "19605940" ) ) ,
382+ ( "GITHUB_ACTOR" , Some ( "adriencaccia" ) ) ,
383+ ( "GITHUB_BASE_REF" , Some ( "main" ) ) ,
384+ ( "GITHUB_EVENT_NAME" , Some ( "pull_request" ) ) ,
385+ (
386+ "GITHUB_EVENT_PATH" ,
387+ Some (
388+ format ! (
389+ "{}/src/run/ci_provider/github_actions/samples/pr-event.json" ,
390+ env!( "CARGO_MANIFEST_DIR" )
391+ )
392+ . as_str ( ) ,
393+ ) ,
394+ ) ,
395+ ( "GITHUB_HEAD_REF" , Some ( "feat/codspeed-runner" ) ) ,
396+ ( "GITHUB_JOB" , Some ( "log-env" ) ) ,
397+ ( "GITHUB_REF" , Some ( "refs/pull/22/merge" ) ) ,
398+ ( "GITHUB_REPOSITORY" , Some ( "my-org/adrien-python-test" ) ) ,
399+ ( "GITHUB_RUN_ID" , Some ( "6957110437" ) ) ,
400+ ( "VERSION" , Some ( "0.1.0" ) ) ,
401+ (
402+ "GH_MATRIX" ,
403+ Some (
404+ r#"{
405+ "runner-version":"3.2.1",
406+ "numeric-value":123456789
407+ }"# ,
408+ ) ,
409+ ) ,
410+ (
411+ "GH_STRATEGY" ,
412+ Some (
413+ r#"{
414+ "fail-fast":true,
415+ "job-index":1,
416+ "job-total":2,
417+ "max-parallel":2
418+ }"# ,
419+ ) ,
420+ ) ,
421+ ] ,
422+ || {
423+ let config = Config {
424+ token : Some ( "token" . into ( ) ) ,
425+ ..Config :: test ( )
426+ } ;
427+ let github_actions_provider = GitHubActionsProvider :: try_from ( & config) . unwrap ( ) ;
428+ let provider_metadata = github_actions_provider. get_ci_provider_metadata ( ) . unwrap ( ) ;
429+ let run_part = github_actions_provider. get_platform_run_part ( ) . unwrap ( ) ;
430+
431+ assert_json_snapshot ! ( provider_metadata, {
432+ ".runner.version" => insta:: dynamic_redaction( |value, _path| {
433+ assert_eq!( value. as_str( ) . unwrap( ) , VERSION . to_string( ) ) ;
434+ "[version]"
435+ } ) ,
436+ } ) ;
437+ assert_json_snapshot ! ( run_part) ;
438+ } ,
439+ ) ;
440+ }
441+
442+ #[ test]
443+ fn test_get_run_part_no_matrix ( ) {
444+ with_vars ( [ ( "GITHUB_ACTIONS" , Some ( "true" ) ) ] , || {
445+ let github_actions_provider = GitHubActionsProvider {
446+ owner : "owner" . into ( ) ,
447+ repository : "repository" . into ( ) ,
448+ ref_ : "refs/head/my-branch" . into ( ) ,
449+ head_ref : Some ( "my-branch" . into ( ) ) ,
450+ base_ref : None ,
451+ sender : None ,
452+ gh_data : GhData {
453+ job : "my_job" . into ( ) ,
454+ run_id : "123789" . into ( ) ,
455+ } ,
456+ event : RunEvent :: Push ,
457+ repository_root_path : "/home/work/my-repo" . into ( ) ,
458+ } ;
459+
460+ let run_part = github_actions_provider. get_platform_run_part ( ) . unwrap ( ) ;
461+
462+ assert_eq ! ( run_part. run_id, "123789" ) ;
463+ assert_eq ! ( run_part. job_name, "my_job" ) ;
464+ assert_eq ! ( run_part. run_part_id, "my_job" ) ;
465+ assert_json_snapshot ! ( run_part. metadata, @"{}" ) ;
466+ } )
467+ }
468+
469+ #[ test]
470+ fn test_get_run_part_null_matrix ( ) {
471+ with_vars (
472+ [
473+ ( "GH_MATRIX" , Some ( "null" ) ) ,
474+ (
475+ "GH_STRATEGY" ,
476+ Some (
477+ r#"{
478+ "fail-fast":true,
479+ "job-index":0,
480+ "job-total":1,
481+ "max-parallel":1
482+ }"# ,
483+ ) ,
484+ ) ,
485+ ] ,
486+ || {
487+ let github_actions_provider = GitHubActionsProvider {
488+ owner : "owner" . into ( ) ,
489+ repository : "repository" . into ( ) ,
490+ ref_ : "refs/head/my-branch" . into ( ) ,
491+ head_ref : Some ( "my-branch" . into ( ) ) ,
492+ base_ref : None ,
493+ sender : None ,
494+ gh_data : GhData {
495+ job : "my_job" . into ( ) ,
496+ run_id : "123789" . into ( ) ,
497+ } ,
498+ event : RunEvent :: Push ,
499+ repository_root_path : "/home/work/my-repo" . into ( ) ,
500+ } ;
501+
502+ let run_part = github_actions_provider. get_platform_run_part ( ) . unwrap ( ) ;
503+
504+ assert_eq ! ( run_part. run_id, "123789" ) ;
505+ assert_eq ! ( run_part. job_name, "my_job" ) ;
506+ assert_eq ! ( run_part. run_part_id, "my_job" ) ;
507+ assert_json_snapshot ! ( run_part. metadata, @"{}" ) ;
508+ } ,
509+ )
510+ }
511+
512+ #[ test]
513+ fn test_get_matrix_run_part ( ) {
514+ with_vars (
515+ [
516+ (
517+ "GH_MATRIX" ,
518+ Some (
519+ r#"{
520+ "runner-version":"3.2.1",
521+ "numeric-value":123456789
522+ }"# ,
523+ ) ,
524+ ) ,
525+ (
526+ "GH_STRATEGY" ,
527+ Some (
528+ r#"{
529+ "fail-fast":true,
530+ "job-index":1,
531+ "job-total":2,
532+ "max-parallel":2
533+ }"# ,
534+ ) ,
535+ ) ,
536+ ] ,
537+ || {
538+ let github_actions_provider = GitHubActionsProvider {
539+ owner : "owner" . into ( ) ,
540+ repository : "repository" . into ( ) ,
541+ ref_ : "refs/head/my-branch" . into ( ) ,
542+ head_ref : Some ( "my-branch" . into ( ) ) ,
543+ base_ref : None ,
544+ sender : None ,
545+ gh_data : GhData {
546+ job : "my_job" . into ( ) ,
547+ run_id : "123789" . into ( ) ,
548+ } ,
549+ event : RunEvent :: Push ,
550+ repository_root_path : "/home/work/my-repo" . into ( ) ,
551+ } ;
552+
553+ let run_part = github_actions_provider. get_platform_run_part ( ) . unwrap ( ) ;
554+
555+ assert_eq ! ( run_part. run_id, "123789" ) ;
556+ assert_eq ! ( run_part. job_name, "my_job" ) ;
557+ assert_eq ! ( run_part. run_part_id, "my_job-{\" runner-version\" :\" 3.2.1\" ,\" numeric-value\" :123456789}-{\" fail-fast\" :true,\" job-index\" :1,\" job-total\" :2,\" max-parallel\" :2}" ) ;
558+ assert_json_snapshot ! ( run_part. metadata, @r#"
559+ {
560+ "fail-fast": true,
561+ "job-index": 1,
562+ "job-total": 2,
563+ "max-parallel": 2,
564+ "numeric-value": 123456789,
565+ "runner-version": "3.2.1"
566+ }
567+ "# ) ;
568+ } ,
569+ )
570+ }
571+
572+ #[ test]
573+ fn test_get_inline_matrix_run_part ( ) {
574+ with_vars (
575+ [
576+ (
577+ "GH_MATRIX" ,
578+ Some ( "{\" runner-version\" :\" 3.2.1\" ,\" numeric-value\" :123456789}" ) ,
579+ ) ,
580+ (
581+ "GH_STRATEGY" ,
582+ Some ( "{\" fail-fast\" :true,\" job-index\" :1,\" job-total\" :2,\" max-parallel\" :2}" ) ,
583+ ) ,
584+ ] ,
585+ || {
586+ let github_actions_provider = GitHubActionsProvider {
587+ owner : "owner" . into ( ) ,
588+ repository : "repository" . into ( ) ,
589+ ref_ : "refs/head/my-branch" . into ( ) ,
590+ head_ref : Some ( "my-branch" . into ( ) ) ,
591+ base_ref : None ,
592+ sender : None ,
593+ gh_data : GhData {
594+ job : "my_job" . into ( ) ,
595+ run_id : "123789" . into ( ) ,
596+ } ,
597+ event : RunEvent :: Push ,
598+ repository_root_path : "/home/work/my-repo" . into ( ) ,
599+ } ;
600+
601+ let run_part = github_actions_provider. get_platform_run_part ( ) . unwrap ( ) ;
602+
603+ assert_eq ! ( run_part. run_id, "123789" ) ;
604+ assert_eq ! ( run_part. job_name, "my_job" ) ;
605+ assert_eq ! ( run_part. run_part_id, "my_job-{\" runner-version\" :\" 3.2.1\" ,\" numeric-value\" :123456789}-{\" fail-fast\" :true,\" job-index\" :1,\" job-total\" :2,\" max-parallel\" :2}" ) ;
606+ assert_json_snapshot ! ( run_part. metadata, @r#"
607+ {
608+ "fail-fast": true,
609+ "job-index": 1,
610+ "job-total": 2,
611+ "max-parallel": 2,
612+ "numeric-value": 123456789,
613+ "runner-version": "3.2.1"
614+ }
615+ "# ) ;
616+ } ,
617+ )
618+ }
310619}
0 commit comments