From 0682a96ebab22886211f68b8bc7726a0fadf2e0c Mon Sep 17 00:00:00 2001 From: jdicorpo Date: Tue, 5 May 2026 09:21:01 -0700 Subject: [PATCH] fix(pull): resolve remote results dir by platform UUID, not project name (2.4.6) `cf pull` previously looked up `outgoing/results/` using whatever name was in the local `.cf/project.json` (or `--project-name`). This broke whenever the project was renamed on the platform or the case differed between the SFTP directory and the local file (e.g. customer had `kyttar` locally but the SFTP directory was `Kyttar`), producing "No results found for project ..." even though the data was right there. Resolution priority (when --project-name is not explicitly passed): 1. Fetch canonical project name from the platform API via the stored platform_project_id. 2. Try outgoing/results/. 3. If that path is missing, scan outgoing/results/*/config/project.json and match on platform_project_id (authoritative UUID match). 4. Otherwise surface a clear error. The post-download merge step already overwrites the local .cf/project.json with the canonical config (preserving platform_project_id), so the local file self-heals on the next successful pull. --project-name still works as a literal-name override / escape hatch. Also aligns chipfoundry_cli/__init__.py (was 2.4.1) with pyproject.toml. Co-authored-by: Cursor --- README.md | 18 +++++-- chipfoundry_cli/__init__.py | 2 +- chipfoundry_cli/main.py | 98 ++++++++++++++++++++++++++++++++++--- pyproject.toml | 2 +- 4 files changed, 106 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index fbfe1cc..4f28be1 100644 --- a/README.md +++ b/README.md @@ -655,6 +655,10 @@ cf pull [--project-name NAME] **Prerequisites:** `cf login`, `cf link` (or `cf init`), `cf config` - Downloads project results from SFTP server +- **Resolves the remote results directory by `platform_project_id` (UUID), not by project name** — survives case changes (e.g. `kyttar` → `Kyttar`) and renames on the platform without manual intervention + - First asks the platform API for the canonical project name and tries `outgoing/results/` + - If that path is missing, falls back to scanning `outgoing/results/*/config/project.json` for a matching `platform_project_id` + - Pass `--project-name NAME` to bypass UUID resolution and force a literal directory lookup (debugging / unlinked legacy use) - Saves to `sftp-output//` - **Automatically updates** your local `.cf/project.json` with the pulled version (preserving the platform link) - **Syncs with the platform** and displays admin review notes if your project has been reviewed @@ -859,20 +863,26 @@ The CLI tracks your project submission state through the `submission_state` fiel - Connects to SFTP server securely - Shows clean connection status -2. **Download:** +2. **Resolve remote directory by UUID:** + - Looks up the canonical project name from the platform via `platform_project_id` + - Tries `outgoing/results/` first + - If that path is missing, scans `outgoing/results/*/config/project.json` for a directory whose embedded `platform_project_id` matches yours + - Warns if your local project name differs from the canonical platform name (the local copy is corrected automatically in step 4) + +3. **Download:** - Downloads all project results recursively - Shows professional download progress - Saves to `sftp-output//` -3. **Config Update:** +4. **Config Update:** - **Automatically merges** the pulled `project.json` with your local version (preserving the platform link) -4. **Platform Sync:** +5. **Platform Sync:** - Sends the updated `project.json` to the platform - Records the pull timestamp on the platform - Fetches and displays any admin review notes -5. **Success:** +6. **Success:** - Shows confirmation of downloaded files, sync status, and review notes --- diff --git a/chipfoundry_cli/__init__.py b/chipfoundry_cli/__init__.py index bf5d4c7..206d981 100644 --- a/chipfoundry_cli/__init__.py +++ b/chipfoundry_cli/__init__.py @@ -1,2 +1,2 @@ """ChipFoundry CLI package: Automate project submission to SFTP.""" -__version__ = "2.4.1" \ No newline at end of file +__version__ = "2.4.6" \ No newline at end of file diff --git a/chipfoundry_cli/main.py b/chipfoundry_cli/main.py index f22cad2..09cdd8f 100644 --- a/chipfoundry_cli/main.py +++ b/chipfoundry_cli/main.py @@ -2078,6 +2078,9 @@ def push(project_root, sftp_host, sftp_username, sftp_key, project_id, project_n @click.option('--sftp-key', type=click.Path(exists=True, dir_okay=False), help='Path to SFTP private key file (defaults to config).', default=None, show_default=False) def pull(project_name, output_dir, sftp_host, sftp_username, sftp_key): """Download results/artifacts from SFTP output dir to local sftp-output/.""" + # Track whether the user explicitly passed --project-name (overrides + # canonical-name resolution via the platform API below). + explicit_project_name = project_name # If .cf/project.json exists in cwd, use its project name as default _, cwd_project_name = get_project_json_from_cwd() if not project_name and cwd_project_name: @@ -2142,16 +2145,67 @@ def pull(project_name, output_dir, sftp_host, sftp_username, sftp_key): raise click.Abort() try: + # Resolve the remote results directory. + # + # Priority: + # 1. If the user passed --project-name explicitly, honor that name + # verbatim (escape hatch / debugging). + # 2. Otherwise, ask the platform API for the canonical project name + # via the platform_project_id (UUID) and try that name first. + # 3. If that directory does not exist on SFTP (e.g. the platform was + # renamed but the old export directory still has the previous + # name), scan `outgoing/results/*/config/project.json` and match + # on `platform_project_id`. This is the authoritative UUID match + # and survives case changes and renames. + if explicit_project_name: + resolved_name = explicit_project_name + try: + sftp.stat(f"outgoing/results/{resolved_name}") + except Exception: + console.print(f"[yellow]No results found for project '{resolved_name}' on SFTP server.[/yellow]") + return + else: + try: + platform_proj = _api_get(f"/projects/{platform_id}") + except SystemExit: + console.print(f"[red]Could not resolve canonical project name for platform_project_id={platform_id} from the platform API.[/red]") + raise click.Abort() + canonical_name = platform_proj.get("name") if isinstance(platform_proj, dict) else None + if not canonical_name: + console.print(f"[red]Platform did not return a name for project {platform_id}; cannot resolve SFTP directory.[/red]") + raise click.Abort() + + try: + sftp.stat(f"outgoing/results/{canonical_name}") + resolved_name = canonical_name + if cwd_project_name and cwd_project_name != canonical_name: + console.print( + f"[yellow]Local project name '{cwd_project_name}' does not match the platform " + f"name '{canonical_name}'. Using the platform name; your local .cf/project.json " + f"will be updated after the pull completes.[/yellow]" + ) + except Exception: + console.print( + f"[yellow]'outgoing/results/{canonical_name}' not found on SFTP. " + f"Searching by project UUID ({platform_id})...[/yellow]" + ) + matched_dir = _find_remote_results_dir_by_uuid(sftp, platform_id) + if matched_dir is None: + console.print( + f"[yellow]No results found for project '{canonical_name}' (UUID {platform_id}) on SFTP server.[/yellow]" + ) + return + resolved_name = matched_dir + console.print( + f"[yellow]Found a results directory matching this project's UUID at " + f"'outgoing/results/{matched_dir}'. The directory name on SFTP differs from the " + f"platform name '{canonical_name}' — using the SFTP directory.[/yellow]" + ) + + project_name = resolved_name remote_dir = f"outgoing/results/{project_name}" output_dir = os.path.join(os.getcwd(), "sftp-output", project_name) - - # Check if remote directory exists - try: - sftp.stat(remote_dir) - except Exception: - console.print(f"[yellow]No results found for project '{project_name}' on SFTP server.[/yellow]") - return - + # Create output directory os.makedirs(output_dir, exist_ok=True) @@ -4735,6 +4789,34 @@ def _load_project_platform_id(project_root: str): return data.get('project', {}).get('platform_project_id') +def _find_remote_results_dir_by_uuid(sftp, platform_id: str) -> Optional[str]: + """Scan outgoing/results/*/config/project.json for a directory whose embedded + platform_project_id matches `platform_id`. Returns the bare directory name + (not the full path) of the first match, or None if no match is found. + + Used by `cf pull` as a UUID-based fallback when the canonical project + name from the platform does not resolve to an SFTP directory (e.g. the + project was renamed on the platform but the old SFTP results directory + still has the previous name on disk). + """ + try: + dirs = sftp.listdir("outgoing/results") + except Exception: + return None + + for d in dirs: + cfg_path = f"outgoing/results/{d}/config/project.json" + try: + with sftp.open(cfg_path, "r") as f: + data = json.loads(f.read().decode("utf-8")) + except Exception: + continue + proj = data.get("project", {}) if isinstance(data, dict) else {} + if isinstance(proj, dict) and proj.get("platform_project_id") == platform_id: + return d + return None + + def _save_platform_id(project_root: str, platform_id: str, project_name: str = None): """Write platform_project_id (and optionally project name) into .cf/project.json.""" pj = Path(project_root) / '.cf' / 'project.json' diff --git a/pyproject.toml b/pyproject.toml index 2030311..80dd673 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "chipfoundry-cli" -version = "2.4.5" +version = "2.4.6" description = "CLI tool to automate ChipFoundry project submission to SFTP server" authors = ["ChipFoundry "] readme = "README.md"