diff --git a/.github/workflows/release-validation.yml b/.github/workflows/release-validation.yml
index 1455fbce9..004fd0dcb 100644
--- a/.github/workflows/release-validation.yml
+++ b/.github/workflows/release-validation.yml
@@ -297,7 +297,11 @@ jobs:
BURR_VERSION: ${{ needs.build-artifacts.outputs.version }}
run: |
mkdir -p /tmp/sdist-extract /tmp/sdist-wheel
- tar -xzf "dist/apache-burr-${BURR_VERSION}-incubating-sdist.tar.gz" \
+ SDIST_PATH="dist/apache-burr-${BURR_VERSION}-incubating-sdist.tar.gz"
+ export SOURCE_DATE_EPOCH=$(python -c \
+ "import sys, tarfile; print(int(tarfile.open(sys.argv[1]).next().mtime))" \
+ "$SDIST_PATH")
+ tar -xzf "$SDIST_PATH" \
-C /tmp/sdist-extract
# Find the single top-level directory the tarball extracted into
SDIST_ROOT=$(find /tmp/sdist-extract -maxdepth 1 -mindepth 1 -type d | head -1)
diff --git a/.github/workflows/ui.yml b/.github/workflows/ui.yml
index 6433209ef..477e922e2 100644
--- a/.github/workflows/ui.yml
+++ b/.github/workflows/ui.yml
@@ -44,9 +44,9 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- - run: npm install --ignore-scripts
+ - run: npm ci --ignore-scripts
- run: npm run build
# `npm test` starts vitest in watch mode, which never exits on CI.
- run: npx vitest run
- - run: npm run lint:fix
- - run: npm run format:fix
+ - run: npm run lint
+ - run: npm run format
diff --git a/pyproject.toml b/pyproject.toml
index 8ce1bf8a5..f2c27aafd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -88,6 +88,7 @@ release = [
tests = [
"pytest",
"pytest-asyncio",
+ "apache-burr[graphviz]",
"apache-burr[hamilton]",
"langchain_core",
"langchain_community",
diff --git a/scripts/apache_release.py b/scripts/apache_release.py
index e8659ef00..1ec8c096a 100644
--- a/scripts/apache_release.py
+++ b/scripts/apache_release.py
@@ -48,6 +48,7 @@
VERSION_FILE = "pyproject.toml"
VERSION_PATTERN = r'version\s*=\s*"(\d+\.\d+\.\d+)"'
TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
+PROJECT_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_DOWNLOADS_URL = f"https://downloads.apache.org/incubator/{PROJECT_SHORT_NAME}/"
DEFAULT_DEV_SVN_ROOT = f"https://dist.apache.org/repos/dist/dev/incubator/{PROJECT_SHORT_NAME}"
DEFAULT_RELEASE_SVN_ROOT = (
@@ -692,7 +693,64 @@ def _remove_ui_build_artifacts() -> None:
print(" ✓ UI build artifacts removed")
-def _build_sdist_from_git(version: str, output_dir: str = "dist") -> str:
+def _release_build_environment(source_date_epoch: int) -> dict[str, str]:
+ """Return the shared environment for reproducible Flit builds."""
+ env = os.environ.copy()
+ env["FLIT_USE_VCS"] = "0"
+ env["SOURCE_DATE_EPOCH"] = str(source_date_epoch)
+ return env
+
+
+def _git_source_date_epoch() -> int:
+ """Return the anchored HEAD commit timestamp for official release builds."""
+ result = _run_command(
+ [
+ "git",
+ "-C",
+ str(PROJECT_ROOT),
+ "-c",
+ "log.showSignature=false",
+ "show",
+ "-s",
+ "--format=%ct",
+ "HEAD",
+ ],
+ description="Reading the source commit timestamp...",
+ error_message="Could not determine the HEAD commit timestamp",
+ )
+ raw_epoch = result.stdout.strip()
+ if not raw_epoch.isdigit():
+ _fail(f"Git returned an invalid commit timestamp: {raw_epoch!r}")
+
+ ambient_epoch = os.environ.get("SOURCE_DATE_EPOCH", "").strip()
+ if ambient_epoch and ambient_epoch != raw_epoch:
+ print(
+ " ⚠️ Ignoring ambient SOURCE_DATE_EPOCH="
+ f"{ambient_epoch}; using HEAD commit timestamp {raw_epoch}"
+ )
+ return int(raw_epoch)
+
+
+def _environment_source_date_epoch() -> Optional[int]:
+ """Return an explicitly configured rebuild epoch, treating blank as unset."""
+ raw_epoch = os.environ.get("SOURCE_DATE_EPOCH", "").strip()
+ if not raw_epoch:
+ return None
+ try:
+ return int(raw_epoch)
+ except ValueError:
+ _fail("SOURCE_DATE_EPOCH must be an integer Unix timestamp")
+
+
+def _wheel_source_date_epoch() -> int:
+ """Use an explicit rebuild epoch, or the anchored commit timestamp in Git."""
+ configured_epoch = _environment_source_date_epoch()
+ return configured_epoch if configured_epoch is not None else _git_source_date_epoch()
+
+
+def _build_sdist_from_git(
+ version: str, output_dir: str = "dist", source_date_epoch: Optional[int] = None
+) -> str:
"""Build source distribution from git using flit."""
_print_step(1, 2, "Building sdist with flit")
@@ -700,17 +758,13 @@ def _build_sdist_from_git(version: str, output_dir: str = "dist") -> str:
_remove_ui_build_artifacts()
_check_git_working_tree()
- env = os.environ.copy()
- env["FLIT_USE_VCS"] = "0"
- source_epoch = _source_date_epoch(version, output_dir)
- if source_epoch is not None:
- env["SOURCE_DATE_EPOCH"] = str(source_epoch)
+ epoch = source_date_epoch if source_date_epoch is not None else _git_source_date_epoch()
_run_command(
["flit", "build", "--format", "sdist"],
description="Running flit build --format sdist...",
error_message="Failed to build sdist",
success_message="flit sdist created successfully",
- env=env,
+ env=_release_build_environment(epoch),
)
# Find and rename sdist
@@ -734,14 +788,6 @@ def _build_sdist_from_git(version: str, output_dir: str = "dist") -> str:
return apache_sdist
-def _source_date_epoch(version: str, output_dir: str = "dist") -> Optional[int]:
- """Use the source archive timestamp when available so local rebuilds are comparable."""
- source_archive = os.path.join(output_dir, f"apache-burr-{version}-incubating-src.tar.gz")
- if os.path.exists(source_archive):
- return int(os.path.getmtime(source_archive))
- return None
-
-
# ============================================================================
# Step 3: Build Wheel (SIMPLIFIED!)
# ============================================================================
@@ -877,7 +923,9 @@ def _cleanup_wheel_contents(
shutil.rmtree(backup_dir)
-def _build_wheel_from_current_dir(version: str, output_dir: str = "dist") -> str:
+def _build_wheel_from_current_dir(
+ version: str, output_dir: str = "dist", source_date_epoch: Optional[int] = None
+) -> str:
"""Build wheel from current directory (matches what voters do).
This is MUCH simpler than the old approach:
@@ -894,18 +942,14 @@ def _build_wheel_from_current_dir(version: str, output_dir: str = "dist") -> str
_print_step(3, 3, "Building wheel with flit")
try:
- env = os.environ.copy()
- env["FLIT_USE_VCS"] = "0"
- source_epoch = _source_date_epoch(version, output_dir)
- if source_epoch is not None:
- env["SOURCE_DATE_EPOCH"] = str(source_epoch)
+ epoch = source_date_epoch if source_date_epoch is not None else _wheel_source_date_epoch()
_run_command(
["flit", "build", "--format", "wheel"],
description="",
error_message="Wheel build failed",
success_message="Wheel built successfully",
- env=env,
+ env=_release_build_environment(epoch),
)
# Find the wheel
@@ -1486,6 +1530,7 @@ def cmd_all(args) -> bool:
_verify_project_root()
_validate_version(args.version)
_check_git_working_tree()
+ source_date_epoch = _git_source_date_epoch()
# Step 1: Git Archive
_print_step(1, 4, "Creating git archive")
@@ -1493,14 +1538,18 @@ def cmd_all(args) -> bool:
# Step 2: Build sdist
_print_step(2, 4, "Building sdist")
- sdist_path = _build_sdist_from_git(args.version, args.output_dir)
+ sdist_path = _build_sdist_from_git(
+ args.version, args.output_dir, source_date_epoch=source_date_epoch
+ )
_sign_artifact(sdist_path, skip_signing=skip_signing)
if not _verify_artifact_complete(sdist_path, skip_signing=skip_signing):
_fail("sdist verification failed!")
# Step 3: Build wheel
_print_step(3, 4, "Building wheel")
- wheel_path = _build_wheel_from_current_dir(args.version, args.output_dir)
+ wheel_path = _build_wheel_from_current_dir(
+ args.version, args.output_dir, source_date_epoch=source_date_epoch
+ )
if not _verify_wheel_with_twine(wheel_path):
_fail("Twine verification failed!")
_sign_artifact(wheel_path, skip_signing=skip_signing)
diff --git a/scripts/verify_apache_artifacts.py b/scripts/verify_apache_artifacts.py
index 17b29f240..a4a4bc5f8 100755
--- a/scripts/verify_apache_artifacts.py
+++ b/scripts/verify_apache_artifacts.py
@@ -509,11 +509,15 @@ def verify_signatures(artifacts_dir: str, summary: VerificationSummary | None =
return all_valid
-def _safe_extract_tar(tar_handle: tarfile.TarFile, extract_dir: str) -> None:
+def _safe_extract_tar(
+ tar_handle: tarfile.TarFile,
+ extract_dir: str,
+ members: list[tarfile.TarInfo] | None = None,
+) -> None:
try:
- tar_handle.extractall(extract_dir, filter="data")
+ tar_handle.extractall(extract_dir, members=members, filter="data")
except TypeError:
- tar_handle.extractall(extract_dir)
+ tar_handle.extractall(extract_dir, members=members)
def _build_rat_command(
@@ -783,14 +787,27 @@ def _release_artifact_map(artifacts_dir: str) -> dict[str, list[str]]:
}
-def _extract_project_root(source_artifact: str, destination: str) -> Path:
+def _extract_project_root_and_epoch(source_artifact: str, destination: str) -> tuple[Path, int]:
with tarfile.open(source_artifact, "r:gz") as tar:
- _safe_extract_tar(tar, destination)
+ members = tar.getmembers()
+ if not members:
+ raise ValueError(f"source archive is empty: {source_artifact}")
+ source_date_epoch = int(members[0].mtime)
+ _safe_extract_tar(tar, destination, members=members)
entries = [entry for entry in Path(destination).iterdir()]
if len(entries) == 1 and entries[0].is_dir():
- return entries[0]
- return Path(destination)
+ return entries[0], source_date_epoch
+ return Path(destination), source_date_epoch
+
+
+def _reproducible_build_environment(source_date_epoch: int) -> dict[str, str]:
+ """Return the shared environment for voter rebuilds."""
+ env = os.environ.copy()
+ env["FLIT_USE_VCS"] = "0"
+ env["SOURCE_DATE_EPOCH"] = str(source_date_epoch)
+ env["PATH"] = f"{Path(sys.executable).parent}{os.pathsep}{env.get('PATH', '')}"
+ return env
def _load_apache_release_module(project_root: Path):
@@ -824,16 +841,13 @@ def _build_reproducible_wheel(
version,
output_dir,
]
- env = os.environ.copy()
- env["SOURCE_DATE_EPOCH"] = str(source_epoch)
- env["PATH"] = f"{Path(sys.executable).parent}{os.pathsep}{env.get('PATH', '')}"
result = subprocess.run(
command,
cwd=project_root,
capture_output=True,
text=True,
check=False,
- env=env,
+ env=_reproducible_build_environment(source_epoch),
)
output = "\n".join(item for item in [result.stdout, result.stderr] if item)
return result.returncode == 0, output
@@ -841,8 +855,10 @@ def _build_reproducible_wheel(
def _build_reproducible_artifacts(source_artifact: str, output_dir: str) -> tuple[bool, str]:
with tempfile.TemporaryDirectory() as temp_dir:
- project_root = _extract_project_root(source_artifact, temp_dir)
- source_epoch = int(os.path.getmtime(source_artifact))
+ try:
+ project_root, source_epoch = _extract_project_root_and_epoch(source_artifact, temp_dir)
+ except (OSError, tarfile.TarError, ValueError) as exc:
+ return False, f"unable to read source epoch: {exc}"
version_match = re.search(r"(\d+\.\d+\.\d+)", os.path.basename(source_artifact))
if version_match is None:
return False, f"unable to determine version from {os.path.basename(source_artifact)}"
@@ -852,17 +868,13 @@ def _build_reproducible_artifacts(source_artifact: str, output_dir: str) -> tupl
if os.path.exists(dist_dir):
shutil.rmtree(dist_dir)
- env = os.environ.copy()
- env["FLIT_USE_VCS"] = "0"
- env["SOURCE_DATE_EPOCH"] = str(source_epoch)
- env["PATH"] = f"{Path(sys.executable).parent}{os.pathsep}{env.get('PATH', '')}"
sdist_result = subprocess.run(
["flit", "build", "--format", "sdist"],
cwd=project_root,
capture_output=True,
text=True,
check=False,
- env=env,
+ env=_reproducible_build_environment(source_epoch),
)
if sdist_result.returncode != 0:
return False, sdist_result.stderr or sdist_result.stdout
diff --git a/telemetry/ui/.prettierrc.json b/telemetry/ui/.prettierrc.json
index f829f0a09..646b17cca 100644
--- a/telemetry/ui/.prettierrc.json
+++ b/telemetry/ui/.prettierrc.json
@@ -3,6 +3,5 @@
"tabWidth": 2,
"printWidth": 100,
"singleQuote": true,
- "trailingComma": "none",
- "jsxBracketSameLine": true
+ "trailingComma": "none"
}
diff --git a/telemetry/ui/package-lock.json b/telemetry/ui/package-lock.json
index 0c3ddb768..a7f709a2f 100644
--- a/telemetry/ui/package-lock.json
+++ b/telemetry/ui/package-lock.json
@@ -8006,9 +8006,9 @@
}
},
"node_modules/nanoid": {
- "version": "3.3.16",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
- "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"funding": [
{
"type": "github",
diff --git a/telemetry/ui/package.json b/telemetry/ui/package.json
index 3bb190bcc..77a719198 100644
--- a/telemetry/ui/package.json
+++ b/telemetry/ui/package.json
@@ -35,9 +35,10 @@
"build": "tsc && vite build",
"test": "vitest",
"preview": "vite preview",
- "lint": "eslint .",
+ "lint": "eslint . --max-warnings=0",
"lint:fix": "eslint . --fix --max-warnings=0 --config ./.eslintrc.js --ignore-path ./.eslintignore",
- "format:fix": "prettier --write ./**/*.{ts,tsx,css,md,json} --config ./.prettierrc.json --ignore-path ./.prettierignore",
+ "format": "prettier --check \"./**/*.{ts,tsx,css,md,json}\" --config ./.prettierrc.json --ignore-path ./.prettierignore",
+ "format:fix": "prettier --write \"./**/*.{ts,tsx,css,md,json}\" --config ./.prettierrc.json --ignore-path ./.prettierignore",
"precommit": "npm run lint:fix && npm run format",
"prepush": "npm run lint"
},
@@ -77,6 +78,9 @@
"*.tsx": [
"eslint --fix",
"prettier --write"
+ ],
+ "*.{css,md,json}": [
+ "prettier --write"
]
}
}
diff --git a/telemetry/ui/src/components/common/ThemeToggle.tsx b/telemetry/ui/src/components/common/ThemeToggle.tsx
index 569ce9061..7eecf078d 100644
--- a/telemetry/ui/src/components/common/ThemeToggle.tsx
+++ b/telemetry/ui/src/components/common/ThemeToggle.tsx
@@ -39,7 +39,8 @@ export const ThemeToggle = (props: { showLabel?: boolean }) => {
'group flex items-center gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold',
'text-gray-700 hover:bg-gray-50 hover:text-dwdarkblue',
'dark:text-gray-200 dark:hover:bg-gray-800 dark:hover:text-white'
- )}>
+ )}
+ >
{props.showLabel && {isDark ? 'Light mode' : 'Dark mode'}}
diff --git a/telemetry/ui/src/components/nav/appcontainer.tsx b/telemetry/ui/src/components/nav/appcontainer.tsx
index fb3b6ae77..bcc5bcbb0 100644
--- a/telemetry/ui/src/components/nav/appcontainer.tsx
+++ b/telemetry/ui/src/components/nav/appcontainer.tsx
@@ -332,7 +332,6 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
'flex items-center w-full text-left rounded-md p-2 gap-x-3 text-sm leading-6 font-semibold text-gray-700 dark:text-gray-200'
)}
>
-
None:
wheel_name = wheel_name or f"apache_burr-{version}-py3-none-any.whl"
artifact_names = [
diff --git a/tests/test_verify_apache_artifacts.py b/tests/test_verify_apache_artifacts.py
index c8f15f362..3ce4679ec 100644
--- a/tests/test_verify_apache_artifacts.py
+++ b/tests/test_verify_apache_artifacts.py
@@ -22,6 +22,7 @@
import tempfile
import zipfile
from pathlib import Path
+from typing import Optional
def _load_verify_module():
@@ -41,12 +42,16 @@ def _reference_text(filename: str) -> bytes:
return (Path(__file__).resolve().parent.parent / filename).read_bytes()
-def _write_tar_gz(path: Path, root: str, files: dict[str, bytes]) -> None:
+def _write_tar_gz(
+ path: Path, root: str, files: dict[str, bytes], *, member_mtime: Optional[int] = None
+) -> None:
with tarfile.open(path, "w:gz") as tar:
for relative_name, content in files.items():
with tempfile.NamedTemporaryFile(delete=False, dir=path.parent) as temp_file:
temp_path = Path(temp_file.name)
temp_path.write_bytes(content)
+ if member_mtime is not None:
+ os.utime(temp_path, (member_mtime, member_mtime))
tar.add(temp_path, arcname=f"{root}/{relative_name}")
temp_path.unlink()
@@ -390,6 +395,29 @@ def test_compare_wheel_contents_detects_file_missing_from_first_wheel(tmp_path):
assert any("burr/extra.py" in d for d in diffs)
+def test_extract_project_root_gets_epoch_from_member_not_local_mtime(tmp_path):
+ source_tar = tmp_path / "apache-burr-0.43.0-incubating-src.tar.gz"
+ _write_tar_gz(source_tar, "source-root", {"README.md": b"source"}, member_mtime=123456789)
+ os.utime(source_tar, (987654321, 987654321))
+
+ project_root, source_epoch = verify._extract_project_root_and_epoch(
+ str(source_tar), str(tmp_path / "extract")
+ )
+
+ assert source_epoch == 123456789
+ assert (project_root / "README.md").read_bytes() == b"source"
+
+
+def test_reproducible_build_reports_invalid_source_archive(tmp_path):
+ source_tar = tmp_path / "apache-burr-0.43.0-incubating-src.tar.gz"
+ source_tar.write_bytes(b"not a tarball")
+
+ ok, error = verify._build_reproducible_artifacts(str(source_tar), str(tmp_path / "rebuilt"))
+
+ assert ok is False
+ assert error.startswith("unable to read source epoch:")
+
+
def test_verify_licenses_runs_rat_on_wheel_in_addition_to_tarball(tmp_path, monkeypatch):
"""verify_licenses must run Apache RAT on .whl artifacts as well as .tar.gz tarballs."""
tar_path = tmp_path / "apache-burr-0.42.0-incubating-src.tar.gz"