Guides

Analyze a project without GitHub

Push your build files from any pipeline when your code does not live on GitHub.com.

When Deptools cannot read your repository, your pipeline sends the build files instead. You create a CI upload project, add one job to your pipeline, and every run produces an analysis identical to the one a GitHub project gets: same graph, same score, same dashboard.

Deptools only sees what a push delivers, and that push contains only your build files.

When you need this

Take this path when your code lives somewhere Deptools cannot reach:

  • A self hosted GitLab, an internal Bitbucket, or any server closed to the outside.
  • A platform Deptools does not integrate with yet.
  • A repository your team will not grant access to, whatever the tool.
If your code is on GitHub and you only want to start a scan from your pipeline, you do not need this page. Connect the repository normally and call the scan endpoint from your CI, as described in Scan from another CI. Triggering a scan on a GitHub project is allowed on every plan, while pushing build files requires Pro, including for Open Source Max.

This path is Pro only. A CI upload project has no public repository behind it, so it is always private and always occupies one private slot. Organizations and plans explains what occupies a slot and what buying one bills you.

Create an organization without GitHub

A personal organization and a GitHub organization both accept upload projects, as soon as they are on Pro. You can stop reading this section and go create the project.

Create a separate organization when you want one of these:

  • Team members, since a personal organization can never have any.
  • A separate bill and separate plan for a client or a department.
  • A clean split between what you analyze for yourself and what your team owns.

Open the organization switcher in the navbar, Add Organization, then the Create manually tab, and give it a name. No GitHub account is involved, and none can be attached later: the organization has no GitHub tab and holds upload projects only.

It is created on the Free plan, and upload projects need Pro, so it can hold nothing until you upgrade. The projects screen says This organization uses CI upload projects, which require the Pro plan. Upgrade from there, or from the Plan tab of the organization settings.

The three kinds of organization, and how the plan attaches to one, are covered in Organizations and plans.

Create an upload project

Open New project and choose Upload from CI. In an organization created manually and already on Pro, the form opens directly, since it is the only possible source there.

FieldRequiredWhat it does
Project nameyesUnique inside the organization.
Build systemyesMaven, Gradle, sbt, npm or Composer. Fixed at creation.
Dependency scopesnoThe scopes counted in the graph and in the score. Every scope is selected by default except the test one. Fixed at creation. See Exclude dependency scopes
BranchnoA label displayed in the interface. Deptools reads no branch here, your pipeline decides what it sends. Editable later
Subfolder / modulenoThe module to analyze, for example services/api. Empty analyzes the repository root. Editable later

Two of those five are decided once. The build system and the dependency scopes cannot be changed: to correct either one, delete the project and create it again. The freed slot stays paid and reusable, so that costs nothing.

The subfolder is the opposite. Deptools has no files to look at yet, so nothing can be validated at creation. Enter your best guess: the first push checks it against your archive, and if it matches no module, the error lists the modules it did find. Change it in the project settings, and the next push uses the new value.

New CI upload project form, with the build system, dependency scopes, branch and subfolder fields

The project appears immediately, with no analysis. That is expected: there are no files yet. The first analysis comes from the first push.

Creating more than a handful of projects by hand can mean an hour of clicking. One API request creates them all and returns the name to UUID table your pipelines need: Create many projects at once.

Push your build files from your pipeline

Three things go into your pipeline: a key, two variables, and one job.

Get an API key

Two scopes are possible:

  • A project key (dt_proj_...), valid on this project only. To create it, open the project, then the Integrations tab.
  • An organization key (dt_org_...), valid on every project of the organization. Pick this one when several repositories push from the same CI, since one shared variable then serves them all. To create it, open the organization settings, then the API keys tab.

The key is shown once, at creation. You must be an owner or an admin. See Manage API keys for expiration and rotation.

Declare two variables

VariableValue
DEPTOOLS_API_KEYThe key you just created. Store it as a masked or secret variable, never in the YAML
DEPTOOLS_PROJECT_UUIDThe project UUID, shown in the project's Integrations tab with a copy button

Add the scan job

Deptools serves a POSIX shell script that collects the build files, archives them and pushes them. It needs sh, tar and curl.

# One-off scan from your machine, the quickest way to test your setup
# Run from the repository root
curl -fsSL https://api.deptools.io/deptools-scan.sh -o deptools-scan.sh

DEPTOOLS_API_KEY="dt_proj_..." \
DEPTOOLS_PROJECT_UUID="b9c105b0-..." \
sh deptools-scan.sh --wait

The script takes two options:

  • --wait polls until the analysis ends and exits 1 if it fails, which turns the job into a pipeline gate. Without it, the job pushes and returns immediately.
  • --dir <path> collects from another directory than the current one.

It also reads the branch and the commit SHA from GitLab CI, from GitHub Actions or from the local git checkout, and sends them as labels. DEPTOOLS_BRANCH and DEPTOOLS_COMMIT_SHA override that detection.

A cron job works as well as a pipeline. There is no cooldown on Pro, so you can push on every commit.

What gets collected

The script and the server apply the same allowlist. Everything else in the archive is discarded, so archiving too much is harmless, only slower to transfer.

EcosystemFiles collected
Mavenpom.xml, at any depth
Gradleevery *.gradle and *.gradle.kts, including settings.gradle, convention plugins and local platform files, plus gradle.properties and libs.versions.toml. init.gradle is ignored
sbtevery *.sbt at any depth, plus project/*.scala and project/build.properties. Never the Scala sources under src/, never the meta build project/project/
npmpackage.json, package-lock.json, npm-shrinkwrap.json, yarn.lock, pnpm-lock.yaml, pnpm-workspace.yaml
Composercomposer.json and composer.lock

The content of node_modules/, .git/, target/, build/ and vendor/ is never collected, at any depth, and neither are the macOS metadata files ._*.

A build file committed under build/ or target/ is dropped with the rest of those directories, since they normally hold compilation output. If that is where your real build file lives, this path cannot read it.
Commit your lockfile. With package-lock.json, pnpm-lock.yaml, composer.lock or yarn.lock in the archive, the analysis describes the tree you actually install instead of resolving your version constraints. It is also what limits the damage when a package is private and cannot be resolved from the public registry.

Limits on the archive

RuleValueError
Compressed archive15 MB413 BUNDLE_TOO_LARGE
Total uncompressed size50 MB413 BUNDLE_TOO_LARGE
One collected file, uncompressed5 MB413 FILE_TOO_LARGE
Entries in the archive5 000413 TOO_MANY_ENTRIES
Encoding of collected filesstrict UTF-8400 INVALID_ENCODING
Pathsrelative, no .., no absolute path400 INVALID_ENTRY_PATH
Entry typesregular files only, no symbolic links400 UNSUPPORTED_ENTRY_TYPE

The entry count is checked on the archive, before the allowlist runs. So prune node_modules/ on your side to keep a large project under the ceiling, even though the server would have discarded those files anyway.

Without the script

Any HTTP client works. The request is one multipart POST:

POST https://api.deptools.io/v1/projects/<uuid>/scan
Authorization: Bearer dt_org_...
Content-Type: multipart/form-data
FieldRequiredDescription
bundleyesThe tar.gz archive, paths relative to the repository root
branchnoBranch label, for traceability
commitShanoCommit SHA, for traceability
# From the repository root: archive the build files
tar czf bundle.tar.gz $(git ls-files '*pom.xml' '*.gradle' '*.gradle.kts' \
  '*gradle.properties' '*libs.versions.toml' '*.sbt' '*project/*.scala' \
  '*project/build.properties' '*package.json' '*package-lock.json' \
  '*yarn.lock' '*pnpm-lock.yaml' '*pnpm-workspace.yaml' '*npm-shrinkwrap.json' \
  '*composer.json' '*composer.lock' \
  ':(exclude,glob)**/node_modules/**' ':(exclude,glob)**/vendor/**' \
  ':(exclude,glob)**/target/**' ':(exclude,glob)**/build/**')

# Push archive to Deptools
curl --fail-with-body -sS \
  -H "Authorization: Bearer $DEPTOOLS_API_KEY" \
  -F "bundle=@bundle.tar.gz" \
  -F "branch=$CI_COMMIT_REF_NAME" \
  -F "commitSha=$CI_COMMIT_SHA" \
  "https://api.deptools.io/v1/projects/$DEPTOOLS_PROJECT_UUID/scan"
# → 202 { "jobId": "abc123", "pollUrl": "/v1/scans/abc123/status" }

A 202 means the archive was accepted and the analysis started. Poll pollUrl with the same key to follow it. The response carries status, then a result block once it reads completed. Gate your pipeline on that block:

{
  "jobId": "abc123",
  "status": "completed",
  "createdAt": "2026-08-09T10:00:00.000Z",
  "result": {
    "overall_score": 7.4,
    "dependency_number": 182,
    "vulnerabilities_by_severity": { "CRITICAL": 0, "HIGH": 2, "MODERATE": 5, "LOW": 1 },
    "commercial_use": false,
    "direct_up_to_date": 84
  },
  "error": null
}

The push endpoint accepts 60 requests per hour and per IP address, the status endpoint 120.

What Deptools keeps from your archive

This is the only path where your files reach Deptools at all.

The archive is filtered as it arrives. It is never written to disk and never extracted into a directory tree: it is read as a stream, every entry outside the allowlist above is dropped immediately, and the rest is kept as text in a single snapshot. Nothing in it is ever executed.

That snapshot is deleted as soon as the analysis reaches a terminal state, whether it succeeded or failed. Deptools keeps the analysis result afterwards, the graph, the scores and the metrics, exactly as for a GitHub project.

So a source file that slipped into your archive is discarded before the analysis starts, and a build file is deleted when the analysis ends. See https://deptools.io/privacy for retention and personal data.

Check that the first push worked

The Integrations tab follows the setup end to end and updates on its own, no refresh needed. Its last step reports what happened to your first push: Waiting for your first push, then the analysis running, then First analysis received, or the failure and its reason.

CI setup checklist of an upload project, waiting for the first push

Once the first analysis arrives, the project behaves like any other: the dashboard, the history and the exports all work the same way. One difference stays visible, and it is deliberate. Where a GitHub project offers a Run scan button, an upload project reads Scans are triggered from your CI. Deptools has no way to fetch your files on its own, so every analysis comes from a push.

When the push is refused

Rejections happen before the analysis starts, and the message names the cause. The script prints it, and prints the module list or the detected build systems when the response carries them.

HTTPCodeMeaning and fix
401Key missing or malformed. The header is Authorization: Bearer dt_org_...
401API_KEY_EXPIREDThe key passed its expiration date. Create a new one
403PRO_PLAN_REQUIREDThe organization is not on Pro, or no longer is
404Unknown UUID, or a project outside the key's scope. A project key reaches its own project only. The answer is deliberately the same in both cases
400BUNDLE_REQUIREDThe request carries no multipart body, or no bundle field
400INVALID_ARCHIVEThe file is not a valid tar.gz
400NO_BUILD_FILES_FOUNDNo recognized build file in the archive. Check that the job runs at the repository root
400BUILD_SYSTEM_MISMATCHThe archive holds no build file of the project's build system. The response lists detectedBuildSystems. The build system is fixed, so fix the archive or recreate the project
400SUBFOLDER_NOT_FOUNDThe configured subfolder matches no module in the archive. The response lists availableModules, copy the right one into the project settings
400INVALID_ENCODING, INVALID_ENTRY_PATH, UNSUPPORTED_ENTRY_TYPEA collected file is not UTF-8, a path escapes the archive root, or an entry is not a regular file
413BUNDLE_TOO_LARGE, FILE_TOO_LARGE, TOO_MANY_ENTRIESA cap above is exceeded
422UPLOAD_NOT_SUPPORTED_FOR_GIT_PROJECTSThe UUID belongs to a project linked to a Git repository, which takes no body. Check DEPTOOLS_PROJECT_UUID
429SCAN_RATE_LIMITEDThe 60 pushes per hour are used up

An archive is never partly accepted. When one of these errors occurs, no analysis runs and no file is stored.

When the analysis itself fails

A 202 means the archive was accepted, not that the analysis will succeed. Two failures are specific to this path, and neither is a bug on your side:

  • Maven parents and BOMs hosted on an internal registry cannot be reached from outside your network. They surface as analysis errors, and the rest of the analysis completes.
  • Private packages, npm or otherwise, cannot be resolved from the public registry and are listed as skipped. A committed lockfile limits the loss, since the versions are then already pinned.

The other failures are the ordinary ones, and the project page names the cause.

Next steps