Skip to content

Multi-host dependency support: lockfile identity, token resolution consistency, error clarity #773

@danielmeppiel

Description

@danielmeppiel

Summary

PR #587 (generic git host / Gitea support) surfaces several pre-existing architectural and DX gaps in the dependency identity, token resolution, and error-reporting paths. None of these block #587 directly, but they should land before multi-host support is GA so we don't ship known sharp edges to users adopting Gitea / Bitbucket / self-hosted GitLab.

Items

1. Lockfile identity collision: get_unique_key() omits host

DependencyReference.get_unique_key() and LockedDependency.get_unique_key() both return the bare repo_url (e.g. team/skills) without the host. Two dependencies with the same owner/repo on different hosts collide silently in apm.lock:

- name: skills
  repo_url: gitea.myorg.com/team/skills
- name: skills
  repo_url: github.com/team/skills

→ second entry overwrites the first; install order becomes load-bearing.

Severity: correctness / data-integrity. Becomes much more likely once #587 lands.

Files: src/apm_cli/models/dependency/reference.py (lines ~176-190), src/apm_cli/deps/lockfile.py (lines ~43-49, ~196).

Suggested fix: include host in get_unique_key() for non-default hosts (preserve bare owner/repo for github.com to keep existing lockfiles stable).

2. Asymmetric token resolution between download and clone paths

_download_github_file calls auth_resolver.resolve() directly (line ~1207 of github_downloader.py), while the clone path correctly routes through _resolve_dep_token() which returns None for generic hosts. The asymmetry is not a security issue (HTTPS is the transport boundary per core/auth.py:377-379), but it produces misleading 401 errors when a GitHub PAT is sent to a Gitea / GitLab server that rejects it — instead of falling through to git-credential-helper as the clone path does.

Severity: DX / error-message clarity.

Suggested fix: route _download_github_file token resolution through _resolve_dep_token() for consistency.

3. is_gitlab_hostname() is convention-based and fragile

startswith("gitlab.") misses org-managed GitLab instances on bespoke hostnames (code.acme.com, git.team.org, etc.). The whole GitLab-vs-other-generic-host bifurcation in #587 falls back to a heuristic that doesn't hold.

Severity: correctness on non-conformant hostnames.

Suggested fix: consider explicit user signaling (type: gitlab in apm.yml) or capability-detection via API probe + cache, rather than relying on hostname conventions.

4. Silent error swallowing in download fallback chain

_download_github_file runs up to 7 attempts (raw + API × ref × API version), and currently swallows non-404 errors at each step — auth failures, 5xx, network errors all surface to the user as "file not found." There's also no verbose_callback breadcrumb per attempt, so --verbose doesn't help users understand which endpoint was tried.

Severity: DX / debuggability.

Suggested fix: classify exceptions, only swallow 404, surface 401/403/5xx with the host + endpoint context. Emit one verbose_callback line per attempt.

5. Pre-existing GitHub-specific error strings reachable for generic hosts

Strings like "GitHub API rate limit exceeded" are now reachable from Gitea / GitLab error paths via the shared download code. Should be parameterized on host.

Severity: polish.

Non-goals

Not changing the deliberate project stance that "HTTPS is the transport security boundary" (auth.py:377-379) — global PATs may be sent to user-configured hosts. This issue is about correctness, identity, and error-clarity, not transport gating.

Why now

PR #587 quadruples the surface area of the download fallback chain and introduces multi-host scenarios the lockfile identity model wasn't designed for. Tracking these explicitly so they don't get forgotten when #587 lands.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions