chipfoundry-cli 2.5.0__tar.gz → 2.5.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: chipfoundry-cli
3
- Version: 2.5.0
3
+ Version: 2.5.3
4
4
  Summary: CLI tool to automate ChipFoundry project submission to SFTP server
5
5
  Home-page: https://chipfoundry.io
6
6
  License: Apache-2.0
@@ -1,2 +1,2 @@
1
1
  """ChipFoundry CLI package: Automate project submission to SFTP."""
2
- __version__ = "2.4.6"
2
+ __version__ = "2.5.3"
@@ -3,7 +3,11 @@ import getpass
3
3
  import hashlib
4
4
  from typing import Optional, List, Tuple
5
5
  from chipfoundry_cli.check_refs import PRECHECK_CHECKS
6
- from chipfoundry_cli.remote_precheck_git import RemotePrecheckGitError, verify_remote_precheck_repo
6
+ from chipfoundry_cli.remote_precheck_git import (
7
+ RemotePrecheckGitError,
8
+ verify_remote_job_repo,
9
+ verify_remote_precheck_repo,
10
+ )
7
11
  from chipfoundry_cli.version_check import maybe_warn_outdated
8
12
  from chipfoundry_cli.utils import (
9
13
  collect_project_files, ensure_cf_directory, update_or_create_project_json,
@@ -3587,10 +3591,12 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
3587
3591
  check=True,
3588
3592
  capture_output=True
3589
3593
  )
3590
-
3591
- # Determine the python executable path in venv
3592
- venv_python = str(venv_cocotb / 'bin' / 'python3')
3593
-
3594
+
3595
+ venv_python = str(venv_cocotb / 'bin' / 'python3')
3596
+
3597
+ # Install (or reinstall) when the package is missing or --overwrite was used.
3598
+ # The venv may already exist from a previous partial failure.
3599
+ if not is_installed or overwrite:
3594
3600
  console.print("[cyan]Installing caravel-cocotb from source (chipfoundry/caravel-sim-infrastructure)...[/cyan]")
3595
3601
  subprocess.run(
3596
3602
  [venv_python, '-m', 'pip', 'install', '--upgrade', '--no-cache-dir', 'pip'],
@@ -3695,7 +3701,208 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
3695
3701
  console.print("[bold green]Installation complete![/bold green]")
3696
3702
  else:
3697
3703
  console.print("[bold green]Setup complete![/bold green]")
3698
-
3704
+
3705
+
3706
+ def _is_nix_toolchain_noise_line(line: str) -> bool:
3707
+ """Match Nix fetch/build chatter posted by the PNR runner."""
3708
+ s = line.strip()
3709
+ if not s:
3710
+ return True
3711
+ bare = s[len("[stderr]") :].lstrip() if s.startswith("[stderr]") else s
3712
+ low = bare.lower()
3713
+ if low.startswith("copying path"):
3714
+ return True
3715
+ if low.startswith("building '/nix/store/") or low.startswith('building "/nix/store/'):
3716
+ return True
3717
+ if low.startswith("downloading '") or low.startswith('downloading "'):
3718
+ return True
3719
+ if low.startswith("unpacking 'github:") or low.startswith('unpacking "github:'):
3720
+ return True
3721
+ if "paths will be fetched" in low or "paths will be copied" in low:
3722
+ return True
3723
+ if low.startswith("copying ") and " path" in low:
3724
+ return True
3725
+ if low.startswith("waiting for lock") or low.startswith("waiting for locks"):
3726
+ return True
3727
+ if bare.startswith("/nix/store/") or bare.startswith(" /nix/store/"):
3728
+ return True
3729
+ return False
3730
+
3731
+
3732
+ def _print_remote_progress_message(msg: object, *, style: str = "dim") -> None:
3733
+ """Print worker progress, collapsing Nix toolchain noise into a one-liner."""
3734
+ text = str(msg)
3735
+ lines = text.splitlines()
3736
+ kept: List[str] = []
3737
+ noise = 0
3738
+ for line in lines:
3739
+ if _is_nix_toolchain_noise_line(line):
3740
+ noise += 1
3741
+ else:
3742
+ kept.append(line)
3743
+ if noise and not kept:
3744
+ console.print(
3745
+ Text(
3746
+ f"Nix toolchain resolving… ({noise} build/fetch lines suppressed)",
3747
+ style="dim",
3748
+ )
3749
+ )
3750
+ return
3751
+ if noise:
3752
+ kept.append(f"(also suppressed {noise} Nix build/fetch lines)")
3753
+ out = "\n".join(kept).rstrip()
3754
+ if out:
3755
+ console.print(Text(out, style=style))
3756
+
3757
+
3758
+ def _queue_and_maybe_poll_remote_job(
3759
+ *,
3760
+ create_path: str,
3761
+ job_get_path_template: str,
3762
+ params: list,
3763
+ dry_run: bool,
3764
+ poll: bool,
3765
+ wait_timeout: int,
3766
+ label: str,
3767
+ ) -> None:
3768
+ """POST a remote platform job and optionally poll until terminal status."""
3769
+ import time
3770
+ from urllib.parse import urlencode
3771
+
3772
+ import httpx as httpx_remote
3773
+
3774
+ if dry_run:
3775
+ console.print(f"[cyan]Would POST[/cyan] {create_path}?" + urlencode(params))
3776
+ return
3777
+ if poll and wait_timeout < 0:
3778
+ console.print(
3779
+ "[red]✗[/red] --wait-timeout must be >= 0 (0 means no limit while polling)."
3780
+ )
3781
+ raise SystemExit(1)
3782
+ config = load_user_config()
3783
+ api_key = config.get('api_key')
3784
+ if not api_key:
3785
+ console.print("[yellow]Not logged in.[/yellow] Run [bold]cf login[/bold] first.")
3786
+ raise SystemExit(1)
3787
+ api_url = _get_api_url()
3788
+ client = httpx_remote.Client(
3789
+ base_url=f"{api_url}/api/v1",
3790
+ headers={
3791
+ 'Authorization': f'Bearer {api_key}',
3792
+ 'User-Agent': _cf_user_agent(),
3793
+ },
3794
+ timeout=120.0,
3795
+ )
3796
+ try:
3797
+ resp = client.post(create_path, params=params)
3798
+ if resp.status_code == 401:
3799
+ console.print("[red]✗[/red] API key is invalid or expired. Run [bold]cf login[/bold].")
3800
+ raise SystemExit(1)
3801
+ if not resp.is_success:
3802
+ try:
3803
+ detail = resp.json().get("detail", resp.text)
3804
+ except Exception:
3805
+ detail = resp.text
3806
+ console.print(f"[red]✗[/red] {detail}")
3807
+ raise SystemExit(1)
3808
+ job = resp.json()
3809
+ jid = job["id"]
3810
+ st0 = job.get("status") or "unknown"
3811
+ if st0 == "failed":
3812
+ console.print(f"[cyan]{label}[/cyan] job_id={jid} status={st0}")
3813
+ elif st0 == "running":
3814
+ console.print(f"[cyan]{label} started[/cyan] job_id={jid} status={st0}")
3815
+ else:
3816
+ console.print(f"[cyan]Queued {label.lower()}[/cyan] job_id={jid} status={st0}")
3817
+ if job.get("status") == "failed" and job.get("error_message"):
3818
+ console.print(f"[red]✗[/red] {job['error_message']}")
3819
+ raise SystemExit(1)
3820
+ if job.get("status") == "completed":
3821
+ console.print(f"[green]✓[/green] {label} completed")
3822
+ if job.get("github_pr_url"):
3823
+ console.print(f" Pull request: {job['github_pr_url']}")
3824
+ return
3825
+ if not poll:
3826
+ console.print(
3827
+ f"[dim]Not waiting: use [bold]--remote --poll[/bold] to stream progress "
3828
+ f"([bold]--wait-timeout 0[/bold] = no time limit while polling).[/dim]"
3829
+ )
3830
+ return
3831
+ deadline = None if wait_timeout == 0 else time.monotonic() + wait_timeout
3832
+ if wait_timeout == 0:
3833
+ console.print("[dim]Polling until the job completes (no timeout).[/dim]")
3834
+ else:
3835
+ console.print(
3836
+ f"[dim]Polling every 5s; stops after {wait_timeout}s if still queued or running. "
3837
+ f"Use [bold]--wait-timeout 0[/bold] for no limit.[/dim]"
3838
+ )
3839
+ last_status_seen = st0
3840
+ terminal = None
3841
+ github_pr_url = None
3842
+ fail_message = None
3843
+ progress_emitted = 0
3844
+ get_path = job_get_path_template.format(jid=jid)
3845
+ console.print("[dim]Worker log batches appear below as the platform receives them (5s poll).[/dim]")
3846
+ while True:
3847
+ if deadline is not None and time.monotonic() > deadline:
3848
+ console.print(
3849
+ f"[yellow]⚠[/yellow] Timed out waiting for {label.lower()} (job still queued or running)."
3850
+ )
3851
+ console.print(
3852
+ f"[dim]job_id={jid} — open the project in the portal or run [bold]cf status[/bold].[/dim]"
3853
+ )
3854
+ console.print(
3855
+ "[dim]Cancel a stuck run in the portal, or retry with e.g. "
3856
+ "[bold]--remote --poll --wait-timeout 14400[/bold].[/dim]"
3857
+ )
3858
+ raise SystemExit(1)
3859
+ time.sleep(5)
3860
+ r2 = client.get(get_path)
3861
+ if r2.status_code == 401:
3862
+ console.print("[red]✗[/red] API key is invalid or expired.")
3863
+ raise SystemExit(1)
3864
+ r2.raise_for_status()
3865
+ j2 = r2.json()
3866
+ st = j2.get("status")
3867
+ prog = j2.get("progress")
3868
+ if isinstance(prog, list) and len(prog) > progress_emitted:
3869
+ for row in prog[progress_emitted:]:
3870
+ if not isinstance(row, dict):
3871
+ continue
3872
+ msg = row.get("message")
3873
+ if msg:
3874
+ _print_remote_progress_message(msg)
3875
+ progress_emitted = len(prog)
3876
+ if st == "completed":
3877
+ terminal = "completed"
3878
+ github_pr_url = j2.get("github_pr_url")
3879
+ break
3880
+ if st == "failed":
3881
+ terminal = "failed"
3882
+ fail_message = j2.get("error_message") or "unknown error"
3883
+ break
3884
+ if st != last_status_seen:
3885
+ console.print(
3886
+ f"[dim]… job status[/dim] [cyan]{st or 'unknown'}[/cyan]"
3887
+ )
3888
+ last_status_seen = st
3889
+
3890
+ if terminal == "completed":
3891
+ console.print(f"[green]✓[/green] {label} completed")
3892
+ if github_pr_url:
3893
+ console.print(f" Pull request: {github_pr_url}")
3894
+ elif terminal == "failed":
3895
+ console.print(f"[red]✗[/red] {label} failed: {fail_message}")
3896
+ raise SystemExit(1)
3897
+ except SystemExit:
3898
+ raise
3899
+ except Exception as e:
3900
+ console.print(f"[red]✗[/red] {label} request failed: {e}")
3901
+ raise SystemExit(1)
3902
+ finally:
3903
+ client.close()
3904
+
3905
+
3699
3906
  @main.command(
3700
3907
  'harden',
3701
3908
  cls=CategorizedCommand,
@@ -3704,6 +3911,7 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
3704
3911
  ("Run Controls", ["tag", "from_step"]),
3705
3912
  ("GUI Modes", ["open_in_openroad", "open_in_klayout"]),
3706
3913
  ("Execution Backend", ["pdk", "use_nix", "use_docker"]),
3914
+ ("Remote", ["dry_run", "remote", "poll", "git_ref", "wait_timeout"]),
3707
3915
  ],
3708
3916
  )
3709
3917
  @click.argument('macro', required=False)
@@ -3717,8 +3925,46 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
3717
3925
  @click.option('--pdk', help='PDK to use (defaults to sky130A)')
3718
3926
  @click.option('--use-nix', is_flag=True, help='Force use of Nix (fails if Nix not available)')
3719
3927
  @click.option('--use-docker', is_flag=True, help='Force use of Docker (fails if Docker not available)')
3720
- def harden(macro, project_root, list_designs, list_from_steps, tag, from_step, open_in_openroad, open_in_klayout, pdk, use_nix, use_docker):
3721
- """Harden a macro using LibreLane (OpenLane 2)."""
3928
+ @click.option('--dry-run', is_flag=True, help='Show the configuration without running')
3929
+ @click.option('--remote', is_flag=True, help='Queue PNR on the ChipFoundry platform (requires cf login + linked project)')
3930
+ @click.option(
3931
+ '--poll',
3932
+ is_flag=True,
3933
+ help='With --remote: poll until the job finishes and print progress (5s interval).',
3934
+ )
3935
+ @click.option('--git-ref', default='main', show_default=True, help='Git branch or tag for remote PNR')
3936
+ @click.option(
3937
+ '--wait-timeout',
3938
+ type=int,
3939
+ default=7200,
3940
+ show_default=True,
3941
+ help='With --remote --poll: max seconds to wait (0 = no limit). Ignored without --poll.',
3942
+ )
3943
+ def harden(
3944
+ macro,
3945
+ project_root,
3946
+ list_designs,
3947
+ list_from_steps,
3948
+ tag,
3949
+ from_step,
3950
+ open_in_openroad,
3951
+ open_in_klayout,
3952
+ pdk,
3953
+ use_nix,
3954
+ use_docker,
3955
+ dry_run,
3956
+ remote,
3957
+ poll,
3958
+ git_ref,
3959
+ wait_timeout,
3960
+ ):
3961
+ """Harden a macro using LibreLane (OpenLane 2).
3962
+
3963
+ Examples:
3964
+ cf harden user_proj_example
3965
+ cf harden --list
3966
+ cf harden user_proj_example --remote --poll
3967
+ """
3722
3968
  from datetime import datetime
3723
3969
 
3724
3970
  # If .cf/project.json exists in cwd, use it as default project_root
@@ -3729,6 +3975,49 @@ def harden(macro, project_root, list_designs, list_from_steps, tag, from_step, o
3729
3975
  project_root = os.getcwd()
3730
3976
 
3731
3977
  project_root_path = Path(project_root)
3978
+
3979
+ if poll and not remote:
3980
+ console.print("[red]✗[/red] --poll requires --remote.")
3981
+ raise SystemExit(1)
3982
+
3983
+ if remote:
3984
+ if list_designs or not macro:
3985
+ console.print("[red]✗[/red] --remote requires a macro name (cannot use --list).")
3986
+ raise SystemExit(1)
3987
+ if list_from_steps or from_step or open_in_openroad or open_in_klayout:
3988
+ console.print(
3989
+ "[red]✗[/red] --remote cannot be combined with --from, --list-from-steps, or GUI modes."
3990
+ )
3991
+ raise SystemExit(1)
3992
+ if not check_project_initialized(project_root_path, 'harden', dry_run=dry_run, allow_graceful=True):
3993
+ console.print(f"[red]✗[/red] Project not initialized. Please run 'cf init' first.")
3994
+ return
3995
+ platform_id = _load_project_platform_id(str(project_root_path))
3996
+ if not platform_id:
3997
+ console.print(
3998
+ "[red]✗[/red] Link this repo to a platform project (set platform_project_id via [bold]cf link[/bold])."
3999
+ )
4000
+ raise SystemExit(1)
4001
+ try:
4002
+ verify_remote_job_repo(project_root_path, git_ref)
4003
+ except RemotePrecheckGitError as e:
4004
+ console.print(f"[red]✗[/red] {e}")
4005
+ raise SystemExit(1)
4006
+ remote_params = [("macro", macro), ("git_ref", git_ref)]
4007
+ if pdk:
4008
+ remote_params.append(("pdk", pdk))
4009
+ if tag:
4010
+ remote_params.append(("run_tag", tag))
4011
+ _queue_and_maybe_poll_remote_job(
4012
+ create_path=f"/projects/{platform_id}/pnr-jobs",
4013
+ job_get_path_template=f"/projects/{platform_id}/pnr-jobs/{{jid}}",
4014
+ params=remote_params,
4015
+ dry_run=dry_run,
4016
+ poll=poll,
4017
+ wait_timeout=wait_timeout,
4018
+ label="Remote PNR",
4019
+ )
4020
+ return
3732
4021
 
3733
4022
  # Check if project is initialized (skip check for --list or when no macro specified, allow graceful return)
3734
4023
  if not list_designs and macro:
@@ -4094,7 +4383,6 @@ def harden(macro, project_root, list_designs, list_from_steps, tag, from_step, o
4094
4383
  str(venv_bin / 'python3'), '-m', 'librelane',
4095
4384
  '-m', str(project_root_path),
4096
4385
  '-m', str(pdk_root),
4097
- '--dockerized',
4098
4386
  ]
4099
4387
 
4100
4388
  # Add --docker-no-tty if not running in a TTY (e.g., CI environments)
@@ -4104,7 +4392,10 @@ def harden(macro, project_root, list_designs, list_from_steps, tag, from_step, o
4104
4392
  except:
4105
4393
  # If we can't detect TTY, assume non-TTY (safer for CI)
4106
4394
  cmd.append('--docker-no-tty')
4107
-
4395
+
4396
+ # --docker-no-tty must come before --dockerized
4397
+ cmd.append('--dockerized')
4398
+
4108
4399
  cmd.extend([
4109
4400
  '--manual-pdk',
4110
4401
  '--pdk-root', str(pdk_root),
@@ -4489,9 +4780,9 @@ def precheck(project_root, skip_checks, magic_drc, checks, list_checks, dry_run,
4489
4780
  isinstance(det, dict)
4490
4781
  and det.get("event") == "check_done"
4491
4782
  ):
4492
- console.print(Text(str(msg), style="bold"))
4783
+ _print_remote_progress_message(msg, style="bold")
4493
4784
  else:
4494
- console.print(Text(str(msg), style="dim"))
4785
+ _print_remote_progress_message(msg)
4495
4786
  progress_emitted = len(prog)
4496
4787
  if st == "completed":
4497
4788
  terminal = "completed"
@@ -4666,7 +4957,21 @@ def precheck(project_root, skip_checks, magic_drc, checks, list_checks, dry_run,
4666
4957
  @click.option('--all', 'run_all', is_flag=True, help='Run all tests')
4667
4958
  @click.option('--tag', help='Test list tag/yaml file (e.g., all_tests or user_proj_tests)')
4668
4959
  @click.option('--dry-run', is_flag=True, help='Show the configuration without running')
4669
- def verify(test, project_root, sim, list_tests, run_all, tag, dry_run):
4960
+ @click.option('--remote', is_flag=True, help='Queue simulation on the ChipFoundry platform (requires cf login + linked project)')
4961
+ @click.option(
4962
+ '--poll',
4963
+ is_flag=True,
4964
+ help='With --remote: poll until the job finishes and print progress (5s interval).',
4965
+ )
4966
+ @click.option('--git-ref', default='main', show_default=True, help='Git branch or tag for remote simulation')
4967
+ @click.option(
4968
+ '--wait-timeout',
4969
+ type=int,
4970
+ default=7200,
4971
+ show_default=True,
4972
+ help='With --remote --poll: max seconds to wait (0 = no limit). Ignored without --poll.',
4973
+ )
4974
+ def verify(test, project_root, sim, list_tests, run_all, tag, dry_run, remote, poll, git_ref, wait_timeout):
4670
4975
  """Run cocotb verification tests.
4671
4976
 
4672
4977
  Examples:
@@ -4675,6 +4980,8 @@ def verify(test, project_root, sim, list_tests, run_all, tag, dry_run):
4675
4980
  cf verify counter_la --sim gl # Run gate-level simulation
4676
4981
  cf verify --all # Run all tests
4677
4982
  cf verify --tag all_tests # Run tests from a yaml list
4983
+ cf verify counter_la --remote
4984
+ cf verify --all --remote --poll
4678
4985
  """
4679
4986
  # If .cf/project.json exists in cwd, use it as default project_root
4680
4987
  cwd_root, _ = get_project_json_from_cwd()
@@ -4684,6 +4991,53 @@ def verify(test, project_root, sim, list_tests, run_all, tag, dry_run):
4684
4991
  project_root = os.getcwd()
4685
4992
 
4686
4993
  project_root_path = Path(project_root)
4994
+
4995
+ if poll and not remote:
4996
+ console.print("[red]✗[/red] --poll requires --remote.")
4997
+ raise SystemExit(1)
4998
+
4999
+ if remote:
5000
+ if list_tests:
5001
+ console.print("[red]✗[/red] --remote cannot be combined with --list.")
5002
+ raise SystemExit(1)
5003
+ if not check_project_initialized(project_root_path, 'verify', dry_run=dry_run, allow_graceful=True):
5004
+ console.print(f"[red]✗[/red] Project not initialized. Please run 'cf init' first.")
5005
+ return
5006
+ if not test and not run_all and not tag:
5007
+ console.print("[red]Error: Specify a test name, use --all, or --tag <test_list>[/red]")
5008
+ raise SystemExit(1)
5009
+ mode_count = int(bool(test)) + int(bool(run_all)) + int(bool(tag))
5010
+ if mode_count != 1:
5011
+ console.print("[red]Error: Use exactly one of: test name, --all, or --tag[/red]")
5012
+ raise SystemExit(1)
5013
+ platform_id = _load_project_platform_id(str(project_root_path))
5014
+ if not platform_id:
5015
+ console.print(
5016
+ "[red]✗[/red] Link this repo to a platform project (set platform_project_id via [bold]cf link[/bold])."
5017
+ )
5018
+ raise SystemExit(1)
5019
+ try:
5020
+ verify_remote_job_repo(project_root_path, git_ref)
5021
+ except RemotePrecheckGitError as e:
5022
+ console.print(f"[red]✗[/red] {e}")
5023
+ raise SystemExit(1)
5024
+ remote_params = [("git_ref", git_ref), ("sim_type", (sim or "rtl").lower())]
5025
+ if run_all:
5026
+ remote_params.append(("run_all", "true"))
5027
+ elif tag:
5028
+ remote_params.append(("test_list_tag", tag))
5029
+ else:
5030
+ remote_params.append(("test", test))
5031
+ _queue_and_maybe_poll_remote_job(
5032
+ create_path=f"/projects/{platform_id}/simulation-jobs",
5033
+ job_get_path_template=f"/projects/{platform_id}/simulation-jobs/{{jid}}",
5034
+ params=remote_params,
5035
+ dry_run=dry_run,
5036
+ poll=poll,
5037
+ wait_timeout=wait_timeout,
5038
+ label="Remote simulation",
5039
+ )
5040
+ return
4687
5041
 
4688
5042
  # Check if project is initialized (skip check if just listing tests, allow graceful return)
4689
5043
  if not list_tests:
@@ -5392,4 +5746,4 @@ def whoami_cmd():
5392
5746
 
5393
5747
 
5394
5748
  if __name__ == "__main__":
5395
- main()
5749
+ main()
@@ -228,6 +228,47 @@ def verify_remote_precheck_repo(
228
228
  )
229
229
 
230
230
 
231
+ def verify_remote_job_repo(project_root: Path, git_ref: str) -> None:
232
+ """
233
+ Thin git consistency check for remote harden / verify.
234
+
235
+ Ensures origin/{git_ref} tip matches HEAD. Does not require wrapper GDS or
236
+ other precheck layout inputs (those may not exist yet when hardening).
237
+ """
238
+ repo = project_root.resolve()
239
+ git_marker = repo / ".git"
240
+ if not (git_marker.is_dir() or git_marker.is_file()):
241
+ raise RemotePrecheckGitError(
242
+ "Remote jobs require a git checkout with .git "
243
+ "(clone your GitHub repo, not a plain folder copy)."
244
+ )
245
+
246
+ remote_sha = _resolve_origin_tip_sha(repo, git_ref)
247
+ head_sha = _local_head_sha(repo)
248
+ if head_sha != remote_sha:
249
+ raise RemotePrecheckGitError(
250
+ f"Local HEAD ({head_sha[:7]}) must match origin {git_ref!r} ({remote_sha[:7]}). "
251
+ f"git checkout {git_ref} && git pull, or push your commits, then retry."
252
+ )
253
+
254
+ # If .cf/project.json is tracked, require it clean so the remote clone matches.
255
+ if _path_tracked_in_git(repo, CF_PROJECT_JSON_REL):
256
+ dirty = _porcelain_paths(repo)
257
+ for entry in dirty:
258
+ path = entry[2:] if entry.startswith("??") else entry
259
+ if path == CF_PROJECT_JSON_REL:
260
+ raise RemotePrecheckGitError(
261
+ f"{CF_PROJECT_JSON_REL!r} has uncommitted changes. "
262
+ "Commit or stash before queueing a remote job."
263
+ )
264
+ r = _run_git(repo, "diff-index", "--quiet", "HEAD", "--", CF_PROJECT_JSON_REL)
265
+ if r.returncode != 0:
266
+ raise RemotePrecheckGitError(
267
+ f"{CF_PROJECT_JSON_REL!r} has uncommitted changes. "
268
+ "Commit or stash before queueing a remote job."
269
+ )
270
+
271
+
231
272
  class RemotePushGitError(Exception):
232
273
  """Local repository state is not consistent with origin for remote push."""
233
274
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "chipfoundry-cli"
3
- version = "2.5.0"
3
+ version = "2.5.3"
4
4
  description = "CLI tool to automate ChipFoundry project submission to SFTP server"
5
5
  authors = ["ChipFoundry <marwan.abbas@chipfoundry.io>"]
6
6
  readme = "README.md"
File without changes