This is an automated email from the ASF dual-hosted git repository. ebenizzy pushed a commit to branch asf-release-scripts in repository https://gitbox.apache.org/repos/asf/burr.git
commit 670500d6cba018aa04188673b14a0a91199ed497 Author: Elijah ben Izzy <[email protected]> AuthorDate: Sun Nov 16 21:07:31 2025 -0800 Adds scripts for releasing burr --- scripts/build_artifacts.py | 143 +++++++++++++++++ scripts/release_helper.py | 383 +++++++++++++++++++++++++++++++++++++++++++++ scripts/setup_keys.sh | 95 +++++++++++ 3 files changed, 621 insertions(+) diff --git a/scripts/build_artifacts.py b/scripts/build_artifacts.py new file mode 100644 index 00000000..26590095 --- /dev/null +++ b/scripts/build_artifacts.py @@ -0,0 +1,143 @@ +# 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. + +""" +Script to build UI artifacts for Burr. + +This script is intended to be run after extracting the Apache source distribution +to build the UI assets that will be included in the PyPI distribution. + +Usage: + python scripts/build_artifacts.py +""" + +import os +import shutil +import subprocess +import sys + + +def check_prerequisites(): + """Checks for necessary command-line tools.""" + 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.") + sys.exit(1) + + print("All required tools found.\n") + + +def install_burr(): + """Installs burr from source in editable mode.""" + 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 e: + print(f"✗ Error installing burr: {e}") + return False + + +def build_ui(): + """Runs the burr-admin-build-ui command to build the UI assets.""" + print("Building UI assets...") + try: + subprocess.run(["burr-admin-build-ui"], check=True) + print("✓ UI build completed successfully.\n") + return True + except subprocess.CalledProcessError as e: + print(f"✗ Error building UI: {e}") + return False + + +def verify_build(): + """Verifies that the UI build output exists.""" + 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 not found: {build_dir}") + return False + + # Check if the directory has content + if not os.listdir(build_dir): + print(f"✗ Build directory is empty: {build_dir}") + return False + + print("✓ Build output verified.\n") + return True + + +def main(): + """Main function to orchestrate the build process.""" + print("=" * 80) + print("Burr Build Artifacts Script") + print("=" * 80) + print() + + # Check we're in the right directory + 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.") + sys.exit(1) + + # Check prerequisites + check_prerequisites() + + # Install burr from source + if not install_burr(): + print("\n❌ Failed to install burr from source.") + sys.exit(1) + + # Build UI + if not build_ui(): + print("\n❌ Failed to build UI assets.") + sys.exit(1) + + # Verify build output + if not verify_build(): + print("\n❌ Build output verification failed.") + sys.exit(1) + + print("=" * 80) + print("✅ Build Complete!") + print("=" * 80) + print() + print("UI assets have been built and are ready for distribution.") + print("You can now create PyPI distributions with:") + print(" python -m build") + print() + + +if __name__ == "__main__": + main() diff --git a/scripts/release_helper.py b/scripts/release_helper.py new file mode 100644 index 00000000..79e265d1 --- /dev/null +++ b/scripts/release_helper.py @@ -0,0 +1,383 @@ +# 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 shutil +import subprocess +import sys +from typing import Optional + +# --- 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 get_version_from_file(file_path: str) -> str: + """Get the version from a file.""" + import re + + 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"] + for tool in required_tools: + if shutil.which(tool) is None: + print(f"Error: '{tool}' not found. Please install it and ensure it's in your PATH.") + sys.exit(1) + + try: + import build # noqa:F401 + + print("Python 'build' module found.") + except ImportError: + print( + "Error: The 'build' module is not installed. Please install it with 'pip install build'." + ) + sys.exit(1) + + print("All required tools found.") + + +def update_version(version, rc_num): + """Updates the version number in the specified file.""" + import re + + 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: + print(f"Error: {VERSION_FILE} not found.") + return False + except Exception as e: + print(f"An error occurred while updating the version: {e}") + return False + + +def sign_artifacts(archive_name: str) -> Optional[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: + print(f"Error signing tarball: {e}") + return None + + # 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) -> Optional[tuple[list[str], list[str]]]: + """Creates the source tarball, GPG signature, and checksums using `python -m build`.""" + print("Creating release artifacts with 'python -m build'...") + + # Clean the dist directory before building. + if os.path.exists("dist"): + shutil.rmtree("dist") + + # Use python -m build to create the source distribution. + try: + subprocess.run(["python", "-m", "build", "--sdist", "."], 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. + expected_tar_ball = f"dist/burr-{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 apache-burr-{version.lower()}-incubating.tar.gz + new_tar_ball = f"dist/apache-burr-{version.lower()}-incubating.tar.gz" + shutil.copy(tarball_path[0], new_tar_ball) + archive_name = new_tar_ball + print(f"Found source tarball: {archive_name}") + main_signed_files = sign_artifacts(archive_name) + if main_signed_files is None: + raise ValueError("Could not sign the main release artifacts.") + # create burr release artifacts + burr_signed_files = sign_artifacts(expected_tar_ball) + if burr_signed_files is None: + raise ValueError("Could not sign the burr release artifacts.") + return [new_tar_ball] + main_signed_files, [expected_tar_ball] + burr_signed_files + + +def svn_upload(version, rc_num, archive_files, burr_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 + burr_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 the `build` module**: + ```bash + pip install build + ``` + 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 the `build` 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. + + + 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)", + ) + args = parser.parse_args() + + version = args.version + rc_num = args.rc_num + apache_id = args.apache_id + dry_run = args.dry_run + + 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: + 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}" + 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: + print(f"Error checking or creating Git tag: {e}") + sys.exit(1) + + # Create artifacts + artifacts = create_release_artifacts(version) + if not artifacts: + sys.exit(1) + main_archive_files, burr_archive_files = artifacts + + # 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, main_archive_files, burr_archive_files, apache_id) + if not svn_url: + sys.exit(1) + + # 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 " "
