Skip to content

feat(manifest)!: record TimeNet git provenance in each written dataset - #684

Merged
karhu merged 19 commits into
mainfrom
feature/build-env-git-sha
Nov 10, 2026
Merged

karhu merged 19 commits into
mainfrom
feature/build-env-git-sha

Conversation

@karhu

@karhu karhu commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

A local checkout reported the same package version for every commit,
so two datasets written from different code looked the same.

  • build_env.git records the commit of each installed TimeNet package,
    and whether its directory had local changes.
  • An editable install asks git, a git URL install reads
    direct_url.json, and a wheel reads the _build_info.json that
    scripts/build_dists.py writes into it.
  • The writer reads the commit before it writes any data.

BREAKING CHANGE: A TimeNet install without git provenance fails every
dataset build and write with TimeNetBuildError.

BREAKING CHANGE: build_env with all its fields is required, so older
manifests do not load, and older clients reject new ones.

BREAKING CHANGE: BuildEnvironment is a model, not a TypedDict, and
timenet.provenance.build_env() is gone. Use BuildEnvironment.capture().

@karhu
karhu added this pull request to stack #686 November 5, 2026 14:47
@karhu
karhu force-pushed the feature/build-env-git-sha branch from cf9bd6b to d297a14 Compare November 5, 2026 15:56
Comment thread AGENTS.md Resolved
Comment thread scripts/build_dists.py Resolved
Comment thread scripts/build_dists.py Resolved
@karhu
karhu marked this pull request as ready for review November 6, 2026 15:25
@karhu
karhu requested review from TimoStoff and removed request for TimoStoff November 6, 2026 15:25

@RealLast RealLast left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Would prefer something that is more declarative with types that own the interfaces and implemetations : )

@karhu
karhu force-pushed the feature/build-env-git-sha branch from 39272a7 to c9999c2 Compare November 9, 2026 09:36
@karhu

karhu commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

@RealLast Thanks for the review! Addressed your feedback. Please take another look!

@karhu

karhu commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

@RealLast Addressed the second round of feedback which we discussed on Slack. Please take another look at the PR stack!

@RealLast RealLast left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank youu! Much cleaner, really appreciate it. Have some final nits on clarity and ambiguity, but feel free to address them as you go : )

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings marked 🟡 are optional suggestions and need no follow-up push.

karhu added 17 commits November 10, 2026 19:27
A manifest now records which TimeNet code built a release. Before, a
local checkout reported the same package version for every commit, so
two builds from different code looked the same.

`build_env.git` maps each installed TimeNet distribution to a
`GitSource` with `sha` and `local_changes`. A local checkout asks git,
and `local_changes` covers only the package directory, untracked files
included. A wheel reads the commit from `_build_info.json`.
`scripts/write_build_info.py` writes that file before `uv build`, and
`make build` and the publish workflow run the script. A distribution
with no known commit is absent from the map.

`local_source_path` moves from the connector builder to
`timenet.provenance`, so both use the same function.
`test_a_built_version_starts_no_isolated_child` now stores its dataset
before it blocks subprocesses, because a manifest write runs git.

A reader without this change rejects a new manifest, because the
manifest forbids unknown keys. This change lands before 1.0, so
`timef_format_version` stays at 1.

Part of TNET-165
Review findings on the first version:

- A non-editable path install recorded the live checkout, not the
  installed copy. Only an editable install now asks git. Any other
  install reads the `_build_info.json` in the package.
- A git install (`pip install git+https://...`) now records the commit
  in its `direct_url.json`.
- `make build` left `_build_info.json` in the source tree, so a later
  plain `uv build` shipped an old commit. `scripts/build_dists.py`
  replaces `scripts/write_build_info.py`. It writes the file, runs
  `uv build`, and removes the file again, also on failure.
- The publish workflow builds through the script. Its smoke step now
  makes sure that each artifact reports `$GITHUB_SHA` with no local
  changes.
- git runs with `--no-optional-locks`, so a build never takes the index
  lock, and with a timeout.
- A lookup that fails keeps its reason, so a later check can report it.
  A malformed `direct_url.json` or `_build_info.json`, or one with
  unknown keys, raises `TimeNetBuildError`.

Tests: `packages/conftest.py` reports a clean commit to every test
unless the test has the `real_git` marker. Test results no longer depend
on the state of the checkout, and the core suite runs about 40% faster.
The provenance tests go through `git_sources()` with fake installed
distributions, and switch off the user's git configuration.

Part of TNET-165
Drop the test that ran real git against this repository. Its one
check, that build_env() carries the git block, moves into the
editable-install test. Drop the invalid-sha case, which the manifest
tests already cover, and keep the extra-key case, the only test of
the forbid rule on GitSource. Remove a setup line that no assertion
needed.

Part of TNET-165
- Allow only 40-digit SHA-1 commit ids, because TimeNet uses SHA-1.
- Shorten the docstrings of the provenance helpers and of
  `scripts/build_dists.py` to one line. Ruff now exempts a one-line
  docstring from the Returns and Raises sections
  (`ignore-one-line-docstrings`).
- Rename `_validated` to `_validate_recorded_commit`.
- `scripts/build_dists.py` registers each record for cleanup before it
  writes it, so a write that fails halfway leaves nothing behind. It
  compares the commit and `git status` of each package before and after
  `uv build`, and fails if a file changed during the build.
- Simplify the build bullet in AGENTS.md.

Part of TNET-165
The record sits at a known place, <import package>/_build_info.json,
so look it up there instead of walking the distribution's RECORD.

Part of TNET-165
Review feedback asked for types that own the provenance logic, instead
of module functions and raw dicts.

- `GitSource` is a strict model with `sha: GitSHA` and
  `contains_local_changes`, and it loads a wheel's build info itself.
  `GitSourceMap` names the `build_env.git` block. The manifest key
  `local_changes` is now `contains_local_changes`.
- `TimeNetDistribution` owns one installed distribution: it lists the
  installed ones, finds the on-disk project directory, and reads the
  commit of the code it runs. The editable, git-install and wheel cases
  are its private methods.
- `GitSnapshot` holds the commit of every installed distribution, read
  at one time. `GitSnapshot.read()` is the only call that runs git, and
  `recorded_sources()` gives the block a manifest records.
- A private model validates PEP 610 `direct_url.json`, so the lookup
  reads named fields instead of dict keys.

`builder/env.py`, the test stub, the publish workflow and
`scripts/build_dists.py` use the new names.

Part of TNET-165
PEP 610 lets a directory install name the repository in url and the
project in subdirectory. The lookup ignored subdirectory, so it asked
git about the whole repository and builder/env.py installed the
repository root. Both now use the package directory.

Part of TNET-165
It only renamed TimeNetDistribution.NAMES. The loop in _base_args reads
the names directly, and its comment keeps the reason for the pin.

Part of TNET-165
…nown

Split the commit lookup into two layers. InstalledDistribution picks
the InstallSource of one package: an editable checkout, a git install,
or a built package. Each install source reads the commit in its own
place. PackageSnapshot, GroupSnapshot and TimeNetSnapshot are typed
views of the commits that a manifest records in build_env.git.
TimeNetSnapshot is now the one list of the TimeNet distributions. The
build environment and the test stub use it.

An unknown commit now stops the build, also for a local registry. The
error gives the reason: git is not on PATH, git failed, or a built
package has no _build_info.json. UnknownGitSource is gone.

BuildEnvironment is now a model with a capture constructor. The
manifest requires it, because each manifest that the writer produces
records it.

Part of TNET-165
TimeNetSnapshot now derives directly from Snapshot. It owns read,
from_source_map and to_source_map. The GroupSnapshot layer is gone.

A validator makes each PackageSnapshot field hold the commit of its own
distribution. The timenet field must hold timenet, and the
timenet-connectors field must hold timenet-connectors.

BuiltPackage now reads its _build_info.json file. GitSource only
validates the JSON, so load_from_build_info is gone.

The pytest marker real_git is now real_provenance, because it turns off
the stub for every install type, wheels included. The autouse fixture is
now _stub_installed_sources.

Part of TNET-165
The snapshot and manifest layer now says package: PackageName,
PackageSnapshot.package_name and TimeNetSnapshot.package_names().
"Distribution" also reads as a data distribution in TimeNet, and the
classes are already called PackageSnapshot. InstalledDistribution and
InstallSource keep the packaging term, because they wrap
importlib.metadata.Distribution.

Part of TNET-165
The field holds a package name, not an importlib Distribution, so it
now reads like every other name in the module. Only
InstalledDistribution keeps the packaging term, because it wraps
importlib.metadata.Distribution.

Part of TNET-165
The docs for build_env.git and one test name still used "distribution"
after the rename to "package". They now say "package". The
TimeNetSnapshot docstring loses its second paragraph, because the
one-line summary is enough.
…uild record

An editable install read HEAD before git status. A commit between the
two reads then looked like the old commit without local changes, so a
build whose code changed could still pass the upload check. The lookup
now reads the status first, so such a commit shows as a new commit.

scripts/build_dists.py writes and removes the records of both
packages. Two builds at the same time in one checkout could remove each
other's records, and git status cannot see that, because git ignores
the files. The script now makes sure that each record still holds what
it wrote, and fails otherwise.
…heckout

Review follow-ups for the provenance types:

- EditableCheckout is now EditableInstall, and BuiltPackage is now
  WheelInstall, to match GitInstall. Their docstrings say why each
  reads its commit from a different place.
- A private _GitCheckout class runs git. It owns the PATH lookup, the
  timeout and the error handling, and replaces _run_git.
- The docstrings of InstallSource, PackageSnapshot and
  InstalledDistribution.from_name give examples and name the cases.
Main added tests that build a Manifest without build_env, call
LocalRegistry.store(), or patch env.BASE_DISTRIBUTIONS. This branch makes
build_env required and removes the other two names. The tests now pass a
full build_env, call put_dataset(), and patch
TimeNetSnapshot.package_names.

Part of TNET-165
The writer read the TimeNet commit only when it wrote the manifest,
after every data file. An unknown commit then failed after the whole
write. A commit or checkout during the write also changed the record.
The writer now reads the commit when it starts, and the manifest records
that commit.

Part of TNET-165
@karhu
karhu force-pushed the feature/build-env-git-sha branch from f1bc8f6 to db88bfc Compare November 10, 2026 18:58
build_dists.py removed the _build_info.json files only in a finally. A
kill or a closed terminal ended the process before the finally ran. The
files stayed, and git ignores them, so a later plain uv build packed a
record of an old commit. SIGTERM and SIGHUP now raise SystemExit, so the
finally removes the files. Only SIGKILL still leaves them.

Part of TNET-165
@karhu karhu changed the title feat(manifest): record the git commit of each TimeNet distribution feat(manifest)!: record TimeNet git provenance in each written dataset Nov 10, 2026
@karhu
karhu merged commit fb96239 into main Nov 10, 2026
4 checks passed
@karhu
karhu deleted the feature/build-env-git-sha branch November 10, 2026 19:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants