ansys / ansys/pydynamicreporting

Add deterministic source identity to development builds

Open
#593 0 comments 0 reactions 1 assignee Claimed by @viseshrp View on GitHub
enhancement
Dominant language
Python
Stars
12
Forks
5
Avg merge
3d 14h
Merged PRs (30d)
9

Description

### 📝 Description of the feature

Development-build versions should have deterministic source identity. At present, two builds from the same clean Git commit can receive different distribution versions because the version is derived from the wall clock at build time rather than from the source revision.

The current configuration is:

```toml
[build-system]
requires = ["hatchling", "hatch-timestamp-version"]
build-backend = "hatchling.build"

[tool.hatch.version]
path = "src/ansys/dynamicreporting/core/_version.py"
source = "vcs-dev-timestamp"
validate-bump = true

[tool.hatch.version.raw-options]
local_scheme = "no-local-version"
timestamp_format = "long"
```

`hatch-timestamp-version` replaces the SCM-generated `.devN` component with `datetime.now(timezone.utc)`. The resulting version identifies the time when a particular artifact was built, not the source used to build it.

This has been observed with commit `e53d58d373588e259cde1a7bbbe44e1e750ed0d2`, which produced both of these normalized versions in separate builds:

```text
1.0.0.dev20260803154115
1.0.0.dev20260803154126
```

The commit was identical; only the build time changed.

This makes the version string unsuitable as stable source provenance and creates avoidable churn in dependency locks, wheel caches, SBOMs, diagnostics, and downstream reproducibility checks. A user comparing two environments cannot tell from `ansys-dynamicreporting-core.__version__` whether the source differs or only the build time differs.

Desired behavior for a clean checkout:

- Rebuilding the same commit produces the same normalized distribution version.
- A tagged release continues to produce its release version.
- Development versions remain PEP 440 compliant and sort after the preceding release as intended by the project.
- Dirty-tree behavior is explicit and covered by tests; release jobs should reject dirty trees.
- The source commit remains recoverable from build provenance even when a registry strips direct-URL metadata.

This is a packaging reproducibility improvement, not a request to publish a new PyDynamicReporting release.

### 💡 Steps for implementing the feature

#### Recommended implementation: use SCM-derived development versions

Replace the wall-clock version source with the standard `hatch-vcs` source so the development component is derived from repository state. The exact scheme should be chosen to preserve the project's current release ordering, but the minimal configuration is:

```toml
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[tool.hatch.version]
source = "vcs"

[tool.hatch.version.raw-options]
version_scheme = "no-guess-dev"
local_scheme = "no-local-version"
```

Because `src/ansys/dynamicreporting/core/_version.py` already reads the installed distribution version through `importlib.metadata`, a VCS build hook is not required merely to expose `__version__` at runtime.

If the project requires the existing `YYYYMMDDHHMMSS` shape for development uploads, extend `hatch-timestamp-version` with a deterministic source mode and configure PyDynamicReporting to use the commit timestamp instead of `datetime.now()`:

```toml
[tool.hatch.version]
source = "vcs-dev-timestamp"

[tool.hatch.version.raw-options]
local_scheme = "no-local-version"
timestamp_format = "long"
timestamp_source = "commit"
```

Conceptually, that mode should derive the timestamp from the resolved SCM revision:

```python
def deterministic_timestamp(commit: Commit, timestamp_format: str) -> str:
return commit.committer_datetime.astimezone(timezone.utc).strftime(timestamp_format)
```

The build clock should remain separate build provenance rather than part of source-version identity.

#### Add a regression test

Build the same clean checkout twice in isolated output directories and compare the wheel metadata versions, not only the filenames:

```bash
tmp_dir="$(mktemp -d)"
uv build --wheel --out-dir "$tmp_dir/first"
uv build --wheel --out-dir "$tmp_dir/second"
python - "$tmp_dir" <<'PY'
from email.parser import BytesParser
from pathlib import Path
from zipfile import ZipFile
import sys

def wheel_version(wheel: Path) -> str:
with ZipFile(wheel) as archive:
metadata_name = next(
name for name in archive.namelist() if name.endswith(".dist-info/METADATA")
)
metadata = BytesParser().parsebytes(archive.read(metadata_name))
return metadata["Version"]

root = Path(sys.argv[1])
first = wheel_version(next((root / "first").glob("*.whl")))
second = wheel_version(next((root / "second").glob("*.whl")))
assert first == second, (first, second)
print(first)
PY
```

Also cover:

1. an exact release tag;
2. a clean development commit;
3. a dirty checkout, which should fail in the release workflow or carry an explicitly documented non-publishable identity;
4. an sdist rebuilt into a wheel, which should preserve the sdist's version;
5. two builds performed in different time zones, which must still agree.

#### Preserve explicit commit provenance

In addition to a deterministic version, record the full source revision in generated build metadata or a small generated module so installed registry wheels retain their source identity. For example:

```python
__version__ = "1.0.0.dev42"
__commit__ = "e53d58d373588e259cde1a7bbbe44e1e750ed0d2"
```

The value must be generated during the build and must not require a `.git` directory at runtime.

#### Acceptance criteria

- Two wheels built from the same clean commit have the same `Version` metadata.
- The version is derived from SCM state, not the current wall clock.
- Release-tag behavior remains unchanged.
- The full commit SHA is available as installed provenance.
- The behavior and dirty-tree policy are documented for maintainers.
- CI contains a regression check that would fail under the current wall-clock implementation.

### 🔗 Useful links and references

- [Current PyDynamicReporting version configuration](https://github.com/ansys/pydynamicreporting/blob/d98b45adad663d015d4e17ad93dcd3749f2f3d81/pyproject.toml#L101-L112)
- [`hatch-vcs` version source documentation](https://github.com/ofek/hatch-vcs#version-source)
- [`setuptools-scm` configuration documentation](https://setuptools-scm.readthedocs.io/en/latest/config/)

An open-and-closed issue audit found no existing PyDynamicReporting issue covering deterministic development versions or stable source identity.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.