This is an automated email from the ASF dual-hosted git repository.
ebenizzy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/burr.git
The following commit(s) were added to refs/heads/main by this push:
new d33d044f Adds scripts for releasing burr -- prep for first apache
release (#595)
d33d044f is described below
commit d33d044fe2f9ff7fa81240e80106fb5493b36de5
Author: Elijah ben Izzy <[email protected]>
AuthorDate: Tue Nov 25 22:21:58 2025 -0800
Adds scripts for releasing burr -- prep for first apache release (#595)
* Adds scripts for releasing burr
- uses flit as the build system
- one script for helping with the release
- the other for helping with the build of artifacts
- tested out fairly extensively
* Adds license headers to all files that needed it
* Skips test examples include/exclude for python <3.11
* Changed project name to apache-burr
This allows us to publish under apache. We will also publish under burr
as well as needed.
* Reverts version bumps
---------
Co-authored-by: Elijah ben Izzy <[email protected]>
---
CONTRIBUTING.rst | 19 +
__init__.py | 0
burr/cli/__main__.py | 31 +-
burr/telemetry.py | 6 +-
burr/tracking/server/run.py | 8 +-
.../tracking/server/s3/deployment/terraform/alb.tf | 17 +
.../server/s3/deployment/terraform/auto_scaling.tf | 17 +
.../tracking/server/s3/deployment/terraform/ecs.tf | 16 +
.../tracking/server/s3/deployment/terraform/iam.tf | 17 +
.../server/s3/deployment/terraform/logs.tf | 17 +
.../server/s3/deployment/terraform/network.tf | 17 +
.../server/s3/deployment/terraform/outputs.tf | 17 +
.../server/s3/deployment/terraform/provider.tf | 17 +
.../server/s3/deployment/terraform/security.tf | 17 +
.../server/s3/deployment/terraform/variable.tf | 17 +
burr/version.py | 6 +-
pyproject.toml | 176 +++++---
scripts/README.md | 229 +++++++++++
scripts/build_artifacts.py | 290 ++++++++++++++
scripts/release_helper.py | 444 +++++++++++++++++++++
scripts/setup_keys.sh | 95 +++++
telemetry/ui/.eslintrc.js | 17 +
telemetry/ui/package-lock.json | 2 +
telemetry/ui/package.json | 2 +
tests/test_release_config.py | 153 +++++++
25 files changed, 1583 insertions(+), 64 deletions(-)
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index c22b7996..db48538f 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -1,3 +1,22 @@
+..
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+
============
Contributing
============
diff --git a/__init__.py b/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/burr/cli/__main__.py b/burr/cli/__main__.py
index cc89b06f..109eb835 100644
--- a/burr/cli/__main__.py
+++ b/burr/cli/__main__.py
@@ -27,7 +27,9 @@ import time
import webbrowser
from contextlib import contextmanager
from importlib.resources import files
+from pathlib import Path
from types import ModuleType
+from typing import Optional
from burr import system, telemetry
from burr.core.persistence import PersistedStateData
@@ -54,7 +56,7 @@ def _telemetry_if_enabled(event: str):
telemetry.create_and_send_cli_event(event)
-def _command(command: str, capture_output: bool, addl_env: dict = None) -> str:
+def _command(command: str, capture_output: bool, addl_env: dict | None = None)
-> str:
"""Runs a simple command"""
if addl_env is None:
addl_env = {}
@@ -78,7 +80,27 @@ def _command(command: str, capture_output: bool, addl_env:
dict = None) -> str:
def _get_git_root() -> str:
- return _command("git rev-parse --show-toplevel", capture_output=True)
+ env_root = os.environ.get("BURR_PROJECT_ROOT")
+ if env_root:
+ return env_root
+ try:
+ return _command("git rev-parse --show-toplevel", capture_output=True)
+ except subprocess.CalledProcessError:
+ package_root = _locate_package_root()
+ if package_root is not None:
+ logger.warning("Not inside a git repository; using package root
%s.", package_root)
+ return package_root
+ logger.warning("Not inside a git repository; defaulting to current
directory.")
+ return os.getcwd()
+
+
+def _locate_package_root() -> Optional[str]:
+ path = Path(__file__).resolve()
+ for candidate in (path.parent,) + tuple(path.parents):
+ telemetry_dir = candidate / "telemetry" / "ui"
+ if telemetry_dir.exists():
+ return str(candidate)
+ return None
def open_when_ready(check_url: str, open_url: str):
@@ -118,13 +140,16 @@ def _build_ui():
# create a symlink so we can get packages inside it...
cmd = "rm -rf burr/tracking/server/build"
_command(cmd, capture_output=False)
- cmd = "cp -R telemetry/ui/build burr/tracking/server/build"
+ cmd = "mkdir -p burr/tracking/server/build"
+ _command(cmd, capture_output=False)
+ cmd = "cp -a telemetry/ui/build/. burr/tracking/server/build/"
_command(cmd, capture_output=False)
@cli.command()
def build_ui():
git_root = _get_git_root()
+ logger.info("UI build: using project root %s", git_root)
with cd(git_root):
_build_ui()
diff --git a/burr/telemetry.py b/burr/telemetry.py
index 3d24e722..6dc542c5 100644
--- a/burr/telemetry.py
+++ b/burr/telemetry.py
@@ -47,7 +47,11 @@ from urllib import request
if TYPE_CHECKING:
from burr.lifecycle import internal
-VERSION = importlib.metadata.version("burr")
+try:
+ VERSION = importlib.metadata.version("apache-burr")
+except importlib.metadata.PackageNotFoundError:
+ # Fallback for older installations or development
+ VERSION = importlib.metadata.version("burr")
logger = logging.getLogger(__name__)
diff --git a/burr/tracking/server/run.py b/burr/tracking/server/run.py
index 7c63df8d..0f3303de 100644
--- a/burr/tracking/server/run.py
+++ b/burr/tracking/server/run.py
@@ -328,9 +328,13 @@ async def version() -> dict:
import pkg_resources
try:
- version = pkg_resources.get_distribution("burr").version
+ version = pkg_resources.get_distribution("apache-burr").version
except pkg_resources.DistributionNotFound:
- version = "unknown"
+ try:
+ # Fallback for older installations or development
+ version = pkg_resources.get_distribution("burr").version
+ except pkg_resources.DistributionNotFound:
+ version = "unknown"
return {"version": version}
diff --git a/burr/tracking/server/s3/deployment/terraform/alb.tf
b/burr/tracking/server/s3/deployment/terraform/alb.tf
index cb1b0b14..6abdeb77 100644
--- a/burr/tracking/server/s3/deployment/terraform/alb.tf
+++ b/burr/tracking/server/s3/deployment/terraform/alb.tf
@@ -1,3 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
resource "aws_alb" "main" {
name = "burr-load-balancer"
subnets = aws_subnet.public.*.id
diff --git a/burr/tracking/server/s3/deployment/terraform/auto_scaling.tf
b/burr/tracking/server/s3/deployment/terraform/auto_scaling.tf
index 39aa5a96..d2c13e92 100644
--- a/burr/tracking/server/s3/deployment/terraform/auto_scaling.tf
+++ b/burr/tracking/server/s3/deployment/terraform/auto_scaling.tf
@@ -1,3 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
# auto_scaling.tf
resource "aws_appautoscaling_target" "target" {
diff --git a/burr/tracking/server/s3/deployment/terraform/ecs.tf
b/burr/tracking/server/s3/deployment/terraform/ecs.tf
index 0c9e6b12..a50daa13 100644
--- a/burr/tracking/server/s3/deployment/terraform/ecs.tf
+++ b/burr/tracking/server/s3/deployment/terraform/ecs.tf
@@ -1,3 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
resource "aws_ecs_cluster" "main" {
name = "burr-cluster"
diff --git a/burr/tracking/server/s3/deployment/terraform/iam.tf
b/burr/tracking/server/s3/deployment/terraform/iam.tf
index cf333719..fc197edc 100644
--- a/burr/tracking/server/s3/deployment/terraform/iam.tf
+++ b/burr/tracking/server/s3/deployment/terraform/iam.tf
@@ -1,3 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
resource "aws_iam_role" "ecs_task_execution_role" {
name = "burr-ecs"
diff --git a/burr/tracking/server/s3/deployment/terraform/logs.tf
b/burr/tracking/server/s3/deployment/terraform/logs.tf
index 03d9ecf6..86b2dadb 100644
--- a/burr/tracking/server/s3/deployment/terraform/logs.tf
+++ b/burr/tracking/server/s3/deployment/terraform/logs.tf
@@ -1,3 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
# logs.tf
# Set up CloudWatch group and log stream and retain logs for 30 days
diff --git a/burr/tracking/server/s3/deployment/terraform/network.tf
b/burr/tracking/server/s3/deployment/terraform/network.tf
index 80934198..7b1ba405 100644
--- a/burr/tracking/server/s3/deployment/terraform/network.tf
+++ b/burr/tracking/server/s3/deployment/terraform/network.tf
@@ -1,3 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
# network.tf
# Fetch AZs in the current region
diff --git a/burr/tracking/server/s3/deployment/terraform/outputs.tf
b/burr/tracking/server/s3/deployment/terraform/outputs.tf
index 323347bb..1a6993e8 100644
--- a/burr/tracking/server/s3/deployment/terraform/outputs.tf
+++ b/burr/tracking/server/s3/deployment/terraform/outputs.tf
@@ -1,3 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
output "alb_hostname" {
value = "${aws_alb.main.dns_name}:3000"
}
diff --git a/burr/tracking/server/s3/deployment/terraform/provider.tf
b/burr/tracking/server/s3/deployment/terraform/provider.tf
index 8909ec46..d11b4762 100644
--- a/burr/tracking/server/s3/deployment/terraform/provider.tf
+++ b/burr/tracking/server/s3/deployment/terraform/provider.tf
@@ -1,3 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
# Specify the provider and access details
provider "aws" {
diff --git a/burr/tracking/server/s3/deployment/terraform/security.tf
b/burr/tracking/server/s3/deployment/terraform/security.tf
index 547b5184..2cabc4ac 100644
--- a/burr/tracking/server/s3/deployment/terraform/security.tf
+++ b/burr/tracking/server/s3/deployment/terraform/security.tf
@@ -1,3 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
# ALB security Group: Edit to restrict access to the application
resource "aws_security_group" "lb" {
name = "burr-load-balancer-security-group"
diff --git a/burr/tracking/server/s3/deployment/terraform/variable.tf
b/burr/tracking/server/s3/deployment/terraform/variable.tf
index 6ab5861f..b1ea69f4 100644
--- a/burr/tracking/server/s3/deployment/terraform/variable.tf
+++ b/burr/tracking/server/s3/deployment/terraform/variable.tf
@@ -1,3 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
# variables.tf
variable "aws_access_key" {
diff --git a/burr/version.py b/burr/version.py
index 34caa5d4..8555b4cc 100644
--- a/burr/version.py
+++ b/burr/version.py
@@ -17,4 +17,8 @@
import importlib.metadata
-__version__ = importlib.metadata.version("burr")
+try:
+ __version__ = importlib.metadata.version("apache-burr")
+except importlib.metadata.PackageNotFoundError:
+ # Fallback for older installations or development
+ __version__ = importlib.metadata.version("burr")
diff --git a/pyproject.toml b/pyproject.toml
index 7839fb3e..4bc53a3e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,9 +1,26 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
[build-system]
-requires = ["setuptools >= 61.0"]
-build-backend = "setuptools.build_meta"
+requires = ["flit_core >=3.2,<4"]
+build-backend = "flit_core.buildapi"
[project]
-name = "burr"
+name = "apache-burr"
version = "0.40.2"
dependencies = [] # yes, there are none
requires-python = ">=3.9"
@@ -82,38 +99,38 @@ redis = [
tests = [
"pytest",
"pytest-asyncio",
- "burr[hamilton]",
- "burr[hamilton]",
+ "apache-burr[hamilton]",
+ "apache-burr[hamilton]",
"langchain_core",
"langchain_community",
"pandas",
"pydantic[email]",
"pyarrow",
- "burr[aiosqlite]",
- "burr[asyncpg]",
- "burr[psycopg2]",
- "burr[pymongo]",
- "burr[redis]",
- "burr[opentelemetry]",
- "burr[haystack]",
- "burr[ray]"
+ "apache-burr[aiosqlite]",
+ "apache-burr[asyncpg]",
+ "apache-burr[psycopg2]",
+ "apache-burr[pymongo]",
+ "apache-burr[redis]",
+ "apache-burr[opentelemetry]",
+ "apache-burr[haystack]",
+ "apache-burr[ray]"
]
documentation = [
- "burr[tests]",
+ "apache-burr[tests]",
"sphinx",
"sphinx-autobuild",
"myst-nb",
"furo",
"sphinx-sitemap",
"sphinx-toolbox",
- "burr[aiosqlite]",
- "burr[asyncpg]",
- "burr[psycopg2]",
- "burr[pymongo]",
- "burr[redis]",
- "burr[ray]",
- "burr[streamlit]",
+ "apache-burr[aiosqlite]",
+ "apache-burr[asyncpg]",
+ "apache-burr[psycopg2]",
+ "apache-burr[pymongo]",
+ "apache-burr[redis]",
+ "apache-burr[ray]",
+ "apache-burr[streamlit]",
"sphinxcontrib-googleanalytics"
]
@@ -122,7 +139,7 @@ tracking-client = [
]
tracking-client-s3 = [
- "burr[tracking-client]",
+ "apache-burr[tracking-client]",
"boto3"
]
@@ -131,7 +148,7 @@ tracking-server-s3 = [
"aiobotocore",
"fastapi",
"tortoise-orm[accel, asyncmy]",
- "burr[tracking-server]",
+ "apache-burr[tracking-server]",
"typing-inspect"
]
@@ -165,16 +182,16 @@ cli = [
]
tracking = [
- "burr[tracking-client]",
- "burr[tracking-server]",
+ "apache-burr[tracking-client]",
+ "apache-burr[tracking-server]",
]
learn = [
- "burr[tracking,streamlit,graphviz,hamilton,cli,inappexamples]"
+ "apache-burr[tracking,streamlit,graphviz,hamilton,cli,inappexamples]"
]
start = [
- "burr[learn]"
+ "apache-burr[learn]"
]
inappexamples = [
"opentelemetry-api",
@@ -191,17 +208,17 @@ examples = [
"langchain",
"langchain-community",
"langchain-openai",
- "burr[inappexamples]"
+ "apache-burr[inappexamples]"
]
# just install everything for developers
developer = [
- "burr[streamlit]",
- "burr[graphviz]",
- "burr[tracking]",
- "burr[tests]",
- "burr[documentation]",
- "burr[bloat]",
+ "apache-burr[streamlit]",
+ "apache-burr[graphviz]",
+ "apache-burr[tracking]",
+ "apache-burr[tests]",
+ "apache-burr[documentation]",
+ "apache-burr[bloat]",
"build",
"twine",
"pre-commit",
@@ -216,21 +233,6 @@ ray = [
"ray[default]"
]
-[tool.setuptools]
-include-package-data = true
-
-[tool.setuptools.packages.find]
-include = ["burr", "burr.*"]
-
-# we need to ensure this is there...
-[tool.setuptools.package-data]
-burr = [
- "burr/tracking/server/build/**/*",
- "burr/tracking/server/demo_data/**/*",
- "py.typed",
-]
-
-
[tool.aerich]
tortoise_orm = "burr.tracking.server.s3.settings.TORTOISE_ORM"
location = "./burr/tracking/server/s3/migrations"
@@ -254,11 +256,71 @@ burr-test-case = "burr.cli.__main__:cli_test_case"
name = "burr"
[tool.flit.sdist]
-include = ["LICENSE", "NOTICE", "DISCLAIMER",
"burr/tracking/server/build/**/*",
- "burr/tracking/server/demo_data/**/*",
- "examples/email-assistant/*",
- "examples/multi-modal-chatbot/*",
- "examples/streaming-fastapi/*",
- "examples/deep-researcher/*",
- "py.typed",]
-exclude = []
+include = [
+ "LICENSE",
+ "NOTICE",
+ "DISCLAIMER",
+ "scripts/**",
+ "telemetry/ui/**",
+ "examples/__init__.py",
+ "examples/email-assistant/**",
+ "examples/multi-modal-chatbot/**",
+ "examples/streaming-fastapi/**",
+ "examples/deep-researcher/**",
+]
+exclude = [
+ "telemetry/ui/node_modules/**",
+ "telemetry/ui/build/**",
+ "telemetry/ui/dist/**",
+ "telemetry/ui/.cache/**",
+ "telemetry/ui/.next/**",
+ "**/__pycache__/**",
+ "**/*.pyc",
+ ".git/**",
+ ".github/**",
+ "docs/**",
+ # Exclude VCS and system files
+ ".gitignore",
+ ".gitmodules",
+ "**/.gitignore",
+ "**/.DS_Store",
+ # Exclude unwanted examples - flit auto-includes examples/ as a package
because of __init__.py,
+ # and exclude patterns take precedence over include patterns. We must
explicitly exclude each
+ # unwanted example directory. Only email-assistant, multi-modal-chatbot,
streaming-fastapi,
+ # and deep-researcher should be included (see include list above).
+ # NOTE: If you add/remove examples, update this list AND
tests/test_release_config.py
+ "examples/README.md",
+ "examples/validate_examples.py",
+ "examples/adaptive-crag/**",
+ "examples/conversational-rag/**",
+ "examples/custom-serde/**",
+ "examples/deployment/**",
+ "examples/hamilton-integration/**",
+ "examples/haystack-integration/**",
+ "examples/hello-world-counter/**",
+ "examples/image-telephone/**",
+ "examples/instructor-gemini-flash/**",
+ "examples/integrations/**",
+ "examples/llm-adventure-game/**",
+ "examples/ml-training/**",
+ "examples/multi-agent-collaboration/**",
+ "examples/openai-compatible-agent/**",
+ "examples/opentelemetry/**",
+ "examples/other-examples/**",
+ "examples/parallelism/**",
+ "examples/pytest/**",
+ "examples/rag-lancedb-ingestion/**",
+ "examples/ray/**",
+ "examples/recursive/**",
+ "examples/simple-chatbot-intro/**",
+ "examples/simulation/**",
+ "examples/streaming-overview/**",
+ "examples/talks/**",
+ "examples/templates/**",
+ "examples/test-case-creation/**",
+ "examples/tool-calling/**",
+ "examples/tracing-and-spans/**",
+ "examples/typed-state/**",
+ "examples/web-server/**",
+ "examples/youtube-to-social-media-post/**",
+]
diff --git a/scripts/README.md b/scripts/README.md
new file mode 100644
index 00000000..8db4f235
--- /dev/null
+++ b/scripts/README.md
@@ -0,0 +1,229 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+# Burr Release Scripts
+
+This directory contains helper scripts to automate the Apache release workflow.
+
+## Overview
+
+The release process has two phases:
+
+1. **Source-only release** (for Apache voting): Contains source code, build
scripts, and UI source—but NO pre-built artifacts
+2. **Wheel build** (for PyPI): Built from the source release, includes
pre-built UI assets
+
+All packaging configuration lives in `pyproject.toml`:
+- `[build-system]` uses `flit_core` as the build backend
+- `[tool.flit.sdist]` controls what goes in the source tarball
+- Wheel contents are controlled by what exists in `burr/` when `flit build
--format wheel` runs
+
+## 1. Create the Source Release Candidate
+
+From the repo root:
+
+```bash
+python scripts/release_helper.py <version> <rc-num> <apache-id> [--dry-run]
[--build-wheel]
+```
+
+Example:
+
+```bash
+# Dry run (no git tag or SVN upload)
+python scripts/release_helper.py 0.41.0 0 myid --dry-run
+
+# Real release
+python scripts/release_helper.py 0.41.0 0 myid
+
+# With optional wheel
+python scripts/release_helper.py 0.41.0 0 myid --build-wheel
+```
+
+**What it does:**
+1. Reads version from `pyproject.toml`
+2. Cleans `dist/` directory
+3. **Removes `burr/tracking/server/build/`** to ensure no pre-built UI in
source tarball
+4. Runs `flit build --format sdist`
+ - Includes files specified in `[tool.flit.sdist] include`
+ - Excludes files specified in `[tool.flit.sdist] exclude`
+5. Creates Apache-branded tarball with GPG signatures and SHA512 checksums
+6. Tags git as `v{version}-incubating-RC{num}` (unless `--dry-run`)
+7. Uploads to Apache SVN (unless `--dry-run`)
+
+**Output:**
+- `dist/apache-burr-<version>-incubating.tar.gz` — ASF-branded source tarball
+- `dist/apache-burr-<version>-incubating.tar.gz.asc` — GPG signature
+- `dist/apache-burr-<version>-incubating.tar.gz.sha512` — SHA512 checksum
+
+## 2. Test the Source Release (Voter Simulation)
+
+This simulates what Apache voters and release managers will do when validating
the release.
+
+**Automated testing:**
+
+```bash
+bash scripts/simulate_release.sh
+```
+
+This script:
+1. Cleans `/tmp/burr-release-test/`
+2. Extracts the Apache tarball
+3. Creates a fresh virtual environment
+4. Builds UI artifacts and wheel (next step)
+5. Verifies both packages and prints their locations
+
+**Manual testing:**
+
+```bash
+cd /tmp
+tar -xzf /path/to/dist/apache-burr-<version>-incubating.tar.gz
+cd apache-burr-<version>-incubating
+
+# Verify source contents
+ls scripts/ # Build scripts should be present
+ls telemetry/ui/ # UI source should be present
+ls examples/ # Example directories should be present
+ls burr/tracking/server/build/ # Should NOT exist (no pre-built UI)
+
+# Create clean environment
+python -m venv venv && source venv/bin/activate
+pip install -e .
+pip install flit
+
+# Build artifacts and wheel (see step 3)
+python scripts/build_artifacts.py all --clean
+ls dist/*.whl
+deactivate
+```
+
+## 3. Build Artifacts and Wheel
+
+The `build_artifacts.py` script has three subcommands:
+
+### Build everything (recommended):
+
+```bash
+python scripts/build_artifacts.py all --clean
+```
+
+This runs both `artifacts` and `wheel` subcommands in sequence.
+
+### Build UI artifacts only:
+
+```bash
+python scripts/build_artifacts.py artifacts [--skip-install]
+```
+
+**What it does:**
+1. Checks for Node.js and npm
+2. **Cleans `burr/tracking/server/build/`** to ensure fresh UI build
+3. Installs burr from source: `pip install -e .` (unless `--skip-install`)
+4. Runs `burr-admin-build-ui`:
+ - `npm install --prefix telemetry/ui`
+ - `npm run build --prefix telemetry/ui`
+ - **Creates `burr/tracking/server/build/`** and copies built UI into it
+5. Verifies UI assets exist in `burr/tracking/server/build/`
+
+### Build wheel only (assumes artifacts exist):
+
+```bash
+python scripts/build_artifacts.py wheel [--clean]
+```
+
+**What it does:**
+1. Checks for `flit`
+2. Verifies `burr/tracking/server/build/` contains UI assets
+3. Optionally cleans `dist/` (with `--clean`)
+4. Runs `flit build --format wheel`
+ - **Packages all files in `burr/` directory, including
`burr/tracking/server/build/`**
+ - Does NOT include files outside `burr/` (e.g., `telemetry/ui/`,
`scripts/`, `examples/`)
+5. Verifies `.whl` file was created
+
+**Output:** `dist/apache_burr-<version>-py3-none-any.whl` (includes bundled UI)
+
+**Note:** Flit normalizes the package name `apache-burr` to `apache_burr`
(underscore) in the filename.
+
+## 4. Upload to PyPI
+
+After building the wheel:
+
+```bash
+twine upload dist/apache_burr-<version>-py3-none-any.whl
+```
+
+**Note:** For PyPI, you may want to publish as `burr` instead of
`apache-burr`. See the dual distribution strategy documentation.
+
+## Package Contents Reference
+
+Understanding what goes in each package type:
+
+### Source tarball (`apache-burr-{version}-incubating.tar.gz`)
+
+**Controlled by:** `[tool.flit.sdist]` in `pyproject.toml` +
`release_helper.py` cleanup
+
+**Includes:**
+- ✅ `burr/` — Full package source code
+- ✅ `scripts/` — Build helper scripts (this directory!)
+- ✅ `telemetry/ui/` — UI source code (package.json, src/, public/, etc.)
+- ✅ `examples/email-assistant/`, `examples/multi-modal-chatbot/`, etc. —
Selected example directories
+- ✅ `LICENSE`, `NOTICE`, `DISCLAIMER` — Apache license files
+
+**Excludes:**
+- ❌ `burr/tracking/server/build/` — Deleted by `release_helper.py` before build
+- ❌ `telemetry/ui/node_modules/` — Excluded by `[tool.flit.sdist]`
+- ❌ `telemetry/ui/build/`, `telemetry/ui/dist/` — Excluded by
`[tool.flit.sdist]`
+- ❌ `docs/`, `.git/`, `.github/` — Excluded by `[tool.flit.sdist]`
+
+**How it's built:**
+```bash
+rm -rf burr/tracking/server/build # Ensure no pre-built UI
+flit build --format sdist # Build from [tool.flit.sdist] config
+```
+
+---
+
+### Wheel (`apache_burr-{version}-py3-none-any.whl`)
+
+**Controlled by:** What exists in `burr/` directory when `flit build --format
wheel` runs
+
+**Includes:**
+- ✅ `burr/` — Complete package (all `.py` files, `py.typed`, etc.)
+- ✅ `burr/tracking/server/build/` — **Pre-built UI assets** (created by
`build_artifacts.py`)
+- ✅ `burr/tracking/server/demo_data/` — Demo data files
+
+**Excludes:**
+- ❌ `telemetry/ui/` — Not in `burr/` package
+- ❌ `examples/` — Not in `burr/` package (sdist-only)
+- ❌ `scripts/` — Not in `burr/` package (sdist-only)
+- ❌ `LICENSE`, `NOTICE`, `DISCLAIMER` — Not needed in wheel (sdist-only)
+
+**How it's built:**
+```bash
+burr-admin-build-ui # Creates burr/tracking/server/build/
+flit build --format wheel # Packages everything in burr/
+```
+
+---
+
+### Key Insight
+
+The **same `burr/` source directory** produces different outputs based on
**when you build** and **what format**:
+
+1. **sdist (source tarball):** Includes external files (`scripts/`,
`telemetry/ui/`, `examples/`) via `[tool.flit.sdist]` config, but excludes
`burr/tracking/server/build/` because we delete it first.
+
+2. **wheel (binary distribution):** Only packages `burr/` directory contents,
but includes `burr/tracking/server/build/` because we create it before building
the wheel.
diff --git a/scripts/build_artifacts.py b/scripts/build_artifacts.py
new file mode 100644
index 00000000..9a62027a
--- /dev/null
+++ b/scripts/build_artifacts.py
@@ -0,0 +1,290 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+Build artifacts/wheels helper with subcommands:
+
+ python scripts/build_artifacts.py artifacts [--skip-install]
+ python scripts/build_artifacts.py wheel [--clean]
+ python scripts/build_artifacts.py all [--skip-install] [--clean]
+
+Subcommands:
+ artifacts -> Build UI artifacts only
+ wheel -> Build wheel (requires artifacts to exist)
+ all -> Run both steps (artifacts then wheel)
+"""
+
+import argparse
+import os
+import shutil
+import subprocess
+import sys
+
+
+def _ensure_project_root() -> bool:
+ if not os.path.exists("pyproject.toml"):
+ print("Error: pyproject.toml not found.")
+ print("Please run this script from the root of the Burr source
directory.")
+ return False
+ return True
+
+
+def _check_node_prereqs() -> bool:
+ print("Checking for required tools...")
+ required_tools = ["node", "npm"]
+ missing_tools = []
+
+ for tool in required_tools:
+ if shutil.which(tool) is None:
+ missing_tools.append(tool)
+ print(f" ✗ '{tool}' not found")
+ else:
+ print(f" ✓ '{tool}' found")
+
+ if missing_tools:
+ print(f"\nError: Missing required tools: {', '.join(missing_tools)}")
+ print("Please install Node.js and npm to build the UI.")
+ return False
+
+ print("All required tools found.\n")
+ return True
+
+
+def _require_flit() -> bool:
+ if shutil.which("flit") is None:
+ print("✗ flit CLI not found. Please install it with: pip install flit")
+ return False
+ print("✓ flit CLI found.\n")
+ return True
+
+
+def _install_burr(skip_install: bool) -> bool:
+ if skip_install:
+ print("Skipping burr installation as requested.\n")
+ return True
+
+ print("Installing burr from source...")
+ try:
+ subprocess.run(
+ [sys.executable, "-m", "pip", "install", "-e", "."],
+ check=True,
+ cwd=os.getcwd(),
+ )
+ print("✓ Burr installed successfully.\n")
+ return True
+ except subprocess.CalledProcessError as exc:
+ print(f"✗ Error installing burr: {exc}")
+ return False
+
+
+def _build_ui() -> bool:
+ print("Building UI assets...")
+ try:
+ env = os.environ.copy()
+ env["BURR_PROJECT_ROOT"] = os.getcwd()
+ subprocess.run(["burr-admin-build-ui"], check=True, env=env)
+ print("✓ UI build completed successfully.\n")
+ return True
+ except subprocess.CalledProcessError as exc:
+ print(f"✗ Error building UI: {exc}")
+ return False
+
+
+def _verify_artifacts() -> bool:
+ build_dir = "burr/tracking/server/build"
+ print(f"Verifying build output in {build_dir}...")
+
+ if not os.path.exists(build_dir):
+ print(f"Build directory missing, creating placeholder at
{build_dir}...")
+ os.makedirs(build_dir, exist_ok=True)
+
+ if not os.listdir(build_dir):
+ print(f"✗ Build directory is empty: {build_dir}")
+ return False
+
+ print("✓ Build output verified.\n")
+ return True
+
+
+def _clean_dist():
+ if os.path.exists("dist"):
+ print("Cleaning dist/ directory...")
+ shutil.rmtree("dist")
+ print("✓ dist/ directory cleaned.\n")
+
+
+def _clean_ui_build():
+ """Remove any existing UI build directory to ensure clean state."""
+ ui_build_dir = "burr/tracking/server/build"
+ if os.path.exists(ui_build_dir):
+ print(f"Cleaning existing UI build directory: {ui_build_dir}")
+ shutil.rmtree(ui_build_dir)
+ print("✓ UI build directory cleaned.\n")
+
+
+def _build_wheel() -> bool:
+ print("Building wheel distribution with 'flit build --format wheel'...")
+ try:
+ env = os.environ.copy()
+ env["FLIT_USE_VCS"] = "0"
+ subprocess.run(["flit", "build", "--format", "wheel"], check=True,
env=env)
+ print("✓ Wheel build completed successfully.\n")
+ return True
+ except subprocess.CalledProcessError as exc:
+ print(f"✗ Error building wheel: {exc}")
+ return False
+
+
+def _verify_wheel() -> bool:
+ print("Verifying wheel output...")
+
+ if not os.path.exists("dist"):
+ print("✗ dist/ directory not found")
+ return False
+
+ wheel_files = [f for f in os.listdir("dist") if f.endswith(".whl")]
+ if not wheel_files:
+ print("✗ No wheel files found in dist/")
+ if os.listdir("dist"):
+ print("Contents of dist/ directory:")
+ for item in os.listdir("dist"):
+ print(f" - {item}")
+ return False
+
+ print(f"✓ Found {len(wheel_files)} wheel file(s):")
+ for wheel_file in wheel_files:
+ wheel_path = os.path.join("dist", wheel_file)
+ size = os.path.getsize(wheel_path)
+ print(f" - {wheel_file} ({size:,} bytes)")
+
+ print()
+ return True
+
+
+def create_artifacts(skip_install: bool) -> bool:
+ if not _ensure_project_root():
+ print("Failed to confirm project root.")
+ return False
+ if not _check_node_prereqs():
+ print("Node/npm prerequisite check failed.")
+ return False
+ # Clean any existing UI build to ensure fresh state
+ _clean_ui_build()
+ if not _install_burr(skip_install):
+ print("Installing burr from source failed.")
+ return False
+ if not _build_ui():
+ print("UI build failed.")
+ return False
+ if not _verify_artifacts():
+ print("UI artifact verification failed.")
+ return False
+ return True
+
+
+def create_wheel(clean: bool) -> bool:
+ if not _ensure_project_root():
+ print("Failed to confirm project root.")
+ return False
+ if not _require_flit():
+ print("Missing flit CLI.")
+ return False
+ if not _verify_artifacts():
+ print("Please run the 'artifacts' subcommand first.")
+ return False
+ if clean:
+ _clean_dist()
+ if not _build_wheel():
+ return False
+ if not _verify_wheel():
+ return False
+ return True
+
+
+def build_all(skip_install: bool, clean: bool) -> bool:
+ if not create_artifacts(skip_install=skip_install):
+ return False
+ if not create_wheel(clean=clean):
+ return False
+ return True
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Build artifacts/wheels for Burr using subcommands."
+ )
+ subparsers = parser.add_subparsers(dest="command", required=True)
+
+ artifacts_parser = subparsers.add_parser("artifacts", help="Build UI
artifacts only.")
+ artifacts_parser.add_argument(
+ "--skip-install",
+ action="store_true",
+ help="Skip reinstalling burr when building artifacts",
+ )
+
+ wheel_parser = subparsers.add_parser(
+ "wheel", help="Build wheel distribution (requires artifacts)."
+ )
+ wheel_parser.add_argument(
+ "--clean",
+ action="store_true",
+ help="Clean dist/ directory before building wheel",
+ )
+
+ all_parser = subparsers.add_parser("all", help="Build artifacts and wheel
in sequence.")
+ all_parser.add_argument(
+ "--skip-install",
+ action="store_true",
+ help="Skip reinstalling burr when building artifacts",
+ )
+ all_parser.add_argument(
+ "--clean",
+ action="store_true",
+ help="Clean dist/ directory before building wheel",
+ )
+
+ args = parser.parse_args()
+
+ print("=" * 80)
+ print(f"Burr Build Helper - command: {args.command}")
+ print("=" * 80)
+ print()
+
+ success = False
+ if args.command == "artifacts":
+ success = create_artifacts(skip_install=args.skip_install)
+ elif args.command == "wheel":
+ success = create_wheel(clean=args.clean)
+ elif args.command == "all":
+ success = build_all(skip_install=args.skip_install, clean=args.clean)
+
+ if success:
+ print("=" * 80)
+ print("✅ Build Complete!")
+ print("=" * 80)
+ if args.command in {"wheel", "all"}:
+ print("\nWheel files are in the dist/ directory.")
+ print("You can now upload to PyPI with:")
+ print(" twine upload dist/*.whl")
+ print()
+ else:
+ print("\n❌ Build failed.")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/release_helper.py b/scripts/release_helper.py
new file mode 100644
index 00000000..23a7d896
--- /dev/null
+++ b/scripts/release_helper.py
@@ -0,0 +1,444 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import argparse
+import glob
+import hashlib
+import os
+import re
+import shutil
+import subprocess
+import sys
+
+# --- Configuration ---
+# You need to fill these in for your project.
+# The name of your project's short name (e.g., 'myproject').
+PROJECT_SHORT_NAME = "burr"
+# The file where you want to update the version number.
+VERSION_FILE = "pyproject.toml"
+# A regular expression pattern to find the version string in the VERSION_FILE.
+VERSION_PATTERN = r'version\s*=\s*"(\d+\.\d+\.\d+)"'
+
+
+def _fail(message: str) -> None:
+ print(f"\n❌ {message}")
+ sys.exit(1)
+
+
+def get_version_from_file(file_path: str) -> str:
+ """Get the version from a file."""
+ with open(file_path, encoding="utf-8") as f:
+ content = f.read()
+ match = re.search(VERSION_PATTERN, content)
+ if match:
+ version = match.group(1)
+ return version
+ raise ValueError(f"Could not find version in {file_path}")
+
+
+def check_prerequisites():
+ """Checks for necessary command-line tools and Python modules."""
+ print("Checking for required tools...")
+ required_tools = ["git", "gpg", "svn", "flit"]
+ for tool in required_tools:
+ if shutil.which(tool) is None:
+ _fail(
+ f"Required tool '{tool}' not found. Please install it and
ensure it's in your PATH."
+ )
+
+ print("All required tools found.")
+
+
+def update_version(version, _rc_num):
+ """Updates the version number in the specified file."""
+ print(f"Updating version in {VERSION_FILE} to {version}...")
+ try:
+ with open(VERSION_FILE, "r", encoding="utf-8") as f:
+ content = f.read()
+ # For pyproject.toml, we just update the version string directly
+ new_version_string = f'version = "{version}"'
+ new_content = re.sub(VERSION_PATTERN, new_version_string, content)
+ if new_content == content:
+ print("Error: Could not find or replace version string. Check your
VERSION_PATTERN.")
+ return False
+
+ with open(VERSION_FILE, "w", encoding="utf-8") as f:
+ f.write(new_content)
+
+ print("Version updated successfully.")
+ return True
+
+ except FileNotFoundError:
+ _fail(f"{VERSION_FILE} not found.")
+ except (OSError, re.error) as e:
+ _fail(f"An error occurred while updating the version: {e}")
+
+
+def sign_artifacts(archive_name: str) -> list[str]:
+ """Creates signed files for the designated artifact."""
+ files = []
+ # Sign the tarball with GPG. The user must have a key configured.
+ try:
+ subprocess.run(
+ ["gpg", "--armor", "--output", f"{archive_name}.asc",
"--detach-sig", archive_name],
+ check=True,
+ )
+ files.append(f"{archive_name}.asc")
+ print(f"Created GPG signature: {archive_name}.asc")
+ except subprocess.CalledProcessError as e:
+ _fail(f"Error signing tarball {archive_name}: {e}")
+
+ # Generate SHA512 checksum.
+ sha512_hash = hashlib.sha512()
+ with open(archive_name, "rb") as f:
+ while True:
+ data = f.read(65536)
+ if not data:
+ break
+ sha512_hash.update(data)
+
+ with open(f"{archive_name}.sha512", "w", encoding="utf-8") as f:
+ f.write(f"{sha512_hash.hexdigest()}\n")
+ print(f"Created SHA512 checksum: {archive_name}.sha512")
+ files.append(f"{archive_name}.sha512")
+ return files
+
+
+def create_release_artifacts(version, build_wheel=False) -> list[str]:
+ """Creates the source tarball, GPG signatures, and checksums using flit.
+
+ Args:
+ version: The version string for the release
+ build_wheel: If True, also build and sign a wheel distribution
+ """
+ print("\n[Step 1/1] Creating source release artifacts with 'flit
build'...")
+
+ # Clean the dist directory before building.
+ if os.path.exists("dist"):
+ shutil.rmtree("dist")
+ # Ensure no pre-built UI assets slip into the source package.
+ ui_build_dir = os.path.join("burr", "tracking", "server", "build")
+ if os.path.exists(ui_build_dir):
+ print("Removing previously built UI artifacts...")
+ shutil.rmtree(ui_build_dir)
+
+ # Warn if git working tree is dirty/untracked
+ try:
+ dirty = (
+ subprocess.check_output(["git", "status", "--porcelain"],
stderr=subprocess.DEVNULL)
+ .decode()
+ .strip()
+ )
+ if dirty:
+ print(
+ "⚠️ Detected untracked or modified files. flit may refuse to
build; "
+ "consider committing/stashing or verify FLIT_USE_VCS=0."
+ )
+ print(" Git status summary:")
+ for line in dirty.splitlines():
+ print(f" {line}")
+ except subprocess.CalledProcessError:
+ pass
+
+ # Use flit to create the source distribution.
+ try:
+ env = os.environ.copy()
+ env["FLIT_USE_VCS"] = "0"
+ subprocess.run(["flit", "build", "--format", "sdist"], check=True,
env=env)
+ print("✓ flit sdist created successfully.")
+ except subprocess.CalledProcessError as e:
+ _fail(f"Error creating source distribution: {e}")
+
+ # Find the created tarball in the dist directory.
+ # Note: flit normalizes hyphens to underscores in filenames
+ expected_tar_ball = f"dist/apache_burr-{version.lower()}.tar.gz"
+ tarball_path = glob.glob(expected_tar_ball)
+
+ if not tarball_path:
+ details = []
+ if os.path.exists("dist"):
+ details.append("Contents of 'dist':")
+ for item in os.listdir("dist"):
+ details.append(f"- {item}")
+ else:
+ details.append("'dist' directory not found.")
+ _fail(
+ "Could not find the generated source tarball in the 'dist'
directory.\n"
+ + "\n".join(details)
+ )
+
+ # Rename the tarball to apache-burr-{version.lower()}-incubating.tar.gz
+ apache_tar_ball = f"dist/apache-burr-{version.lower()}-incubating.tar.gz"
+ shutil.move(tarball_path[0], apache_tar_ball)
+ print(f"✓ Created source tarball: {apache_tar_ball}")
+
+ # Sign the Apache tarball
+ signed_files = sign_artifacts(apache_tar_ball)
+ all_files = [apache_tar_ball] + signed_files
+
+ # Optionally build the wheel (without built UI artifacts)
+ if build_wheel:
+ print("\n[Step 2/2] Creating wheel distribution with 'flit build
--format wheel'...")
+ try:
+ env = os.environ.copy()
+ env["FLIT_USE_VCS"] = "0"
+ subprocess.run(["flit", "build", "--format", "wheel"], check=True,
env=env)
+ print("✓ flit wheel created successfully.")
+ except subprocess.CalledProcessError as e:
+ _fail(f"Error creating wheel distribution: {e}")
+
+ # Find the created wheel in the dist directory.
+ # Note: flit normalizes hyphens to underscores in filenames
+ expected_wheel = f"dist/apache_burr-{version.lower()}-*.whl"
+ wheel_path = glob.glob(expected_wheel)
+
+ if not wheel_path:
+ details = []
+ if os.path.exists("dist"):
+ details.append("Contents of 'dist':")
+ for item in os.listdir("dist"):
+ details.append(f"- {item}")
+ else:
+ details.append("'dist' directory not found.")
+ _fail(
+ "Could not find the generated wheel in the 'dist'
directory.\n" + "\n".join(details)
+ )
+
+ # Rename the wheel to
apache-burr-{version.lower()}-incubating-{rest}.whl
+ # Extract the wheel tags (e.g., py3-none-any.whl)
+ original_wheel = os.path.basename(wheel_path[0])
+ # Pattern: apache_burr-{version}-{tags}.whl ->
apache-burr-{version}-incubating-{tags}.whl
+ wheel_tags = original_wheel.replace(f"apache_burr-{version.lower()}-",
"")
+ apache_wheel =
f"dist/apache-burr-{version.lower()}-incubating-{wheel_tags}"
+ shutil.move(wheel_path[0], apache_wheel)
+ print(f"✓ Created wheel: {apache_wheel}")
+
+ # Sign the Apache wheel
+ wheel_signed_files = sign_artifacts(apache_wheel)
+ all_files.extend([apache_wheel] + wheel_signed_files)
+
+ return all_files
+
+
+def svn_upload(version, rc_num, archive_files, apache_id):
+ """Uploads the artifacts to the ASF dev distribution repository."""
+ print("Uploading artifacts to ASF SVN...")
+ svn_path =
f"https://dist.apache.org/repos/dist/dev/incubator/{PROJECT_SHORT_NAME}/apache-burr/{version}-incubating-RC{rc_num}"
+
+ try:
+ # Create a new directory for the release candidate.
+ subprocess.run(
+ [
+ "svn",
+ "mkdir",
+ "-m",
+ f"Creating directory for {version}-incubating-RC{rc_num}",
+ svn_path,
+ ],
+ check=True,
+ )
+
+ # Get the files to import (tarball, asc, sha512).
+ files_to_import = archive_files
+
+ # Use svn import for the new directory.
+ for file_path in files_to_import:
+ subprocess.run(
+ [
+ "svn",
+ "import",
+ file_path,
+ f"{svn_path}/{os.path.basename(file_path)}",
+ "-m",
+ f"Adding {os.path.basename(file_path)}",
+ "--username",
+ apache_id,
+ ],
+ check=True,
+ )
+
+ print(f"Artifacts successfully uploaded to: {svn_path}")
+ return svn_path
+
+ except subprocess.CalledProcessError as e:
+ print(f"Error during SVN upload: {e}")
+ print("Make sure you have svn access configured for your Apache ID.")
+ return None
+
+
+def generate_email_template(version, rc_num, svn_url):
+ """Generates the content for the [VOTE] email."""
+ print("Generating email template...")
+ version_with_incubating = f"{version}-incubating"
+ tag = f"v{version}"
+
+ email_content = f"""[VOTE] Release Apache {PROJECT_SHORT_NAME}
{version_with_incubating} (release candidate {rc_num})
+
+Hi all,
+
+This is a call for a vote on releasing Apache {PROJECT_SHORT_NAME}
{version_with_incubating},
+release candidate {rc_num}.
+
+This release includes the following changes (see CHANGELOG for details):
+- [List key changes here]
+
+The artifacts for this release candidate can be found at:
+{svn_url}
+
+The Git tag to be voted upon is:
+{tag}
+
+The release hash is:
+[Insert git commit hash here]
+
+
+Release artifacts are signed with the following key:
+[Insert your GPG key ID here]
+The KEYS file is available at:
+https://downloads.apache.org/incubator/{PROJECT_SHORT_NAME}/KEYS
+
+Please download, verify, and test the release candidate.
+
+For testing, please run some of the examples, scripts/qualify.sh has
+a sampling of them to run.
+
+The vote will run for a minimum of 72 hours.
+Please vote:
+
+[ ] +1 Release this package as Apache {PROJECT_SHORT_NAME}
{version_with_incubating}
+[ ] +0 No opinion
+[ ] -1 Do not release this package because... (Please provide a reason)
+
+Checklist for reference:
+[ ] Download links are valid.
+[ ] Checksums and signatures.
+[ ] LICENSE/NOTICE files exist
+[ ] No unexpected binary files
+[ ] All source files have ASF headers
+[ ] Can compile from source
+
+On behalf of the Apache {PROJECT_SHORT_NAME} PPMC,
+[Your Name]
+"""
+ print("\n" + "=" * 80)
+ print("EMAIL TEMPLATE (COPY AND PASTE TO YOUR MAILING LIST)")
+ print("=" * 80)
+ print(email_content)
+ print("=" * 80)
+
+
+def main():
+ """
+ ### How to Use the Updated Script
+
+ 1. **Install flit**:
+ ```bash
+ pip install flit
+ ```
+ 2. **Configure the Script**: Open `apache_release_helper.py` in a text
editor and update the three variables at the top of the file with your
project's details:
+ * `PROJECT_SHORT_NAME`
+ * `VERSION_FILE` and `VERSION_PATTERN`
+ 3. **Prerequisites**:
+ * You must have `git`, `gpg`, `svn`, and `flit` installed.
+ * Your GPG key and SVN access must be configured for your Apache ID.
+ 4. **Run the Script**:
+ Open your terminal, navigate to the root of your project directory,
and run the script with the desired version, release candidate number, and
Apache ID.
+
+
+ python apache_release_helper.py 1.2.3 0 your_apache_id
+ """
+ parser = argparse.ArgumentParser(description="Automates parts of the
Apache release process.")
+ parser.add_argument("version", help="The new release version (e.g.,
'1.0.0').")
+ parser.add_argument("rc_num", help="The release candidate number (e.g.,
'0' for RC0).")
+ parser.add_argument("apache_id", help="Your apache user ID.")
+ parser.add_argument(
+ "--dry-run",
+ action="store_true",
+ help="Run in dry-run mode (skip git tag creation and SVN upload)",
+ )
+ parser.add_argument(
+ "--build-wheel",
+ action="store_true",
+ help="Also build and sign a wheel distribution (in addition to the
source tarball)",
+ )
+ args = parser.parse_args()
+
+ version = args.version
+ rc_num = args.rc_num
+ apache_id = args.apache_id
+ dry_run = args.dry_run
+ build_wheel = args.build_wheel
+
+ if dry_run:
+ print("\n*** DRY RUN MODE - No git tags or SVN uploads will be
performed ***\n")
+
+ check_prerequisites()
+
+ current_version = get_version_from_file(VERSION_FILE)
+ print(current_version)
+ if current_version != version:
+ _fail(
+ "Version mismatch. Update pyproject.toml to the requested version
before running the script."
+ )
+
+ tag_name = f"v{version}-incubating-RC{rc_num}"
+ if dry_run:
+ print(f"\n[DRY RUN] Would create git tag '{tag_name}'")
+ else:
+ print(f"\nChecking for git tag '{tag_name}'...")
+ try:
+ # Check if the tag already exists
+ existing_tag = subprocess.check_output(["git", "tag", "-l",
tag_name]).decode().strip()
+ if existing_tag == tag_name:
+ print(f"Git tag '{tag_name}' already exists.")
+ response = (
+ input("Do you want to continue without creating a new tag?
(y/n): ")
+ .lower()
+ .strip()
+ )
+ if response != "y":
+ print("Aborting.")
+ sys.exit(1)
+ else:
+ # Tag does not exist, create it
+ print(f"Creating git tag '{tag_name}'...")
+ subprocess.run(["git", "tag", tag_name], check=True)
+ print(f"Git tag {tag_name} created.")
+ except subprocess.CalledProcessError as e:
+ _fail(f"Error checking or creating Git tag: {e}")
+
+ # Create artifacts
+ archive_files = create_release_artifacts(version, build_wheel=build_wheel)
+
+ # Upload artifacts
+ # NOTE: You MUST have your SVN client configured to use your Apache ID and
have permissions.
+ if dry_run:
+ svn_url =
f"https://dist.apache.org/repos/dist/dev/incubator/{PROJECT_SHORT_NAME}/apache-burr/{version}-incubating-RC{rc_num}"
+ print(f"\n[DRY RUN] Would upload artifacts to: {svn_url}")
+ else:
+ svn_url = svn_upload(version, rc_num, archive_files, apache_id)
+ if not svn_url:
+ _fail("SVN upload failed.")
+
+ # Generate email
+ generate_email_template(version, rc_num, svn_url)
+
+ print("\nProcess complete. Please copy the email template to your mailing
list.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/setup_keys.sh b/scripts/setup_keys.sh
new file mode 100755
index 00000000..a4f12615
--- /dev/null
+++ b/scripts/setup_keys.sh
@@ -0,0 +1,95 @@
+#!/bin/bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# This script helps new Apache committers set up their GPG keys for releases.
+# It guides you through creating a new key, exports the public key, and
+# provides instructions on how to add it to your project's KEYS file.
+
+echo "========================================================"
+echo " Apache GPG Key Setup Script"
+echo "========================================================"
+echo " "
+echo "Step 1: Generating a new GPG key."
+echo " "
+echo "Please be aware of Apache's best practices for GPG keys:"
+echo "- **Key Type:** Select **(1) RSA and RSA**."
+echo "- **Key Size:** Enter **4096**."
+echo "- **Email Address:** Use your official **@apache.org** email address."
+echo "- **Passphrase:** Use a strong, secure passphrase."
+echo " "
+read -p "Press [Enter] to start the GPG key generation..."
+
+# Generate a new GPG key
+# The --batch and --passphrase-fd 0 options are used for automation,
+# but the script will still require interactive input.
+gpg --full-gen-key
+
+if [ $? -ne 0 ]; then
+ echo "Error: GPG key generation failed. Please check your GPG installation."
+ exit 1
+fi
+
+echo " "
+echo "Step 2: Listing your GPG keys to find the new key ID."
+echo "Your new key is listed under 'pub' with a string of 8 or 16 characters
after the '/'."
+
+# List all GPG keys
+gpg --list-keys
+
+echo " "
+read -p "Please copy and paste your new key ID here (e.g., A1B2C3D4 or
1234ABCD5678EF01): " KEY_ID
+
+if [ -z "$KEY_ID" ]; then
+ echo "Error: Key ID cannot be empty. Exiting."
+ exit 1
+fi
+
+echo " "
+echo "Step 3: Exporting your public key to a file."
+
+# Export the public key in ASCII armored format
+gpg --armor --export "$KEY_ID" > "$KEY_ID.asc"
+
+if [ $? -ne 0 ]; then
+ echo "Error: Public key export failed. Please ensure the Key ID is correct."
+ rm -f "$KEY_ID.asc"
+ exit 1
+fi
+
+echo "Checking out dist repository to update KEYS file"
+svn checkout --depth immediates https://dist.apache.org/repos/dist dist
+cd dist/release
+svn checkout https://dist.apache.org/repos/dist/release/incubator/burr
incubator/burr
+
+cd ../../
+gpg --list-keys "$KEY_ID" >> dist/release/incubator/burr/KEYS
+cat "$KEY_ID.asc" >> dist/release/incubator/burr/KEYS
+cd dist/release/incubator/burr
+
+echo " "
+echo "========================================================"
+echo " Setup Complete!"
+echo "========================================================"
+echo "Your public key has been saved to: $KEY_ID.asc"
+echo " "
+echo "NEXT STEPS (VERY IMPORTANT):"
+echo "1. Please inspect the KEYS file to ensure the new key is added
correctly. It should be in the current directory."
+echo "2. If all good run: svn update KEYS && svn commit -m \"Adds new key
$KEY_ID for YOUR NAME\""
+echo "3. Inform the mailing list that you've updated the KEYS file."
+echo " The updated KEYS file is essential for others to verify your release
signatures."
+echo " "
diff --git a/telemetry/ui/.eslintrc.js b/telemetry/ui/.eslintrc.js
index 01becbf9..da3d675e 100644
--- a/telemetry/ui/.eslintrc.js
+++ b/telemetry/ui/.eslintrc.js
@@ -1,3 +1,20 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
module.exports = {
env: {
browser: true,
diff --git a/telemetry/ui/package-lock.json b/telemetry/ui/package-lock.json
index b71fbefc..21461ed4 100644
--- a/telemetry/ui/package-lock.json
+++ b/telemetry/ui/package-lock.json
@@ -25,6 +25,7 @@
"@uiw/react-json-view": "^2.0.0-alpha.12",
"clsx": "^2.1.0",
"dagre": "^0.8.5",
+ "es-abstract": "^1.22.4",
"fuse.js": "^7.0.0",
"heroicons": "^2.1.1",
"react": "^18.2.0",
@@ -38,6 +39,7 @@
"react-syntax-highlighter": "^15.5.0",
"reactflow": "^11.10.4",
"remark-gfm": "^4.0.0",
+ "string.prototype.matchall": "^4.0.10",
"tailwindcss-question-mark": "^0.4.0",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
diff --git a/telemetry/ui/package.json b/telemetry/ui/package.json
index 1c492bf3..1a391fcc 100644
--- a/telemetry/ui/package.json
+++ b/telemetry/ui/package.json
@@ -20,6 +20,7 @@
"@uiw/react-json-view": "^2.0.0-alpha.12",
"clsx": "^2.1.0",
"dagre": "^0.8.5",
+ "es-abstract": "^1.22.4",
"fuse.js": "^7.0.0",
"heroicons": "^2.1.1",
"react": "^18.2.0",
@@ -33,6 +34,7 @@
"react-syntax-highlighter": "^15.5.0",
"reactflow": "^11.10.4",
"remark-gfm": "^4.0.0",
+ "string.prototype.matchall": "^4.0.10",
"tailwindcss-question-mark": "^0.4.0",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
diff --git a/tests/test_release_config.py b/tests/test_release_config.py
new file mode 100644
index 00000000..f0f57ea5
--- /dev/null
+++ b/tests/test_release_config.py
@@ -0,0 +1,153 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+Tests to validate release configuration in pyproject.toml.
+
+This ensures the examples include/exclude lists stay in sync with the actual
+examples directory structure.
+"""
+
+import sys
+from pathlib import Path
+
+import pytest
+
+# tomllib is only available in Python 3.11+
+if sys.version_info >= (3, 11):
+ import tomllib
+else:
+ tomllib = None
+
+
[email protected](sys.version_info < (3, 11), reason="tomllib requires
Python 3.11+")
+def test_examples_include_exclude_coverage():
+ """
+ Verify that pyproject.toml's [tool.flit.sdist] include/exclude lists cover
+ all example directories.
+
+ WHY THIS TEST EXISTS:
+ Flit automatically includes the examples/ directory in the release tarball
because
+ it's a Python package (has __init__.py). Without explicit include/exclude
rules,
+ ALL examples would be shipped in the Apache release, which is not intended.
+
+ For Apache releases, we only want to include 4 specific examples for
voters to test:
+ - email-assistant
+ - multi-modal-chatbot
+ - streaming-fastapi
+ - deep-researcher
+
+ All other examples must be explicitly excluded. This test ensures the
configuration
+ stays in sync with the filesystem when examples are added/removed.
+
+ If this test fails, you need to update pyproject.toml:
+ - To INCLUDE an example: add it to [tool.flit.sdist] include list
+ - To EXCLUDE an example: add it to [tool.flit.sdist] exclude list
+ """
+ # Load pyproject.toml
+ project_root = Path(__file__).parent.parent
+ pyproject_path = project_root / "pyproject.toml"
+
+ with open(pyproject_path, "rb") as f:
+ config = tomllib.load(f)
+
+ flit_sdist = config.get("tool", {}).get("flit", {}).get("sdist", {})
+ include_patterns = flit_sdist.get("include", [])
+ exclude_patterns = flit_sdist.get("exclude", [])
+
+ # Extract example directories from include patterns
+ included_examples = set()
+ for pattern in include_patterns:
+ if pattern.startswith("examples/") and pattern.endswith("/**"):
+ # Extract directory name from patterns like
"examples/email-assistant/**"
+ dir_name = pattern.removeprefix("examples/").removesuffix("/**")
+ included_examples.add(dir_name)
+
+ # Extract example directories from exclude patterns
+ excluded_examples = set()
+ excluded_files = set()
+ for pattern in exclude_patterns:
+ if pattern.startswith("examples/"):
+ if pattern.endswith("/**"):
+ # Directory pattern like "examples/adaptive-crag/**"
+ dir_name =
pattern.removeprefix("examples/").removesuffix("/**")
+ excluded_examples.add(dir_name)
+ else:
+ # File pattern like "examples/__init__.py"
+ file_name = pattern.removeprefix("examples/")
+ excluded_files.add(file_name)
+
+ # Get actual example directories from filesystem
+ examples_dir = project_root / "examples"
+ actual_dirs = set()
+ actual_files = set()
+
+ if examples_dir.exists():
+ for item in examples_dir.iterdir():
+ if item.name.startswith(".") or item.name == "__pycache__":
+ continue
+ if item.is_dir():
+ actual_dirs.add(item.name)
+ else:
+ actual_files.add(item.name)
+
+ # Check coverage
+ configured_dirs = included_examples | excluded_examples
+ missing_from_config = actual_dirs - configured_dirs
+ extra_in_config = configured_dirs - actual_dirs
+
+ # Build error message if mismatch found
+ errors = []
+
+ if missing_from_config:
+ errors.append(
+ f"\n❌ Example directories exist but are NOT in pyproject.toml
config:\n"
+ f" {sorted(missing_from_config)}\n"
+ f"\n WHY THIS MATTERS:\n"
+ f" Flit auto-discovers examples/ as a package (it has
__init__.py) and will\n"
+ f" include ALL subdirectories in the release tarball unless
explicitly excluded.\n"
+ f" Every example directory MUST be either included or excluded
to ensure the\n"
+ f" Apache release contains only the intended examples for voters
to test.\n"
+ f"\n To fix: Add to pyproject.toml [tool.flit.sdist]:\n"
+ f" - To INCLUDE in Apache release: add 'examples/<name>/**' to
'include' list\n"
+ f" - To EXCLUDE from Apache release: add 'examples/<name>/**' to
'exclude' list\n"
+ f"\n Currently only these 4 examples should be included:\n"
+ f" email-assistant, multi-modal-chatbot, streaming-fastapi,
deep-researcher\n"
+ )
+
+ if extra_in_config:
+ errors.append(
+ f"\n❌ Example directories in pyproject.toml but NOT in
filesystem:\n"
+ f" {sorted(extra_in_config)}\n"
+ f"\n WHY THIS MATTERS:\n"
+ f" These entries reference examples that no longer exist and
should be removed\n"
+ f" to keep the configuration accurate and maintainable.\n"
+ f"\n To fix: Remove these entries from pyproject.toml
[tool.flit.sdist]\n"
+ )
+
+ # Report what's currently configured (for debugging)
+ if errors:
+ summary = (
+ f"\n📋 Current configuration:\n"
+ f" Included examples ({len(included_examples)}):
{sorted(included_examples)}\n"
+ f" Excluded examples ({len(excluded_examples)}):
{sorted(excluded_examples)}\n"
+ f" Excluded files ({len(excluded_files)}):
{sorted(excluded_files)}\n"
+ f" Actual directories ({len(actual_dirs)}):
{sorted(actual_dirs)}\n"
+ )
+ errors.append(summary)
+
+ assert not errors, "\n".join(errors)