This is an automated email from the ASF dual-hosted git repository.

skrawcz pushed a commit to branch stefan/update-package-names
in repository https://gitbox.apache.org/repos/asf/hamilton.git

commit 2d3baede6e67176cfefaae55e2e5ffe5b7e7f670
Author: Stefan Krawczyk <[email protected]>
AuthorDate: Sat Feb 21 17:19:53 2026 -0800

    Update apache_release_helper.py to support per-package builds
    
    - Add PACKAGE_CONFIGS dict with settings for all 5 packages (hamilton, sdk, 
lsp, contrib, ui)
    - Add --package CLI argument to select which package to build
    - Update all functions to work with package-specific directories and names
    - Follow Burr script structure for better organization
    - Support building from different working directories (root, ui/sdk, 
dev_tools/language_server, contrib, ui/backend)
    - Update SVN paths to include package name for multi-package support
    - Update git tags to include package name (e.g., 
apache-hamilton-sdk-v0.8.0-incubating-RC0)
    - Update email templates for per-package releases
---
 scripts/apache_release_helper.py | 352 ++++++++++++++++++++++++++-------------
 1 file changed, 236 insertions(+), 116 deletions(-)

diff --git a/scripts/apache_release_helper.py b/scripts/apache_release_helper.py
index 027a2427..3c3b7937 100644
--- a/scripts/apache_release_helper.py
+++ b/scripts/apache_release_helper.py
@@ -27,31 +27,68 @@ import tempfile
 import zipfile
 
 # --- Configuration ---
-# You need to fill these in for your project.
-# The name of your project's short name (e.g., 'myproject').
 PROJECT_SHORT_NAME = "hamilton"
-# The file where you want to update the version number.
-# Common options are setup.py, __init__.py, or a dedicated VERSION file.
-# For example: "src/main/python/myproject/__init__.py"
+
+# Package configurations: each Hamilton package has its own settings
+PACKAGE_CONFIGS = {
+    "hamilton": {
+        "name": "apache-hamilton",
+        "working_dir": ".",
+        "version_file": "hamilton/version.py",
+        "version_pattern": r"VERSION = \((\d+), (\d+), (\d+)(, \"(\w+)\")?\)",
+        "version_extractor": lambda match: 
f"{match.group(1)}.{match.group(2)}.{match.group(3)}",
+    },
+    "sdk": {
+        "name": "apache-hamilton-sdk",
+        "working_dir": "ui/sdk",
+        "version_file": "ui/sdk/pyproject.toml",
+        "version_pattern": r'version\s*=\s*"(\d+\.\d+\.\d+)"',
+        "version_extractor": lambda match: match.group(1),
+    },
+    "lsp": {
+        "name": "apache-hamilton-lsp",
+        "working_dir": "dev_tools/language_server",
+        "version_file": "dev_tools/language_server/pyproject.toml",
+        "version_pattern": r'version\s*=\s*"(\d+\.\d+\.\d+)"',
+        "version_extractor": lambda match: match.group(1),
+    },
+    "contrib": {
+        "name": "apache-hamilton-contrib",
+        "working_dir": "contrib",
+        "version_file": "contrib/pyproject.toml",
+        "version_pattern": r'version\s*=\s*"(\d+\.\d+\.\d+)"',
+        "version_extractor": lambda match: match.group(1),
+    },
+    "ui": {
+        "name": "apache-hamilton-ui",
+        "working_dir": "ui/backend",
+        "version_file": "ui/backend/pyproject.toml",
+        "version_pattern": r'version\s*=\s*"(\d+\.\d+\.\d+)"',
+        "version_extractor": lambda match: match.group(1),
+    },
+}
+
+# Legacy configuration (kept for backward compatibility with single 
VERSION_FILE references)
 VERSION_FILE = "hamilton/version.py"
-# A regular expression pattern to find the version string in the VERSION_FILE.
-# For example: r"__version__ = \"(\d+\.\d+\.\d+)\""
-# The capture group (parentheses) should capture the version number.
 VERSION_PATTERN = r"VERSION = \((\d+), (\d+), (\d+)(, \"(\w+)\")?\)"
 
 
-def get_version_from_file(file_path: str) -> str:
-    """Get the version from a file."""
+def get_version_from_file(package_config: dict) -> str:
+    """Get the version from a file using package-specific configuration."""
     import re
 
+    file_path = package_config["version_file"]
+    pattern = package_config["version_pattern"]
+    extractor = package_config["version_extractor"]
+
     with open(file_path) as f:
         content = f.read()
-    match = re.search(VERSION_PATTERN, content)
+    match = re.search(pattern, content)
     if match:
-        major, minor, patch, rc_group, rc = match.groups()
-        version = f"{major}.{minor}.{patch}"
-        if rc:
+        # Check for RC in the match (only for main hamilton package)
+        if len(match.groups()) >= 5 and match.group(5):
             raise ValueError("Do not commit RC to the version file.")
+        version = extractor(match)
         return version
     raise ValueError(f"Could not find version in {file_path}")
 
@@ -78,32 +115,46 @@ def check_prerequisites():
     print("All required tools found.")
 
 
-def update_version(version, rc_num):
+def update_version(package_config: dict, version, rc_num):
     """Updates the version number in the specified file."""
     import re
 
-    print(f"Updating version in {VERSION_FILE} to {version} RC{rc_num}...")
+    version_file = package_config["version_file"]
+    pattern = package_config["version_pattern"]
+
+    print(f"Updating version in {version_file} to {version} RC{rc_num}...")
     try:
-        with open(VERSION_FILE, "r") as f:
+        with open(version_file, "r") as f:
             content = f.read()
-        major, minor, patch = version.split(".")
-        if int(rc_num) >= 0:
-            new_version_tuple = f'VERSION = ({major}, {minor}, {patch}, 
"RC{rc_num}")'
+
+        # Only the main hamilton package uses the tuple format with RC
+        if package_config["name"] == "apache-hamilton":
+            major, minor, patch = version.split(".")
+            if int(rc_num) >= 0:
+                new_version_tuple = f'VERSION = ({major}, {minor}, {patch}, 
"RC{rc_num}")'
+            else:
+                new_version_tuple = f"VERSION = ({major}, {minor}, {patch})"
+            new_content = re.sub(pattern, new_version_tuple, content)
         else:
-            new_version_tuple = f"VERSION = ({major}, {minor}, {patch})"
-        new_content = re.sub(VERSION_PATTERN, new_version_tuple, content)
+            # Other packages use pyproject.toml with simple version string
+            # For now, we don't update these with RC numbers in pyproject.toml
+            print(
+                f"Note: Version updates for {package_config['name']} are 
manual in pyproject.toml"
+            )
+            return True
+
         if new_content == content:
             print("Error: Could not find or replace version string. Check your 
VERSION_PATTERN.")
             return False
 
-        with open(VERSION_FILE, "w") as f:
+        with open(version_file, "w") as f:
             f.write(new_content)
 
         print("Version updated successfully.")
         return True
 
     except FileNotFoundError:
-        print(f"Error: {VERSION_FILE} not found.")
+        print(f"Error: {version_file} not found.")
         return False
     except Exception as e:
         print(f"An error occurred while updating the version: {e}")
@@ -141,16 +192,17 @@ def sign_artifacts(archive_name: str) -> list[str] | None:
     return files
 
 
-def _modify_wheel_for_apache_release(original_wheel: str, new_wheel_path: str):
+def _modify_wheel_for_apache_release(original_wheel: str, new_wheel_path: str, 
package_name: str):
     """Helper to modify the wheel for apache release.
 
     # Flit somehow builds something incorrectly.
     # 1. change PKG-INFO's first line to be `Metadata-Version: 2.4`
-    # 2. make sure the second line is `Name: apache-hamilton`
-    # 3. remove the `Import-Name: hamilton` line from PKG-INFO.
+    # 2. make sure the second line is `Name: {package_name}`
+    # 3. remove the `Import-Name:` line from PKG-INFO.
 
     :param original_wheel: Path to the original wheel.
     :param new_wheel_path: Path to the new wheel to create.
+    :param package_name: The Apache package name (e.g., 'apache-hamilton')
     """
     with tempfile.TemporaryDirectory() as tmpdir:
         # Unzip the wheel
@@ -164,7 +216,7 @@ def _modify_wheel_for_apache_release(original_wheel: str, 
new_wheel_path: str):
         dist_info_dir = dist_info_dirs[0]
         pkg_info = os.path.join(dist_info_dir, "PKG-INFO")
 
-        _modify_pkg_info_file(pkg_info)
+        _modify_pkg_info_file(pkg_info, package_name)
 
         # Create the new wheel
         with zipfile.ZipFile(new_wheel_path, "w", zipfile.ZIP_DEFLATED) as 
zip_ref:
@@ -175,12 +227,12 @@ def _modify_wheel_for_apache_release(original_wheel: str, 
new_wheel_path: str):
                     )
 
 
-def _modify_pkg_info_file(pkg_info_path: str):
+def _modify_pkg_info_file(pkg_info_path: str, package_name: str):
     """
     Flit somehow builds something incorrectly.
     1. change PKG-INFO's first line to be `Metadata-Version: 2.4`
-    2. make sure the second line is `Name: apache-hamilton`
-    3. remove the `Import-Name: hamilton` line from PKG-INFO.
+    2. make sure the second line is `Name: {package_name}`
+    3. remove the `Import-Name:` line from PKG-INFO if present.
     """
     with open(pkg_info_path, "r") as f:
         lines = f.readlines()
@@ -190,8 +242,8 @@ def _modify_pkg_info_file(pkg_info_path: str):
         if i == 0:
             new_lines.append("Metadata-Version: 2.4\n")
         elif i == 1:
-            new_lines.append("Name: apache-hamilton\n")
-        elif line.strip() == "Import-Name: hamilton":
+            new_lines.append(f"Name: {package_name}\n")
+        elif line.startswith("Import-Name:"):
             continue  # Skip this line
         else:
             new_lines.append(line)
@@ -200,16 +252,19 @@ def _modify_pkg_info_file(pkg_info_path: str):
         f.writelines(new_lines)
 
 
-def _modify_tarball_for_apache_release(original_tarball: str, 
new_tarball_path: str):
+def _modify_tarball_for_apache_release(
+    original_tarball: str, new_tarball_path: str, package_name: str
+):
     """Helper to modify the tarball for apache release.
 
     # Flit somehow builds something incorrectly.
     # 1. change PKG-INFO's first line to be `Metadata-Version: 2.4`
-    # 2. make sure the second line is `Name: apache-hamilton`
-    # 3. remove the `Import-Name: hamilton` line from PKG-INFO.
+    # 2. make sure the second line is `Name: {package_name}`
+    # 3. remove the `Import-Name:` line from PKG-INFO.
 
     :param original_tarball: Path to the original tarball.
     :param new_tarball_path: Path to the new tarball to create.
+    :param package_name: The Apache package name (e.g., 'apache-hamilton')
     """
     with tempfile.TemporaryDirectory() as tmpdir:
         # Extract the tarball
@@ -221,92 +276,117 @@ def _modify_tarball_for_apache_release(original_tarball: 
str, new_tarball_path:
         extracted_dir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
         pkg_info_path = os.path.join(extracted_dir, "PKG-INFO")
 
-        _modify_pkg_info_file(pkg_info_path)
+        _modify_pkg_info_file(pkg_info_path, package_name)
 
         # Create the new tarball
         with tarfile.open(new_tarball_path, "w:gz") as tar:
             tar.add(extracted_dir, arcname=os.path.basename(extracted_dir))
 
 
-def create_release_artifacts(version) -> list[str]:
-    """Creates the source tarball, GPG signature, and checksums using `python 
-m build`."""
-    print("Creating release artifacts with 'flit build'...")
-    # Clean the dist directory before building.
-    if os.path.exists("dist"):
-        shutil.rmtree("dist")
+def create_release_artifacts(package_config: dict, version) -> list[str]:
+    """Creates the source tarball, GPG signature, and checksums using flit 
build."""
+    package_name = package_config["name"]
+    working_dir = package_config["working_dir"]
 
-    # Use python -m build to create the source distribution.
-    try:
-        subprocess.run(
-            [
-                "flit",
-                "build",
-            ],
-            check=True,
-        )
-        print("Source distribution created successfully.")
-    except subprocess.CalledProcessError as e:
-        print(f"Error creating source distribution: {e}")
-        return None
+    print(f"Creating release artifacts for {package_name} with 'flit 
build'...")
 
-    # Find the created tarball in the dist directory.
-    expected_tar_ball = f"dist/apache-hamilton-{version.lower()}.tar.gz"
-    tarball_path = glob.glob(expected_tar_ball)
+    # Save current directory and change to package working directory
+    original_dir = os.getcwd()
+    if working_dir != ".":
+        os.chdir(working_dir)
 
-    if not tarball_path:
-        print(
-            f"Error: Could not find {expected_tar_ball} the generated source 
tarball in the 'dist' directory."
-        )
+    try:
+        # Clean the dist directory before building.
         if os.path.exists("dist"):
-            print("Contents of 'dist' directory:")
-            for item in os.listdir("dist"):
-                print(f"- {item}")
-        else:
-            print("'dist' directory not found.")
-        raise ValueError("Could not find the generated source tarball in the 
'dist' directory.")
-
-    # copy the tarball to be 
apache-hamilton-{version.lower()}-incubating.tar.gz
-    new_tar_ball = f"dist/apache-hamilton-{version.lower()}-incubating.tar.gz"
-    # shutil.copy(tarball_path[0], new_tar_ball)
-    _modify_tarball_for_apache_release(tarball_path[0], new_tar_ball)
-    archive_name = new_tar_ball
-    print(f"Found source tarball: {archive_name}")
-    new_tar_ball_singed = sign_artifacts(archive_name)
-    if new_tar_ball_singed is None:
-        raise ValueError("Could not sign the main release artifacts.")
-    # create wheel release artifacts
-    expected_wheel = f"dist/apache-hamilton-{version.lower()}-py3-none-any.whl"
-    wheel_path = glob.glob(expected_wheel)
-    # create incubator wheel release artifacts
-    expected_incubator_wheel = 
f"dist/apache-hamilton-{version.lower()}-incubating-py3-none-any.whl"
-    shutil.copy(wheel_path[0], expected_incubator_wheel)
-    incubator_wheel_signed_files = sign_artifacts(expected_incubator_wheel)
-    files_to_upload = (
-        [new_tar_ball]
-        + new_tar_ball_singed
-        + [expected_incubator_wheel]
-        + incubator_wheel_signed_files
-    )
-    return files_to_upload
+            shutil.rmtree("dist")
 
+        # Use flit build to create the source distribution.
+        try:
+            subprocess.run(
+                [
+                    "flit",
+                    "build",
+                    "--no-use-vcs",
+                ],
+                check=True,
+            )
+            print("Source distribution created successfully.")
+        except subprocess.CalledProcessError as e:
+            print(f"Error creating source distribution: {e}")
+            return None
+
+        # Find the created tarball in the dist directory.
+        # Convert package name with underscores for file naming
+        package_file_name = package_name.replace("-", "_")
+        expected_tar_ball = 
f"dist/{package_file_name}-{version.lower()}.tar.gz"
+        tarball_path = glob.glob(expected_tar_ball)
+
+        if not tarball_path:
+            print(
+                f"Error: Could not find {expected_tar_ball} the generated 
source tarball in the 'dist' directory."
+            )
+            if os.path.exists("dist"):
+                print("Contents of 'dist' directory:")
+                for item in os.listdir("dist"):
+                    print(f"- {item}")
+            else:
+                print("'dist' directory not found.")
+            raise ValueError("Could not find the generated source tarball in 
the 'dist' directory.")
+
+        # Copy the tarball to be {package-name}-{version}-incubating.tar.gz
+        new_tar_ball = 
f"dist/{package_name}-{version.lower()}-incubating.tar.gz"
+        _modify_tarball_for_apache_release(tarball_path[0], new_tar_ball, 
package_name)
+        archive_name = new_tar_ball
+        print(f"Found source tarball: {archive_name}")
+        new_tar_ball_singed = sign_artifacts(archive_name)
+        if new_tar_ball_singed is None:
+            raise ValueError("Could not sign the main release artifacts.")
+
+        # Create wheel release artifacts
+        expected_wheel = 
f"dist/{package_file_name}-{version.lower()}-py3-none-any.whl"
+        wheel_path = glob.glob(expected_wheel)
+
+        # Create incubator wheel release artifacts
+        expected_incubator_wheel = (
+            
f"dist/{package_name}-{version.lower()}-incubating-py3-none-any.whl"
+        )
+        shutil.copy(wheel_path[0], expected_incubator_wheel)
+        incubator_wheel_signed_files = sign_artifacts(expected_incubator_wheel)
+
+        files_to_upload = (
+            [new_tar_ball]
+            + new_tar_ball_singed
+            + [expected_incubator_wheel]
+            + incubator_wheel_signed_files
+        )
+        return files_to_upload
+
+    finally:
+        # Always return to original directory
+        os.chdir(original_dir)
 
-def svn_upload(version, rc_num, files_to_import: list[str], apache_id):
+
+def svn_upload(package_name: str, version, rc_num, files_to_import: list[str], 
apache_id):
     """Uploads the artifacts to the ASF dev distribution repository.
 
     files_to_import: Get the files to import (tarball, asc, sha512).
     """
     print("Uploading artifacts to ASF SVN...")
-    svn_path = 
f"https://dist.apache.org/repos/dist/dev/incubator/{PROJECT_SHORT_NAME}/{version}-RC{rc_num}";
+    # Include package name in SVN path for multi-package support
+    svn_path = 
f"https://dist.apache.org/repos/dist/dev/incubator/{PROJECT_SHORT_NAME}/{package_name}/{version}-RC{rc_num}";
 
     try:
         # Create a new directory for the release candidate.
-        print(f"Creating directory for {version}-incubating-RC{rc_num}... at 
{svn_path}")
+        print(
+            f"Creating directory for {package_name} 
{version}-incubating-RC{rc_num}... at {svn_path}"
+        )
         subprocess.run(
             [
                 "svn",
                 "mkdir",
+                "--parents",
                 "-m",
-                f"Creating directory for {version}-incubating-RC{rc_num}",
+                f"Creating directory for {package_name} 
{version}-incubating-RC{rc_num}",
                 svn_path,
             ],
             check=True,
@@ -338,17 +418,17 @@ def svn_upload(version, rc_num, files_to_import: 
list[str], apache_id):
         return None
 
 
-def generate_email_template(version, rc_num, svn_url):
+def generate_email_template(package_name: str, 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}"
+    tag = f"{package_name}-v{version}"
 
-    email_content = f"""[VOTE] Release Apache {PROJECT_SHORT_NAME} 
{version_with_incubating} (release candidate {rc_num})
+    email_content = f"""[VOTE] Release Apache {PROJECT_SHORT_NAME} - 
{package_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},
+This is a call for a vote on releasing Apache {PROJECT_SHORT_NAME} 
{package_name} {version_with_incubating},
 release candidate {rc_num}.
 
 This release includes the following changes (see CHANGELOG for details):
@@ -377,7 +457,7 @@ 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}
+[ ] +1 Release this package as Apache {PROJECT_SHORT_NAME} {package_name} 
{version_with_incubating}
 [ ] +0 No opinion
 [ ] -1 Do not release this package because... (Please provide a reason)
 
@@ -408,38 +488,62 @@ def main():
         ```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`
+    2.  **Configure the Script**: The script now supports multiple Hamilton 
packages.
+        Available packages: hamilton, sdk, lsp, contrib, ui
     3.  **Prerequisites**:
-        * You must have `git`, `gpg`, `svn`, and the `build` Python module 
installed.
+        * You must have `git`, `gpg`, `svn`, and the `flit` Python module 
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.
+        Open your terminal, navigate to the root of your project directory, 
and run the script
+        with the desired package, version, release candidate number, and 
Apache ID.
 
     Note: if you have multiple gpg keys, specify the default in 
~/.gnupg/gpg.conf add a line with `default-key <KEYID>`.
 
-    python apache_release_helper.py 1.2.3 0 your_apache_id
+    Examples:
+        python apache_release_helper.py --package hamilton 1.89.0 0 
your_apache_id
+        python apache_release_helper.py --package sdk 0.8.0 0 your_apache_id
+        python apache_release_helper.py --package lsp 0.1.0 0 your_apache_id
+        python apache_release_helper.py --package contrib 0.0.8 0 
your_apache_id
+        python apache_release_helper.py --package ui 0.0.17 0 your_apache_id
     """
-    parser = argparse.ArgumentParser(description="Automates parts of the 
Apache release process.")
+    parser = argparse.ArgumentParser(
+        description="Automates parts of the Apache release process for 
Hamilton packages."
+    )
+    parser.add_argument(
+        "--package",
+        required=True,
+        choices=list(PACKAGE_CONFIGS.keys()),
+        help="Which Hamilton package to release (hamilton, sdk, lsp, contrib, 
ui)",
+    )
     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.")
     args = parser.parse_args()
 
+    package_key = args.package
     version = args.version
     rc_num = args.rc_num
     apache_id = args.apache_id
 
+    # Get package configuration
+    package_config = PACKAGE_CONFIGS[package_key]
+    package_name = package_config["name"]
+
+    print(f"\n{'=' * 80}")
+    print(f"  Apache Hamilton Release Helper - {package_name}")
+    print(f"{'=' * 80}\n")
+
     check_prerequisites()
 
-    current_version = get_version_from_file(VERSION_FILE)
-    print(current_version)
+    # Validate version matches what's in the version file
+    current_version = get_version_from_file(package_config)
+    print(f"Current version in {package_config['version_file']}: 
{current_version}")
     if current_version != version:
         print("Update the version in the version file to match the expected 
version.")
         sys.exit(1)
 
-    tag_name = f"v{version}-incubating-RC{rc_num}"
+    # Create git tag (from repo root)
+    tag_name = f"{package_name}-v{version}-incubating-RC{rc_num}"
     print(f"\nChecking for git tag '{tag_name}'...")
     try:
         # Check if the tag already exists
@@ -460,20 +564,36 @@ def main():
         sys.exit(1)
 
     # Create artifacts
-    files_to_upload = create_release_artifacts(version)
+    print(f"\n{'=' * 80}")
+    print("  Building Release Artifacts")
+    print(f"{'=' * 80}\n")
+    files_to_upload = create_release_artifacts(package_config, version)
     if not files_to_upload:
         sys.exit(1)
 
     # Upload artifacts
+    print(f"\n{'=' * 80}")
+    print("  Uploading to Apache SVN")
+    print(f"{'=' * 80}\n")
     # NOTE: You MUST have your SVN client configured to use your Apache ID and 
have permissions.
-    svn_url = svn_upload(version, rc_num, files_to_upload, apache_id)
+    svn_url = svn_upload(package_name, version, rc_num, files_to_upload, 
apache_id)
     if not svn_url:
         sys.exit(1)
 
     # Generate email
-    generate_email_template(version, rc_num, svn_url)
+    print(f"\n{'=' * 80}")
+    print("  Vote Email Template")
+    print(f"{'=' * 80}\n")
+    generate_email_template(package_name, version, rc_num, svn_url)
 
-    print("\nProcess complete. Please copy the email template to your mailing 
list.")
+    print("\n" + "=" * 80)
+    print("  Process Complete!")
+    print("=" * 80)
+    print("\nNext steps:")
+    print(f"1. Push the git tag: git push origin {tag_name}")
+    print("2. Copy the email template above and send to 
[email protected]")
+    print("3. Wait for votes (minimum 72 hours)")
+    print("\n")
 
 
 if __name__ == "__main__":

Reply via email to