[OOM] Add alternative method of chart compilation 07/143107/1
authorMarek Szwałkiewicz <marek.szwalkiewicz@external.t-mobile.pl>
Tue, 3 Feb 2026 10:29:00 +0000 (11:29 +0100)
committerMarek Szwałkiewicz <marek.szwalkiewicz@external.t-mobile.pl>
Tue, 3 Feb 2026 10:29:18 +0000 (11:29 +0100)
make_all.py is a replacement for set of Makefiles that allows for dependency
aware compilation in a concurrent way.

Issue-ID: OOM-3380
Change-Id: I50f62dab41de69391ddb994aa678d9bb51021360
Signed-off-by: Marek Szwałkiewicz <marek.szwalkiewicz@external.t-mobile.pl>
kubernetes/make_all.py [new file with mode: 0755]

diff --git a/kubernetes/make_all.py b/kubernetes/make_all.py
new file mode 100755 (executable)
index 0000000..f7aa548
--- /dev/null
@@ -0,0 +1,766 @@
+#!/usr/bin/env python3
+"""
+Helm Chart Builder
+
+This script replaces the functionality of the Makefile for building Helm charts.
+It discovers, validates, packages, and optionally pushes Helm charts to a chart museum.
+
+Features:
+- Discovers Helm charts in the current directory and subdirectories
+- Validates chart dependencies and sorts charts by dependency order
+- Packages charts with Helm
+- Optionally pushes charts to a chart museum
+- Supports concurrent processing with configurable limits
+- Provides detailed logging and error handling
+
+Usage:
+    make_all.py                    # Build all charts
+    make_all.py --skip-lint        # Build all charts without linting
+    make_all.py --clean            # Clean build artifacts
+    make_all.py --list-charts      # List all charts without building
+    make_all.py --max-concurrent 2 # Build charts with max 2 concurrent processes
+"""
+
+import argparse
+import glob
+import logging
+import os
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+import yaml
+import re
+import asyncio
+import concurrent.futures
+from typing import Dict, List, Optional, Tuple, Union, Any
+
+
+# Constants
+IGNORED_DIRECTORIES = ["helm", "authentication", "onap"]
+DEFAULT_OUTPUT_DIR = Path(__file__).parent.resolve() / 'dist'
+
+
+class BuildError(Exception):
+    """Exception raised when a build error occurs that should stop execution."""
+    pass
+
+
+class FailFastLogger:
+    """Custom logger that raises BuildError on ERROR level logs."""
+
+    def __init__(self, name: str):
+        self._logger = logging.getLogger(name)
+        self.level = self._logger.level
+
+    def debug(self, msg: str, *args, **kwargs) -> None:
+        self._logger.debug(msg, *args, **kwargs)
+
+    def info(self, msg: str, *args, **kwargs) -> None:
+        self._logger.info(msg, *args, **kwargs)
+
+    def warning(self, msg: str, *args, **kwargs) -> None:
+        self._logger.warning(msg, *args, **kwargs)
+
+    def error(self, msg: str, *args, **kwargs) -> None:
+        self._logger.error(msg, *args, **kwargs)
+        raise BuildError(f"Build error occurred: {msg}")
+
+    def critical(self, msg: str, *args, **kwargs) -> None:
+        self._logger.critical(msg, *args, **kwargs)
+        raise BuildError(f"Critical build error occurred: {msg}")
+
+
+# Set up logging
+logging.basicConfig(
+    level=logging.INFO,
+    format='%(asctime)s - %(levelname)s - %(message)s'
+)
+logger = FailFastLogger(__name__)
+
+# Global set to track package names already pushed to chartmuseum
+pushed_packages = set()
+
+
+def check_helm_version(helm_bin: str) -> bool:
+    """
+    Check if the Helm version starts with 3.7
+
+    Args:
+        helm_bin: Path to the Helm binary
+
+    Returns:
+        True if Helm version is compatible, False otherwise
+    """
+    try:
+        # Get Helm version
+        version_result = subprocess.run([helm_bin, "version", "--template", "{{.Version}}"],
+                                       check=False, capture_output=True, text=True)
+
+        if version_result.returncode != 0:
+            logger.error(f"Failed to get Helm version: {version_result.stderr}")
+            return False
+
+        version_output = version_result.stdout.strip()
+
+        # Extract just the version number (remove leading 'v' if present)
+        version_clean = version_output.lstrip('v')
+
+        # Check if the version starts with "3.7"
+        if version_clean.startswith("3.7"):
+            logger.info(f"Helm version {version_output} is compatible (starts with 3.7)")
+            return True
+        else:
+            logger.error(f"Helm version {version_output} does not start with 3.7 as required")
+            return False
+
+    except Exception as e:
+        logger.debug(f"Error checking Helm version: {e}")
+        return False
+
+def update_dependencies(chart_path: Union[str, Path], helm_bin: str) -> bool:
+    """
+    Update chart dependencies using Helm dependency update command.
+
+    This function executes the equivalent of the 'dep-%' step from the Makefile,
+    which updates the dependencies for a Helm chart if a Chart.yaml file exists.
+
+    Args:
+        chart_path: Path to the chart directory containing Chart.yaml
+        helm_bin: Path to the Helm binary to use for dependency update
+
+    Returns:
+        True if dependencies were updated successfully or no Chart.yaml exists, False otherwise
+    """
+    chart_path_obj = Path(chart_path)
+    if (chart_path_obj / "Chart.yaml").is_file():
+        logger.info(f"Updating dependencies for chart in {chart_path_obj}")
+        dep_cmd = [helm_bin, "dependency", "update", str(chart_path_obj)]
+        dep_result = subprocess.run(dep_cmd, check=False, capture_output=True, text=True)
+        if dep_result.returncode != 0:
+            logger.error(f"Helm dependency update failed for {chart_path_obj}: {dep_result.stderr}")
+            return False
+        else:
+            logger.info(f"Dependencies updated for {chart_path_obj}")
+            return True
+    return True
+
+
+def lint_chart(chart_path: Union[str, Path], helm_bin: str, skip_lint: bool) -> bool:
+    """
+    Lint the chart using Helm lint command if not skipped.
+
+    This function executes the equivalent of the 'lint-%' step from the Makefile,
+    which runs Helm lint on a chart if a Chart.yaml file exists and skip_lint is False.
+
+    Args:
+        chart_path: Path to the chart directory containing Chart.yaml
+        helm_bin: Path to the Helm binary to use for linting
+        skip_lint: Whether to skip the linting step
+
+    Returns:
+        True if linting passed or was skipped, False otherwise
+    """
+    chart_path_obj = Path(chart_path)
+    if not skip_lint and (chart_path_obj / "Chart.yaml").is_file():
+        logger.info(f"Linting chart in {chart_path_obj}")
+        lint_cmd = [helm_bin, "lint", str(chart_path_obj)]
+        lint_result = subprocess.run(lint_cmd, check=False, capture_output=True, text=True)
+        if lint_result.returncode != 0:
+            logger.error(f"Helm lint failed for {chart_path_obj}: {lint_result.stderr}")
+            return False
+        else:
+            logger.info(f"Linting passed for {chart_path_obj}")
+    return True
+
+
+def package_chart(chart_path: Union[str, Path], helm_bin: str, output_dir: Union[str, Path]) -> Optional[str]:
+    """
+    Package the chart to the output directory using Helm package command.
+
+    This function executes the equivalent of the 'package-%' step from the Makefile,
+    which packages a Helm chart to the specified output directory if a Chart.yaml file exists.
+
+    Args:
+        chart_path: Path to the chart directory containing Chart.yaml
+        helm_bin: Path to the Helm binary to use for packaging
+        output_dir: Output directory where the packaged chart should be stored
+
+    Returns:
+        Path to the packaged chart file if successful, or None if packaging failed
+    """
+    chart_path_obj = Path(chart_path)
+    output_dir_obj = Path(output_dir)
+
+    if (chart_path_obj / "Chart.yaml").is_file():
+        # Create the output directory if it doesn't exist
+        package_dir = output_dir_obj / "packages"
+        package_dir.mkdir(parents=True, exist_ok=True)
+
+        # Prepare the helm package command
+        package_cmd = [helm_bin, "package", str(chart_path_obj), "--destination", str(package_dir)]
+
+        logger.info(f"Running command: {' '.join(package_cmd)}")
+
+        # Execute the package command
+        package_result = subprocess.run(package_cmd, check=False, capture_output=True, text=True)
+        if package_result.returncode != 0:
+            logger.error(f"Helm package failed for {chart_path_obj}: {package_result.stderr}")
+            return None
+
+        # Extract the package name from the output
+        package_output = package_result.stdout.strip()
+        if ":" in package_output:
+            # Extract package name from output like "Successfully packaged chart and saved it to: /path/to/chart-1.0.0.tgz"
+            package_parts = package_output.split(":")
+            if len(package_parts) >= 2:
+                package_name = package_parts[1].strip()
+            else:
+                # Alternative parsing if format is different
+                package_lines = package_output.split("\n")
+                for line in package_lines:
+                    if line.strip().endswith(".tgz") or line.strip().endswith(".tar.gz"):
+                        package_name = line.strip()
+                        break
+                else:
+                    package_name = None
+        else:
+            # Get the chart directory name to help find the packaged file
+            chart_dir_name = chart_path_obj.name
+            # Try to find the .tgz file in the package directory
+            tgz_files = list(package_dir.glob(f"{chart_dir_name}*.tgz"))
+            if tgz_files:
+                package_name = str(tgz_files[-1])  # Take the most recently created one
+            else:
+                tar_gz_files = list(package_dir.glob(f"{chart_dir_name}*.tar.gz"))
+                if tar_gz_files:
+                    package_name = str(tar_gz_files[-1])
+                else:
+                    package_name = None
+
+        if package_name:
+            logger.info(f"Package created: {package_name}")
+            return package_name
+        else:
+            logger.error(f"Could not determine package name from output: {package_output}")
+            return None
+    return None
+
+def push_to_chartmuseum(chart_path: Union[str, Path], package_name: str, helm_bin: str) -> bool:
+    """
+    Push the packaged chart to chart museum using Helm cm-push command.
+
+    This function executes the push portion of the 'package-%' step from the Makefile,
+    which pushes a packaged Helm chart to a chart museum repository.
+
+    Args:
+        chart_path: Path to the original chart directory (used for logging)
+        package_name: Full path to the packaged chart file (.tgz)
+        helm_bin: Path to the Helm binary to use for pushing
+
+    Returns:
+        True if push was successful, False otherwise
+    """
+    chart_path_obj = Path(chart_path)
+    package_path_obj = Path(package_name)
+
+    # Check and log the size of the package before pushing
+    if package_path_obj.exists():
+        package_size = package_path_obj.stat().st_size
+        logger.info(f"Package {package_path_obj} size: {package_size} bytes ({package_size / (1024*1024):.2f} MB)")
+    else:
+        logger.error(f"Package file does not exist: {package_path_obj}")
+        return False
+
+    # Push to chart museum using cm-push
+    # Note: This assumes chartmuseum plugin is installed
+    push_cmd = [helm_bin, "cm-push", '-f', str(package_path_obj), "local"]
+    logger.info(f"Pushing to chart museum: {' '.join(push_cmd)}")
+
+    push_result = subprocess.run(push_cmd, check=False, capture_output=True, text=True)
+    if push_result.returncode != 0:
+        logger.error(f"Chart museum push failed for {package_path_obj}: {push_result.stderr}")
+        logger.info("Note: This may fail if the chartmuseum plugin is not installed.")
+        return False
+    else:
+        logger.info(f"Successfully pushed {package_path_obj} to chart museum")
+        return True
+
+
+def process_chart(chart_path: Union[str, Path], helm_bin: str, skip_lint: bool, output_dir: Union[str, Path]) -> bool:
+    """
+    Process a single chart following the complete Makefile workflow.
+
+    This function performs the complete workflow for a single Helm chart:
+    1. Checks if the chart has already been processed
+    2. Updates dependencies
+    3. Lints the chart (unless skipped)
+    4. Packages the chart
+    5. Pushes the packaged chart to chart museum
+
+    Args:
+        chart_path: Path to the chart directory containing Chart.yaml
+        helm_bin: Path to the Helm binary to use for operations
+        skip_lint: Whether to skip the linting step
+        output_dir: Output directory where packaged charts should be stored
+
+    Returns:
+        True if the chart was processed successfully, False otherwise
+    """
+    chart_path_obj = Path(chart_path)
+    # Get the chart name from Chart.yaml to use as package identifier
+    chart_yaml_path = chart_path_obj / "Chart.yaml"
+    chart_name = None
+
+    if chart_yaml_path.exists():
+        try:
+            with open(chart_yaml_path, 'r') as f:
+                chart_data = yaml.safe_load(f)
+                chart_name = chart_data.get('name')
+        except yaml.YAMLError as e:
+            logger.debug(f"Could not parse Chart.yaml in {chart_path_obj}: {e}")
+        except Exception as e:
+            logger.debug(f"Error reading Chart.yaml in {chart_path_obj}: {e}")
+
+    # Check if this chart has already been pushed (using chart name as identifier)
+    if chart_name and chart_name in pushed_packages:
+        logger.info(f"Chart {chart_name} already pushed to chart museum, skipping...")
+        return True
+
+    # dep-%: Update dependencies
+    deps_success = update_dependencies(chart_path_obj, helm_bin)
+    if not deps_success:
+        return False
+
+    # lint-%: Run helm lint if not skipped
+    lint_success = lint_chart(chart_path_obj, helm_bin, skip_lint)
+    if not lint_success:
+        return False
+
+    # package-%: Package the chart and push to chart museum
+    package_name = package_chart(chart_path_obj, helm_bin, output_dir)
+    if package_name is None:
+        return False
+
+    push_success = push_to_chartmuseum(chart_path_obj, package_name, helm_bin)
+    if not push_success:
+        return False
+
+    # Add chart name to the set of pushed packages after successful processing
+    if chart_name:
+        pushed_packages.add(chart_name)
+
+    logger.info(f"Successfully processed chart in {chart_path_obj}")
+    return True
+
+
+async def run_process_chart_async(chart_path: Union[str, Path], helm_bin: str, skip_lint: bool, output_dir: Union[str, Path], chart_name: str) -> bool:
+    """
+    Wrapper to run process_chart in a thread-safe way.
+
+    This function wraps the synchronous process_chart function to run it in a thread pool
+    executor, allowing it to be called from an async context without blocking the event loop.
+
+    Args:
+        chart_path: Path to the chart directory containing Chart.yaml
+        helm_bin: Path to the Helm binary to use for operations
+        skip_lint: Whether to skip the linting step
+        output_dir: Output directory where packaged charts should be stored
+        chart_name: Name of the chart (for logging purposes)
+
+    Returns:
+        True if the chart was processed successfully, False otherwise
+    """
+    def run_in_thread():
+        return process_chart(
+            chart_path=chart_path,
+            helm_bin=helm_bin,
+            skip_lint=skip_lint,
+            output_dir=output_dir
+        )
+
+    # Run the synchronous function in a thread pool to avoid blocking the event loop
+    loop = asyncio.get_event_loop()
+    return await loop.run_in_executor(None, run_in_thread)
+
+def discover_helm_charts(charts_dir: Union[str, Path]) -> List[Dict[str, Any]]:
+    """
+    Discover all Helm charts in the current directory and subdirectories based on the Makefile logic.
+
+    This function walks through the directory tree starting from charts_dir, looking for directories
+    that contain a Chart.yaml file. For each chart found, it extracts the chart name and dependencies
+    from the Chart.yaml file.
+
+    Args:
+        charts_dir: Root directory to search for Helm charts
+
+    Returns:
+        List of dictionaries containing chart information with keys:
+        - 'name': Name of the chart
+        - 'dependencies': List of dependency names
+        - 'path': Relative path to the chart directory from charts_dir
+    """
+
+    # Discover chart directories that contain Chart.yaml
+    charts_info = []
+
+    # Walk through all subdirectories recursively
+    for root, dirs, files in os.walk(charts_dir):
+        # Skip ignored directories
+        original_dirs = dirs[:]
+        dirs[:] = [d for d in dirs if d not in IGNORED_DIRECTORIES]
+
+        # Log skipped directories
+        skipped_dirs = set(original_dirs) - set(dirs)
+        for skipped_dir in skipped_dirs:
+            logger.debug(f"Skipping ignored directory: {Path(root) / skipped_dir}")
+
+        # Check if Chart.yaml exists in the current directory
+        if "Chart.yaml" in files:
+            chart_path = Path(root)
+            chart_yaml_path = chart_path / "Chart.yaml"
+
+            # Parse the Chart.yaml to get the chart name and dependencies
+            try:
+                with open(chart_yaml_path, 'r') as f:
+                    chart_data = yaml.safe_load(f)
+                    chart_name = chart_data.get('name', chart_path.name)  # Use directory name as fallback
+
+                    # Extract dependencies if they exist
+                    dependencies = []
+                    if 'dependencies' in chart_data:
+                        for dep in chart_data.get('dependencies', []):
+                            if 'name' in dep:
+                                dependencies.append(dep['name'])
+
+            except yaml.YAMLError as e:
+                logger.error(f"Could not parse Chart.yaml in {chart_path}: {e}")
+                raise
+            except Exception as e:
+                logger.error(f"Error reading Chart.yaml in {chart_path}: {e}")
+                raise
+
+            # Use relative path from current directory
+            relative_path = chart_path.relative_to(charts_dir)
+
+            # Add chart info as a dictionary to the list
+            chart_info = {
+                'name': chart_name,
+                'dependencies': dependencies,
+                'path': str(relative_path)
+            }
+            charts_info.append(chart_info)
+
+    logger.debug(f"Discovered charts: {charts_info}")
+    return charts_info
+
+
+def sort_helm_charts(charts_info: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+    """
+    Sort Helm charts based on their dependencies using topological sorting.
+
+    This function implements Kahn's algorithm for topological sorting to arrange charts
+    in an order where dependencies are built before the charts that depend on them.
+    Charts with no dependencies come first, followed by charts that depend on those,
+    and so on.
+
+    Args:
+        charts_info: List of dictionaries containing chart information as returned by discover_helm_charts
+
+    Returns:
+        List of chart dictionaries sorted by dependency order (independent first)
+
+    Raises:
+        ValueError: If there are circular dependencies among charts
+    """
+    # Create a mapping from chart name to its info for quick lookup
+    chart_map = {chart['name']: chart for chart in charts_info}
+
+    # Build dependency graph - for each chart, store which other charts depend on it
+    dependents_graph = {chart['name']: [] for chart in charts_info}
+    dependencies_count = {chart['name']: 0 for chart in charts_info}
+
+    # Calculate direct dependencies for each chart
+    for chart in charts_info:
+        chart_name = chart['name']
+        for dep_name in chart['dependencies']:
+            if dep_name in chart_map:  # Only consider dependencies that exist in our chart list
+                dependents_graph[dep_name].append(chart_name)
+                dependencies_count[chart_name] += 1
+
+    # Topological sort using Kahn's algorithm
+    # Start with charts that have no dependencies
+    queue = []
+    for chart_name, count in dependencies_count.items():
+        if count == 0:
+            queue.append(chart_name)
+
+    sorted_charts = []
+
+    while queue:
+        # Pop a chart with no remaining dependencies
+        current_chart_name = queue.pop(0)
+        sorted_charts.append(chart_map[current_chart_name])
+
+        # Reduce the dependency count for all charts that depended on this one
+        for dependent_chart in dependents_graph[current_chart_name]:
+            dependencies_count[dependent_chart] -= 1
+            if dependencies_count[dependent_chart] == 0:
+                queue.append(dependent_chart)
+
+    return sorted_charts
+
+
+def get_ready_charts(remaining_charts: List[str], chart_info_by_name: Dict[str, Any],
+                     pushed_packages_set: set) -> List[Tuple[Dict[str, Any], str, Path]]:
+    """
+    Get list of charts that have all dependencies met and are ready to be processed.
+
+    Args:
+        remaining_charts: List of chart names that still need to be processed
+        chart_info_by_name: Dictionary mapping chart names to their info
+        pushed_packages_set: Set of packages already pushed to chartmuseum
+
+    Returns:
+        List of tuples containing (chart_info, chart_name, chart_path) for ready charts
+    """
+    ready_charts = []
+
+    for chart_name in remaining_charts:
+        # Find the chart in the list by name
+        chart_info = chart_info_by_name.get(chart_name)
+
+        if chart_info:
+            # Check if all dependencies for this chart have been pushed
+            dependencies = chart_info.get('dependencies', [])
+            missing_deps = [dep for dep in dependencies if dep not in pushed_packages_set]
+
+            # Only process if all dependencies have been pushed or there are no dependencies
+            if not missing_deps:
+                chart_path = Path(chart_info['path']).resolve()
+                logger.info(f"Building chart: {chart_name} at {chart_path}")
+                ready_charts.append((chart_info, chart_name, chart_path))
+            else:
+                logger.debug(f"Chart '{chart_name}' has unmet dependencies {missing_deps}, skipping for now...")
+        else:
+            logger.warning(f"Chart '{chart_name}' not found, skipping...")
+
+    return ready_charts
+
+
+async def process_ready_charts(ready_charts: List[Tuple[Dict[str, Any], str, Path]],
+                               semaphore: asyncio.Semaphore, args) -> List[Tuple[bool, str]]:
+    """
+    Process the ready charts concurrently with semaphore control.
+
+    Args:
+        ready_charts: List of tuples containing (chart_info, chart_name, chart_path) for ready charts
+        semaphore: Semaphore to limit concurrent processing
+        args: Parsed command-line arguments object
+
+    Returns:
+        List of tuples containing (success, chart_name) for each processed chart
+    """
+    async def process_chart_with_semaphore(chart_path, helm_bin, skip_lint, output_dir, chart_name):
+        async with semaphore:
+            return await run_process_chart_async(
+                chart_path=chart_path,
+                helm_bin=helm_bin,
+                skip_lint=skip_lint,
+                output_dir=output_dir,
+                chart_name=chart_name
+            )
+
+    tasks = []
+    for chart_info, chart_name, chart_path in ready_charts:
+        # Schedule the chart processing asynchronously with semaphore
+        task = asyncio.create_task(
+            process_chart_with_semaphore(
+                chart_path=str(chart_path),
+                helm_bin=args.helm_bin,
+                skip_lint=args.skip_lint,
+                output_dir=str(args.output_dir),
+                chart_name=chart_name
+            )
+        )
+        tasks.append((task, chart_name))
+
+    # Wait for all scheduled tasks to complete and collect results
+    results = []
+    for task, chart_name in tasks:
+        try:
+            success = await task
+            results.append((success, chart_name))
+        except Exception as e:
+            logger.error(f"Exception while building chart {chart_name}: {e}")
+            results.append((False, chart_name))  # Mark as failed
+
+    return results
+
+
+async def build_charts_with_dependencies(all_charts: List[Dict[str, Any]], args) -> bool:
+    """
+    Build charts with proper dependency handling and concurrency control.
+
+    This function manages the building of charts while respecting their dependencies
+    and limiting concurrent processing based on the max_concurrent setting.
+
+    Args:
+        all_charts: List of chart dictionaries to build
+        args: Parsed command-line arguments object
+
+    Returns:
+        True if all charts were built successfully, False otherwise
+    """
+    # Create a semaphore to limit concurrent processing
+    semaphore = asyncio.Semaphore(args.max_concurrent)
+
+    # Create a copy of the charts to build to manage the iteration
+    remaining_charts = [c['name'] for c in all_charts]
+    processed_any = True  # Flag to track if we processed any chart in the current iteration
+
+    chart_info_by_name = {chi['name']: chi for chi in all_charts}
+
+    while remaining_charts and processed_any:
+        processed_any = False
+        charts_to_remove = []  # Track charts that were successfully processed
+
+        # Get charts that are ready to be processed (all dependencies met)
+        ready_charts = get_ready_charts(remaining_charts, chart_info_by_name, pushed_packages)
+
+        # Process ready charts concurrently
+        if ready_charts:
+            results = await process_ready_charts(ready_charts, semaphore, args)
+
+            for success, chart_name in results:
+                if success:
+                    charts_to_remove.append(chart_name)
+                    processed_any = True
+                else:
+                    logger.error(f"Failed to build chart: {chart_name}")
+                    return False  # Indicate failure
+
+        # Remove successfully processed charts from remaining list
+        for chart_name in charts_to_remove:
+            if chart_name in remaining_charts:
+                remaining_charts.remove(chart_name)
+
+    # Check if there are still unprocessed charts (possible circular dependencies)
+    if remaining_charts:
+        logger.error(f"Could not process the following charts due to unmet dependencies or circular dependencies: {remaining_charts}")
+        return False
+
+    return True
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description='Build Helm charts from the kubernetes directory',
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+Examples:
+  %(prog)s                           # Build all charts
+  %(prog)s --skip-lint               # Build all charts without linting
+  %(prog)s --clean                   # Clean build artifacts
+  %(prog)s --list-charts             # List all charts without building
+  %(prog)s --max-concurrent 2        # Build charts with max 2 concurrent processes
+        """
+    )
+
+    parser.add_argument(
+        '--skip-lint',
+        action='store_true',
+        help='Skip linting of charts (equivalent to SKIP_LINT=TRUE in Makefile)'
+    )
+
+    parser.add_argument(
+        '--helm-bin',
+        default='helm',
+        help='Helm binary to use (default: helm)'
+    )
+
+    parser.add_argument(
+        '--clean',
+        action='store_true',
+        help='Clean build artifacts before building'
+    )
+
+    parser.add_argument(
+        '--list-charts',
+        action='store_true',
+        help='List discovered charts without building them'
+    )
+
+    parser.add_argument(
+        '--output-dir',
+        default=DEFAULT_OUTPUT_DIR,
+        help='Output directory for built charts (default: dist)'
+    )
+
+    parser.add_argument(
+        '--max-concurrent',
+        type=int,
+        default=4,
+        help='Maximum number of charts to process concurrently (default: 4)'
+    )
+
+    # Parse arguments first to get the helm-bin option
+    temp_args = parser.parse_known_args()[0]
+    helm_bin = temp_args.helm_bin
+
+    # Check Helm version compatibility at startup
+    if not check_helm_version(helm_bin):
+        print("ERROR: Helm version 3.7.x or compatible is required to build charts.")
+        print("Using an incompatible version will result in hardcoded size limits preventing successful builds.")
+        print(f"You can specify an alternative Helm binary using the --helm-bin parameter. Current binary: {helm_bin}")
+        return sys.exit(1)
+
+    args = parser.parse_args()
+
+    try:
+        # Discover Helm charts
+        all_charts = sort_helm_charts(discover_helm_charts(Path.cwd()))
+
+        if args.list_charts:
+            logger.info("Discovered Helm charts:")
+            for chart_info in all_charts:
+                logger.info(f"{chart_info['name']}")
+
+            return sys.exit()
+
+        # Determine which charts to build
+        charts_to_build = all_charts
+
+        logger.debug(f"Charts to build: {charts_to_build}")
+        logger.info(f"Arguments: {args}")
+
+        # If cleaning, remove output directory
+        if args.clean:
+            output_dir_path = Path(args.output_dir)
+            if output_dir_path.exists():
+                logger.info(f"Cleaning output directory: {output_dir_path}")
+                shutil.rmtree(output_dir_path)
+
+        # Build the selected charts, considering dependencies
+        # Run the async chart building function
+        success = asyncio.run(build_charts_with_dependencies(charts_to_build, args))
+
+        if not success:
+            return sys.exit(1)
+
+        logger.info("All charts built successfully!")
+        return sys.exit(0)
+
+    except BuildError as e:
+        logger.error(f"Build stopped due to error: {e}")
+        return sys.exit(1)
+    except KeyboardInterrupt:
+        logger.error("Build interrupted by user")
+        return sys.exit(1)
+    except Exception as e:
+        logger.error(f"Unexpected error during build: {e}")
+        return sys.exit(1)
+
+
+if __name__ == "__main__":
+    sys.exit(main())
\ No newline at end of file