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' )} > -