Dependency resolution for python projects in CI pipelines
Problem to solve
GitLab's dependency scanning relies on lockfiles or graphfiles for accurate dependency detection and vulnerability analysis. Python projects requiring resolution represent approximately 10% of projects with SBOM data in production - the largest single technology cohort after Maven.
Proposal
Following the outcomes of two internal spikes:
- Architecture spike: https://gitlab.com/gitlab-org/gitlab/-/work_items/582607
- Python validation spike: #586904 (closed)
The proposal is further described in the architecture design document: gitlab-com/content-sites/handbook!18223 (merged)
Manifest support
| Manifest | Command | Output | Parser | Notes |
|---|---|---|---|---|
requirements.txt / .in / .pip / requires.txt |
pip-compile |
pip-compile txt | pipcompile | No mutation side effects; all network I/O via system OpenSSL (FIPS-compatible) |
setup.py |
pip-compile |
pip-compile txt | pipcompile | |
setup.cfg |
pip-compile |
pip-compile txt | pipcompile | Not supported by Gemnasium — new coverage |
pyproject.toml (non-Poetry) |
pip-compile |
pip-compile txt | pipcompile | |
Pipfile |
— | — | — | Deferred to backlog (0.6% market share) |
Validated success rate (spike #586904 (closed)): 83% overall across 23 projects; 87.5% for Python 3.10+ projects. Failures are limited to: missing wheels for new Python versions, obsolete build metadata (use_2to3), packages with invalid metadata, and APIs removed in Python 3.10+.
Out of scope:
- Pipfile (backlog)
- Python < 3.10 (3.9 EOL Oct 2025; per product decision)
- Git/VCS dependencies (
git executable not found) - Local/editable path dependencies (
-e .,file:, path references) - Poetry without
poetry.lockwhen Poetry is not installable
Python version strategy
Use a single Python version (3.12) in the resolution job image. Python 3.12 is used (not 3.13) as it is the current stable RHEL9 UBI Python version in the resolution image. Testing showed no meaningful difference in resolution outcomes between 3.12 and 3.13. Users targeting a specific Python version can override the job image via python_resolution_image input.
Per product decision: Python 3.10+ minimum support only.
Known parser limitation
The DS analyzer's uv.lock parser keeps only the first package entry per name, silently discarding entries with different environment markers. This affects any path that produces uv.lock output. Paths using pip-compile → pip-compile txt (pipcompile parser) are not affected, as pip-compile resolves for a single version/platform.
This is an existing limitation in the analyzer, now extended to additional manifest types. Documented in MR Document uv.lock limitation in both DS and Gemn... (!222372 - merged) • Zamir Martins • 18.9. Not a blocker for this implementation but should be tracked as a follow-up.
Known gitlink limitation
pipcompile.lock.txt is a generated file with no git history of its own. The DS analyzer's gitlink resolver checks which source manifest (requirements.txt, pyproject.toml, setup.py, etc.) is committed in the same directory and reports that as input_file in the SBOM. In environments where git is unavailable, the resolver falls back to pipcompile.lock.txt itself as the input file. The fix is implemented and unit tested in the analyzer; it activates in production where the DS image includes git.
Implementation plan
- Update CI configuration
- V2 Dependency-Scanning CI/CD template: add
dependency-scanning:python-resolutionjob - Job triggers on:
**/requirements.txt,**/requirements.in,**/requirements.pip,**/requires.txt,**/pyproject.toml,**/setup.py,**/setup.cfg - Job image:
ubi9/python-312-minimal+ pip-tools 7.5.3 (image MR open: gitlab-org/security-products/analyzers/dependency-resolution!1 (merged)) - Artifacts:
**/pipcompile.lock.txt
- V2 Dependency-Scanning CI/CD template: add
- Update DS analyzer - Python service mode
- Implement manifest detection logic for Python (routing per manifest type table above)
- Implement script generation for each supported path
- Support
DS_EXCLUDED_PATHS,DS_MAX_DEPTH,DS_INCLUDE_DEV_DEPENDENCIES,DS_PIPCOMPILE_REQUIREMENTS_FILE_NAME_PATTERN - Pass through
PIP_INDEX_URL,PIP_EXTRA_INDEX_URLnatively (pip-compile reads these as standard pip env vars) PIP_CERTis a native pip env var; pip-compile reads it directly (no mapping needed)- Support
DS_PIP_DEPENDENCY_PATH: appends--find-links <path> --no-indexfor offline/local wheel resolution PIP_*env vars passed through natively;UV_*vars not applicable
- Implement integration tests
- Test fixtures covering all manifest types and key scenarios
- See Testing plan section
- Update documentation
- Supported manifest types and commands
- Python version strategy and override instructions
- Known limitations (parser, missing wheels)
- Env var mapping from Gemnasium equivalents
Resolved decisions
1. requirements.txt / .in: pip-compile vs uvx migrate-to-uv
Decision: pip-compile
pip-compile |
uvx migrate-to-uv |
|
|---|---|---|
| Output | pip-compile txt | uv.lock |
| Parser | pipcompile (existing) | uv (existing) |
| Side effects | None | Creates/modifies pyproject.toml in working dir |
| uv.lock parser marker issue | Not affected | Affected |
| Originally recommended by | Expert evaluation, spike #586904 (closed) | Spike #582607 |
migrate-to-uv is a project-migration tool, not a resolution primitive. pip-compile produces deterministic, platform-resolved output with no side effects.
2. pyproject.toml (non-Poetry): pip-compile vs uv lock
Decision: pip-compile
pip-compile |
uv lock |
|
|---|---|---|
| Output | pip-compile txt | uv.lock |
| Parser | pipcompile (existing) | uv (existing) |
Requires valid [project] table |
No | Yes |
| uv.lock parser marker issue | Not affected | Affected |
Works on pyproject.toml without [project] |
Yes | No (silent failure or error) |
3. Python 3.12 pip-compile image
Decision: ubi9/python-312-minimal + pip-tools 7.5.3 (pip-compile)
4b. pip-compile vs uv pip compile — FIPS compliance
Decision: pip-compile (pip-tools)
uv bundles its own TLS stack (rustls + aws-lc-rs) and bypasses system OpenSSL entirely, making it incompatible with FIPS 140-2/140-3 requirements. GitLab's FIPS/FedRAMP posture (Dedicated for Government) requires all network I/O to flow through the system OpenSSL FIPS provider.
pip-compile (pip-tools) is pure Python. All network I/O flows through Python ssl → system OpenSSL → FIPS provider on UBI9/RHEL9. No bundled crypto, no Rust.
Spike results: pip-compile has identical pass/fail outcomes to uv across all tested projects (11/14 pass, 3/14 fail, same root cause). See spike analysis: #593859 (note_3214525429 reply).
4. uv.lock parser fix
Not a blocker. All current Python paths produce pip-compile output; the parser limitation does not affect this implementation. Tracked as a separate follow-up.
Resolved considerations
- Partial lockfile presence in monorepos: resolution is skipped per-directory when a lockfile is already present.
requirements.txtlockfile detection: an existing pip-compile or uv lockfile inrequirements.txtformat is detected; resolution is skipped.-c constraints.txt: detected in preprocessing; passed as--constrainttopip-compile.- Local/editable installs: stripped before resolution; a warning is emitted.
DS_INCLUDE_DEV_DEPENDENCIES: implemented forpyproject.toml([dependency-groups] dev);requirements.txthas no dev concept;setup.py/setup.cfgdev extras are out of scope.
Testing plan
Manifest types
| Test | What it verifies |
|---|---|
| requirements txt happy path | A requirements.txt with loose version constraints is compiled to a fully pinned pip-compile output; all transitive dependencies appear in the SBOM. |
| requirements in file | An unpinned .in file is compiled to a pinned output; the resulting SBOM contains resolved transitive deps not present in the source file. |
| setup py | A setup.py with an install_requires list is resolved via pip-compile and produces a correct SBOM. |
| setup cfg | A setup.cfg with [options] install_requires is resolved via pip-compile; this represents new coverage not present in Gemnasium. |
| pyproject toml PEP 621 | A PEP 621 pyproject.toml without [tool.poetry] is resolved via pip-compile and produces a correct SBOM. |
| pyproject toml Poetry | A pyproject.toml with [tool.poetry] is detected and resolved via poetry lock && poetry export; the resulting pip-compile output is scanned. |
| uv lock already committed | A pre-committed uv.lock causes the resolution job to skip; the DS job scans the existing lockfile directly without re-resolving. |
| poetry lock already committed | A pre-committed poetry.lock causes the resolution job to skip; this exercises a distinct skip path from the uv.lock case. |
| requirements txt already pinned | A requirements.txt that is already a fully pinned pip-compile lockfile is detected by the analyzer and scanned directly without re-resolution. |
| pyproject toml no project table | A pyproject.toml with no [project] and no [tool.poetry] (e.g. build-system config only) is skipped with a logged warning and not passed to pip-compile. |
| setup py with dynamic requires | A setup.py where install_requires reads from a file at runtime cannot be resolved statically; the job either fails explicitly with a clear message or falls back to a co-located requirements.txt if one is present. |
Monorepo and multi-manifest
| Test | What it verifies |
|---|---|
| multiple requirements files at different depths | Multiple requirements.txt files in different subdirectories are each resolved independently; each produces its own SBOM. |
| Python and another language in the same repo | A repo containing both a requirements.txt and a non-Python manifest (e.g. pom.xml) runs Python resolution and language-specific scanning independently without interference. |
| one manifest fails and the rest succeed | One manifest in a monorepo triggers a resolution failure (e.g. a missing wheel); the job continues and the remaining manifests are resolved and scanned successfully. |
Configuration
| Test | What it verifies |
|---|---|
| private PyPI registry with credentials | PIP_INDEX_URL points to a GitLab-hosted PyPI registry authenticated via CI_JOB_TOKEN; resolution succeeds. A second step omits credentials and confirms the expected auth failure. |
| custom CA certificate bundle | PIP_CERT is mapped to SSL_CERT_FILE and a self-signed registry is reachable; a second step omits the cert and confirms the TLS failure. |
| subdirectory excluded by DS_EXCLUDED_PATHS | A manifest inside a path matching DS_EXCLUDED_PATHS is not resolved and does not appear in the SBOM. |
| manifest beyond DS_MAX_DEPTH not resolved | A requirements.txt nested deeper than DS_MAX_DEPTH is skipped; a shallower sibling in the same repo is resolved normally. |
| dev dependencies included and excluded | DS_INCLUDE_DEV_DEPENDENCIES=true causes optional dependency groups to appear in the SBOM; false excludes them. Tested on a pyproject.toml with [dependency-groups]. |
| resolution image overridden via input | Setting python_resolution_image to a non-default image causes the resolution job to run with that image instead of the default. |
| resolution disabled via template input | Setting enable_dependency_resolution: false removes the resolution job from the pipeline entirely. |
| resolution disabled via CI variable | Setting the equivalent CI variable causes the resolution job to be skipped at runtime. |
| pip environment variable passthrough | PIP_INDEX_URL is set in the project's CI config; resolution succeeds using it, confirming that PIP_* env vars are passed through by the service and script generation layer. |
Requirements.txt edge cases
| Test | What it verifies |
|---|---|
| requirements txt with a constraints file | A requirements.txt that references a constraints.txt via -c is compiled with the constraint passed as --constraint to pip-compile; the resolved versions respect the upper bounds in the constraint file. |
| requirements txt with includes | A requirements.txt that pulls in another file via -r produces a resolved output containing packages from both files. |
| requirements txt with an editable install entry | A requirements.txt containing a -e ./local-package line has that entry skipped with a warning; all other packages in the file are resolved and appear in the SBOM. |