Skip to content

Commit dce996f

Browse files
committed
feat(build): compute ddtrace version dynamically from git state
Replace setuptools_scm's abandoned release-branch-semver scheme (broken by detached-HEAD branch detection) and the manual version-bump PR process with scripts/compute_version.py: a dependency-free script that derives PEP 440 versions from git tags/branches, using explicit CI-provided ref info (never `git rev-parse --abbrev-ref HEAD` alone) and ancestry-independent global tag comparison for main's "latest shipped release" (release branches never merge back, so ancestry-based lookup misses sibling-branch releases). Validated so far: - Unit tests against synthetic git repos (tests/scripts/test_compute_version.py), covering all 4 version rules, detached HEAD, GH/GitLab ref resolution, and the main-after-sibling-branch-release regression case. - Historical replay against this repo's real git history (via disposable worktrees) for rules 1-3: exact tag builds, release-branch patch/dev versions, and freshly-cut release branches all matched expected output. Still outstanding before this can be trusted in production CI: - Cross-CI determinism: confirm GitHub Actions and GitLab CI resolve the same branch name and produce an identical version string for the same commit. - cibuildwheel's manylinux/musllinux containers: verify `git` is available (needed since setup.py recomputes the version inside those containers too). - sdist-then-wheel round trip: confirm ddtrace/_version_frozen.py, written when git is available, is correctly read back when a wheel is built from an sdist with no .git present. - GIT_DEPTH: "0" on the GitLab package/build jobs: confirm against a live pipeline that full tag history is actually fetched (rule 4's global tag scan needs it). Pre-commit hooks (mypy, bandit) skipped for this commit with explicit sign-off: both are triggered by pre-existing, unrelated issues in scripts/gen_gitlab_config.py's import graph (tests/suitespec.py, scripts/needs_testrun.py) that predate this change and are outside mypy.ini's official `files = ddtrace, docs` scope, plus expected Low-severity bandit findings on this script's inherent subprocess/assert usage.
1 parent 1e220c0 commit dce996f

13 files changed

Lines changed: 759 additions & 165 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,6 @@ ddtrace/profiling/collector/build-test/
248248
# file created when running scripts/lint
249249
uv.lock
250250
.riot/*.installed
251+
252+
# generated by scripts/compute_version.py at build time
253+
ddtrace/_version_frozen.py

.gitlab/package.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ variables:
3232
.build_base:
3333
stage: package
3434
variables:
35+
GIT_DEPTH: "0"
3536
CMAKE_BUILD_PARALLEL_LEVEL: "12"
3637
CARGO_BUILD_JOBS: "12"
3738
CMAKE_ARGS: "-DNATIVE_TESTING=OFF"
@@ -283,6 +284,7 @@ variables:
283284
- PYTHON_VERSION: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
284285
WINDOWS_ARCH: ["amd64", "x86"]
285286
variables:
287+
GIT_DEPTH: "0"
286288
WINDOWS_BUILD_IMAGE: "registry.ddbuild.io/images/mirror/dd-trace-py/windows-build:d4d18a67d43d400d3ecc6bda777a5e233a24f434@sha256:58c3e28646ddf8a1021a079a380c8840e51f0b88c1489a522beee6ec99413019"
287289
script:
288290
- bash .gitlab/scripts/build-wheel-windows.sh
@@ -385,13 +387,15 @@ download_dependency_wheels:
385387
script:
386388
- .gitlab/scripts/upload-debug-symbols-to-backend.sh
387389

388-
# Extract package version from pyproject.toml and save to dotenv artifact
390+
# Compute package version from git state and save to dotenv artifact
389391
"package version":
390392
image: registry.ddbuild.io/images/mirror/python:3.14.0
391393
tags: [ "arch:amd64" ]
392394
stage: package
395+
variables:
396+
GIT_DEPTH: "0"
393397
script:
394-
- PACKAGE_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
398+
- PACKAGE_VERSION=$(scripts/compute_version.py)
395399
- echo "PACKAGE_VERSION=${PACKAGE_VERSION}" | tee -a package_version.env
396400
# Validate commit tag matches version if tag is set
397401
- |

docs/contributing-release.rst

Lines changed: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ High-Level Steps
2424
These are the high-level steps involved in all ddtrace releases:
2525

2626
1. Set / Validate branch structure
27-
2. Set / Validate version strings on branches
28-
3. Make a GitHub release
27+
2. Make a GitHub release
28+
29+
There is no version-string pull request in any of these steps: the version is computed automatically
30+
from git state at build time by ``scripts/compute_version.py``. See "Version String" below. The only
31+
manual action that determines a release's version is the exact tag created by the GitHub release
32+
itself.
2933

3034
Here are the specifics of those steps for all of the different types of releases. See even more detail in the sections that follow.
3135

@@ -36,14 +40,9 @@ Patch Release
3640

3741
* A branch named after the relevant minor version line (``X.Y``) is expected to already exist. This is the "release branch".
3842

39-
2. Set / Validate version strings on branches
40-
41-
* The version string on the release branch is currently set to a patch version on the relevant minor release line (for example, ``1.2.3``)
42-
* Pull request a change to the version string on the release branch that sets it to the patch version you're releasing.
43-
44-
3. Make a GitHub release
43+
2. Make a GitHub release
4544

46-
* Target: the merged commit from the version string pull request above
45+
* Target: the tip of the release branch
4746
* "Set as latest" may be applicable
4847
* "Pre-release" unchecked
4948

@@ -54,15 +53,9 @@ Minor or Major Release Candidate
5453

5554
* The main branch exists
5655

57-
2. Set / Validate version strings on branches
58-
59-
* main's version string is currently a release candidate ``rc`` version.
60-
* Pull request a change to the version string on the main branch that sets it to the next release candidate on the current minor release line.
61-
In practice this means incrementing the ``rcX`` number by one.
62-
63-
3. Make a GitHub release
56+
2. Make a GitHub release
6457

65-
* Target: the merged commit from the version string pull request above
58+
* Target: the tip of the main branch
6659
* "Pre-release" checked
6760

6861
Minor or Major Release
@@ -80,43 +73,54 @@ Minor or Major Release
8073
$ git merge main -Xtheirs # this keeps the tags intact so that reno will work properly
8174
$ git push -u origin X.Y
8275
83-
2. Set / Validate version strings on branches
76+
2. Make a GitHub release
8477

85-
* main's version string is currently a release candidate ``rc`` version.
86-
* Pull request a change to the version string on the main branch that sets it to ``rc1`` on the next release line.
87-
For example: in step 1 we created branch ``1.3``, and we will change the version string on main to ``1.4.0rc1``.
88-
* The version string on the release branch is currently set to a patch version on the previous minor release line.
89-
For example: in step 1 we created branch ``1.3``, and it has a version string of ``1.2.9``
90-
* Pull request a change to the version string on the release branch that sets it to the minor version you're releasing.
91-
92-
3. Make a GitHub release
93-
94-
* Target: the merged commit on the release branch from the version string pull request above
78+
* Target: the tip of the release branch created above
9579
* "Set as latest" checked
9680
* "Pre-release" unchecked
9781

9882
Version String
9983
--------------
10084

101-
The ``[project.version]`` attribute in ``pyproject.toml`` is the source of truth about the version of ddtrace.
102-
If you inspect a ddtrace wheel via directory exploration or ``print(ddtrace.__version__)``, the version you find will match the
103-
version in the wheel's name.
104-
105-
If you check the attribute on a checkout of the ddtrace main branch, you will find it set to the ``rcX`` release candidate version
106-
that comes next on the currently under-development minor release line. For example:
85+
ddtrace's version is not a literal anywhere in the source tree. It's computed at build time by
86+
``scripts/compute_version.py`` from git state, and exposed as ``ddtrace.__version__`` via the
87+
installed package's metadata. If you inspect a ddtrace wheel via directory exploration or
88+
``print(ddtrace.__version__)``, the version you find will match the version in the wheel's name.
89+
90+
The rules, applied in order:
91+
92+
1. If the commit being built is exactly tagged ``vX.Y.Z`` or ``vX.Y.ZrcN``, the version is that tag,
93+
verbatim (``v`` stripped). This is the only way a final or ``rc`` version is ever produced — always
94+
by a human creating that exact tag, e.g. as part of making a GitHub release.
95+
2. Otherwise, on a release branch (named ``X.Y``): the next patch version after the latest ``vX.Y.Z``
96+
tag on that branch, suffixed ``.devN`` (``N`` = commits since that tag), or ``X.Y.0.devN`` if the
97+
branch was just cut and has no tag yet.
98+
3. Otherwise (``main``, feature branches, PRs): the next minor version after the latest final release
99+
anywhere in the repo, suffixed ``.devN``.
100+
101+
For example:
102+
103+
* Latest release: ``4.4.0`` -> version on main: ``4.5.0.devN``
104+
* Latest release: ``4.3.0rc4`` -> version on main: ``4.4.0.devN`` (rc's don't count as "the latest
105+
final release" — only an actual ``vX.Y.Z`` tag does)
106+
* Latest release: ``4.4.0`` -> version on the ``4.4`` branch: ``4.4.1.devN``
107+
* Latest release: ``4.2.1`` -> version on the freshly-cut ``4.3`` branch, no tag yet: ``4.3.0.devN``
108+
109+
Branch identity for rules 2/3 comes from CI-provided ref info (GitHub Actions'
110+
``GITHUB_HEAD_REF``/``GITHUB_REF_NAME``, GitLab's ``CI_COMMIT_BRANCH``/``CI_COMMIT_REF_NAME``), not
111+
from ``git rev-parse --abbrev-ref HEAD`` — that returns the literal string ``"HEAD"`` under a detached
112+
checkout, which is the norm for CI, and silently misclassifies release branches as "main-like" if
113+
relied upon. To compute what a particular branch's version would be from a local, possibly-detached
114+
checkout (for example, testing a release branch cut before pushing it), set
115+
``_DD_TRACE_BUILD_VERSION=X.Y`` in the environment to override branch detection:
107116

108-
* Latest release: ``4.4.0`` -> version string on main: ``4.5.0rc1``
109-
* Latest release: ``4.3.0rc4`` -> version string on main: ``4.3.0rc5``
110-
* Latest release: ``4.2.1`` -> version string on main: ``4.3.0rc1``
111-
112-
If you check the attribute on a checkout of a ddtrace release branch with a name like ``X.Y``, you will find it set to the patch release version
113-
that comes next on that minor release line. For example:
117+
.. code-block:: bash
114118
115-
* Latest release: ``4.4.0`` -> version string on 4.4 branch: ``4.4.1dev``
116-
* Latest release: ``4.3.0rc4`` -> version string on 4.3 branch: not applicable, branch doesn't exist
117-
* Latest release: ``4.2.1`` -> version string on 4.2: ``4.2.2dev``
119+
$ _DD_TRACE_BUILD_VERSION=4.13 scripts/compute_version.py
118120
119-
If ever you discover that one of these guarantees is not upheld, please open a pull request adjusting the version string accordingly.
121+
If you ever find a computed version doesn't match one of the rules above, that's a bug in
122+
``scripts/compute_version.py`` (see ``tests/scripts/test_compute_version.py``) — not something to work
123+
around by hand-editing a version string, since there is no longer one to edit.
120124

121125

122126
Pre-Release Performance Gates

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
99

1010
[project]
1111
name = "ddtrace"
12-
version = "4.15.0rc1"
12+
dynamic = ["version"]
1313
description = "Datadog APM client library"
1414
readme = "README.md"
1515
license = { text = "LICENSE.BSD3" }

scripts/check_suitespec_coverage.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
# Ignore any embedded documentation
2626
IGNORE_PATTERNS.add("**/*.md")
2727

28+
# Standalone scripts/*.py tests run directly (e.g. via scripts/gen_gitlab_config.py's own check
29+
# list), not through a riot-managed suite, so they have no suite pattern to match against.
30+
IGNORE_PATTERNS.add("tests/scripts/*.py")
31+
2832

2933
def owners(path: str) -> str:
3034
return ", ".join(CODEOWNERS.of(path))

0 commit comments

Comments
 (0)