Package matching

How we resolve a package name to a GitHub repository, and what happens when we can't.

For each package in your lockfile we follow a deterministic lookup path to find the GitHub repo that publishes it. The path varies by ecosystem (see Supported formats for the per-ecosystem registry hop), but the shape is the same: parse → registry lookup → extract the repository URL → confirm it's a public GitHub repo.

When a match works #

Most popular packages publish a repository URL in their registry metadata that points at GitHub. When that URL is present, the match is one-shot and the resolved source becomes a tracked source automatically.

When a match fails #

A package can fail to match for several reasons:

  • No repository field in the registry metadata. Maintainer just didn't set it.
  • Non-GitHub host. Sources hosted on GitLab, Bitbucket, Heptapod, Launchpad, Codeberg, or internal registries are not yet supported. When we recognise the host, the unmatched entry names it ("Links to Heptapod, not GitHub") so you can tell an unsupported-host package apart from one that's simply missing a link. Public GitHub mirrors do work if the package metadata points at them.
  • Renamed or unpublished package. The registry returns 404.

When a package can't be matched, it shows up in the upload result summary as "unmatched": no failure, just an honest "we couldn't find a public GitHub repo for this." You can come back later and add the source manually if you find the URL another way.

On a lockfile's detail page, any package whose discovery failed keeps a persistent Discovery failed badge in the package table, even after you deselect it, so the fact that it couldn't be resolved never quietly disappears from view. Hover the badge for the specific reason.

A matched source remembers where it came from: on the dashboard card and the detail page, each source's Used in row links back to the lockfile(s) it was discovered from, each one labeled by its connected repo (owner/name) (see Sources). When the same package is in several of your lockfiles, the source lists each one, and those links repeat inside each release card, so you can jump from a risky release straight to the repos that depend on it. Each entry links to that lockfile's detail page; for GitHub-connected lockfiles it also links straight to the repo on GitHub. Sources added manually or via starred-repo sync have no originating lockfile, so they don't show the row.

When the discovered repo isn't really the package #

Registry metadata sometimes points a package at a monorepo whose releases belong to a different (usually the flagship) project. client-only points at the React monorepo, so the repo's v19.x tags say nothing about client-only's own versions. Discovery still tracks the repo (its releases are often relevant context), but the upgrade pipeline validates every offered version against the package's own registry before claiming it: tags that name versions the package never published are discarded, and if none survive, the package simply produces no upgrade. See Available upgrades for the full computation.

Transitive dependencies #

Lockfiles include transitive dependencies (the dependencies of your dependencies), and we match all of them. This is the point: the risky release is rarely the library you remember installing; it's something five hops down.

For formats that carry a dependency graph (package-lock.json v2/v3, uv.lock) we also record who requires what, with which version ranges. That's what lets an available upgrade say it's direct vs. transitive, and, when a parent's constraint excludes the newer version, mark it blocked by parent instead of pretending it's independently takeable.

When the same package is pinned at more than one version in a single lockfile (common with npm, which hoists one copy to the top level and nests conflicting copies under their parents) we report the most-hoisted (shallowest) copy, and break ties on the higher version. That's the copy most of your tree resolves against, and (unlike whichever nested copy happened to sort last) it's stable across re-installs, so version diffs don't flip-flop between syncs. Individual transitive dependencies may still resolve their own nested copy at a different version; we surface the one representative version per package rather than every nested pin.

On the Hobbyist tier you pick up to 1,000 packages to track from the lockfile: enough to monitor a modern frontend lockfile in full. A good rule: pick the high-traffic ones at the top of your dependency tree plus any libraries with a history of breaking changes. Re-evaluate as the project grows.