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__":
