| #!/usr/bin/env python3 |
| # |
| # 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 and Push Script for Juneau |
| |
| This script automates the build, test, and deployment workflow: |
| 1. Runs all tests |
| 2. Builds and installs the project |
| 3. Commits changes to Git |
| 4. Pushes to remote repository |
| |
| Usage: python3 push.py "commit message" |
| python3 push.py "commit message" --skip-tests |
| """ |
| |
| # Sound file paths |
| MACOS_SUCCESS_SOUND = "/System/Library/Sounds/Glass.aiff" |
| MACOS_FAILURE_SOUND = "/System/Library/Sounds/Basso.aiff" |
| LINUX_SUCCESS_SOUND = "/usr/share/sounds/freedesktop/stereo/complete.oga" |
| LINUX_FAILURE_SOUND = "/usr/share/sounds/freedesktop/stereo/dialog-error.oga" |
| |
| import argparse |
| import os |
| import platform |
| import subprocess |
| import sys |
| from pathlib import Path |
| |
| |
| def run_command(cmd, description, cwd=None): |
| """ |
| Run a shell command and handle errors. |
| |
| Args: |
| cmd: Command to run (string or list) |
| description: Description of the step for output |
| cwd: Working directory (defaults to script parent directory) |
| |
| Returns: |
| True if successful, False otherwise |
| """ |
| if cwd is None: |
| cwd = Path(__file__).parent.parent |
| |
| print(f"\n{description}") |
| print(f"Running: {' '.join(cmd) if isinstance(cmd, list) else cmd}") |
| |
| try: |
| subprocess.run( |
| cmd, |
| cwd=cwd, |
| shell=isinstance(cmd, str), |
| check=True, |
| capture_output=False, |
| text=True |
| ) |
| print(f"โ
{description} - SUCCESS") |
| return True |
| except subprocess.CalledProcessError as e: |
| print(f"โ {description} - FAILED (exit code: {e.returncode})") |
| return False |
| except Exception as e: |
| print(f"โ {description} - FAILED: {e}") |
| return False |
| |
| |
| # NOSONAR python:S3776 -- Cognitive complexity is acceptable for this utility function |
| def play_sound(success=True): |
| """ |
| Play a system sound to indicate success or failure. |
| |
| Args: |
| success: True for success sound, False for failure sound |
| """ |
| try: |
| system = platform.system() |
| if system == "Darwin": # macOS |
| if success: |
| # Success sound |
| sound_path = MACOS_SUCCESS_SOUND |
| else: |
| # Failure sound |
| sound_path = MACOS_FAILURE_SOUND |
| |
| if os.path.exists(sound_path): |
| subprocess.run( |
| ["afplay", sound_path], |
| capture_output=True, |
| timeout=5 |
| ) |
| elif system == "Linux": |
| # Try to use paplay (PulseAudio) or aplay (ALSA) |
| if success: |
| # Try to play a beep or use speaker-test |
| try: |
| subprocess.run( |
| ["paplay", LINUX_SUCCESS_SOUND], |
| capture_output=True, |
| timeout=5 |
| ) |
| except OSError: |
| # Fallback to speaker-test |
| subprocess.run( |
| ["speaker-test", "-t", "sine", "-f", "1000", "-l", "1"], |
| capture_output=True, |
| timeout=2 |
| ) |
| else: |
| try: |
| subprocess.run( |
| ["paplay", LINUX_FAILURE_SOUND], |
| capture_output=True, |
| timeout=5 |
| ) |
| except OSError: |
| # Fallback to speaker-test with lower frequency |
| subprocess.run( |
| ["speaker-test", "-t", "sine", "-f", "400", "-l", "1"], |
| capture_output=True, |
| timeout=2 |
| ) |
| elif system == "Windows": |
| # Use winsound module |
| import winsound |
| if success: |
| winsound.MessageBeep(winsound.MB_OK) |
| else: |
| winsound.MessageBeep(winsound.MB_ICONHAND) |
| except Exception: |
| # Silently fail if sound can't be played |
| pass |
| |
| |
| # NOSONAR python:S3776 -- Cognitive complexity is acceptable for this utility function |
| def play_sound(success=True): |
| """ |
| Play a system sound to indicate success or failure. |
| |
| Args: |
| success: True for success sound, False for failure sound |
| """ |
| try: |
| system = platform.system() |
| if system == "Darwin": # macOS |
| if success: |
| # Success sound |
| sound_path = MACOS_SUCCESS_SOUND |
| else: |
| # Failure sound |
| sound_path = MACOS_FAILURE_SOUND |
| |
| if os.path.exists(sound_path): |
| subprocess.run( |
| ["afplay", sound_path], |
| capture_output=True, |
| timeout=5 |
| ) |
| elif system == "Linux": |
| # Try to use paplay (PulseAudio) or aplay (ALSA) |
| if success: |
| # Try to play a beep or use speaker-test |
| try: |
| subprocess.run( |
| ["paplay", LINUX_SUCCESS_SOUND], |
| capture_output=True, |
| timeout=5 |
| ) |
| except OSError: |
| # Fallback to speaker-test |
| subprocess.run( |
| ["speaker-test", "-t", "sine", "-f", "1000", "-l", "1"], |
| capture_output=True, |
| timeout=2 |
| ) |
| else: |
| try: |
| subprocess.run( |
| ["paplay", LINUX_FAILURE_SOUND], |
| capture_output=True, |
| timeout=5 |
| ) |
| except OSError: |
| # Fallback to speaker-test with lower frequency |
| subprocess.run( |
| ["speaker-test", "-t", "sine", "-f", "400", "-l", "1"], |
| capture_output=True, |
| timeout=2 |
| ) |
| elif system == "Windows": |
| # Use winsound module |
| import winsound |
| if success: |
| winsound.MessageBeep(winsound.MB_OK) |
| else: |
| winsound.MessageBeep(winsound.MB_ICONHAND) |
| except Exception: |
| # Silently fail if sound can't be played |
| pass |
| |
| |
| def check_git_status(repo_dir): |
| """Check if there are any changes to commit.""" |
| try: |
| result = subprocess.run( |
| ["git", "status", "--porcelain"], |
| cwd=repo_dir, |
| capture_output=True, |
| text=True, |
| check=True |
| ) |
| return len(result.stdout.strip()) > 0 |
| except Exception as e: |
| print(f"โ Warning: Could not check git status: {e}") |
| return True |
| |
| |
| def check_upstream_changes(repo_dir): |
| """ |
| Check if local branch is behind upstream/remote branch. |
| |
| Returns: |
| tuple: (is_behind, error_message) |
| - is_behind: True if local is behind remote, False otherwise |
| - error_message: Error message if check failed, None otherwise |
| """ |
| try: |
| # Get current branch name |
| result = subprocess.run( |
| ["git", "rev-parse", "--abbrev-ref", "HEAD"], |
| cwd=repo_dir, |
| capture_output=True, |
| text=True, |
| check=True |
| ) |
| current_branch = result.stdout.strip() |
| |
| # Fetch latest from remote |
| subprocess.run( |
| ["git", "fetch"], |
| cwd=repo_dir, |
| capture_output=True, |
| text=True, |
| check=True |
| ) |
| |
| # Check if there's a remote tracking branch |
| result = subprocess.run( |
| ["git", "rev-parse", "--abbrev-ref", f"{current_branch}@{{upstream}}"], |
| cwd=repo_dir, |
| capture_output=True, |
| text=True, |
| check=False |
| ) |
| |
| if result.returncode != 0: |
| # No upstream branch configured, skip check |
| return (False, None) |
| |
| upstream_branch = result.stdout.strip() |
| |
| # Get commit counts |
| result = subprocess.run( |
| ["git", "rev-list", "--left-right", "--count", f"{current_branch}...{upstream_branch}"], |
| cwd=repo_dir, |
| capture_output=True, |
| text=True, |
| check=True |
| ) |
| |
| # Output format: "X\tY" where X is commits ahead, Y is commits behind |
| counts = result.stdout.strip().split('\t') |
| if len(counts) != 2: |
| return (False, "Could not parse upstream comparison") |
| |
| commits_behind = int(counts[1]) |
| return (commits_behind > 0, None) |
| |
| except subprocess.CalledProcessError as e: |
| return (False, f"Git command failed: {e}") |
| except Exception as e: |
| return (False, f"Error checking upstream changes: {e}") |
| |
| |
| # NOSONAR python:S3776 -- Cognitive complexity is acceptable for this utility function |
| def play_sound(success=True): |
| """ |
| Play a system sound to indicate success or failure. |
| |
| Args: |
| success: True for success sound, False for failure sound |
| """ |
| try: |
| system = platform.system() |
| if system == "Darwin": # macOS |
| if success: |
| # Success sound |
| sound_path = MACOS_SUCCESS_SOUND |
| else: |
| # Failure sound |
| sound_path = MACOS_FAILURE_SOUND |
| |
| if os.path.exists(sound_path): |
| subprocess.run( |
| ["afplay", sound_path], |
| capture_output=True, |
| timeout=5 |
| ) |
| elif system == "Linux": |
| # Try to use paplay (PulseAudio) or aplay (ALSA) |
| if success: |
| # Try to play a beep or use speaker-test |
| try: |
| subprocess.run( |
| ["paplay", LINUX_SUCCESS_SOUND], |
| capture_output=True, |
| timeout=5 |
| ) |
| except OSError: |
| # Fallback to speaker-test |
| subprocess.run( |
| ["speaker-test", "-t", "sine", "-f", "1000", "-l", "1"], |
| capture_output=True, |
| timeout=2 |
| ) |
| else: |
| try: |
| subprocess.run( |
| ["paplay", LINUX_FAILURE_SOUND], |
| capture_output=True, |
| timeout=5 |
| ) |
| except OSError: |
| # Fallback to speaker-test with lower frequency |
| subprocess.run( |
| ["speaker-test", "-t", "sine", "-f", "400", "-l", "1"], |
| capture_output=True, |
| timeout=2 |
| ) |
| elif system == "Windows": |
| # Use winsound module |
| import winsound |
| if success: |
| winsound.MessageBeep(winsound.MB_OK) |
| else: |
| winsound.MessageBeep(winsound.MB_ICONHAND) |
| except Exception: |
| # Silently fail if sound can't be played |
| pass |
| |
| |
| # NOSONAR python:S3776 -- Cognitive complexity is acceptable for this main function |
| def main(): |
| parser = argparse.ArgumentParser( |
| description="Build, test, and push Juneau project to Git repository", |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| epilog=""" |
| Examples: |
| python3 push.py "Fixed bug in RestClient" |
| python3 push.py "Updated documentation" --skip-tests |
| python3 push.py "Quick fix" --skip-tests |
| """ |
| ) |
| |
| parser.add_argument( |
| "message", |
| help="Git commit message" |
| ) |
| |
| parser.add_argument( |
| "--skip-tests", |
| action="store_true", |
| help="Skip running tests (useful for documentation-only changes)" |
| ) |
| |
| parser.add_argument( |
| "--dry-run", |
| action="store_true", |
| help="Show what would be done without actually doing it" |
| ) |
| |
| args = parser.parse_args() |
| |
| # Get the Juneau root directory |
| script_dir = Path(__file__).parent |
| juneau_root = script_dir.parent |
| |
| print("=" * 70) |
| print("๐ Juneau Build and Push Script") |
| print("=" * 70) |
| print(f"Working directory: {juneau_root}") |
| print(f"Commit message: '{args.message}'") |
| if args.skip_tests: |
| print("โ Tests will be SKIPPED") |
| if args.dry_run: |
| print("๐ DRY RUN MODE - No actual changes will be made") |
| print("=" * 70) |
| |
| if args.dry_run: |
| print("\nSteps that would be executed:") |
| step_num = 1 |
| print(f" {step_num}. Prompt for PGP passphrase (dummy call)") |
| step_num += 1 |
| if not args.skip_tests: |
| print(f" {step_num}. Run tests: python3 scripts/test.py --full") |
| step_num += 1 |
| print(f" {step_num}. Build and install: mvn clean package install") |
| step_num += 1 |
| print(f" {step_num}. Commit changes: git add . && git commit -m \"{args.message}\"") |
| step_num += 1 |
| print(f" {step_num}. Push to remote: git push") |
| print("\nDry run complete. Use without --dry-run to execute.") |
| return 0 |
| |
| # Prompt for PGP passphrase early (before any time-consuming operations) |
| prompt_script = script_dir / 'prompt-pgp-passphrase.py' |
| if prompt_script.exists(): |
| try: |
| subprocess.run( |
| [sys.executable, str(prompt_script)], |
| check=False # Don't fail if this doesn't work |
| ) |
| except Exception as e: |
| print(f"โ Could not run PGP passphrase prompt: {e}") |
| |
| step_num = 1 |
| |
| # Step 1: Run tests (optional) |
| if not args.skip_tests: |
| test_script = script_dir / 'test.py' |
| if test_script.exists(): |
| print(f"\n๐งช Step {step_num}: Running tests via test.py...") |
| try: |
| result = subprocess.run( |
| [sys.executable, str(test_script), "--full"], |
| cwd=juneau_root, |
| check=False |
| ) |
| if result.returncode != 0: |
| print("\nโ Build process aborted due to test failures.") |
| play_sound(success=False) |
| return 1 |
| print(f"โ
Step {step_num}: Tests passed") |
| except Exception as e: |
| print(f"\nโ Error running tests: {e}") |
| play_sound(success=False) |
| return 1 |
| else: |
| # Fallback to direct mvn test if test.py doesn't exist |
| if not run_command( |
| ["mvn", "test"], |
| f"๐งช Step {step_num}: Running tests...", |
| juneau_root |
| ): |
| print("\nโ Build process aborted due to test failures.") |
| play_sound(success=False) |
| return 1 |
| step_num += 1 |
| else: |
| print(f"\nโญ๏ธ Step {step_num}: Skipping tests (--skip-tests flag)") |
| step_num += 1 |
| |
| # Step 2: Build and install |
| if not run_command( |
| ["mvn", "clean", "package", "install"], |
| f"๐๏ธ Step {step_num}: Building and installing project...", |
| juneau_root |
| ): |
| print("\nโ Build process aborted due to build failure.") |
| play_sound(success=False) |
| return 1 |
| step_num += 1 |
| |
| # Check if local branch is behind upstream |
| print("\n๐ Checking for upstream changes...") |
| is_behind, error_msg = check_upstream_changes(juneau_root) |
| if error_msg: |
| print(f"\nโ Warning: Could not check upstream changes: {error_msg}") |
| print("Continuing anyway...") |
| elif is_behind: |
| print("\nโ ERROR: Local branch is behind upstream/remote branch.") |
| print("Please pull/merge upstream changes before pushing.") |
| print("Run: git pull") |
| play_sound(success=False) |
| return 1 |
| |
| # Check if there are changes to commit |
| if not check_git_status(juneau_root): |
| print("\nโ Warning: No changes detected. Skipping commit and push.") |
| print("๐ Build completed successfully (nothing to commit)!") |
| play_sound(success=True) |
| return 0 |
| |
| # Step 4: Git add and commit |
| print(f"\n๐ Step {step_num}: Committing changes to Git...") |
| if not run_command( |
| ["git", "add", "."], |
| f" {step_num}.1: Staging all changes...", |
| juneau_root |
| ): |
| print("\nโ Build process aborted due to git add failure.") |
| play_sound(success=False) |
| return 1 |
| |
| if not run_command( |
| ["git", "commit", "-m", args.message], |
| f" {step_num}.2: Creating commit...", |
| juneau_root |
| ): |
| print("\nโ Build process aborted due to git commit failure.") |
| play_sound(success=False) |
| return 1 |
| print(f"โ
Step {step_num}: Git commit completed.") |
| step_num += 1 |
| |
| # Step 5: Push to remote |
| if not run_command( |
| ["git", "push"], |
| f"๐ Step {step_num}: Pushing changes to remote repository...", |
| juneau_root |
| ): |
| print("\nโ Build process aborted due to git push failure.") |
| print("โ Your changes have been committed locally but not pushed.") |
| play_sound(success=False) |
| return 1 |
| |
| # Success! |
| print("\n" + "=" * 70) |
| print("๐ All operations completed successfully!") |
| print(f"๐ฆ Commit message: '{args.message}'") |
| print("=" * 70) |
| play_sound(success=True) |
| return 0 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |
| |