You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hamilton.apache.org by sk...@apache.org on 2026/02/22 01:30:05 UTC
(hamilton) 06/07: Update apache_release_helper.py to support per-package builds
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 <st...@cs.stanford.edu>
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 dev@hamilton.apache.org")
+ print("3. Wait for votes (minimum 72 hours)")
+ print("\n")
if __name__ == "__main__":