fix(code): bake release commit into dcode doctor (#4225)

`dcode doctor` now reports the exact commit a released wheel was built
from (once the release workflow sets `DEEPAGENTS_CODE_BUILD_COMMIT`).

---

Released wheels and sdists ship no `.git` metadata, so `dcode doctor`
could only resolve a `Commit hash` for *editable* installs that probe a
live working tree — every normal pip/pipx/wheel install correctly showed
`unknown`. This stamps the release commit into the package at build time
so installed wheels report the real commit.

A hatchling build hook (`hatch_build.py`) writes a generated
`_build_info.py` with `BUILD_COMMIT` **only** when
`DEEPAGENTS_CODE_BUILD_COMMIT` is set (CI release builds); it validates
the value is a hex SHA, registers the file as a build artifact, and
removes it afterward so the working tree stays clean. `doctor` now
prefers that stamped commit and falls back to the existing live `git
rev-parse` probe, so editable/dev installs keep reporting the live
working-tree commit (which reflects local changes).

Made by [Open
SWE](https://openswe.vercel.app/agents/f53414ff-5e6b-da73-65d1-9091c5d31a94)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Mason Daugherty
2026-06-24 17:11:21 -04:00
committed by GitHub
parent ad4c16a517
commit 6dc0246f5c
8 changed files with 359 additions and 2 deletions
+4
View File
@@ -282,6 +282,10 @@ jobs:
- name: Build project for distribution
run: uv build
working-directory: ${{ env.WORKING_DIR }}
env:
# Stamp the exact release commit into deepagents-code so `dcode doctor`
# reports it on installed wheels. Other packages ignore this env var.
DEEPAGENTS_CODE_BUILD_COMMIT: ${{ needs.setup.outputs.release-sha }}
- name: Upload build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+3
View File
@@ -251,3 +251,6 @@ CLAUDE.md
libs/cli/frontend/node_modules/
libs/cli/frontend/dist/
node_modules/
# Generated at build time by libs/code/hatch_build.py (stamps the release commit)
libs/code/deepagents_code/_build_info.py
+36 -2
View File
@@ -75,15 +75,49 @@ def _sdk_version() -> tuple[str, bool]:
return "unknown", False
def _build_commit() -> str | None:
"""Return the commit stamped into the package at build time, if present.
Released wheels carry a generated `_build_info.py` (see `hatch_build.py`);
editable and local installs do not, so this returns `None` for them.
"""
try:
from deepagents_code._build_info import ( # ty: ignore[unresolved-import] # generated at build time
BUILD_COMMIT,
)
except ImportError:
return None
except Exception: # a corrupt stamp must never crash `doctor`
logger.debug("Build-info module present but failed to import", exc_info=True)
return None
commit = (BUILD_COMMIT or "").strip()
return commit or None
def _commit_hash(path: str) -> str:
"""Return the short git commit hash for a source path, if available.
"""Return the short git commit hash for the install, if available.
Prefers the commit stamped into a released wheel at build time, but only for
non-editable installs: an editable install may carry a stale stamp from a
prior local build (the generated file is gitignored and survives a failed
build), so it always probes the live git working tree, which reflects local
changes.
Args:
path: Directory used as the git command working directory.
Returns:
The short commit hash, or `unknown` when git metadata cannot be read.
The short commit hash, or `unknown` when no commit can be determined.
"""
baked = _build_commit()
if baked:
from deepagents_code.config import _is_editable_install
# A baked commit only describes a built wheel; ignore it for editable
# installs so a stale stamp can't mask the live working-tree commit.
if not _is_editable_install():
return baked
import shutil
import subprocess # noqa: S404 # fixed-argv git metadata probe
from pathlib import Path
+67
View File
@@ -0,0 +1,67 @@
"""Hatchling build hook that stamps the release commit into the package.
When `DEEPAGENTS_CODE_BUILD_COMMIT` is set (CI release builds), this writes
`deepagents_code/_build_info.py` so `dcode doctor` can report the exact commit
a wheel was built from. Editable and local builds leave the env var unset, so
no file is generated and `dcode doctor` falls back to a live `git` probe of the
working tree.
"""
from __future__ import annotations
import os
import re
from pathlib import Path
from typing import Any
from hatchling.builders.hooks.plugin.interface import ( # ty: ignore[unresolved-import] # build-time-only dependency
BuildHookInterface,
)
_COMMIT_ENV = "DEEPAGENTS_CODE_BUILD_COMMIT"
_COMMIT_RE = re.compile(r"^[0-9a-fA-F]{7,40}$")
_TARGET = Path("deepagents_code") / "_build_info.py"
class CustomBuildHook(BuildHookInterface):
"""Stamps `_build_info.py` with the release commit when the env var is set."""
PLUGIN_NAME = "custom"
def initialize(self, version: str, build_data: dict[str, Any]) -> None: # noqa: ARG002 # `version` is part of the hatchling hook signature
"""Write the generated build-info module before the build collects files.
Raises:
ValueError: If the commit env var is set but not a hex git SHA.
"""
commit = os.environ.get(_COMMIT_ENV, "").strip()
if not commit:
return
if not _COMMIT_RE.match(commit):
msg = f"{_COMMIT_ENV} must be a hex git SHA, got: {commit!r}"
raise ValueError(msg)
short = commit[:7].lower()
target = Path(self.root) / _TARGET
target.write_text(
'"""Generated at build time. Do not edit or commit."""\n\n'
f'BUILD_COMMIT = "{short}"\n',
encoding="utf-8",
)
# `_TARGET` is gitignored, so hatchling excludes it from the build by
# default; registering it as an artifact force-includes it in the dist.
build_data.setdefault("artifacts", []).append(str(_TARGET))
def finalize(
self,
version: str, # noqa: ARG002 # part of the hatchling hook signature
build_data: dict[str, Any], # noqa: ARG002 # part of the hook signature
artifact_path: str, # noqa: ARG002 # part of the hook signature
) -> None:
"""Remove the generated module so the working tree stays clean.
Runs only when the stamp env var was set, mirroring `initialize`, so
editable and local builds (which never wrote the file) skip the unlink.
"""
if not os.environ.get(_COMMIT_ENV, "").strip():
return
(Path(self.root) / _TARGET).unlink(missing_ok=True)
+6
View File
@@ -139,6 +139,8 @@ Reddit = "https://www.reddit.com/r/LangChain/"
test = [
"textual-dev>=1.8.0,<2.0.0",
"build>=1.5.0,<2.0.0",
# Build backend; imported by tests that exercise the `hatch_build.py` hook.
"hatchling>=1.27.0,<2.0.0",
"ruff>=0.15.18,<1.0.0",
"ty>=0.0.52,<1.0.0",
"langchain-quickjs>=0.3.1,<0.4.0",
@@ -156,6 +158,10 @@ test = [
"twine>=6.2.0,<7.0.0",
]
[tool.hatch.build.hooks.custom]
# Stamps deepagents_code/_build_info.py with the release commit when
# DEEPAGENTS_CODE_BUILD_COMMIT is set (CI release builds). See hatch_build.py.
[tool.hatch.build.targets.wheel]
packages = ["deepagents_code"]
include = [
+58
View File
@@ -8,11 +8,13 @@ from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from rich.console import Console
from deepagents_code.doctor import (
DiagnosticItem,
DiagnosticSection,
_build_commit,
_commit_hash,
collect_sections,
run_doctor_command,
@@ -174,6 +176,7 @@ class TestCommitHash:
git.chmod(0o755)
with (
patch("deepagents_code.doctor._build_commit", return_value=None),
patch("shutil.which", return_value=str(git)),
patch(
"subprocess.run",
@@ -189,6 +192,7 @@ class TestCommitHash:
def test_missing_git_returns_unknown(self) -> None:
"""Missing Git should degrade to `unknown` without spawning a process."""
with (
patch("deepagents_code.doctor._build_commit", return_value=None),
patch("shutil.which", return_value=None),
patch("subprocess.run") as run,
):
@@ -196,6 +200,60 @@ class TestCommitHash:
run.assert_not_called()
def test_baked_commit_preferred_over_git(self) -> None:
"""A build-stamped commit wins for a wheel and skips the live git probe."""
with (
patch("deepagents_code.doctor._build_commit", return_value="deadbee"),
patch("deepagents_code.config._is_editable_install", return_value=False),
patch("shutil.which") as which,
patch("subprocess.run") as run,
):
assert _commit_hash("/tmp") == "deadbee"
which.assert_not_called()
run.assert_not_called()
def test_editable_install_ignores_baked_commit(self) -> None:
"""An editable install ignores a (possibly stale) stamp and probes git."""
with (
patch("deepagents_code.doctor._build_commit", return_value="deadbee"),
patch("deepagents_code.config._is_editable_install", return_value=True),
patch("shutil.which", return_value=None),
patch("subprocess.run") as run,
):
assert _commit_hash("/tmp") == "unknown"
run.assert_not_called()
def test_build_commit_missing_module(self) -> None:
"""No generated module (editable/dev install) yields `None`."""
with patch.dict(sys.modules, {"deepagents_code._build_info": None}):
assert _build_commit() is None
def test_build_commit_reads_stamped_value(self) -> None:
"""A generated module exposes its stamped commit."""
stub = SimpleNamespace(BUILD_COMMIT="abc1234")
with patch.dict(sys.modules, {"deepagents_code._build_info": stub}):
assert _build_commit() == "abc1234"
@pytest.mark.parametrize("value", ["", " ", None])
def test_build_commit_blank_value_is_none(self, value: str | None) -> None:
"""A present module with a blank stamp yields `None` (falls back to git)."""
stub = SimpleNamespace(BUILD_COMMIT=value)
with patch.dict(sys.modules, {"deepagents_code._build_info": stub}):
assert _build_commit() is None
def test_build_commit_corrupt_module_returns_none(self) -> None:
"""A present-but-corrupt stamp degrades to `None` instead of crashing."""
class _Corrupt:
def __getattr__(self, name: str) -> str:
msg = "corrupt stamp"
raise ValueError(msg)
with patch.dict(sys.modules, {"deepagents_code._build_info": _Corrupt()}):
assert _build_commit() is None
class TestRunDoctorCommand:
"""Tests for the text and JSON rendering paths."""
@@ -0,0 +1,159 @@
"""Unit tests for the `hatch_build.py` release-commit stamping hook.
`hatch_build.py` lives at the package root (not inside `deepagents_code`) and is
only on `sys.path` during a build, so it is loaded here directly from its file
path. The hook subclasses hatchling's `BuildHookInterface`, so these tests
require `hatchling` (declared in the `test` dependency group).
"""
import importlib.util
from pathlib import Path
from types import ModuleType
import pytest
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
_HATCH_BUILD_PATH = Path(__file__).resolve().parents[2] / "hatch_build.py"
def _load_hatch_build() -> ModuleType:
"""Load `hatch_build.py` from its on-disk path (it is not importable)."""
spec = importlib.util.spec_from_file_location("hatch_build", _HATCH_BUILD_PATH)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
_hatch_build = _load_hatch_build()
CustomBuildHook = _hatch_build.CustomBuildHook
_COMMIT_ENV = _hatch_build._COMMIT_ENV
def _make_hook(root: Path) -> BuildHookInterface:
"""Build a hook rooted at `root`.
The hook only reads `self.root`, so the remaining `BuildHookInterface`
constructor arguments (config, build config, metadata) are inert here.
"""
return CustomBuildHook(str(root), {}, None, None, str(root), "wheel")
def _stamp_path(root: Path) -> Path:
return root / "deepagents_code" / "_build_info.py"
class TestInitialize:
"""Tests for the `initialize` build hook (writes the stamp)."""
def test_env_unset_writes_nothing(self, tmp_path: Path, monkeypatch) -> None:
"""With the env var unset, no file is written and no artifact is added."""
monkeypatch.delenv(_COMMIT_ENV, raising=False)
(tmp_path / "deepagents_code").mkdir()
build_data: dict[str, object] = {}
_make_hook(tmp_path).initialize("1.0", build_data)
assert not _stamp_path(tmp_path).exists()
assert build_data == {}
def test_valid_sha_is_shortened_lowercased_and_registered(
self, tmp_path: Path, monkeypatch
) -> None:
"""A full uppercase SHA stamps a 7-char lowercase value and an artifact."""
monkeypatch.setenv(_COMMIT_ENV, "ABCDEF1234567890ABCDEF1234567890ABCDEF12")
(tmp_path / "deepagents_code").mkdir()
build_data: dict[str, object] = {}
_make_hook(tmp_path).initialize("1.0", build_data)
assert 'BUILD_COMMIT = "abcdef1"' in _stamp_path(tmp_path).read_text(
encoding="utf-8"
)
assert build_data["artifacts"] == ["deepagents_code/_build_info.py"]
@pytest.mark.parametrize(
"bad",
[
"main", # not hex
"g123abc", # non-hex char
"abcdef", # too short (< 7)
"0" * 41, # too long (> 40)
],
)
def test_invalid_sha_raises_and_writes_nothing(
self, tmp_path: Path, monkeypatch, bad: str
) -> None:
"""A set-but-malformed SHA fails the build loudly and writes no file."""
monkeypatch.setenv(_COMMIT_ENV, bad)
(tmp_path / "deepagents_code").mkdir()
with pytest.raises(ValueError, match=_COMMIT_ENV):
_make_hook(tmp_path).initialize("1.0", {})
assert not _stamp_path(tmp_path).exists()
@pytest.mark.parametrize("blank", ["", " "])
def test_blank_env_writes_nothing(
self, tmp_path: Path, monkeypatch, blank: str
) -> None:
"""A blank or whitespace-only env var is treated as unset."""
monkeypatch.setenv(_COMMIT_ENV, blank)
(tmp_path / "deepagents_code").mkdir()
build_data: dict[str, object] = {}
_make_hook(tmp_path).initialize("1.0", build_data)
assert not _stamp_path(tmp_path).exists()
assert build_data == {}
class TestFinalize:
"""Tests for the `finalize` build hook (cleans up the stamp)."""
def test_removes_stamp_when_env_set(self, tmp_path: Path, monkeypatch) -> None:
"""Cleanup deletes the generated file when the env var is set."""
monkeypatch.setenv(_COMMIT_ENV, "abc1234")
(tmp_path / "deepagents_code").mkdir()
stamp = _stamp_path(tmp_path)
stamp.write_text("x", encoding="utf-8")
_make_hook(tmp_path).finalize("1.0", {}, "/tmp/wheel") # not a real path
assert not stamp.exists()
def test_noop_when_env_unset(self, tmp_path: Path, monkeypatch) -> None:
"""Cleanup is gated on the env var, so an unrelated file is untouched."""
monkeypatch.delenv(_COMMIT_ENV, raising=False)
(tmp_path / "deepagents_code").mkdir()
stamp = _stamp_path(tmp_path)
stamp.write_text("x", encoding="utf-8")
_make_hook(tmp_path).finalize("1.0", {}, "/tmp/wheel") # not a real path
assert stamp.exists()
def test_missing_file_does_not_raise(self, tmp_path: Path, monkeypatch) -> None:
"""Cleanup tolerates an already-absent file (`missing_ok=True`)."""
monkeypatch.setenv(_COMMIT_ENV, "abc1234")
(tmp_path / "deepagents_code").mkdir()
_make_hook(tmp_path).finalize("1.0", {}, "/tmp/wheel") # not a real path
assert not _stamp_path(tmp_path).exists()
def test_initialize_then_finalize_leaves_tree_clean(
tmp_path: Path, monkeypatch
) -> None:
"""A full stamp/cleanup cycle writes the file then removes it."""
monkeypatch.setenv(_COMMIT_ENV, "deadbeef")
(tmp_path / "deepagents_code").mkdir()
hook = _make_hook(tmp_path)
hook.initialize("1.0", {})
assert _stamp_path(tmp_path).exists()
hook.finalize("1.0", {}, "/tmp/wheel") # not a real path
assert not _stamp_path(tmp_path).exists()
+26
View File
@@ -1225,6 +1225,7 @@ xai = [
[package.dev-dependencies]
test = [
{ name = "build" },
{ name = "hatchling" },
{ name = "langchain-quickjs" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
@@ -1309,6 +1310,7 @@ provides-extras = ["anthropic", "baseten", "bedrock", "cohere", "deepseek", "fir
[package.metadata.requires-dev]
test = [
{ name = "build", specifier = ">=1.5.0,<2.0.0" },
{ name = "hatchling", specifier = ">=1.27.0,<2.0.0" },
{ name = "langchain-quickjs", editable = "../partners/quickjs" },
{ name = "pytest", specifier = ">=9.1.1,<10.0.0" },
{ name = "pytest-asyncio", specifier = ">=1.4.0,<2.0.0" },
@@ -2068,6 +2070,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
]
[[package]]
name = "hatchling"
version = "1.30.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "pathspec" },
{ name = "pluggy" },
{ name = "trove-classifiers" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/4c/8717ccb844b4fa5a5ba6352e97d743ed24e9a22cf90b7c109c17030a46a1/hatchling-1.30.1.tar.gz", hash = "sha256:eee4fd45357f72ebb3d7a42e5d72cfb5e29ed426d79e8836288926c4258d5f2e", size = 56929, upload-time = "2026-06-02T00:09:41.487Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/49/2797ec0ef88008a653a8867bb8d1e5c223cd2df8e40390dd5c6a0279cbc5/hatchling-1.30.1-py3-none-any.whl", hash = "sha256:161eacafb3c6f91526e92116d21426369f2c36e98c36a864f11a96345ad4ee31", size = 77489, upload-time = "2026-06-02T00:09:40.139Z" },
]
[[package]]
name = "hf-xet"
version = "1.5.1"
@@ -6126,6 +6143,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" },
]
[[package]]
name = "trove-classifiers"
version = "2026.6.1.19"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/e3/7ca82ee24c82d344584abd5b8637b3bd056f2900226e8d82fc22f1184b92/trove_classifiers-2026.6.1.19.tar.gz", hash = "sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745", size = 17059, upload-time = "2026-06-01T19:41:34.649Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl", hash = "sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3", size = 14211, upload-time = "2026-06-01T19:41:33.434Z" },
]
[[package]]
name = "truststore"
version = "0.10.4"