apm-cli 0.8.1__tar.gz → 0.8.2__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.
- {apm_cli-0.8.1/src/apm_cli.egg-info → apm_cli-0.8.2}/PKG-INFO +1 -1
- {apm_cli-0.8.1 → apm_cli-0.8.2}/pyproject.toml +1 -1
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/commands/install.py +10 -3
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/commands/prune.py +2 -1
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/commands/uninstall.py +6 -2
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/core/token_manager.py +23 -2
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/deps/github_downloader.py +336 -14
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/models/dependency.py +127 -23
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/utils/github_host.py +63 -1
- apm_cli-0.8.2/src/apm_cli/utils/path_security.py +58 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2/src/apm_cli.egg-info}/PKG-INFO +1 -1
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli.egg-info/SOURCES.txt +1 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/tests/test_token_manager.py +112 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/AUTHORS +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/LICENSE +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/README.md +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/setup.cfg +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/adapters/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/adapters/client/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/adapters/client/base.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/adapters/client/codex.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/adapters/client/copilot.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/adapters/client/cursor.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/adapters/client/opencode.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/adapters/client/vscode.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/adapters/package_manager/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/adapters/package_manager/base.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/adapters/package_manager/default_manager.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/bundle/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/bundle/lockfile_enrichment.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/bundle/packer.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/bundle/unpacker.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/cli.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/commands/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/commands/_helpers.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/commands/audit.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/commands/compile.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/commands/config.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/commands/deps.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/commands/init.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/commands/list_cmd.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/commands/mcp.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/commands/pack.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/commands/run.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/commands/runtime.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/commands/update.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/compilation/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/compilation/agents_compiler.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/compilation/claude_formatter.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/compilation/constants.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/compilation/constitution.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/compilation/constitution_block.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/compilation/context_optimizer.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/compilation/distributed_compiler.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/compilation/injector.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/compilation/link_resolver.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/compilation/template_builder.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/config.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/core/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/core/conflict_detector.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/core/docker_args.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/core/operations.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/core/safe_installer.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/core/script_runner.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/core/target_detection.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/deps/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/deps/aggregator.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/deps/apm_resolver.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/deps/collection_parser.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/deps/dependency_graph.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/deps/lockfile.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/deps/package_validator.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/deps/plugin_parser.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/deps/verifier.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/drift.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/factory.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/integration/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/integration/agent_integrator.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/integration/base_integrator.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/integration/command_integrator.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/integration/hook_integrator.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/integration/instruction_integrator.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/integration/mcp_integrator.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/integration/prompt_integrator.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/integration/skill_integrator.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/integration/skill_transformer.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/integration/targets.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/integration/utils.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/models/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/models/apm_package.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/models/plugin.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/models/validation.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/output/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/output/formatters.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/output/models.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/output/script_formatters.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/primitives/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/primitives/discovery.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/primitives/models.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/primitives/parser.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/registry/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/registry/client.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/registry/integration.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/registry/operations.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/runtime/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/runtime/base.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/runtime/codex_runtime.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/runtime/copilot_runtime.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/runtime/factory.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/runtime/llm_runtime.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/runtime/manager.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/security/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/security/audit_report.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/security/content_scanner.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/security/gate.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/utils/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/utils/console.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/utils/diagnostics.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/utils/helpers.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/utils/version_checker.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/version.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/workflow/__init__.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/workflow/discovery.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/workflow/parser.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli/workflow/runner.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli.egg-info/dependency_links.txt +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli.egg-info/entry_points.txt +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli.egg-info/requires.txt +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/src/apm_cli.egg-info/top_level.txt +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/tests/test_apm_package_models.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/tests/test_apm_resolver.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/tests/test_codex_docker_args_fix.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/tests/test_codex_empty_string_and_defaults.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/tests/test_collision_integration.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/tests/test_console.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/tests/test_distributed_compilation.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/tests/test_empty_string_and_defaults.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/tests/test_enhanced_discovery.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/tests/test_github_downloader.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/tests/test_github_downloader_token_precedence.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/tests/test_lockfile.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/tests/test_runnable_prompts.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/tests/test_runtime_manager_token_precedence.py +0 -0
- {apm_cli-0.8.1 → apm_cli-0.8.2}/tests/test_virtual_package_multi_install.py +0 -0
|
@@ -11,6 +11,7 @@ from ..drift import build_download_ref, detect_orphans, detect_ref_change
|
|
|
11
11
|
from ..utils.console import _rich_error, _rich_info, _rich_success, _rich_warning
|
|
12
12
|
from ..utils.diagnostics import DiagnosticCollector
|
|
13
13
|
from ..utils.github_host import default_host, is_valid_fqdn
|
|
14
|
+
from ..utils.path_security import safe_rmtree
|
|
14
15
|
from ._helpers import (
|
|
15
16
|
_create_minimal_apm_yml,
|
|
16
17
|
_get_default_config,
|
|
@@ -802,7 +803,10 @@ def _copy_local_package(dep_ref, install_path, project_root):
|
|
|
802
803
|
# Ensure parent exists and clean target (always re-copy for local deps)
|
|
803
804
|
install_path.parent.mkdir(parents=True, exist_ok=True)
|
|
804
805
|
if install_path.exists():
|
|
805
|
-
|
|
806
|
+
# install_path is already validated by get_install_path() (Layer 2),
|
|
807
|
+
# but use safe_rmtree for defense-in-depth.
|
|
808
|
+
apm_modules_dir = install_path.parent.parent # _local/<name> → apm_modules
|
|
809
|
+
safe_rmtree(install_path, apm_modules_dir)
|
|
806
810
|
|
|
807
811
|
shutil.copytree(local, install_path, dirs_exist_ok=False, symlinks=True)
|
|
808
812
|
return install_path
|
|
@@ -875,10 +879,13 @@ def _install_apm_dependencies(
|
|
|
875
879
|
return result_path
|
|
876
880
|
return None
|
|
877
881
|
|
|
878
|
-
# Build repo_ref string - include host for GHE/ADO, plus reference if specified
|
|
882
|
+
# Build repo_ref string - include host for GHE/ADO/Artifactory, plus reference if specified
|
|
879
883
|
repo_ref = dep_ref.repo_url
|
|
880
884
|
if dep_ref.host and dep_ref.host not in ("github.com", None):
|
|
881
|
-
|
|
885
|
+
if dep_ref.artifactory_prefix:
|
|
886
|
+
repo_ref = f"{dep_ref.host}/{dep_ref.artifactory_prefix}/{dep_ref.repo_url}"
|
|
887
|
+
else:
|
|
888
|
+
repo_ref = f"{dep_ref.host}/{dep_ref.repo_url}"
|
|
882
889
|
if dep_ref.virtual_path:
|
|
883
890
|
repo_ref = f"{repo_ref}/{dep_ref.virtual_path}"
|
|
884
891
|
|
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
import click
|
|
8
8
|
|
|
9
9
|
from ..utils.console import _rich_error, _rich_info, _rich_success, _rich_warning
|
|
10
|
+
from ..utils.path_security import PathTraversalError, safe_rmtree
|
|
10
11
|
from ._helpers import _build_expected_install_paths, _scan_installed_packages
|
|
11
12
|
|
|
12
13
|
# APM Dependencies
|
|
@@ -80,7 +81,7 @@ def prune(ctx, dry_run):
|
|
|
80
81
|
path_parts = org_repo_name.split("/")
|
|
81
82
|
pkg_path = apm_modules_dir.joinpath(*path_parts)
|
|
82
83
|
try:
|
|
83
|
-
|
|
84
|
+
safe_rmtree(pkg_path, apm_modules_dir)
|
|
84
85
|
_rich_info(f"+ Removed {org_repo_name}")
|
|
85
86
|
removed_count += 1
|
|
86
87
|
pruned_keys.append(org_repo_name)
|
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
import click
|
|
9
9
|
|
|
10
10
|
from ..utils.console import _rich_error, _rich_info, _rich_success, _rich_warning
|
|
11
|
+
from ..utils.path_security import PathTraversalError, safe_rmtree
|
|
11
12
|
|
|
12
13
|
# APM Dependencies
|
|
13
14
|
try:
|
|
@@ -210,6 +211,9 @@ def uninstall(ctx, packages, dry_run):
|
|
|
210
211
|
try:
|
|
211
212
|
dep_ref = _parse_dependency_entry(package)
|
|
212
213
|
package_path = dep_ref.get_install_path(apm_modules_dir)
|
|
214
|
+
except (PathTraversalError,) as e:
|
|
215
|
+
_rich_error(f"x Refusing to remove {package}: {e}")
|
|
216
|
+
continue
|
|
213
217
|
except (ValueError, TypeError, AttributeError, KeyError):
|
|
214
218
|
# Fallback for invalid format: use raw path segments
|
|
215
219
|
package_str = package if isinstance(package, str) else str(package)
|
|
@@ -221,7 +225,7 @@ def uninstall(ctx, packages, dry_run):
|
|
|
221
225
|
|
|
222
226
|
if package_path.exists():
|
|
223
227
|
try:
|
|
224
|
-
|
|
228
|
+
safe_rmtree(package_path, apm_modules_dir)
|
|
225
229
|
_rich_info(f"+ Removed {package} from apm_modules/")
|
|
226
230
|
removed_from_modules += 1
|
|
227
231
|
deleted_pkg_paths.append(package_path)
|
|
@@ -306,7 +310,7 @@ def uninstall(ctx, packages, dry_run):
|
|
|
306
310
|
|
|
307
311
|
if orphan_path.exists():
|
|
308
312
|
try:
|
|
309
|
-
|
|
313
|
+
safe_rmtree(orphan_path, apm_modules_dir)
|
|
310
314
|
_rich_info(f"+ Removed transitive dependency {orphan_key} from apm_modules/")
|
|
311
315
|
removed_from_modules += 1
|
|
312
316
|
deleted_orphan_paths.append(orphan_path)
|
|
@@ -32,6 +32,7 @@ class GitHubTokenManager:
|
|
|
32
32
|
'models': ['GITHUB_TOKEN', 'GITHUB_APM_PAT'], # GitHub Models prefers user-scoped PAT, falls back to APM PAT
|
|
33
33
|
'modules': ['GITHUB_APM_PAT', 'GITHUB_TOKEN', 'GH_TOKEN'], # APM module access (GitHub)
|
|
34
34
|
'ado_modules': ['ADO_APM_PAT'], # APM module access (Azure DevOps)
|
|
35
|
+
'artifactory_modules': ['ARTIFACTORY_APM_TOKEN'], # APM module access (JFrog Artifactory)
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
# Runtime-specific environment variable mappings
|
|
@@ -50,6 +51,24 @@ class GitHubTokenManager:
|
|
|
50
51
|
self.preserve_existing = preserve_existing
|
|
51
52
|
self._credential_cache: Dict[str, Optional[str]] = {}
|
|
52
53
|
|
|
54
|
+
@staticmethod
|
|
55
|
+
def _is_valid_credential_token(token: str) -> bool:
|
|
56
|
+
"""Validate that a credential-fill token looks like a real credential.
|
|
57
|
+
|
|
58
|
+
Rejects garbage values that can appear when GIT_ASKPASS or credential
|
|
59
|
+
helpers return prompt text instead of actual tokens.
|
|
60
|
+
"""
|
|
61
|
+
if not token:
|
|
62
|
+
return False
|
|
63
|
+
if len(token) > 1024:
|
|
64
|
+
return False
|
|
65
|
+
if any(c in token for c in (' ', '\t', '\n', '\r')):
|
|
66
|
+
return False
|
|
67
|
+
prompt_fragments = ('Password for', 'Username for', 'password for', 'username for')
|
|
68
|
+
if any(fragment in token for fragment in prompt_fragments):
|
|
69
|
+
return False
|
|
70
|
+
return True
|
|
71
|
+
|
|
53
72
|
@staticmethod
|
|
54
73
|
def resolve_credential_from_git(host: str) -> Optional[str]:
|
|
55
74
|
"""Resolve a credential from the git credential store.
|
|
@@ -71,7 +90,7 @@ class GitHubTokenManager:
|
|
|
71
90
|
capture_output=True,
|
|
72
91
|
text=True,
|
|
73
92
|
timeout=5,
|
|
74
|
-
env={**os.environ, 'GIT_TERMINAL_PROMPT': '0', 'GIT_ASKPASS': '
|
|
93
|
+
env={**os.environ, 'GIT_TERMINAL_PROMPT': '0', 'GIT_ASKPASS': ''},
|
|
75
94
|
)
|
|
76
95
|
if result.returncode != 0:
|
|
77
96
|
return None
|
|
@@ -79,7 +98,9 @@ class GitHubTokenManager:
|
|
|
79
98
|
for line in result.stdout.splitlines():
|
|
80
99
|
if line.startswith('password='):
|
|
81
100
|
token = line[len('password='):]
|
|
82
|
-
|
|
101
|
+
if token and GitHubTokenManager._is_valid_credential_token(token):
|
|
102
|
+
return token
|
|
103
|
+
return None
|
|
83
104
|
return None
|
|
84
105
|
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
85
106
|
return None
|
|
@@ -28,13 +28,14 @@ from ..models.apm_package import (
|
|
|
28
28
|
APMPackage
|
|
29
29
|
)
|
|
30
30
|
from ..utils.github_host import (
|
|
31
|
-
build_https_clone_url,
|
|
32
|
-
build_ssh_url,
|
|
31
|
+
build_https_clone_url,
|
|
32
|
+
build_ssh_url,
|
|
33
33
|
build_ado_https_clone_url,
|
|
34
34
|
build_ado_ssh_url,
|
|
35
35
|
build_ado_api_url,
|
|
36
36
|
build_raw_content_url,
|
|
37
|
-
|
|
37
|
+
build_artifactory_archive_url,
|
|
38
|
+
sanitize_token_url_in_message,
|
|
38
39
|
default_host,
|
|
39
40
|
is_azure_devops_hostname,
|
|
40
41
|
is_github_hostname
|
|
@@ -199,8 +200,12 @@ class GitHubPackageDownloader:
|
|
|
199
200
|
# Azure DevOps: ADO_APM_PAT
|
|
200
201
|
self.ado_token = self.token_manager.get_token_for_purpose('ado_modules', env)
|
|
201
202
|
self.has_ado_token = self.ado_token is not None
|
|
202
|
-
|
|
203
|
-
|
|
203
|
+
|
|
204
|
+
# JFrog Artifactory: ARTIFACTORY_APM_TOKEN
|
|
205
|
+
self.artifactory_token = self.token_manager.get_token_for_purpose('artifactory_modules', env)
|
|
206
|
+
self.has_artifactory_token = self.artifactory_token is not None
|
|
207
|
+
|
|
208
|
+
_debug(f"Token setup: has_github_token={self.has_github_token}, has_ado_token={self.has_ado_token}, has_artifactory_token={self.has_artifactory_token}"
|
|
204
209
|
f"{', source=credential_helper' if self._github_token_from_credential_fill else ''}")
|
|
205
210
|
|
|
206
211
|
# Configure Git security settings
|
|
@@ -218,7 +223,157 @@ class GitHubPackageDownloader:
|
|
|
218
223
|
env['GIT_CONFIG_GLOBAL'] = '/dev/null'
|
|
219
224
|
|
|
220
225
|
return env
|
|
221
|
-
|
|
226
|
+
|
|
227
|
+
# --- Artifactory VCS archive download support ---
|
|
228
|
+
|
|
229
|
+
def _get_artifactory_headers(self) -> Dict[str, str]:
|
|
230
|
+
"""Build HTTP headers for Artifactory requests."""
|
|
231
|
+
headers = {}
|
|
232
|
+
if self.artifactory_token:
|
|
233
|
+
headers['Authorization'] = f'Bearer {self.artifactory_token}'
|
|
234
|
+
return headers
|
|
235
|
+
|
|
236
|
+
def _download_artifactory_archive(self, host: str, prefix: str, owner: str, repo: str,
|
|
237
|
+
ref: str, target_path: Path, scheme: str = "https") -> None:
|
|
238
|
+
"""Download and extract a zip archive from Artifactory VCS proxy.
|
|
239
|
+
|
|
240
|
+
Tries multiple URL patterns (GitHub-style and GitLab-style).
|
|
241
|
+
GitHub archives contain a single root directory named {repo}-{ref}/;
|
|
242
|
+
this method strips that prefix on extraction so files land directly
|
|
243
|
+
in *target_path*.
|
|
244
|
+
|
|
245
|
+
Raises RuntimeError on failure.
|
|
246
|
+
"""
|
|
247
|
+
import io
|
|
248
|
+
import zipfile
|
|
249
|
+
|
|
250
|
+
archive_urls = build_artifactory_archive_url(host, prefix, owner, repo, ref, scheme=scheme)
|
|
251
|
+
headers = self._get_artifactory_headers()
|
|
252
|
+
|
|
253
|
+
# Guard: reject unreasonably large archives (default 500 MB)
|
|
254
|
+
max_archive_bytes = int(
|
|
255
|
+
os.environ.get('ARTIFACTORY_MAX_ARCHIVE_MB', '500')
|
|
256
|
+
) * 1024 * 1024
|
|
257
|
+
|
|
258
|
+
last_error = None
|
|
259
|
+
for url in archive_urls:
|
|
260
|
+
_debug(f"Trying Artifactory archive: {url}")
|
|
261
|
+
try:
|
|
262
|
+
resp = self._resilient_get(url, headers=headers, timeout=60)
|
|
263
|
+
if resp.status_code == 200:
|
|
264
|
+
if len(resp.content) > max_archive_bytes:
|
|
265
|
+
last_error = f"Archive too large ({len(resp.content)} bytes) from {url}"
|
|
266
|
+
_debug(last_error)
|
|
267
|
+
continue
|
|
268
|
+
# Extract zip, stripping the top-level directory
|
|
269
|
+
target_path.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
with zipfile.ZipFile(io.BytesIO(resp.content)) as zf:
|
|
271
|
+
# Identify the root prefix (e.g., "repo-main/")
|
|
272
|
+
names = zf.namelist()
|
|
273
|
+
if not names:
|
|
274
|
+
raise RuntimeError(f"Empty archive from {url}")
|
|
275
|
+
root_prefix = names[0]
|
|
276
|
+
if not root_prefix.endswith('/'):
|
|
277
|
+
# Single file archive; extract as-is
|
|
278
|
+
zf.extractall(target_path)
|
|
279
|
+
return
|
|
280
|
+
for member in zf.infolist():
|
|
281
|
+
# Strip root prefix
|
|
282
|
+
if member.filename == root_prefix:
|
|
283
|
+
continue
|
|
284
|
+
rel = member.filename[len(root_prefix):]
|
|
285
|
+
if not rel:
|
|
286
|
+
continue
|
|
287
|
+
# Guard: prevent zip path traversal (CWE-22)
|
|
288
|
+
dest = target_path / rel
|
|
289
|
+
if not dest.resolve().is_relative_to(target_path.resolve()):
|
|
290
|
+
_debug(f"Skipping zip entry escaping target: {member.filename}")
|
|
291
|
+
continue
|
|
292
|
+
if member.is_dir():
|
|
293
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
294
|
+
else:
|
|
295
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
296
|
+
with zf.open(member) as src, open(dest, 'wb') as dst:
|
|
297
|
+
dst.write(src.read())
|
|
298
|
+
_debug(f"Extracted Artifactory archive to {target_path}")
|
|
299
|
+
return
|
|
300
|
+
else:
|
|
301
|
+
last_error = f"HTTP {resp.status_code} from {url}"
|
|
302
|
+
_debug(last_error)
|
|
303
|
+
except zipfile.BadZipFile:
|
|
304
|
+
last_error = f"Invalid zip archive from {url}"
|
|
305
|
+
_debug(last_error)
|
|
306
|
+
except requests.RequestException as e:
|
|
307
|
+
last_error = str(e)
|
|
308
|
+
_debug(f"Request failed: {last_error}")
|
|
309
|
+
|
|
310
|
+
raise RuntimeError(
|
|
311
|
+
f"Failed to download package {owner}/{repo}#{ref} from Artifactory "
|
|
312
|
+
f"({host}/{prefix}). Last error: {last_error}"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def _download_file_from_artifactory(self, host: str, prefix: str, owner: str,
|
|
316
|
+
repo: str, file_path: str, ref: str, scheme: str = "https") -> bytes:
|
|
317
|
+
"""Download a single file from Artifactory by fetching the full archive and extracting it."""
|
|
318
|
+
import io
|
|
319
|
+
import zipfile
|
|
320
|
+
|
|
321
|
+
archive_urls = build_artifactory_archive_url(host, prefix, owner, repo, ref, scheme=scheme)
|
|
322
|
+
headers = self._get_artifactory_headers()
|
|
323
|
+
|
|
324
|
+
for url in archive_urls:
|
|
325
|
+
try:
|
|
326
|
+
resp = self._resilient_get(url, headers=headers, timeout=60)
|
|
327
|
+
if resp.status_code != 200:
|
|
328
|
+
continue
|
|
329
|
+
with zipfile.ZipFile(io.BytesIO(resp.content)) as zf:
|
|
330
|
+
names = zf.namelist()
|
|
331
|
+
root_prefix = names[0] if names else ""
|
|
332
|
+
target_name = root_prefix + file_path
|
|
333
|
+
if target_name in names:
|
|
334
|
+
return zf.read(target_name)
|
|
335
|
+
if file_path in names:
|
|
336
|
+
return zf.read(file_path)
|
|
337
|
+
except (zipfile.BadZipFile, requests.RequestException):
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
raise RuntimeError(
|
|
341
|
+
f"Failed to download file '{file_path}' from Artifactory "
|
|
342
|
+
f"({host}/{prefix}/{owner}/{repo}#{ref})"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
@staticmethod
|
|
346
|
+
def _is_artifactory_only() -> bool:
|
|
347
|
+
"""Return True when ARTIFACTORY_ONLY is set, blocking all direct git operations."""
|
|
348
|
+
return os.environ.get('ARTIFACTORY_ONLY', '').strip().lower() in ('1', 'true', 'yes')
|
|
349
|
+
|
|
350
|
+
def _should_use_artifactory_proxy(self, dep_ref: 'DependencyReference') -> bool:
|
|
351
|
+
"""Check if a dependency should be routed through the Artifactory transparent proxy."""
|
|
352
|
+
if dep_ref.is_artifactory():
|
|
353
|
+
return False # already explicit Artifactory
|
|
354
|
+
if self._is_artifactory_only():
|
|
355
|
+
return True
|
|
356
|
+
if dep_ref.is_azure_devops():
|
|
357
|
+
return False
|
|
358
|
+
host = dep_ref.host or default_host()
|
|
359
|
+
return is_github_hostname(host)
|
|
360
|
+
|
|
361
|
+
def _parse_artifactory_base_url(self) -> Optional[tuple]:
|
|
362
|
+
"""Parse ARTIFACTORY_BASE_URL into (host, prefix, scheme)."""
|
|
363
|
+
import urllib.parse as urlparse
|
|
364
|
+
base_url = os.environ.get('ARTIFACTORY_BASE_URL', '').strip().rstrip('/')
|
|
365
|
+
if not base_url:
|
|
366
|
+
return None
|
|
367
|
+
parsed = urlparse.urlparse(base_url)
|
|
368
|
+
if parsed.scheme not in ('https', 'http'):
|
|
369
|
+
_debug(f"ARTIFACTORY_BASE_URL has unsupported scheme: {parsed.scheme}")
|
|
370
|
+
return None
|
|
371
|
+
host = parsed.hostname
|
|
372
|
+
path = parsed.path.strip('/')
|
|
373
|
+
if not host or not path:
|
|
374
|
+
return None
|
|
375
|
+
return (host, path, parsed.scheme)
|
|
376
|
+
|
|
222
377
|
def _resilient_get(self, url: str, headers: Dict[str, str], timeout: int = 30, max_retries: int = 3) -> requests.Response:
|
|
223
378
|
"""HTTP GET with retry on 429/503 and rate-limit header awareness (#171).
|
|
224
379
|
|
|
@@ -514,7 +669,20 @@ class GitHubPackageDownloader:
|
|
|
514
669
|
|
|
515
670
|
# Default to main branch if no reference specified
|
|
516
671
|
ref = dep_ref.reference or "main"
|
|
517
|
-
|
|
672
|
+
|
|
673
|
+
# Artifactory: no git repo to query, return ref-based resolution
|
|
674
|
+
if dep_ref.is_artifactory() or (
|
|
675
|
+
self._parse_artifactory_base_url()
|
|
676
|
+
and self._should_use_artifactory_proxy(dep_ref)
|
|
677
|
+
):
|
|
678
|
+
is_commit = re.match(r'^[a-f0-9]{7,40}$', ref.lower()) is not None
|
|
679
|
+
return ResolvedReference(
|
|
680
|
+
original_ref=repo_ref,
|
|
681
|
+
ref_type=GitReferenceType.COMMIT if is_commit else GitReferenceType.BRANCH,
|
|
682
|
+
resolved_commit=None,
|
|
683
|
+
ref_name=ref
|
|
684
|
+
)
|
|
685
|
+
|
|
518
686
|
# Pre-analyze the reference type to determine the best approach
|
|
519
687
|
is_likely_commit = re.match(r'^[a-f0-9]{7,40}$', ref.lower()) is not None
|
|
520
688
|
|
|
@@ -618,11 +786,30 @@ class GitHubPackageDownloader:
|
|
|
618
786
|
RuntimeError: If download fails or file not found
|
|
619
787
|
"""
|
|
620
788
|
host = dep_ref.host or default_host()
|
|
621
|
-
|
|
789
|
+
|
|
790
|
+
# Check if this is Artifactory (Mode 1: explicit FQDN)
|
|
791
|
+
if dep_ref.is_artifactory():
|
|
792
|
+
repo_parts = dep_ref.repo_url.split('/')
|
|
793
|
+
return self._download_file_from_artifactory(
|
|
794
|
+
dep_ref.host, dep_ref.artifactory_prefix,
|
|
795
|
+
repo_parts[0], repo_parts[1] if len(repo_parts) > 1 else repo_parts[0],
|
|
796
|
+
file_path, ref,
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
# Check if this should go through Artifactory proxy (Mode 2)
|
|
800
|
+
art_proxy = self._parse_artifactory_base_url()
|
|
801
|
+
if art_proxy and self._should_use_artifactory_proxy(dep_ref):
|
|
802
|
+
repo_parts = dep_ref.repo_url.split('/')
|
|
803
|
+
return self._download_file_from_artifactory(
|
|
804
|
+
art_proxy[0], art_proxy[1],
|
|
805
|
+
repo_parts[0], repo_parts[1] if len(repo_parts) > 1 else repo_parts[0],
|
|
806
|
+
file_path, ref, scheme=art_proxy[2],
|
|
807
|
+
)
|
|
808
|
+
|
|
622
809
|
# Check if this is Azure DevOps
|
|
623
810
|
if dep_ref.is_azure_devops():
|
|
624
811
|
return self._download_ado_file(dep_ref, file_path, ref)
|
|
625
|
-
|
|
812
|
+
|
|
626
813
|
# GitHub API
|
|
627
814
|
return self._download_github_file(dep_ref, file_path, ref)
|
|
628
815
|
|
|
@@ -1379,6 +1566,9 @@ author: {dep_ref.repo_url.split('/')[0]}
|
|
|
1379
1566
|
|
|
1380
1567
|
# Check if subdirectory exists
|
|
1381
1568
|
source_subdir = temp_clone_path / subdir_path
|
|
1569
|
+
# Security: ensure subdirectory resolves within the cloned repo
|
|
1570
|
+
from ..utils.path_security import ensure_path_within
|
|
1571
|
+
ensure_path_within(source_subdir, temp_clone_path)
|
|
1382
1572
|
if not source_subdir.exists():
|
|
1383
1573
|
raise RuntimeError(f"Subdirectory '{subdir_path}' not found in repository")
|
|
1384
1574
|
|
|
@@ -1465,6 +1655,116 @@ author: {dep_ref.repo_url.split('/')[0]}
|
|
|
1465
1655
|
package_type=validation_result.package_type
|
|
1466
1656
|
)
|
|
1467
1657
|
|
|
1658
|
+
def _download_subdirectory_from_artifactory(
|
|
1659
|
+
self, dep_ref: 'DependencyReference', target_path: Path,
|
|
1660
|
+
proxy_info: tuple, progress_task_id=None, progress_obj=None,
|
|
1661
|
+
) -> PackageInfo:
|
|
1662
|
+
"""Download an archive from Artifactory and extract a subdirectory."""
|
|
1663
|
+
import tempfile
|
|
1664
|
+
ref = dep_ref.reference or "main"
|
|
1665
|
+
subdir_path = dep_ref.virtual_path
|
|
1666
|
+
repo_parts = dep_ref.repo_url.split('/')
|
|
1667
|
+
owner, repo = repo_parts[0], repo_parts[1] if len(repo_parts) > 1 else repo_parts[0]
|
|
1668
|
+
host, prefix, scheme = proxy_info
|
|
1669
|
+
|
|
1670
|
+
if progress_obj and progress_task_id is not None:
|
|
1671
|
+
progress_obj.update(progress_task_id, completed=10, total=100)
|
|
1672
|
+
|
|
1673
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
1674
|
+
temp_path = Path(temp_dir) / "full_pkg"
|
|
1675
|
+
self._download_artifactory_archive(host, prefix, owner, repo, ref, temp_path, scheme=scheme)
|
|
1676
|
+
if progress_obj and progress_task_id is not None:
|
|
1677
|
+
progress_obj.update(progress_task_id, completed=60, total=100)
|
|
1678
|
+
source_subdir = temp_path / subdir_path
|
|
1679
|
+
if not source_subdir.exists() or not source_subdir.is_dir():
|
|
1680
|
+
raise RuntimeError(
|
|
1681
|
+
f"Subdirectory '{subdir_path}' not found in archive from "
|
|
1682
|
+
f"Artifactory ({host}/{prefix}/{owner}/{repo}#{ref})"
|
|
1683
|
+
)
|
|
1684
|
+
target_path.mkdir(parents=True, exist_ok=True)
|
|
1685
|
+
if target_path.exists() and any(target_path.iterdir()):
|
|
1686
|
+
shutil.rmtree(target_path)
|
|
1687
|
+
target_path.mkdir(parents=True, exist_ok=True)
|
|
1688
|
+
for item in source_subdir.iterdir():
|
|
1689
|
+
src = source_subdir / item.name
|
|
1690
|
+
dst = target_path / item.name
|
|
1691
|
+
if src.is_dir():
|
|
1692
|
+
shutil.copytree(src, dst)
|
|
1693
|
+
else:
|
|
1694
|
+
shutil.copy2(src, dst)
|
|
1695
|
+
|
|
1696
|
+
if progress_obj and progress_task_id is not None:
|
|
1697
|
+
progress_obj.update(progress_task_id, completed=80, total=100)
|
|
1698
|
+
validation_result = validate_apm_package(target_path)
|
|
1699
|
+
if not validation_result.is_valid:
|
|
1700
|
+
raise RuntimeError(f"Subdirectory is not a valid APM package: {'; '.join(validation_result.errors)}")
|
|
1701
|
+
resolved_ref = ResolvedReference(original_ref=ref, ref_name=ref, ref_type=GitReferenceType.BRANCH, resolved_commit=None)
|
|
1702
|
+
if progress_obj and progress_task_id is not None:
|
|
1703
|
+
progress_obj.update(progress_task_id, completed=100, total=100)
|
|
1704
|
+
return PackageInfo(
|
|
1705
|
+
package=validation_result.package, install_path=target_path,
|
|
1706
|
+
resolved_reference=resolved_ref, installed_at=datetime.now().isoformat(),
|
|
1707
|
+
dependency_ref=dep_ref, package_type=validation_result.package_type
|
|
1708
|
+
)
|
|
1709
|
+
|
|
1710
|
+
def _download_package_from_artifactory(
|
|
1711
|
+
self, dep_ref: 'DependencyReference', target_path: Path,
|
|
1712
|
+
proxy_info: Optional[tuple] = None, progress_task_id=None, progress_obj=None,
|
|
1713
|
+
) -> PackageInfo:
|
|
1714
|
+
"""Download a package via Artifactory VCS archive."""
|
|
1715
|
+
ref = dep_ref.reference or "main"
|
|
1716
|
+
repo_parts = dep_ref.repo_url.split('/')
|
|
1717
|
+
if len(repo_parts) < 2 or not repo_parts[0] or not repo_parts[1]:
|
|
1718
|
+
raise ValueError(f"Invalid Artifactory repo reference '{dep_ref.repo_url}': expected 'owner/repo' format")
|
|
1719
|
+
owner, repo = repo_parts[0], repo_parts[1]
|
|
1720
|
+
|
|
1721
|
+
scheme = "https"
|
|
1722
|
+
if dep_ref.is_artifactory():
|
|
1723
|
+
host, prefix = dep_ref.host, dep_ref.artifactory_prefix
|
|
1724
|
+
if not host or not prefix:
|
|
1725
|
+
raise ValueError(f"Artifactory dependency '{dep_ref.repo_url}' is missing host or artifactory prefix")
|
|
1726
|
+
elif proxy_info:
|
|
1727
|
+
host, prefix, scheme = proxy_info
|
|
1728
|
+
else:
|
|
1729
|
+
raise RuntimeError("Artifactory download requires either FQDN or ARTIFACTORY_BASE_URL")
|
|
1730
|
+
|
|
1731
|
+
_debug(f"Downloading from Artifactory: {host}/{prefix}/{owner}/{repo}#{ref}")
|
|
1732
|
+
if target_path.exists() and any(target_path.iterdir()):
|
|
1733
|
+
shutil.rmtree(target_path)
|
|
1734
|
+
target_path.mkdir(parents=True, exist_ok=True)
|
|
1735
|
+
if progress_obj and progress_task_id is not None:
|
|
1736
|
+
progress_obj.update(progress_task_id, total=100, completed=10)
|
|
1737
|
+
try:
|
|
1738
|
+
self._download_artifactory_archive(host, prefix, owner, repo, ref, target_path, scheme=scheme)
|
|
1739
|
+
except RuntimeError:
|
|
1740
|
+
if target_path.exists():
|
|
1741
|
+
shutil.rmtree(target_path, ignore_errors=True)
|
|
1742
|
+
raise
|
|
1743
|
+
if progress_obj and progress_task_id is not None:
|
|
1744
|
+
progress_obj.update(progress_task_id, completed=70, total=100)
|
|
1745
|
+
|
|
1746
|
+
validation_result = validate_apm_package(target_path)
|
|
1747
|
+
if not validation_result.is_valid:
|
|
1748
|
+
if target_path.exists():
|
|
1749
|
+
shutil.rmtree(target_path, ignore_errors=True)
|
|
1750
|
+
error_msg = f"Invalid APM package {dep_ref.repo_url}:\n"
|
|
1751
|
+
for error in validation_result.errors:
|
|
1752
|
+
error_msg += f" - {error}\n"
|
|
1753
|
+
raise RuntimeError(error_msg.strip())
|
|
1754
|
+
if not validation_result.package:
|
|
1755
|
+
raise RuntimeError(f"Package validation succeeded but no package metadata found for {dep_ref.repo_url}")
|
|
1756
|
+
package = validation_result.package
|
|
1757
|
+
package.source = dep_ref.to_github_url()
|
|
1758
|
+
package.resolved_commit = None
|
|
1759
|
+
resolved_ref = ResolvedReference(original_ref=f"{dep_ref.repo_url}#{ref}", ref_type=GitReferenceType.BRANCH, resolved_commit=None, ref_name=ref)
|
|
1760
|
+
if progress_obj and progress_task_id is not None:
|
|
1761
|
+
progress_obj.update(progress_task_id, completed=100, total=100)
|
|
1762
|
+
return PackageInfo(
|
|
1763
|
+
package=package, install_path=target_path, resolved_reference=resolved_ref,
|
|
1764
|
+
installed_at=datetime.now().isoformat(), dependency_ref=dep_ref,
|
|
1765
|
+
package_type=validation_result.package_type
|
|
1766
|
+
)
|
|
1767
|
+
|
|
1468
1768
|
def download_package(
|
|
1469
1769
|
self,
|
|
1470
1770
|
repo_ref: str,
|
|
@@ -1499,19 +1799,41 @@ author: {dep_ref.repo_url.split('/')[0]}
|
|
|
1499
1799
|
# Handle virtual packages differently
|
|
1500
1800
|
if dep_ref.is_virtual:
|
|
1501
1801
|
if dep_ref.is_virtual_file():
|
|
1502
|
-
# Individual file virtual package
|
|
1503
1802
|
return self.download_virtual_file_package(dep_ref, target_path, progress_task_id, progress_obj)
|
|
1504
1803
|
elif dep_ref.is_virtual_collection():
|
|
1505
|
-
# Collection virtual package
|
|
1506
1804
|
return self.download_collection_package(dep_ref, target_path, progress_task_id, progress_obj)
|
|
1507
1805
|
elif dep_ref.is_virtual_subdirectory():
|
|
1508
|
-
#
|
|
1806
|
+
# When ARTIFACTORY_ONLY is set, download full archive and extract subdir
|
|
1807
|
+
art_proxy = self._parse_artifactory_base_url()
|
|
1808
|
+
if self._is_artifactory_only() and art_proxy:
|
|
1809
|
+
return self._download_subdirectory_from_artifactory(
|
|
1810
|
+
dep_ref, target_path, art_proxy, progress_task_id, progress_obj
|
|
1811
|
+
)
|
|
1509
1812
|
return self.download_subdirectory_package(dep_ref, target_path, progress_task_id, progress_obj)
|
|
1510
1813
|
else:
|
|
1511
1814
|
raise ValueError(f"Unknown virtual package type for {dep_ref.virtual_path}")
|
|
1512
|
-
|
|
1815
|
+
|
|
1816
|
+
# Artifactory download path (Mode 1: explicit FQDN, Mode 2: transparent proxy)
|
|
1817
|
+
use_artifactory = dep_ref.is_artifactory()
|
|
1818
|
+
art_proxy = None
|
|
1819
|
+
if not use_artifactory:
|
|
1820
|
+
art_proxy = self._parse_artifactory_base_url()
|
|
1821
|
+
if art_proxy and self._should_use_artifactory_proxy(dep_ref):
|
|
1822
|
+
use_artifactory = True
|
|
1823
|
+
|
|
1824
|
+
if use_artifactory:
|
|
1825
|
+
return self._download_package_from_artifactory(
|
|
1826
|
+
dep_ref, target_path, art_proxy, progress_task_id, progress_obj
|
|
1827
|
+
)
|
|
1828
|
+
|
|
1829
|
+
# When ARTIFACTORY_ONLY is set but no Artifactory proxy matched, block direct git
|
|
1830
|
+
if self._is_artifactory_only():
|
|
1831
|
+
raise RuntimeError(
|
|
1832
|
+
f"ARTIFACTORY_ONLY is set but no Artifactory proxy is configured for '{repo_ref}'. "
|
|
1833
|
+
"Set ARTIFACTORY_BASE_URL or use explicit Artifactory FQDN syntax."
|
|
1834
|
+
)
|
|
1835
|
+
|
|
1513
1836
|
# Regular package download (existing logic)
|
|
1514
|
-
# Resolve the Git reference to get specific commit
|
|
1515
1837
|
resolved_ref = self.resolve_git_reference(repo_ref)
|
|
1516
1838
|
|
|
1517
1839
|
# Create target directory if it doesn't exist
|