-
Notifications
You must be signed in to change notification settings - Fork 108
DX: Improve auth error messages, sparse checkout token consistency, and error deduplication #478
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0fdeb37
b06f841
f85753c
29db49e
0d7c6fb
23bd23f
1a6d91e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1174,7 +1174,7 @@ def _install_apm_dependencies( | |
| downloader = GitHubPackageDownloader(auth_resolver=auth_resolver) | ||
|
|
||
| # Track direct dependency keys so the download callback can distinguish them from transitive | ||
| direct_dep_keys = builtins.set(dep.get_unique_key() for dep in apm_deps) | ||
| direct_dep_keys = builtins.set(dep.get_unique_key() for dep in all_apm_deps) | ||
|
|
||
| # Track paths already downloaded by the resolver callback to avoid re-downloading | ||
| # Maps dep_key -> resolved_commit (SHA or None) so the cached path can use it | ||
|
|
@@ -1184,6 +1184,10 @@ def _install_apm_dependencies( | |
| # diagnostics after the DiagnosticCollector is created (later in the flow). | ||
| transitive_failures: list[tuple[str, str]] = [] # (dep_display, message) | ||
|
|
||
| # Track dep keys that failed in download_callback so the main install loop | ||
| # skips them instead of re-trying and producing a duplicate error entry. | ||
| callback_failures: builtins.set = builtins.set() | ||
|
|
||
| # Create a download callback for transitive dependency resolution | ||
| # This allows the resolver to fetch packages on-demand during tree building | ||
| def download_callback(dep_ref, modules_dir, parent_chain=""): | ||
|
|
@@ -1231,19 +1235,31 @@ def download_callback(dep_ref, modules_dir, parent_chain=""): | |
| callback_downloaded[dep_ref.get_unique_key()] = resolved_sha | ||
| return install_path | ||
| except Exception as e: | ||
| # Build contextual message including the dependency chain breadcrumb | ||
| chain_hint = f" (via {parent_chain})" if parent_chain else "" | ||
| dep_display = dep_ref.get_display_name() | ||
| fail_msg = ( | ||
| f"Failed to resolve transitive dep " | ||
| f"{dep_ref.repo_url}{chain_hint}: {e}" | ||
| ) | ||
| dep_key = dep_ref.get_unique_key() | ||
| is_direct = dep_key in direct_dep_keys | ||
|
|
||
| # Distinguish direct vs transitive failure messages so users | ||
| # don't see a misleading "transitive dep" label for top-level deps. | ||
| if is_direct: | ||
| fail_msg = ( | ||
| f"Failed to download dependency " | ||
| f"{dep_ref.repo_url}: {e}" | ||
| ) | ||
| else: | ||
| chain_hint = f" (via {parent_chain})" if parent_chain else "" | ||
| fail_msg = ( | ||
| f"Failed to resolve transitive dep " | ||
| f"{dep_ref.repo_url}{chain_hint}: {e}" | ||
| ) | ||
|
|
||
| # Verbose: inline detail | ||
| if logger: | ||
| logger.verbose_detail(f" {fail_msg}") | ||
| elif verbose: | ||
| _rich_error(f" └─ {fail_msg}") | ||
| # Collect for deferred diagnostics summary (always, even non-verbose) | ||
|
Comment on lines
1258
to
1261
|
||
| callback_failures.add(dep_key) | ||
| transitive_failures.append((dep_display, fail_msg)) | ||
| return None | ||
|
|
||
|
|
@@ -1556,6 +1572,14 @@ def _collect_descendants(node, visited=None): | |
| # Use the canonical install path from DependencyReference | ||
| install_path = dep_ref.get_install_path(apm_modules_dir) | ||
|
|
||
| # Skip deps that already failed during BFS resolution callback | ||
| # to avoid a duplicate error entry in diagnostics. | ||
| dep_key = dep_ref.get_unique_key() | ||
| if dep_key in callback_failures: | ||
| if logger: | ||
| logger.verbose_detail(f" Skipping {dep_key} (already failed during resolution)") | ||
| continue | ||
|
|
||
| # --- Local package: copy from filesystem (no git download) --- | ||
| if dep_ref.is_local and dep_ref.local_path: | ||
| result_path = _copy_local_package(dep_ref, install_path, project_root) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -596,3 +596,48 @@ def test_ghe_host_is_github(self): | |
| dep_ref = _dep("https://company.ghe.com/org/repo.git") | ||
| from apm_cli.utils.github_host import is_github_hostname | ||
| assert is_github_hostname(dep_ref.host) is True | ||
|
|
||
|
|
||
| # =========================================================================== | ||
| # _try_sparse_checkout -- per-dep token resolution | ||
| # =========================================================================== | ||
|
Comment on lines
+601
to
+603
|
||
|
|
||
| class TestSparseCheckoutTokenResolution: | ||
| """Verify _try_sparse_checkout uses resolve_for_dep() for per-dep tokens.""" | ||
|
|
||
| def test_sparse_checkout_uses_per_org_token(self, tmp_path): | ||
| """Sparse checkout should use per-org token, not the global instance token.""" | ||
| org_token = "ghp_ORG_SPECIFIC" | ||
| global_token = "ghp_GLOBAL" | ||
|
|
||
| with patch.dict(os.environ, { | ||
| "GITHUB_APM_PAT": global_token, | ||
| "GITHUB_APM_PAT_ACME": org_token, | ||
| }, clear=True), patch( | ||
| "apm_cli.core.token_manager.GitHubTokenManager.resolve_credential_from_git", | ||
| return_value=None, | ||
| ): | ||
| dl = GitHubPackageDownloader() | ||
| dep = _dep("acme/mono-repo/subdir") | ||
|
|
||
| # Patch subprocess.run to capture the URL used in 'git remote add' | ||
| captured_urls = [] | ||
|
|
||
| def capture_run(cmd, **kwargs): | ||
| if len(cmd) >= 5 and cmd[:3] == ["git", "remote", "add"]: | ||
| captured_urls.append(cmd[4]) # The URL argument (after 'origin') | ||
| # Fail after capturing to keep the test fast | ||
| return MagicMock(returncode=1, stderr="test abort") | ||
| # Let other commands (git init, etc.) succeed | ||
| return MagicMock(returncode=0, stderr="") | ||
|
|
||
| with patch("subprocess.run", side_effect=capture_run): | ||
| dl._try_sparse_checkout(dep, tmp_path / "sparse", "subdir", ref="main") | ||
|
|
||
| assert len(captured_urls) == 1, f"Expected 1 URL capture, got {captured_urls}" | ||
| # The per-org token should be in the URL, not the global one | ||
| assert org_token in captured_urls[0], ( | ||
| f"Expected org-specific token in sparse checkout URL, " | ||
| f"got: {captured_urls[0]}" | ||
| ) | ||
| assert global_token not in captured_urls[0] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the new
is_direct = dep_key in direct_dep_keyscheck, note thatdirect_dep_keysis currently built only fromapm_deps(not devDependencies). Since the resolver also treats root devDependencies as depth-1/direct, a failing dev dependency will still be labeled as a transitive dep. Consider including dev APM deps indirect_dep_keysso top-level devDependencies get the correct error label.