fix(cli): resolve list/wait projects by repo URL, not directory name (COR-1577) - #122
fix(cli): resolve list/wait projects by repo URL, not directory name (COR-1577)#122juangaitanv wants to merge 9 commits into
Conversation
…(COR-1577) list/wait now resolve the canonical project via GET /api/v1/projects?repo_url= (with an old-backend safety guard and CWD-name fallback when there's no git remote); dropped the brittle client-side scan.project==name filter; added mutually-exclusive --project-name/--repo override flags; replaced the silent empty table and bare "Error querying scan list" with clear miss messages. CLI-only — no backend/persisted-state changes. Residual of COR-1493.
Addresses four review findings on the COR-1577 repo-URL resolver:
1. Boundary-aware repo match (was a substring `contains`): the backend's
`repo_url__icontains` returns siblings like `org/repo-v2` for slug
`org/repo`, and the old guard could confirm the wrong project. Now matches
on a path-segment boundary (equal or ends-with `/<slug>`, after stripping
`.git`/trailing slash).
2. Subdirectory invocation: resolution used `Repository::open`, which only
succeeds at the repo root, so `list`/`wait` from a subdir fell back to the
subdir basename. New `discover_repo_url` walks up via `Repository::discover`.
3. Surface hard resolver failures: `resolve_project` now returns a Result and
propagates network/auth/5xx from `/projects` instead of silently falling
back to the local-directory project; a clean no-match (or 404 on an old
backend) stays a soft fallback.
4. Skip redundant resolution: `list --issues --scan-id` hits
`/scan/{id}/issues`, which ignores the project, so the extra `/projects`
round-trip is now skipped.
Adds unit tests (boundary match, sibling rejection, 404-soft vs 5xx-hard) and
integration tests (no `/projects` call for `--issues --scan-id`, resolution
from a subdirectory).
Ibrahimrahhal
left a comment
There was a problem hiding this comment.
can we reduce the amount of slop comments
Resolutions: - wait.rs: keep COR-1577 resolve_project signature (main's determine_project_name(None) is the bug being fixed). - generic.rs: keep both discover_repo_url (PR) and is_at_repo_root (main). - list.rs: keep the PR's resolution flow; port main's COR-1639 SHA column (header + format_short_sha cell).
- Compare the whole post-host repo path, not a trailing org/repo slug: extract_repo_slug -> extract_repo_path, repo_url_matches_slug -> repo_url_matches_path. A nested mirror (…/mirrors/acme/api) or a differently-rooted path (…/org/team/repo) no longer matches; Azure `_git` and GitLab subgroup paths now compare in full. - Restore ScanUploadResult.project_id and thread it into wait::run, which skips resolution entirely when it has both a scan id and an upload project id, and only lists scans when no scan id was given. The semgrep/snyk post-scan wait makes zero /projects and /scans calls and links the id-form URL again. - Validate the project name before building the name-form scan URL, so a slash-only name can no longer produce /project//?scan_id=. - The unresolved-project listing miss exits 1 in both human and --json mode (JSON still prints its envelope first); a confirmed project with no scans stays exit 0. - Trim comment density to match the surrounding files. New tests: whole-path matching boundaries, wait resolving from a subdirectory, the post-upload wait issuing no /projects or /scans, trailing slash trimming, and the miss/empty exit codes.
|
@Ibrahimrahhal cut the comment density down: the diff went from 209 comment lines on 1383 added (15%) to 158 on 1613 (9.8%), and most of what is left is in the tests. I kept three that carry information not in the code: the doghouse Also in this push: merged |
running_in_ci() is true under GitHub Actions, which sent upload_scan down the GITHUB_REPOSITORY branch and panicked. The test covers the wait phase, so remove CI/GITHUB_ACTIONS from its env and exercise one path everywhere.
| // A scan id plus the project id from the upload response leaves nothing to | ||
| // resolve: everything below keys off the scan, and the id-form URL is | ||
| // already known. | ||
| let resolved: Option<utils::api::ResolvedProject> = if scan_id.is_some() |
There was a problem hiding this comment.
corgea wait <scan-id> still takes the resolver path because the CLI never supplies project_id_override; only post-upload waits satisfy this condition. A hard /projects failure therefore exits at lines 26–35 before check_scan_status, even though both status polling and get_scan_issues use /scan/{id} and need no project. The new wait_with_scan_id_does_not_list_scans test misses this because its stub explicitly serves /projects and only asserts that /scans was skipped. Impact: users with a known scan ID cannot wait during an otherwise unrelated /projects outage. Skip project resolution for any supplied scan ID and derive the canonical project name for the results link from the ScanResponse already fetched by check_scan_status (or otherwise make resolution best-effort on this path); add a test where /projects returns 500 while the scan endpoints succeed.
| "Failed to parse /projects response: {} | body: {}", | ||
| e, text | ||
| )); | ||
| return Ok(None); |
There was a problem hiding this comment.
A successful HTTP response with an invalid or incompatible /projects body is still converted to a clean resolver miss here. resolve_project then falls back to the CWD name (lines 884–893), so a proxy returning HTML or a partially rolled-out schema can make list/wait query a different project instead of surfacing the API failure. This is the same unsafe fallback class as request failures; only an actual 404 or a valid envelope with no exact match should be soft. Propagate this parse error and cover 200 + malformed JSON.
| return Ok(None); | |
| return Err(format!("Failed to parse /projects response: {e}").into()); |
| } | ||
| }; | ||
| let expected = repo_path.to_lowercase(); | ||
| let matched = parsed.projects.unwrap_or_default().into_iter().find(|p| { |
There was a problem hiding this comment.
This searches only the first response page, but GET /projects?repo_url= is a case-insensitive partial-match endpoint with a default page size of 20 and a total_pages field. Because ProjectsResponse discards the pagination metadata and this request sends neither page nor page_size, 20 substring matches such as acme/api-* can fill page 1 while the exact acme/api candidate is on page 2. The exact-path guard then reports None, recreating the CWD-name miss this PR is intended to fix. Add page/total_pages to the response model and continue requesting pages until an exact repo_url_matches_path candidate is found or all pages are exhausted; add a two-page regression test.
Behavior-preserving: - api.rs test stub reuses `read_http_request`/`http_response` instead of hand-rolling the drain loop and the response `format!`. - drop the never-read `ProjectsResponse.status` (serde ignores unknown fields, so it bought no validation). - extract `resolve_or_exit` for the error handler duplicated verbatim in `list` and `wait`. - list: resolve per branch, dropping the `Option<ResolvedProject>` and its `expect`; collapse the duplicated unresolved-miss block to one copy (the JSON envelope still prints before the error). - wait: construct a `ResolvedProject` on the skip path, dropping the four `.as_ref().map(...)` chains; that path's name now falls back to "unknown" like `resolve_project` instead of an empty string. - generic.rs: share the origin-URL extraction via `origin_url`. - resolve_project: drop the `repo_was_explicit` flag and the lazy `cwd` closure. - generic.rs test: run its git commands through one helper that scrubs the inherited GIT_* env, as tests/cli_deps.rs already does, so the temp repo is built even when the suite runs from a git hook.
`wait::run` took five positional params, four of them Option<String>;
`repo_override` and `project_id_override` were adjacent and same-typed, so
transposing them at the scan.rs call sites compiled silently and changed
resolution. `list::run` took nine and carried
#[allow(clippy::too_many_arguments)].
- `ProjectSelector { name, repo }` replaces the two Option<&str> resolver
params, so the pair travels as one value.
- `WaitArgs`/`ListArgs` name every argument at the call sites; the
too_many_arguments allow is gone rather than institutionalized.
- list::run now owns its arguments, so two `is_some()` + `unwrap()` pairs
clippy started flagging become `if let Some(id)`.
`spawn_stub` and `run_list`/`run_wait` were near-identical across list_resolution.rs and wait_resolution.rs, and the Arc<Mutex<Vec<String>>> hit-recording closure was copy-pasted at seven sites. - `common::Routes` is one route table for every endpoint the two suites stub; an unset field 404s, so "this endpoint is never dialed" tests just leave it out. - `spawn_recording_http_stub` wraps `spawn_http_stub` with the hit log, mirroring `vuln_api_stub::spawn_capturing_vuln_api_stub`. - `run_corgea(subcommand, ...)` replaces the two copies of the runner. Scaffolding only: no assertion changed.
The diff wrapped ~290 lines in a new `} else {`. The SCA branch ends at its
print_table call, so returning there puts the issues and scan-listing arms
back at their original indent level.
Reviewable with `git show --ignore-all-space`: the removed `else`, the
`return;`, one closing brace, and one line rustfmt rejoined now that it
fits. Nothing else.
|
Pushed a cleanup pass on top of the reviewed diff ( Four commits, each reviewable on its own: 1.
2. 3. 4. Verified: Deliberately not done here
Filed as follow-up: COR-1718 — |
What & why
corgea list,corgea list --issues, and standalonecorgea waitresolved "which project" by the current-directory basename and asked the backend for an exact, case-sensitive match againstProject.name. When the checkout directory name differs from the stored project name — the Bank of Hope case, dirdotnet-azure-web-tsbvs projectbohappdev/dotnet-azure-web-tsb— three commands misbehaved:corgea list --issues→ exit 1, "Project with name '…' doesn't exist."corgea list→ exit 0, silent empty scan table (no error)corgea wait(no--scan-id) → exit 1, "Error querying scan list"This is the residual of COR-1493 (which fixed only the
corgea scan→ report-results path). Relates to COR-1577.Approach (CLI-only)
The ticket suggested threading
project_id/scan_id, but the listing endpoints accept neither as a query param. Instead this uses an identity the CLI already has and the backend already indexes — the repo URL:GET /api/v1/projects?repo_url=<git remote slug>, then drive the existing name-basedquery_scan_list/get_scan_issueswith the returned canonical name.repo_urlcontains the slug. Pre-COR-1426 doghouse ignores the param and returns all projects; the guard rejects that, so resolution falls back cleanly.scan.project == project_namefilter.--project-name/--repooverride flags tolistandwait.No doghouse changes, no new persisted CLI state, no new backend query param.
Files
src/utils/generic.rs—extract_repo_slug(org/repo slug; keeps Azure_git).src/utils/api.rs—/projectsresolver (resolve_project_by_repo) with the old-backend guard, and theresolve_projectorchestrator.src/main.rs—--project-name/--repoflags onList/Wait, threaded through.src/list.rs— resolve once (non-SCA arm), drop the client filter, clear miss messaging, keep--jsonstdout clean.src/wait.rs— resolve once, build the scan URL from the resolved numeric id, actionable miss message.src/scan.rs— semgrep/snyk paths now thread their existingproject_nameas the override (thewait::runproject_idarg was not dead after all). BLAST flow untouched.tests/— 14 new integration tests (list_resolution.rs,wait_resolution.rs) + shared helpers, plus 5 new unit tests.Testing
cargo build,cargo clippy --all-targets -- -D warnings,cargo fmt --check: clean.cargo test: 456 passed, 0 failed.Notes for reviewers / ship-gates
src/scan.rschange is display-only for the semgrep/snyk post-scan results URL; the primary BLAST scan flow uses a separateUploadZipResultand never calls the modifiedwait::run.list/list --issues/waiton a mismatched checkout against doghouse ≥ COR-1426; the fallback path against a pre-COR-1426 instance;--repo <unknown slug>; the semgrep/snyk results link.