diff --git a/scripts/bootstrap_test.sh b/scripts/bootstrap_test.sh
index e1b41aac..2fd1f34c 100755
--- a/scripts/bootstrap_test.sh
+++ b/scripts/bootstrap_test.sh
@@ -42,27 +42,32 @@ is_from_github_actions=$1
 
 process_and_compare() {
     local config="$1"
-    cargo run --bin rustylr -- "$rustylr_path/rusty_lr_parser/src/parser/parser.rs" out.tab.rs $config > /dev/null
+    cargo run --quiet --bin rustylr -- "$rustylr_path/rusty_lr_parser/src/parser/parser.rs" out.tab.rs $config > /dev/null
     mv out.tab.rs "$rustylr_path/rusty_lr_parser/src/parser/parser_expanded.rs"
-    cargo run --bin rustylr -- "$rustylr_path/rusty_lr_parser/src/parser/parser.rs" out.tab.rs $config > /dev/null
+    cargo run --quiet --bin rustylr -- "$rustylr_path/rusty_lr_parser/src/parser/parser.rs" out.tab.rs $config > /dev/null
     compare_files "$rustylr_path/rusty_lr_parser/src/parser/parser_expanded.rs" out.tab.rs
     if [ $? -ne 0 ]; then
         exit 1
     fi
-    cargo test
-    if [ $? -ne 0 ]; then
+    local test_out=$(mktemp)
+    cargo test --quiet > "$test_out" 2>&1
+    local status=$?
+    if [ $status -ne 0 ]; then
+        cat "$test_out"
         echo "Error: cargo test failed with config '$config'"
+        rm "$test_out"
         exit 1
     fi
+    rm "$test_out"
 }
 
 echo "RustyLR path: $rustylr_path"
 
 
 # to briefly see the difference of the generated parser in the PR, run for the sample calculator and json parsers
-cargo run --bin rustylr -- "$rustylr_path/example/calculator/src/parser.rs" "$rustylr_path/scripts/diff/calculator_new.rs" > /dev/null
-cargo run --bin rustylr -- "$rustylr_path/example/calculator_u8/src/parser.rs" "$rustylr_path/scripts/diff/calculator_u8_new.rs" > /dev/null
-cargo run --bin rustylr -- "$rustylr_path/example/json/src/parser.rs" "$rustylr_path/scripts/diff/json_new.rs" > /dev/null
+cargo run --quiet --bin rustylr -- "$rustylr_path/example/calculator/src/parser.rs" "$rustylr_path/scripts/diff/calculator_new.rs" > /dev/null
+cargo run --quiet --bin rustylr -- "$rustylr_path/example/calculator_u8/src/parser.rs" "$rustylr_path/scripts/diff/calculator_u8_new.rs" > /dev/null
+cargo run --quiet --bin rustylr -- "$rustylr_path/example/json/src/parser.rs" "$rustylr_path/scripts/diff/json_new.rs" > /dev/null
 if [ "$is_from_github_actions" = "true" ]; then
     diff "$rustylr_path/scripts/diff/calculator.rs" "$rustylr_path/scripts/diff/calculator_new.rs" >/dev/null
     if [ $? -ne 0 ]; then
@@ -103,9 +108,14 @@ echo "Normal configuration"
 process_and_compare ""
 mv out.tab.rs "$rustylr_path/rusty_lr_parser/src/parser/parser_expanded.rs"
 
-cargo test --bin glr
-if [ $? -ne 0 ]; then
+test_out=$(mktemp)
+cargo test --quiet --bin glr > "$test_out" 2>&1
+status=$?
+if [ $status -ne 0 ]; then
+    cat "$test_out"
+    rm "$test_out"
     exit 1
 fi
+rm "$test_out"
 
 echo "All tests passed."
\ No newline at end of file
diff --git a/scripts/release.py b/scripts/release.py
new file mode 100755
index 00000000..e2288138
--- /dev/null
+++ b/scripts/release.py
@@ -0,0 +1,421 @@
+#!/usr/bin/env python3
+"""
+RustyLR Release Manager Script
+This script merges the functionality of bumping crate versions, running bootstrap tests,
+and publishing crates to crates.io with strict verification steps.
+
+WARNING:
+  Since the version bumping step modifies Cargo.toml files in place, the git working tree
+  will be dirty. It is highly recommended to pass '--allow-dirty' to 'publish' or 'all' commands.
+
+Usage:
+  python3 scripts/release.py bump
+  python3 scripts/release.py publish [cargo_publish_args...]
+  python3 scripts/release.py publish --allow-dirty
+  python3 scripts/release.py all [cargo_publish_args...]
+  python3 scripts/release.py all --allow-dirty
+"""
+
+import argparse
+import os
+import re
+import sys
+import subprocess
+import shutil
+
+# The publishing order of packages based on internal dependencies
+PUBLISH_ORDER = [
+    "rusty_lr_core",
+    "rusty_lr_parser",
+    "rusty_lr_derive",
+    "rusty_lr_buildscript",
+    "rusty_lr_executable",
+    "rusty_lr"
+]
+
+# ==============================================================================
+# INTENT 1: Version Bumping
+# Automatically increments the minor version by 1 and resets the patch version to 0
+# for all workspace packages (excluding examples), updating all dependency versions
+# and internal references dynamically.
+# ==============================================================================
+
+def get_all_cargo_toml_files(root_dir):
+    """Recursively search for Cargo.toml files in the workspace, excluding target, git, and examples."""
+    cargo_files = []
+    for root, dirs, files in os.walk(root_dir):
+        if 'target' in dirs:
+            dirs.remove('target')
+        if '.git' in dirs:
+            dirs.remove('.git')
+        if 'example' in dirs:
+            dirs.remove('example')
+            
+        for file in files:
+            if file == 'Cargo.toml':
+                cargo_files.append(os.path.join(root, file))
+    return cargo_files
+
+def parse_package_info(file_path):
+    """Parse name and version from a Cargo.toml file under the [package] section."""
+    with open(file_path, 'r', encoding='utf-8') as f:
+        content = f.read()
+        
+    package_name = None
+    package_version = None
+    current_section = None
+    
+    for line in content.splitlines():
+        line_stripped = line.strip()
+        if line_stripped.startswith('[') and line_stripped.endswith(']'):
+            current_section = line_stripped[1:-1].strip()
+        elif current_section == 'package':
+            if line_stripped.startswith('name'):
+                parts = line_stripped.split('=', 1)
+                package_name = parts[1].strip().strip('"').strip("'")
+            elif line_stripped.startswith('version'):
+                parts = line_stripped.split('=', 1)
+                package_version = parts[1].strip().strip('"').strip("'")
+                
+    return package_name, package_version
+
+def calculate_bumped_version(version_str):
+    """Increment minor version by 1 and set patch version to 0 (e.g., 3.36.0 -> 3.37.0)."""
+    parts = version_str.split('.')
+    if len(parts) >= 2:
+        major = parts[0]
+        minor = int(parts[1])
+        return f"{major}.{minor + 1}.0"
+    return version_str
+
+def run_bump(root_dir):
+    """Bumps all project versions and updates their inter-dependencies."""
+    print("=== STARTING VERSION BUMP ===")
+    cargo_files = get_all_cargo_toml_files(root_dir)
+    
+    # Exclude the workspace root Cargo.toml itself
+    root_cargo = os.path.join(root_dir, 'Cargo.toml')
+    if root_cargo in cargo_files:
+        cargo_files.remove(root_cargo)
+        
+    print(f"Found {len(cargo_files)} project Cargo.toml manifests.")
+    
+    # Map each package name to its current and bumped versions
+    package_map = {}
+    for file_path in cargo_files:
+        name, version = parse_package_info(file_path)
+        if name and version:
+            new_version = calculate_bumped_version(version)
+            package_map[name] = {
+                'file_path': file_path,
+                'old_version': version,
+                'new_version': new_version
+            }
+            
+    print("\nPlanned version bumps:")
+    for name, info in package_map.items():
+        print(f"  {name}: {info['old_version']} -> {info['new_version']}")
+        
+    # Update Cargo.toml files (package version and dependencies)
+    for name, info in package_map.items():
+        file_path = info['file_path']
+        old_ver = info['old_version']
+        new_ver = info['new_version']
+        
+        with open(file_path, 'r', encoding='utf-8') as f:
+            lines = f.readlines()
+            
+        new_lines = []
+        current_section = None
+        
+        for line in lines:
+            line_stripped = line.strip()
+            if line_stripped.startswith('[') and line_stripped.endswith(']'):
+                current_section = line_stripped[1:-1].strip()
+                new_lines.append(line)
+                continue
+                
+            # Update package version
+            if current_section == 'package' and line_stripped.startswith('version'):
+                pattern = rf'(version\s*=\s*")[^"]+(")'
+                updated_line = re.sub(pattern, rf'\g<1>{new_ver}\g<2>', line)
+                new_lines.append(updated_line)
+                continue
+                
+            # Update dependency version if any workspace members are referenced
+            updated = False
+            for dep_name, dep_info in package_map.items():
+                if dep_name in line:
+                    dep_old_ver = dep_info['old_version']
+                    dep_new_ver = dep_info['new_version']
+                    
+                    # Pattern for table style: dep_name = { version = "X.Y.Z", ... }
+                    table_pattern = rf'(\b{re.escape(dep_name)}\b.*?version\s*=\s*")[^"]+(")'
+                    if re.search(table_pattern, line):
+                        line = re.sub(table_pattern, rf'\g<1>{dep_new_ver}\g<2>', line)
+                        updated = True
+                        
+                    # Pattern for string style: dep_name = "X.Y.Z"
+                    simple_pattern = rf'(\b{re.escape(dep_name)}\b\s*=\s*")[^"]+(")'
+                    if not updated and re.search(simple_pattern, line):
+                        line = re.sub(simple_pattern, rf'\g<1>{dep_new_ver}\g<2>', line)
+                        updated = True
+                        
+            new_lines.append(line)
+            
+        with open(file_path, 'w', encoding='utf-8') as f:
+            f.writelines(new_lines)
+            
+        print(f"Updated package version in {os.path.relpath(file_path, root_dir)}")
+        
+    # Also update target_rusty_lr_version() inside rusty_lr_parser/src/lib.rs
+    # to maintain consistency with the new version of rusty_lr.
+    rusty_lr_info = package_map.get('rusty_lr')
+    if rusty_lr_info:
+        old_ver = rusty_lr_info['old_version']
+        new_ver = rusty_lr_info['new_version']
+        
+        old_parts = old_ver.split('.')
+        new_parts = new_ver.split('.')
+        if len(old_parts) >= 3 and len(new_parts) >= 3:
+            old_tuple = f"({old_parts[0]}, {old_parts[1]}, {old_parts[2]})"
+            new_tuple = f"({new_parts[0]}, {new_parts[1]}, {new_parts[2]})"
+            
+            lib_rs_path = os.path.join(root_dir, 'rusty_lr_parser', 'src', 'lib.rs')
+            if os.path.exists(lib_rs_path):
+                with open(lib_rs_path, 'r', encoding='utf-8') as f:
+                    lib_content = f.read()
+                    
+                pattern = r'\(\s*' + re.escape(old_parts[0]) + r'\s*,\s*' + re.escape(old_parts[1]) + r'\s*,\s*' + re.escape(old_parts[2]) + r'\s*\)'
+                if re.search(pattern, lib_content):
+                    lib_content = re.sub(pattern, new_tuple, lib_content)
+                    with open(lib_rs_path, 'w', encoding='utf-8') as f:
+                        f.write(lib_content)
+                    print(f"Updated target_rusty_lr_version in {os.path.relpath(lib_rs_path, root_dir)} to {new_tuple}")
+                else:
+                    print(f"Warning: Could not find matching version tuple {old_tuple} in {lib_rs_path}")
+            else:
+                print(f"Warning: {lib_rs_path} does not exist.")
+                
+    print("\n=== SUMMARY OF VERSION CHANGES ===")
+    for name, info in package_map.items():
+        print(f"  {name: <25}: {info['old_version']} -> {info['new_version']}")
+    print("===================================")
+    print("=== VERSION BUMP COMPLETE ===\n")
+    return package_map
+
+
+# ==============================================================================
+# INTENT 2: Bootstrap Testing
+# Runs bootstrap test suite. If tests fail, exits immediately with non-zero code.
+# ==============================================================================
+
+def run_bootstrap(root_dir):
+    """Runs the workspace bootstrap test script to ensure all tests pass."""
+    print("=== STARTING BOOTSTRAP TEST ===")
+    cmd = ["./scripts/bootstrap_test.sh"]
+    print(f"Running '{' '.join(cmd)}' in {root_dir}...")
+    result = subprocess.run(cmd, cwd=root_dir)
+    if result.returncode != 0:
+        print("\nError: Bootstrap tests failed. Aborting pipeline.")
+        sys.exit(1)
+    print("=== BOOTSTRAP TEST PASSED ===\n")
+
+
+# ==============================================================================
+# INTENT 3: Automatic Git Commit
+# Automatically stages and commits all changes (Cargo.toml manifests, lib.rs)
+# after a successful release run, with a detailed summary message.
+# ==============================================================================
+
+def run_git_commit(root_dir, package_map):
+    """Stages and commits all version changes to git with a descriptive release message."""
+    print("=== STARTING GIT COMMIT ===")
+    
+    # Construct a detailed commit message outlining the bumped versions
+    commit_msg = "Release: Bump versions and execute cargo publish\n\nVersion updates:\n"
+    for name, info in package_map.items():
+        commit_msg += f"- {name}: {info['old_version']} -> {info['new_version']}\n"
+        
+    try:
+        # Run 'git add .'
+        print("Running 'git add .'...")
+        subprocess.run(["git", "add", "."], cwd=root_dir, check=True)
+        
+        # Run 'git commit -m ...'
+        print("Running 'git commit -m ...'")
+        subprocess.run(["git", "commit", "-m", commit_msg], cwd=root_dir, check=True)
+        
+        print("Git commit completed successfully!")
+    except subprocess.CalledProcessError as e:
+        print(f"Error during git commit: {e}")
+    print("=== GIT COMMIT COMPLETE ===\n")
+
+
+# ==============================================================================
+# INTENT 4: Workspace Crate Publishing
+# Cargo publish requires that dependencies are either published or can be resolved.
+# When publishing multiple interdependent workspace packages, we temporarily
+# comment out all other members from the root Cargo.toml workspace definition,
+# publish the current package, and move to the next.
+# A backup of root Cargo.toml is automatically created and restored at the end
+# (or in case of errors) to ensure clean repository state.
+# ==============================================================================
+
+def comment_out_workspace_members(content, keep_member):
+    """Modify the workspace members array, commenting out all members except keep_member."""
+    lines = content.splitlines()
+    new_lines = []
+    in_members = False
+    for line in lines:
+        line_stripped = line.strip()
+        if 'members' in line_stripped and '=' in line_stripped:
+            in_members = True
+            new_lines.append(line)
+            continue
+        if in_members and line_stripped.startswith(']'):
+            in_members = False
+            new_lines.append(line)
+            continue
+            
+        if in_members:
+            match = re.search(r'["\']([^"\']+)["\']', line_stripped)
+            if match:
+                member = match.group(1)
+                indent = line[:len(line) - len(line.lstrip())]
+                content_part = line.lstrip()
+                if member != keep_member:
+                    if not content_part.startswith('#'):
+                        new_lines.append(f"{indent}# {content_part}")
+                    else:
+                        new_lines.append(line)
+                else:
+                    if content_part.startswith('#'):
+                        uncommented = content_part[1:].lstrip()
+                        new_lines.append(f"{indent}{uncommented}")
+                    else:
+                        new_lines.append(line)
+            else:
+                new_lines.append(line)
+        else:
+            new_lines.append(line)
+    return '\n'.join(new_lines) + '\n'
+
+def run_publish(root_dir, cargo_args):
+    """Publishes all workspace packages in the correct dependency order."""
+    print("=== STARTING WORKSPACE PUBLISH ===")
+    root_cargo_path = os.path.join(root_dir, 'Cargo.toml')
+    backup_cargo_path = os.path.join(root_dir, 'Cargo.toml.bak')
+    
+    # Clean up previous backup if any exists
+    if os.path.exists(backup_cargo_path):
+        print(f"Warning: backup file {backup_cargo_path} already exists. Restoring it first...")
+        shutil.copyfile(backup_cargo_path, root_cargo_path)
+        os.remove(backup_cargo_path)
+        
+    # Create backup of the original root Cargo.toml
+    shutil.copyfile(root_cargo_path, backup_cargo_path)
+    
+    try:
+        for project in PUBLISH_ORDER:
+            # Retrieve version of this specific project
+            project_cargo_path = os.path.join(root_dir, project, 'Cargo.toml')
+            _, version = parse_package_info(project_cargo_path)
+            
+            cmd = ["cargo", "publish"] + cargo_args
+            
+            print(f"\n==========================================")
+            print(f"Preparing to publish package: {project}")
+            print(f"==========================================")
+            print(f"  Package Name     : {project}")
+            print(f"  Version          : {version}")
+            print(f"  Command          : {' '.join(cmd)}")
+            print(f"  Target Directory : {os.path.join(root_dir, project)}")
+            print(f"==========================================")
+            
+            # Request user confirmation
+            print("\nWARNING: This action will publish to crates.io (unless --dry-run is active).")
+            try:
+                response = input(f"Confirm: Do you want to publish '{project} v{version}'? [y/N]: ").strip().lower()
+            except EOFError:
+                # Fallback if standard input is not available/interactive
+                print("EOFError: standard input is not available. Aborting for safety.")
+                sys.exit(1)
+                
+            if response != 'y':
+                print(f"Publishing of {project} cancelled by user. Aborting publish loop.")
+                sys.exit(0)
+            
+            # Read backup content (original) and comment out other workspace members
+            with open(backup_cargo_path, 'r', encoding='utf-8') as f:
+                original_content = f.read()
+            modified_content = comment_out_workspace_members(original_content, project)
+            with open(root_cargo_path, 'w', encoding='utf-8') as f:
+                f.write(modified_content)
+                
+            # Run cargo publish
+            print(f"Running '{' '.join(cmd)}' in {os.path.join(root_dir, project)}...")
+            result = subprocess.run(cmd, cwd=os.path.join(root_dir, project))
+            if result.returncode != 0:
+                print(f"\nError: Failed to publish {project}. Aborting publish loop.")
+                sys.exit(1)
+                
+            print(f"Successfully processed {project}.")
+            
+        print("\nAll projects processed successfully!")
+        
+    finally:
+        # Guarantee the restoration of the root Cargo.toml
+        if os.path.exists(backup_cargo_path):
+            print(f"\nRestoring original Cargo.toml from backup...")
+            shutil.copyfile(backup_cargo_path, root_cargo_path)
+            os.remove(backup_cargo_path)
+            print("Restore complete.")
+    print("=== WORKSPACE PUBLISH COMPLETE ===")
+
+# ==============================================================================
+# MAIN ENTRYPOINT & CLI PARSING
+# ==============================================================================
+
+def main():
+    script_dir = os.path.dirname(os.path.abspath(__file__))
+    root_dir = os.path.abspath(os.path.join(script_dir, '..'))
+    
+    # SAFETY WARNING: Bumping versions modifies Cargo.toml files, which makes the git directory dirty.
+    # Therefore, 'cargo publish' will fail unless the '--allow-dirty' flag is specified.
+    parser = argparse.ArgumentParser(
+        description="RustyLR Workspace Release Manager - Combines version bumping, testing, and package publishing.\n\n"
+                    "WARNING: Publishing modifies Cargo.toml files in place, making the git working directory dirty.\n"
+                    "You should specify '--allow-dirty' when running 'publish' or 'all' commands to prevent cargo publish from failing.",
+        formatter_class=argparse.RawDescriptionHelpFormatter
+    )
+    parser.add_argument(
+        "command",
+        choices=["bump", "publish", "all"],
+        help="Action to perform: 'bump' (version bump), 'publish' (sequence publish), or 'all' (bump -> bootstrap -> publish)."
+    )
+    
+    # parse_known_args returns the parsed args namespace and a list of remaining arguments
+    args, cargo_args = parser.parse_known_args()
+    
+    # Check for --allow-dirty warning if command involves publishing
+    if args.command in ["publish", "all"]:
+        if "--allow-dirty" not in cargo_args:
+            print("\n" + "!" * 80)
+            print("WARNING: '--allow-dirty' is not specified.")
+            print("Since the release script modifies Cargo.toml files in place, the git working tree")
+            print("is dirty, and 'cargo publish' will fail unless '--allow-dirty' is provided.")
+            print("!" * 80 + "\n")
+            
+    if args.command == "bump":
+        run_bump(root_dir)
+    elif args.command == "publish":
+        run_publish(root_dir, cargo_args)
+    elif args.command == "all":
+        package_map = run_bump(root_dir)
+        run_bootstrap(root_dir)
+        run_publish(root_dir, cargo_args)
+        run_git_commit(root_dir, package_map)
+
+if __name__ == '__main__':
+    main()
