11use std:: collections:: HashMap ;
22use std:: path:: Path ;
3+ use std:: process:: Command ;
34
45use anyhow:: Context ;
56use clap:: Parser ;
67use serde_yaml:: Value ;
78
89const CI_DIRECTORY : & str = concat ! ( env!( "CARGO_MANIFEST_DIR" ) , "/.." ) ;
10+ const DOCKER_DIRECTORY : & str = concat ! ( env!( "CARGO_MANIFEST_DIR" ) , "/../docker" ) ;
911const JOBS_YML_PATH : & str = concat ! ( env!( "CARGO_MANIFEST_DIR" ) , "/../github-actions/jobs.yml" ) ;
1012
1113/// Representation of a job loaded from the jobs.yml file.
@@ -24,6 +26,21 @@ struct Job {
2426 extra_keys : HashMap < String , Value > ,
2527}
2628
29+ impl Job {
30+ fn is_linux ( & self ) -> bool {
31+ self . os . contains ( "ubuntu" )
32+ }
33+
34+ /// By default, the Docker image of a job is based on its name.
35+ /// However, it can be overridden by its IMAGE environment variable.
36+ fn image ( & self ) -> String {
37+ self . env
38+ . get ( "IMAGE" )
39+ . map ( |v| v. as_str ( ) . expect ( "IMAGE value should be a string" ) . to_string ( ) )
40+ . unwrap_or_else ( || self . name . clone ( ) )
41+ }
42+ }
43+
2744#[ derive( serde:: Deserialize , Debug ) ]
2845struct JobEnvironments {
2946 #[ serde( rename = "pr" ) ]
@@ -267,11 +284,75 @@ fn calculate_job_matrix(
267284 Ok ( ( ) )
268285}
269286
287+ fn find_linux_job < ' a > ( jobs : & ' a [ Job ] , name : & str ) -> anyhow:: Result < & ' a Job > {
288+ let Some ( job) = jobs. iter ( ) . find ( |j| j. name == name) else {
289+ let available_jobs: Vec < & Job > = jobs. iter ( ) . filter ( |j| j. is_linux ( ) ) . collect ( ) ;
290+ let mut available_jobs =
291+ available_jobs. iter ( ) . map ( |j| j. name . to_string ( ) ) . collect :: < Vec < _ > > ( ) ;
292+ available_jobs. sort ( ) ;
293+ return Err ( anyhow:: anyhow!(
294+ "Job {name} not found. The following jobs are available:\n {}" ,
295+ available_jobs. join( ", " )
296+ ) ) ;
297+ } ;
298+ if !job. is_linux ( ) {
299+ return Err ( anyhow:: anyhow!( "Only Linux jobs can be executed locally" ) ) ;
300+ }
301+
302+ Ok ( job)
303+ }
304+
305+ fn run_workflow_locally ( db : JobDatabase , job_type : JobType , name : String ) -> anyhow:: Result < ( ) > {
306+ let jobs = match job_type {
307+ JobType :: Auto => & db. auto_jobs ,
308+ JobType :: PR => & db. pr_jobs ,
309+ } ;
310+ let job = find_linux_job ( & jobs, & name) . with_context ( || format ! ( "Cannot find job {name}" ) ) ?;
311+
312+ let mut custom_env: HashMap < String , String > = HashMap :: new ( ) ;
313+ // Replicate src/ci/scripts/setup-environment.sh
314+ // Adds custom environment variables to the job
315+ if name. starts_with ( "dist-" ) {
316+ if name. ends_with ( "-alt" ) {
317+ custom_env. insert ( "DEPLOY_ALT" . to_string ( ) , "1" . to_string ( ) ) ;
318+ } else {
319+ custom_env. insert ( "DEPLOY" . to_string ( ) , "1" . to_string ( ) ) ;
320+ }
321+ }
322+ custom_env. extend ( to_string_map ( & job. env ) ) ;
323+
324+ let mut cmd = Command :: new ( Path :: new ( DOCKER_DIRECTORY ) . join ( "run.sh" ) ) ;
325+ cmd. arg ( job. image ( ) ) ;
326+ cmd. envs ( custom_env) ;
327+
328+ eprintln ! ( "Executing {cmd:?}" ) ;
329+
330+ let result = cmd. spawn ( ) ?. wait ( ) ?;
331+ if !result. success ( ) { Err ( anyhow:: anyhow!( "Job failed" ) ) } else { Ok ( ( ) ) }
332+ }
333+
270334#[ derive( clap:: Parser ) ]
271335enum Args {
272336 /// Calculate a list of jobs that should be executed on CI.
273337 /// Should only be used on CI inside GitHub actions.
274338 CalculateJobMatrix ,
339+ /// Execute a given CI job locally.
340+ #[ clap( name = "run-local" ) ]
341+ RunJobLocally {
342+ /// Name of the job that should be executed.
343+ name : String ,
344+ /// Type of the job that should be executed.
345+ #[ clap( long = "type" , default_value = "auto" ) ]
346+ job_type : JobType ,
347+ } ,
348+ }
349+
350+ #[ derive( clap:: ValueEnum , Clone ) ]
351+ enum JobType {
352+ /// Merge attempt ("auto") job
353+ Auto ,
354+ /// Pull request job
355+ PR ,
275356}
276357
277358fn main ( ) -> anyhow:: Result < ( ) > {
@@ -287,6 +368,7 @@ fn main() -> anyhow::Result<()> {
287368
288369 calculate_job_matrix ( db, gh_ctx, & channel) . context ( "Failed to calculate job matrix" ) ?;
289370 }
371+ Args :: RunJobLocally { job_type, name } => run_workflow_locally ( db, job_type, name) ?,
290372 }
291373
292374 Ok ( ( ) )
0 commit comments