11import click
22import getpass
3+ from chipfoundry_cli .remote_precheck_git import RemotePrecheckGitError , verify_remote_precheck_repo
34from chipfoundry_cli .utils import (
45 collect_project_files , ensure_cf_directory , update_or_create_project_json ,
56 sftp_connect , upload_with_progress , sftp_ensure_dirs , sftp_download_recursive ,
@@ -1653,6 +1654,13 @@ def pull(project_name, output_dir, sftp_host, sftp_username, sftp_key):
16531654 "CHANGES_REQUESTED" : "Changes requested by the review team. See notes above." ,
16541655}
16551656
1657+ REMOTE_PRECHECK_STATUS_COLORS = {
1658+ "queued" : "yellow" ,
1659+ "running" : "cyan bold" ,
1660+ "completed" : "green" ,
1661+ "failed" : "red" ,
1662+ }
1663+
16561664
16571665def _show_platform_status (project_root : str ):
16581666 """Show the platform pipeline panel if the project is linked. Returns True if shown."""
@@ -1695,6 +1703,28 @@ def _show_platform_status(project_root: str):
16951703 lines .append (f"[bold]Tapeout:[/bold] [{ tc } ]{ tl } [/{ tc } ]" )
16961704 if project .get ('gds_hash' ):
16971705 lines .append (f"[bold]GDS Hash:[/bold] { project ['gds_hash' ][:16 ]} ..." )
1706+ rj = project .get ("latest_remote_precheck_job" )
1707+ if isinstance (rj , dict ) and rj .get ("status" ):
1708+ jst = str (rj .get ("status" , "" ))
1709+ jc = REMOTE_PRECHECK_STATUS_COLORS .get (jst , "white" )
1710+ lines .append (f"[bold]Remote precheck:[/bold] [{ jc } ]{ jst } [/{ jc } ]" )
1711+ ref = rj .get ("git_ref" )
1712+ if ref :
1713+ lines .append (f"[dim] git ref: { ref } [/dim]" )
1714+ created = rj .get ("created_at" )
1715+ if created and isinstance (created , str ):
1716+ lines .append (f"[dim] started: { created [:19 ]} [/dim]" )
1717+ if jst in ("completed" , "failed" ):
1718+ done = rj .get ("completed_at" )
1719+ if done and isinstance (done , str ):
1720+ lines .append (f"[dim] finished: { done [:19 ]} [/dim]" )
1721+ if jst == "failed" and rj .get ("error_message" ):
1722+ err = str (rj ["error_message" ])
1723+ if len (err ) > 240 :
1724+ err = err [:237 ] + "..."
1725+ lines .append (f"[red] { err } [/red]" )
1726+ if jst == "completed" and rj .get ("github_pr_url" ):
1727+ lines .append (f"[green] PR:[/green] { rj ['github_pr_url' ]} " )
16981728 if project .get ('updated_at' ):
16991729 lines .append (f"[bold]Updated:[/bold] { project ['updated_at' ][:10 ]} " )
17001730 if project .get ('admin_review_notes' ):
@@ -3248,7 +3278,21 @@ def _upload_precheck_results(project_json_path: Path):
32483278@click .option ('--magic-drc' , is_flag = True , help = 'Include Magic DRC check (optional, off by default)' )
32493279@click .option ('--checks' , multiple = True , help = 'Specific checks to run (can be specified multiple times)' )
32503280@click .option ('--dry-run' , is_flag = True , help = 'Show the command without running' )
3251- def precheck (project_root , skip_checks , magic_drc , checks , dry_run ):
3281+ @click .option ('--remote' , is_flag = True , help = 'Queue precheck on the chipIgnite platform (requires cf login + linked project)' )
3282+ @click .option (
3283+ '--poll' ,
3284+ is_flag = True ,
3285+ help = 'With --remote: poll until the job finishes and print progress (5s interval).' ,
3286+ )
3287+ @click .option ('--git-ref' , default = 'main' , show_default = True , help = 'Git branch or tag for remote precheck' )
3288+ @click .option (
3289+ '--wait-timeout' ,
3290+ type = int ,
3291+ default = 7200 ,
3292+ show_default = True ,
3293+ help = 'With --remote --poll: max seconds to wait (0 = no limit). Ignored without --poll.' ,
3294+ )
3295+ def precheck (project_root , skip_checks , magic_drc , checks , dry_run , remote , poll , git_ref , wait_timeout ):
32523296 """Run precheck validation on the project.
32533297
32543298 This runs the cf-precheck tool to validate your design before
@@ -3259,6 +3303,13 @@ def precheck(project_root, skip_checks, magic_drc, checks, dry_run):
32593303 cf precheck --skip-checks lvs # Skip LVS check
32603304 cf precheck --magic-drc # Include optional Magic DRC
32613305 cf precheck --checks topcell_check # Run specific checks only
3306+ cf precheck --remote # Queue on platform; exit when accepted
3307+ cf precheck --remote --poll # Wait and stream progress
3308+ cf precheck --remote --poll --wait-timeout 0 # Poll until done (no time limit)
3309+
3310+ Remote precheck requires your local HEAD to match origin for --git-ref, and precheck
3311+ inputs (wrapper GDS, verilog/rtl/user_defines.v when the GPIO check runs, and tracked
3312+ .cf/project.json) to match that commit.
32623313 """
32633314 cwd_root , _ = get_project_json_from_cwd ()
32643315 if not project_root and cwd_root :
@@ -3274,6 +3325,180 @@ def precheck(project_root, skip_checks, magic_drc, checks, dry_run):
32743325 return
32753326
32763327 project_json_path = project_root_path / '.cf' / 'project.json'
3328+
3329+ if poll and not remote :
3330+ console .print ("[red]✗[/red] --poll requires --remote." )
3331+ raise SystemExit (1 )
3332+
3333+ if remote :
3334+ import time
3335+ from urllib .parse import urlencode
3336+
3337+ import httpx as httpx_remote
3338+ platform_id = _load_project_platform_id (str (project_root_path ))
3339+ if not platform_id :
3340+ console .print (
3341+ "[red]✗[/red] Link this repo to a platform project (set platform_project_id via [bold]cf link[/bold])."
3342+ )
3343+ raise SystemExit (1 )
3344+ try :
3345+ verify_remote_precheck_repo (
3346+ project_root_path ,
3347+ git_ref ,
3348+ checks = tuple (checks ),
3349+ skip_checks = tuple (skip_checks ),
3350+ )
3351+ except RemotePrecheckGitError as e :
3352+ console .print (f"[red]✗[/red] { e } " )
3353+ raise SystemExit (1 )
3354+ remote_params = [("git_ref" , git_ref )]
3355+ # Single checks= / skip_checks= value so proxies do not drop duplicate query keys.
3356+ if checks :
3357+ remote_params .append (("checks" , "," .join (checks )))
3358+ if skip_checks :
3359+ remote_params .append (("skip_checks" , "," .join (skip_checks )))
3360+ if magic_drc :
3361+ remote_params .append (("magic_drc" , "true" ))
3362+ if dry_run :
3363+ console .print (
3364+ f"[cyan]Would POST[/cyan] /projects/{ platform_id } /precheck-jobs?"
3365+ + urlencode (remote_params )
3366+ )
3367+ return
3368+ if poll and wait_timeout < 0 :
3369+ console .print (
3370+ "[red]✗[/red] --wait-timeout must be >= 0 (0 means no limit while polling)."
3371+ )
3372+ raise SystemExit (1 )
3373+ config = load_user_config ()
3374+ api_key = config .get ('api_key' )
3375+ if not api_key :
3376+ console .print ("[yellow]Not logged in.[/yellow] Run [bold]cf login[/bold] first." )
3377+ raise SystemExit (1 )
3378+ api_url = _get_api_url ()
3379+ client = httpx_remote .Client (
3380+ base_url = f"{ api_url } /api/v1" ,
3381+ headers = {'Authorization' : f'Bearer { api_key } ' },
3382+ timeout = 120.0 ,
3383+ )
3384+ try :
3385+ resp = client .post (
3386+ f"/projects/{ platform_id } /precheck-jobs" ,
3387+ params = remote_params ,
3388+ )
3389+ if resp .status_code == 401 :
3390+ console .print ("[red]✗[/red] API key is invalid or expired. Run [bold]cf login[/bold]." )
3391+ raise SystemExit (1 )
3392+ if not resp .is_success :
3393+ try :
3394+ detail = resp .json ().get ("detail" , resp .text )
3395+ except Exception :
3396+ detail = resp .text
3397+ console .print (f"[red]✗[/red] { detail } " )
3398+ raise SystemExit (1 )
3399+ job = resp .json ()
3400+ jid = job ["id" ]
3401+ st0 = job .get ("status" ) or "unknown"
3402+ if st0 == "failed" :
3403+ console .print (f"[cyan]Remote precheck[/cyan] job_id={ jid } status={ st0 } " )
3404+ elif st0 == "running" :
3405+ console .print (f"[cyan]Remote precheck started[/cyan] job_id={ jid } status={ st0 } " )
3406+ else :
3407+ console .print (f"[cyan]Queued remote precheck[/cyan] job_id={ jid } status={ st0 } " )
3408+ if job .get ("status" ) == "failed" and job .get ("error_message" ):
3409+ console .print (f"[red]✗[/red] { job ['error_message' ]} " )
3410+ raise SystemExit (1 )
3411+ if job .get ("status" ) == "completed" :
3412+ console .print ("[green]✓[/green] Remote precheck completed" )
3413+ if job .get ("github_pr_url" ):
3414+ console .print (f" Pull request: { job ['github_pr_url' ]} " )
3415+ return
3416+ if not poll :
3417+ console .print (
3418+ "[dim]Not waiting: use [bold]cf precheck --remote --poll[/bold] to stream progress "
3419+ "([bold]--wait-timeout 0[/bold] = no time limit while polling).[/dim]"
3420+ )
3421+ return
3422+ deadline = None if wait_timeout == 0 else time .monotonic () + wait_timeout
3423+ if wait_timeout == 0 :
3424+ console .print ("[dim]Polling until the job completes (no timeout).[/dim]" )
3425+ else :
3426+ console .print (
3427+ f"[dim]Polling every 5s; stops after { wait_timeout } s if still queued or running. "
3428+ f"Use [bold]--wait-timeout 0[/bold] for no limit.[/dim]"
3429+ )
3430+ last_status_seen = st0
3431+ terminal = None
3432+ github_pr_url = None
3433+ fail_message = None
3434+ progress_emitted = 0
3435+ console .print ("[dim]Worker log batches appear below as the platform receives them (5s poll).[/dim]" )
3436+ while True :
3437+ if deadline is not None and time .monotonic () > deadline :
3438+ console .print (
3439+ "[yellow]⚠[/yellow] Timed out waiting for remote precheck (job still queued or running)."
3440+ )
3441+ console .print (
3442+ f"[dim]job_id={ jid } — open the project in the portal or run [bold]cf status[/bold].[/dim]"
3443+ )
3444+ console .print (
3445+ "[dim]Cancel a stuck run in the portal, or retry with e.g. "
3446+ "[bold]cf precheck --remote --poll --wait-timeout 14400[/bold].[/dim]"
3447+ )
3448+ raise SystemExit (1 )
3449+ time .sleep (5 )
3450+ r2 = client .get (f"/projects/{ platform_id } /precheck-jobs/{ jid } " )
3451+ if r2 .status_code == 401 :
3452+ console .print ("[red]✗[/red] API key is invalid or expired." )
3453+ raise SystemExit (1 )
3454+ r2 .raise_for_status ()
3455+ j2 = r2 .json ()
3456+ st = j2 .get ("status" )
3457+ prog = j2 .get ("progress" )
3458+ if isinstance (prog , list ) and len (prog ) > progress_emitted :
3459+ for row in prog [progress_emitted :]:
3460+ if not isinstance (row , dict ):
3461+ continue
3462+ msg = row .get ("message" )
3463+ if msg :
3464+ det = row .get ("details" )
3465+ if (
3466+ isinstance (det , dict )
3467+ and det .get ("event" ) == "check_done"
3468+ ):
3469+ console .print (Text (str (msg ), style = "bold" ))
3470+ else :
3471+ console .print (Text (str (msg ), style = "dim" ))
3472+ progress_emitted = len (prog )
3473+ if st == "completed" :
3474+ terminal = "completed"
3475+ github_pr_url = j2 .get ("github_pr_url" )
3476+ break
3477+ if st == "failed" :
3478+ terminal = "failed"
3479+ fail_message = j2 .get ("error_message" ) or "unknown error"
3480+ break
3481+ if st != last_status_seen :
3482+ console .print (
3483+ f"[dim]… job status[/dim] [cyan]{ st or 'unknown' } [/cyan]"
3484+ )
3485+ last_status_seen = st
3486+
3487+ if terminal == "completed" :
3488+ console .print ("[green]✓[/green] Remote precheck completed" )
3489+ if github_pr_url :
3490+ console .print (f" Pull request: { github_pr_url } " )
3491+ elif terminal == "failed" :
3492+ console .print (f"[red]✗[/red] Remote precheck failed: { fail_message } " )
3493+ raise SystemExit (1 )
3494+ except SystemExit :
3495+ raise
3496+ except Exception as e :
3497+ console .print (f"[red]✗[/red] Remote precheck request failed: { e } " )
3498+ raise SystemExit (1 )
3499+ finally :
3500+ client .close ()
3501+ return
32773502
32783503 with open (project_json_path , 'r' ) as f :
32793504 project_data = json .load (f )
@@ -3319,12 +3544,14 @@ def precheck(project_root, skip_checks, magic_drc, checks, dry_run):
33193544
33203545 if magic_drc :
33213546 precheck_args .append ('--magic-drc' )
3322-
3323- if skip_checks :
3324- precheck_args .extend (['--skip-checks' ] + list (skip_checks ))
3325-
3547+
3548+ # Positional check names before --skip-checks (matches cf-precheck argparse; see
3549+ # precheck-runner _cf_precheck_shell_cmd).
33263550 if checks :
33273551 precheck_args .extend (list (checks ))
3552+
3553+ if skip_checks :
3554+ precheck_args .extend (['--skip-checks' ] + list (skip_checks ))
33283555
33293556 inner_cmd = 'pip3 install --upgrade -q --root-user-action=ignore cf-precheck 2>/dev/null && exec cf-precheck ' + ' ' .join (precheck_args )
33303557
0 commit comments