@@ -46,9 +46,25 @@ def parse_args():
4646 parser .add_argument ("--max-failures" , type = int , default = 50 , help = "Stop after this many failures (default: 50)" )
4747 parser .add_argument ("--timeout" , type = int , default = 7200 , help = "Overall timeout in seconds (default: 7200 = 2 hours)" )
4848 parser .add_argument ("--list-jobs" , action = "store_true" , help = "List all jobs grouped by pipeline and exit" )
49+ parser .add_argument ("--jobs" , type = str , default = None , help = "Comma-separated substring patterns; monitor only matching jobs (case-insensitive)" )
4950 return parser .parse_args ()
5051
5152
53+ def parse_job_patterns (spec : str | None ) -> list [str ]:
54+ """Split a comma-separated --jobs spec into lowercased substring patterns."""
55+ if not spec :
56+ return []
57+ return [p .strip ().lower () for p in spec .split ("," ) if p .strip ()]
58+
59+
60+ def job_name_matches (name : str , patterns : list [str ]) -> bool :
61+ """Return True if name contains any pattern (case-insensitive substring). Empty patterns match all."""
62+ if not patterns :
63+ return True
64+ lname = name .lower ()
65+ return any (p in lname for p in patterns )
66+
67+
5268def resolve_sha (ref : str ) -> str :
5369 result = subprocess .run (["git" , "rev-parse" , ref ], capture_output = True , text = True )
5470 if result .returncode != 0 :
@@ -383,8 +399,9 @@ async def gh_download_job_log(session: aiohttp.ClientSession, job_id: int, log_d
383399# list-jobs mode
384400# ---------------------------------------------------------------------------
385401
386- async def list_jobs (session : aiohttp .ClientSession , root_id : int ):
387- """List all GitLab jobs grouped by pipeline, then exit."""
402+ async def list_jobs (session : aiohttp .ClientSession , root_id : int , patterns : list [str ] | None = None ):
403+ """List all GitLab jobs grouped by pipeline, then exit. Filtered to matched jobs when patterns given."""
404+ patterns = patterns or []
388405 pipeline_ids , pipeline_names , jobs_by_pipeline , pipeline_statuses = await discover_pipelines_and_jobs (session , root_id )
389406
390407 for pid in pipeline_ids :
@@ -394,6 +411,10 @@ async def list_jobs(session: aiohttp.ClientSession, root_id: int):
394411 info = pipeline_statuses .get (pid , {"status" : "unknown" , "name" : str (pid )})
395412 display_name = pipeline_names .get (pid , info ["name" ])
396413 jobs = jobs_by_pipeline .get (pid , [])
414+ if patterns :
415+ jobs = [j for j in jobs if job_name_matches (j .get ("name" , "" ), patterns )]
416+ if not jobs :
417+ continue
397418 print (f"\n Pipeline { pid } '{ display_name } ' (status: { info ['status' ]} ):" )
398419 for job in jobs :
399420 job_status = job .get ("status" , "unknown" )
@@ -402,8 +423,9 @@ async def list_jobs(session: aiohttp.ClientSession, root_id: int):
402423 print (f" { job_status :<10} ({ job_id } ) { job_name } " )
403424
404425
405- async def list_github_jobs (session : aiohttp .ClientSession , sha : str ):
406- """List GitHub Actions workflow runs and their jobs for a SHA."""
426+ async def list_github_jobs (session : aiohttp .ClientSession , sha : str , patterns : list [str ] | None = None ):
427+ """List GitHub Actions workflow runs and their jobs for a SHA. Filtered to matched jobs when patterns given."""
428+ patterns = patterns or []
407429 try :
408430 runs = await gh_discover_runs (session , sha , timeout = 15 )
409431 except GitHubAuthError :
@@ -420,6 +442,10 @@ async def list_github_jobs(session: aiohttp.ClientSession, sha: str):
420442 run_conclusion = run .get ("conclusion" ) or ""
421443 status_str = run_status if not run_conclusion else f"{ run_status } , conclusion: { run_conclusion } "
422444 jobs = await gh_get_run_jobs (session , run_id )
445+ if patterns :
446+ jobs = [j for j in jobs if job_name_matches (j .get ("name" , "" ), patterns )]
447+ if not jobs :
448+ continue
423449 print (f"\n GitHub Actions run { run_id } '{ run_name } ' (status: { status_str } ):" )
424450 for job in sorted (jobs , key = lambda j : j .get ("name" , "" )):
425451 job_status = job .get ("status" , "unknown" )
@@ -493,10 +519,14 @@ async def _monitor(args, sha: str | None, gl_session: aiohttp.ClientSession, gh_
493519 else :
494520 root_id = await discover_pipeline (gl_session , sha , args .discovery_timeout )
495521
522+ patterns = parse_job_patterns (args .jobs )
523+ if patterns :
524+ print (f"Filtering to jobs matching (any of): { patterns } " )
525+
496526 if args .list_jobs :
497- await list_jobs (gl_session , root_id )
527+ await list_jobs (gl_session , root_id , patterns )
498528 if gh_session and sha :
499- await list_github_jobs (gh_session , sha )
529+ await list_github_jobs (gh_session , sha , patterns )
500530 return
501531
502532 # Set up working directory
@@ -557,6 +587,8 @@ async def _monitor(args, sha: str | None, gl_session: aiohttp.ClientSession, gh_
557587
558588 pipeline_ids , _ , jobs_by_pipeline , pipeline_statuses = gl_result
559589 all_jobs = [job for jobs in jobs_by_pipeline .values () for job in jobs ]
590+ if patterns :
591+ all_jobs = [job for job in all_jobs if job_name_matches (job .get ("name" , "" ), patterns )]
560592
561593 # Phase 2: fetch GH run-jobs while we can (no GL dependency); process GL jobs locally.
562594 if gh_monitoring :
@@ -609,6 +641,7 @@ async def _monitor(args, sha: str | None, gl_session: aiohttp.ClientSession, gh_
609641 gh_passed = 0
610642 gh_failed_count = 0
611643 gh_all_done = True
644+ gh_matched_total = 0
612645 gh_new_failures : list [tuple [dict , dict ]] = []
613646 gh_fail_log_dir = wdir / "gh_fail_logs"
614647
@@ -618,6 +651,9 @@ async def _monitor(args, sha: str | None, gl_session: aiohttp.ClientSession, gh_
618651 if run .get ("status" , "unknown" ) != "completed" :
619652 gh_all_done = False
620653 for job in run_jobs :
654+ if patterns and not job_name_matches (job .get ("name" , "" ), patterns ):
655+ continue
656+ gh_matched_total += 1
621657 jid = job ["id" ]
622658 job_status = job .get ("status" , "unknown" )
623659 conclusion = job .get ("conclusion" ) or ""
@@ -695,7 +731,15 @@ async def _monitor(args, sha: str | None, gl_session: aiohttp.ClientSession, gh_
695731 print (f"[{ now } ] pipelines={ len (pipeline_ids )} jobs={ len (all_jobs )} running={ gl_running_count } passed={ passed } failed={ failed } allow_fail={ allow_fail } " )
696732
697733 # Check completion
698- all_done = all_gl_done and (gl_running_count == 0 ) and (not gh_monitoring or gh_all_done )
734+ if patterns :
735+ # Finish as soon as the MATCHED subset is terminal (no matched job still
736+ # running), regardless of the rest of the pipeline. Require at least one
737+ # matched job to exist so we keep polling while jobs are still being created.
738+ matched_total = len (all_jobs ) + gh_matched_total
739+ matched_running = gl_running_count + gh_running
740+ all_done = matched_total > 0 and matched_running == 0
741+ else :
742+ all_done = all_gl_done and (gl_running_count == 0 ) and (not gh_monitoring or gh_all_done )
699743
700744 if all_done :
701745 total_failures = failure_count
0 commit comments