From 947e2ab8aca59c916220e49972d0c82b973f1142 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Thu, 13 Nov 2025 21:48:00 +0200 Subject: [PATCH 01/19] calling cf-api while setting up github actions during init command --- codeflash/api/cfapi.py | 25 ++++++ codeflash/cli_cmds/cmd_init.py | 146 ++++++++++++++++++++++++++++----- 2 files changed, 150 insertions(+), 21 deletions(-) diff --git a/codeflash/api/cfapi.py b/codeflash/api/cfapi.py index fad6eaa4d..0cdf2e984 100644 --- a/codeflash/api/cfapi.py +++ b/codeflash/api/cfapi.py @@ -239,6 +239,31 @@ def create_pr( return make_cfapi_request(endpoint="/create-pr", method="POST", payload=payload) +def setup_github_actions( + owner: str, + repo: str, + base_branch: str, + workflow_content: str, + api_key: str | None = None, +) -> Response: + """Setup GitHub Actions workflow by creating a PR with the workflow file. + + :param owner: Repository owner (username or organization) + :param repo: Repository name + :param base_branch: Base branch to create PR against (e.g., "main", "master") + :param workflow_content: Content of the GitHub Actions workflow file (YAML) + :param api_key: Optional API key (uses default if not provided) + :return: Response object with pr_url and pr_number on success + """ + payload = { + "owner": owner, + "repo": repo, + "baseBranch": base_branch, + "workflowContent": workflow_content, + } + return make_cfapi_request(endpoint="/setup-github-actions", method="POST", payload=payload, api_key=api_key) + + def create_staging( original_code: dict[Path, str], new_code: dict[Path, str], diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index a53d18ce6..ffdfb3f4d 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -22,14 +22,14 @@ from rich.table import Table from rich.text import Text -from codeflash.api.cfapi import is_github_app_installed_on_repo +from codeflash.api.cfapi import is_github_app_installed_on_repo, setup_github_actions from codeflash.cli_cmds.cli_common import apologize_and_exit from codeflash.cli_cmds.console import console, logger from codeflash.cli_cmds.extension import install_vscode_extension from codeflash.code_utils.compat import LF from codeflash.code_utils.config_parser import parse_config_file from codeflash.code_utils.env_utils import check_formatter_installed, get_codeflash_api_key -from codeflash.code_utils.git_utils import get_git_remotes, get_repo_owner_and_name +from codeflash.code_utils.git_utils import get_current_branch, get_git_remotes, get_repo_owner_and_name from codeflash.code_utils.github_utils import get_github_secrets_page_url from codeflash.code_utils.shell_utils import get_shell_rc_path, save_api_key_to_rc from codeflash.either import is_successful @@ -807,21 +807,110 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n materialized_optimize_yml_content = customize_codeflash_yaml_content( optimize_yml_content, config, git_root, benchmark_mode ) - with optimize_yaml_path.open("w", encoding="utf8") as optimize_yml_file: - optimize_yml_file.write(materialized_optimize_yml_content) - # Success panel for workflow creation - workflow_success_panel = Panel( - Text( - f"βœ… Created GitHub action workflow at {optimize_yaml_path}\n\n" - "Your repository is now configured for continuous optimization!", - style="green", - justify="center", - ), - title="πŸŽ‰ Workflow Created!", - border_style="bright_green", - ) - console.print(workflow_success_panel) - console.print() + + # Get repository information for API call + git_remote = config.get("git_remote", "origin") + pr_created_via_api = False + pr_url = None + + try: + owner, repo_name = get_repo_owner_and_name(repo, git_remote) + except Exception as e: + logger.error(f"[cmd_init.py:install_github_actions] Failed to get repository owner and name: {e}") + # Fall back to local file creation + workflows_path.mkdir(parents=True, exist_ok=True) + with optimize_yaml_path.open("w", encoding="utf8") as optimize_yml_file: + optimize_yml_file.write(materialized_optimize_yml_content) + workflow_success_panel = Panel( + Text( + f"βœ… Created GitHub action workflow at {optimize_yaml_path}\n\n" + "Your repository is now configured for continuous optimization!", + style="green", + justify="center", + ), + title="πŸŽ‰ Workflow Created!", + border_style="bright_green", + ) + console.print(workflow_success_panel) + console.print() + else: + # Try to create PR via API + try: + base_branch = get_current_branch(repo) if repo.active_branch else "main" + console.print("Creating PR with GitHub Actions workflow...") + logger.info( + f"[cmd_init.py:install_github_actions] Calling setup_github_actions API for {owner}/{repo_name} on branch {base_branch}" + ) + + response = setup_github_actions( + owner=owner, + repo=repo_name, + base_branch=base_branch, + workflow_content=materialized_optimize_yml_content, + ) + + if response.status_code == 200: + response_data = response.json() + if response_data.get("success"): + pr_url = response_data.get("pr_url") + if pr_url: + pr_created_via_api = True + workflow_success_panel = Panel( + Text( + f"βœ… PR created: {pr_url}\n\n" + "Your repository is now configured for continuous optimization!", + style="green", + justify="center", + ), + title="πŸŽ‰ Workflow PR Created!", + border_style="bright_green", + ) + console.print(workflow_success_panel) + console.print() + logger.info( + f"[cmd_init.py:install_github_actions] Successfully created PR #{response_data.get('pr_number')} for {owner}/{repo_name}" + ) + else: + # File already exists with same content + pr_created_via_api = True # Mark as handled (no PR needed) + already_exists_panel = Panel( + Text( + "βœ… Workflow file already exists with the same content.\n\n" + "No changes needed - your repository is already configured!", + style="green", + justify="center", + ), + title="βœ… Already Configured", + border_style="bright_green", + ) + console.print(already_exists_panel) + console.print() + else: + raise Exception(response_data.get("error", "Unknown error")) + else: + # API call failed, fall back to local file creation + raise Exception(f"API returned status {response.status_code}") + + except Exception as api_error: + # Fall back to local file creation if API call fails + logger.warning( + f"[cmd_init.py:install_github_actions] API call failed, falling back to local file creation: {api_error}" + ) + workflows_path.mkdir(parents=True, exist_ok=True) + with optimize_yaml_path.open("w", encoding="utf8") as optimize_yml_file: + optimize_yml_file.write(materialized_optimize_yml_content) + workflow_success_panel = Panel( + Text( + f"βœ… Created GitHub action workflow at {optimize_yaml_path}\n\n" + "Your repository is now configured for continuous optimization!", + style="green", + justify="center", + ), + title="πŸŽ‰ Workflow Created!", + border_style="bright_green", + ) + console.print(workflow_success_panel) + console.print() try: existing_api_key = get_codeflash_api_key() @@ -866,10 +955,25 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n console.print(launch_panel) click.pause() click.echo() - click.echo( - f"Please edit, commit and push this GitHub actions file to your repo, and you're all set!{LF}" - f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" - ) + # Show appropriate message based on whether PR was created via API + if pr_created_via_api: + if pr_url: + click.echo( + f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" + f"Once you merge the PR and add the secret, the workflow will be active.{LF}" + ) + else: + # File already exists + click.echo( + f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" + f"Just add the secret and the workflow will be active.{LF}" + ) + else: + # Fell back to local file creation + click.echo( + f"Please edit, commit and push this GitHub actions file to your repo, and you're all set!{LF}" + f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" + ) ph("cli-github-workflow-created") except KeyboardInterrupt: apologize_and_exit() From bea874f023c90e976bf996090ed5b27be072522c Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Fri, 14 Nov 2025 02:17:12 +0200 Subject: [PATCH 02/19] updating the cf-api request for github action installation --- codeflash/api/cfapi.py | 10 +- codeflash/cli_cmds/cmd_init.py | 190 +++++++++++++++++++++++---------- 2 files changed, 139 insertions(+), 61 deletions(-) diff --git a/codeflash/api/cfapi.py b/codeflash/api/cfapi.py index 0cdf2e984..f05ce9bdb 100644 --- a/codeflash/api/cfapi.py +++ b/codeflash/api/cfapi.py @@ -246,14 +246,14 @@ def setup_github_actions( workflow_content: str, api_key: str | None = None, ) -> Response: - """Setup GitHub Actions workflow by creating a PR with the workflow file. + """Setup GitHub Actions workflow by creating a PR with the workflow file and optionally setting up the repository secret. :param owner: Repository owner (username or organization) :param repo: Repository name :param base_branch: Base branch to create PR against (e.g., "main", "master") :param workflow_content: Content of the GitHub Actions workflow file (YAML) - :param api_key: Optional API key (uses default if not provided) - :return: Response object with pr_url and pr_number on success + :param api_key: API key to store as repository secret (if provided, will attempt to set up secret automatically) + :return: Response object with pr_url, pr_number, secret_setup_success, and secret_setup_error on success """ payload = { "owner": owner, @@ -261,6 +261,10 @@ def setup_github_actions( "baseBranch": base_branch, "workflowContent": workflow_content, } + # Include apiKey in payload if provided - this will be encrypted and stored as a repository secret + if api_key: + payload["apiKey"] = api_key + return make_cfapi_request(endpoint="/setup-github-actions", method="POST", payload=payload, api_key=api_key) diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index ffdfb3f4d..afd1cef65 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -812,6 +812,17 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n git_remote = config.get("git_remote", "origin") pr_created_via_api = False pr_url = None + secret_setup_success = False + secret_setup_error = None + + # Get API key for secret setup + try: + api_key = get_codeflash_api_key() + except OSError: + api_key = None + logger.info( + "[cmd_init.py:install_github_actions] No API key found, will skip secret setup" + ) try: owner, repo_name = get_repo_owner_and_name(repo, git_remote) @@ -847,18 +858,27 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n repo=repo_name, base_branch=base_branch, workflow_content=materialized_optimize_yml_content, + api_key=api_key, ) if response.status_code == 200: response_data = response.json() if response_data.get("success"): pr_url = response_data.get("pr_url") + secret_setup_success = response_data.get("secret_setup_success", False) + secret_setup_error = response_data.get("secret_setup_error") + if pr_url: pr_created_via_api = True + # Build success message with secret status + success_message = f"βœ… PR created: {pr_url}\n\n" + if secret_setup_success: + success_message += "βœ… Repository secret CODEFLASH_API_KEY configured\n\n" + success_message += "Your repository is now configured for continuous optimization!" + workflow_success_panel = Panel( Text( - f"βœ… PR created: {pr_url}\n\n" - "Your repository is now configured for continuous optimization!", + success_message, style="green", justify="center", ), @@ -867,16 +887,40 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n ) console.print(workflow_success_panel) console.print() + + # Show warning if secret setup failed + if not secret_setup_success and api_key: + warning_message = ( + "⚠️ Secret setup failed. You'll need to add CODEFLASH_API_KEY manually.\n\n" + ) + if secret_setup_error: + warning_message += f"Error: {secret_setup_error}\n\n" + warning_message += f"πŸ“ Add secret at: {get_github_secrets_page_url(repo)}" + + warning_panel = Panel( + Text(warning_message, style="yellow"), + title="⚠️ Manual Secret Setup Required", + border_style="yellow", + ) + console.print(warning_panel) + console.print() + logger.info( - f"[cmd_init.py:install_github_actions] Successfully created PR #{response_data.get('pr_number')} for {owner}/{repo_name}" + f"[cmd_init.py:install_github_actions] Successfully created PR #{response_data.get('pr_number')} for {owner}/{repo_name}, secret_setup_success={secret_setup_success}" ) else: # File already exists with same content pr_created_via_api = True # Mark as handled (no PR needed) + already_exists_message = ( + "βœ… Workflow file already exists with the same content.\n\n" + ) + if secret_setup_success: + already_exists_message += "βœ… Repository secret CODEFLASH_API_KEY configured\n\n" + already_exists_message += "No changes needed - your repository is already configured!" + already_exists_panel = Panel( Text( - "βœ… Workflow file already exists with the same content.\n\n" - "No changes needed - your repository is already configured!", + already_exists_message, style="green", justify="center", ), @@ -885,6 +929,23 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n ) console.print(already_exists_panel) console.print() + + # Show warning if secret setup failed + if not secret_setup_success and api_key: + warning_message = ( + "⚠️ Secret setup failed. You'll need to add CODEFLASH_API_KEY manually.\n\n" + ) + if secret_setup_error: + warning_message += f"Error: {secret_setup_error}\n\n" + warning_message += f"πŸ“ Add secret at: {get_github_secrets_page_url(repo)}" + + warning_panel = Panel( + Text(warning_message, style="yellow"), + title="⚠️ Manual Secret Setup Required", + border_style="yellow", + ) + console.print(warning_panel) + console.print() else: raise Exception(response_data.get("error", "Unknown error")) else: @@ -912,64 +973,77 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n console.print(workflow_success_panel) console.print() - try: - existing_api_key = get_codeflash_api_key() - except OSError: - existing_api_key = None - - # GitHub secrets setup panel - secrets_message = ( - "πŸ” Next Step: Add API Key as GitHub Secret\n\n" - "You'll need to add your CODEFLASH_API_KEY as a secret to your GitHub repository.\n\n" - "πŸ“‹ Steps:\n" - "1. Press Enter to open your repo's secrets page\n" - "2. Click 'New repository secret'\n" - "3. Add your API key with the variable name CODEFLASH_API_KEY" - ) - - if existing_api_key: - secrets_message += f"\n\nπŸ”‘ Your API Key: {existing_api_key}" - - secrets_panel = Panel( - Text(secrets_message, style="blue"), title="πŸ” GitHub Secrets Setup", border_style="bright_blue" - ) - console.print(secrets_panel) - - console.print(f"\nπŸ“ Press Enter to open: {get_github_secrets_page_url(repo)}") - console.input() - - click.launch(get_github_secrets_page_url(repo)) - - # Post-launch message panel - launch_panel = Panel( - Text( - "πŸ™ I opened your GitHub secrets page!\n\n" - "Note: If you see a 404, you probably don't have access to this repo's secrets. " - "Ask a repo admin to add it for you, or (not recommended) you can temporarily " - "hard-code your API key into the workflow file.", - style="cyan", - ), - title="🌐 Browser Opened", - border_style="bright_cyan", - ) - console.print(launch_panel) - click.pause() - click.echo() - # Show appropriate message based on whether PR was created via API + # Show appropriate message based on whether PR was created via API and secret setup status if pr_created_via_api: if pr_url: - click.echo( - f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" - f"Once you merge the PR and add the secret, the workflow will be active.{LF}" - ) + if secret_setup_success: + click.echo( + f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" + f"Once you merge the PR, the workflow will be active.{LF}" + ) + else: + # PR created but secret setup failed or skipped + click.echo( + f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" + f"Once you merge the PR and add the secret, the workflow will be active.{LF}" + ) else: # File already exists - click.echo( - f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" - f"Just add the secret and the workflow will be active.{LF}" - ) + if secret_setup_success: + click.echo( + f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" + f"The workflow is ready to use.{LF}" + ) + else: + click.echo( + f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" + f"Just add the secret and the workflow will be active.{LF}" + ) else: - # Fell back to local file creation + # Fell back to local file creation - show manual secret setup + try: + existing_api_key = get_codeflash_api_key() + except OSError: + existing_api_key = None + + # GitHub secrets setup panel (only shown when falling back to local file creation) + secrets_message = ( + "πŸ” Next Step: Add API Key as GitHub Secret\n\n" + "You'll need to add your CODEFLASH_API_KEY as a secret to your GitHub repository.\n\n" + "πŸ“‹ Steps:\n" + "1. Press Enter to open your repo's secrets page\n" + "2. Click 'New repository secret'\n" + "3. Add your API key with the variable name CODEFLASH_API_KEY" + ) + + if existing_api_key: + secrets_message += f"\n\nπŸ”‘ Your API Key: {existing_api_key}" + + secrets_panel = Panel( + Text(secrets_message, style="blue"), title="πŸ” GitHub Secrets Setup", border_style="bright_blue" + ) + console.print(secrets_panel) + + console.print(f"\nπŸ“ Press Enter to open: {get_github_secrets_page_url(repo)}") + console.input() + + click.launch(get_github_secrets_page_url(repo)) + + # Post-launch message panel + launch_panel = Panel( + Text( + "πŸ™ I opened your GitHub secrets page!\n\n" + "Note: If you see a 404, you probably don't have access to this repo's secrets. " + "Ask a repo admin to add it for you, or (not recommended) you can temporarily " + "hard-code your API key into the workflow file.", + style="cyan", + ), + title="🌐 Browser Opened", + border_style="bright_cyan", + ) + console.print(launch_panel) + click.pause() + click.echo() click.echo( f"Please edit, commit and push this GitHub actions file to your repo, and you're all set!{LF}" f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" From 6471a5c0cb42018e87fbfc6437b8eb168c08f95e Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Fri, 14 Nov 2025 02:20:32 +0200 Subject: [PATCH 03/19] User experience improvements --- codeflash/cli_cmds/cmd_init.py | 109 +++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 4 deletions(-) diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index afd1cef65..381e68a80 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -947,13 +947,114 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n console.print(warning_panel) console.print() else: - raise Exception(response_data.get("error", "Unknown error")) + # API returned success=false, extract error details + error_data = response_data + error_msg = error_data.get("error", "Unknown error") + error_message = error_data.get("message", error_msg) + error_help = error_data.get("help", "") + installation_url = error_data.get("installation_url") + + # Show detailed error panel + error_panel_text = f"❌ {error_msg}\n\n{error_message}\n" + if error_help: + error_panel_text += f"\nπŸ’‘ {error_help}\n" + if installation_url: + error_panel_text += f"\nπŸ”— Install GitHub App: {installation_url}" + + error_panel = Panel( + Text(error_panel_text, style="red"), + title="❌ Setup Failed", + border_style="red", + ) + console.print(error_panel) + console.print() + + # For GitHub App not installed, don't fall back - show clear instructions + if response.status_code == 404 and installation_url: + logger.error( + f"[cmd_init.py:install_github_actions] GitHub App not installed on {owner}/{repo_name}" + ) + click.echo( + f"Please install the CodeFlash GitHub App on your repository to continue.{LF}" + f"Visit: {installation_url}{LF}" + ) + return + + # For permission errors, don't fall back - show clear instructions + if response.status_code == 403: + logger.error( + f"[cmd_init.py:install_github_actions] Permission denied for {owner}/{repo_name}" + ) + click.echo( + f"Please ensure you have write access to {owner}/{repo_name} and try again.{LF}" + ) + return + + # For other errors, fall back to local file creation + raise Exception(error_message) else: - # API call failed, fall back to local file creation - raise Exception(f"API returned status {response.status_code}") + # API call returned non-200 status, try to parse error response + try: + error_data = response.json() + error_msg = error_data.get("error", "API request failed") + error_message = error_data.get("message", f"API returned status {response.status_code}") + error_help = error_data.get("help", "") + installation_url = error_data.get("installation_url") + + # Show detailed error panel + error_panel_text = f"❌ {error_msg}\n\n{error_message}\n" + if error_help: + error_panel_text += f"\nπŸ’‘ {error_help}\n" + if installation_url: + error_panel_text += f"\nπŸ”— Install GitHub App: {installation_url}" + + error_panel = Panel( + Text(error_panel_text, style="red"), + title="❌ Setup Failed", + border_style="red", + ) + console.print(error_panel) + console.print() + + # For GitHub App not installed, don't fall back - show clear instructions + if response.status_code == 404 and installation_url: + logger.error( + f"[cmd_init.py:install_github_actions] GitHub App not installed on {owner}/{repo_name}" + ) + click.echo( + f"Please install the CodeFlash GitHub App on your repository to continue.{LF}" + f"Visit: {installation_url}{LF}" + ) + return + + # For permission errors, don't fall back - show clear instructions + if response.status_code == 403: + logger.error( + f"[cmd_init.py:install_github_actions] Permission denied for {owner}/{repo_name}" + ) + click.echo( + f"Please ensure you have write access to {owner}/{repo_name} and try again.{LF}" + ) + return + + # For authentication errors, don't fall back + if response.status_code == 401: + logger.error( + f"[cmd_init.py:install_github_actions] Authentication failed for {owner}/{repo_name}" + ) + click.echo( + f"Authentication failed. Please check your API key and try again.{LF}" + ) + return + + # For other errors, fall back to local file creation + raise Exception(error_message) + except (ValueError, KeyError): + # Couldn't parse error response, use generic message + raise Exception(f"API returned status {response.status_code}") except Exception as api_error: - # Fall back to local file creation if API call fails + # Fall back to local file creation if API call fails (for non-critical errors) logger.warning( f"[cmd_init.py:install_github_actions] API call failed, falling back to local file creation: {api_error}" ) From d971d0803735bba2fca475614452abf4ea391439 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Fri, 14 Nov 2025 02:32:38 +0200 Subject: [PATCH 04/19] fix lint errors --- codeflash/api/cfapi.py | 15 ++----- codeflash/cli_cmds/cmd_init.py | 72 ++++++++++++---------------------- 2 files changed, 28 insertions(+), 59 deletions(-) diff --git a/codeflash/api/cfapi.py b/codeflash/api/cfapi.py index f05ce9bdb..81855b4ed 100644 --- a/codeflash/api/cfapi.py +++ b/codeflash/api/cfapi.py @@ -240,13 +240,9 @@ def create_pr( def setup_github_actions( - owner: str, - repo: str, - base_branch: str, - workflow_content: str, - api_key: str | None = None, + owner: str, repo: str, base_branch: str, workflow_content: str, api_key: str | None = None ) -> Response: - """Setup GitHub Actions workflow by creating a PR with the workflow file and optionally setting up the repository secret. + """Set up GitHub Actions workflow by creating a PR with the workflow file and optionally setting up the repository secret. :param owner: Repository owner (username or organization) :param repo: Repository name @@ -255,12 +251,7 @@ def setup_github_actions( :param api_key: API key to store as repository secret (if provided, will attempt to set up secret automatically) :return: Response object with pr_url, pr_number, secret_setup_success, and secret_setup_error on success """ - payload = { - "owner": owner, - "repo": repo, - "baseBranch": base_branch, - "workflowContent": workflow_content, - } + payload = {"owner": owner, "repo": repo, "baseBranch": base_branch, "workflowContent": workflow_content} # Include apiKey in payload if provided - this will be encrypted and stored as a repository secret if api_key: payload["apiKey"] = api_key diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index 381e68a80..e3c1863c7 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -704,7 +704,7 @@ def create_empty_pyproject_toml(pyproject_toml_path: Path) -> None: apologize_and_exit() -def install_github_actions(override_formatter_check: bool = False) -> None: # noqa: FBT001, FBT002 +def install_github_actions(override_formatter_check: bool = False) -> None: # noqa: FBT001, FBT002, PLR0911 try: config, _config_file_path = parse_config_file(override_formatter_check=override_formatter_check) @@ -820,9 +820,7 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n api_key = get_codeflash_api_key() except OSError: api_key = None - logger.info( - "[cmd_init.py:install_github_actions] No API key found, will skip secret setup" - ) + logger.info("[cmd_init.py:install_github_actions] No API key found, will skip secret setup") try: owner, repo_name = get_repo_owner_and_name(repo, git_remote) @@ -877,11 +875,7 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n success_message += "Your repository is now configured for continuous optimization!" workflow_success_panel = Panel( - Text( - success_message, - style="green", - justify="center", - ), + Text(success_message, style="green", justify="center"), title="πŸŽ‰ Workflow PR Created!", border_style="bright_green", ) @@ -911,19 +905,13 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n else: # File already exists with same content pr_created_via_api = True # Mark as handled (no PR needed) - already_exists_message = ( - "βœ… Workflow file already exists with the same content.\n\n" - ) + already_exists_message = "βœ… Workflow file already exists with the same content.\n\n" if secret_setup_success: already_exists_message += "βœ… Repository secret CODEFLASH_API_KEY configured\n\n" already_exists_message += "No changes needed - your repository is already configured!" already_exists_panel = Panel( - Text( - already_exists_message, - style="green", - justify="center", - ), + Text(already_exists_message, style="green", justify="center"), title="βœ… Already Configured", border_style="bright_green", ) @@ -962,9 +950,7 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n error_panel_text += f"\nπŸ”— Install GitHub App: {installation_url}" error_panel = Panel( - Text(error_panel_text, style="red"), - title="❌ Setup Failed", - border_style="red", + Text(error_panel_text, style="red"), title="❌ Setup Failed", border_style="red" ) console.print(error_panel) console.print() @@ -985,13 +971,11 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n logger.error( f"[cmd_init.py:install_github_actions] Permission denied for {owner}/{repo_name}" ) - click.echo( - f"Please ensure you have write access to {owner}/{repo_name} and try again.{LF}" - ) + click.echo(f"Please ensure you have write access to {owner}/{repo_name} and try again.{LF}") return # For other errors, fall back to local file creation - raise Exception(error_message) + raise Exception(error_message) # noqa: TRY002, TRY301 else: # API call returned non-200 status, try to parse error response try: @@ -1009,9 +993,7 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n error_panel_text += f"\nπŸ”— Install GitHub App: {installation_url}" error_panel = Panel( - Text(error_panel_text, style="red"), - title="❌ Setup Failed", - border_style="red", + Text(error_panel_text, style="red"), title="❌ Setup Failed", border_style="red" ) console.print(error_panel) console.print() @@ -1032,9 +1014,7 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n logger.error( f"[cmd_init.py:install_github_actions] Permission denied for {owner}/{repo_name}" ) - click.echo( - f"Please ensure you have write access to {owner}/{repo_name} and try again.{LF}" - ) + click.echo(f"Please ensure you have write access to {owner}/{repo_name} and try again.{LF}") return # For authentication errors, don't fall back @@ -1042,16 +1022,15 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n logger.error( f"[cmd_init.py:install_github_actions] Authentication failed for {owner}/{repo_name}" ) - click.echo( - f"Authentication failed. Please check your API key and try again.{LF}" - ) + click.echo(f"Authentication failed. Please check your API key and try again.{LF}") return # For other errors, fall back to local file creation - raise Exception(error_message) - except (ValueError, KeyError): + raise Exception(error_message) # noqa: TRY002 + except (ValueError, KeyError) as parse_error: # Couldn't parse error response, use generic message - raise Exception(f"API returned status {response.status_code}") + status_msg = f"API returned status {response.status_code}" + raise Exception(status_msg) from parse_error # noqa: TRY002 except Exception as api_error: # Fall back to local file creation if API call fails (for non-critical errors) @@ -1088,18 +1067,17 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" f"Once you merge the PR and add the secret, the workflow will be active.{LF}" ) + # File already exists + elif secret_setup_success: + click.echo( + f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" + f"The workflow is ready to use.{LF}" + ) else: - # File already exists - if secret_setup_success: - click.echo( - f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" - f"The workflow is ready to use.{LF}" - ) - else: - click.echo( - f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" - f"Just add the secret and the workflow will be active.{LF}" - ) + click.echo( + f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" + f"Just add the secret and the workflow will be active.{LF}" + ) else: # Fell back to local file creation - show manual secret setup try: From 7f6b758ff151fe0060d49561d0ed5113ff46f5da Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Fri, 14 Nov 2025 03:27:36 +0200 Subject: [PATCH 05/19] update Set up GitHub Action message during init command --- codeflash/cli_cmds/cmd_init.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index e3c1863c7..7d3cc4e7a 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -759,7 +759,9 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n creation_questions = [ inquirer.Confirm( - "confirm_creation", message="Set up GitHub Actions for continuous optimization?", default=True + "confirm_creation", + message="Set up GitHub Actions for continuous optimization? We'll create a pull request with the workflow file.", + default=True, ) ] From d3c55d2b992d401f7d600e6519bd6b3461643f19 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Fri, 14 Nov 2025 03:42:02 +0200 Subject: [PATCH 06/19] check if the workflow file exists before setting up the github action --- codeflash/api/cfapi.py | 18 +++++++- codeflash/cli_cmds/cmd_init.py | 84 +++++++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/codeflash/api/cfapi.py b/codeflash/api/cfapi.py index 81855b4ed..396011319 100644 --- a/codeflash/api/cfapi.py +++ b/codeflash/api/cfapi.py @@ -55,13 +55,17 @@ def make_cfapi_request( *, api_key: str | None = None, suppress_errors: bool = False, + params: dict[str, Any] | None = None, ) -> Response: """Make an HTTP request using the specified method, URL, headers, and JSON payload. :param endpoint: The endpoint URL to send the request to. :param method: The HTTP method to use ('GET', 'POST', etc.). :param payload: Optional JSON payload to include in the POST request body. + :param extra_headers: Optional extra headers to include in the request. + :param api_key: Optional API key to use for authentication. :param suppress_errors: If True, suppress error logging for HTTP errors. + :param params: Optional query parameters for GET requests. :return: The response object from the API. """ url = f"{get_cfapi_base_urls().cfapi_base_url}/cfapi{endpoint}" @@ -75,7 +79,7 @@ def make_cfapi_request( cfapi_headers["Content-Type"] = "application/json" response = requests.post(url, data=json_payload, headers=cfapi_headers, timeout=60) else: - response = requests.get(url, headers=cfapi_headers, timeout=60) + response = requests.get(url, headers=cfapi_headers, params=params, timeout=60) response.raise_for_status() return response # noqa: TRY300 except requests.exceptions.HTTPError: @@ -239,6 +243,18 @@ def create_pr( return make_cfapi_request(endpoint="/create-pr", method="POST", payload=payload) +def check_workflow_file_exists(owner: str, repo: str, base_branch: str) -> Response: + """Check if the GitHub Actions workflow file exists on the repository. + + :param owner: Repository owner (username or organization) + :param repo: Repository name + :param base_branch: Base branch to check (e.g., "main", "master") + :return: Response object with exists (bool) and content (str | None) fields + """ + params = {"owner": owner, "repo": repo, "baseBranch": base_branch} + return make_cfapi_request(endpoint="/check-workflow-file-exists", method="GET", params=params) + + def setup_github_actions( owner: str, repo: str, base_branch: str, workflow_content: str, api_key: str | None = None ) -> Response: diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index 7d3cc4e7a..e9155f52b 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -22,7 +22,11 @@ from rich.table import Table from rich.text import Text -from codeflash.api.cfapi import is_github_app_installed_on_repo, setup_github_actions +from codeflash.api.cfapi import ( + check_workflow_file_exists, + is_github_app_installed_on_repo, + setup_github_actions, +) from codeflash.cli_cmds.cli_common import apologize_and_exit from codeflash.cli_cmds.console import console, logger from codeflash.cli_cmds.extension import install_vscode_extension @@ -848,6 +852,84 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n # Try to create PR via API try: base_branch = get_current_branch(repo) if repo.active_branch else "main" + + # Check if workflow file already exists on remote + logger.info( + f"[cmd_init.py:install_github_actions] Checking if workflow file exists for {owner}/{repo_name} on branch {base_branch}" + ) + check_response = check_workflow_file_exists(owner, repo_name, base_branch) + + if check_response.status_code == 200: + check_data = check_response.json() + if check_data.get("exists"): + # Workflow file already exists - check if content matches + existing_content = check_data.get("content", "") + if existing_content == materialized_optimize_yml_content: + # File exists with same content - skip PR creation + pr_created_via_api = True + already_exists_message = "βœ… Workflow file already exists with the same content.\n\n" + already_exists_message += "No changes needed - your repository is already configured!" + + already_exists_panel = Panel( + Text(already_exists_message, style="green", justify="center"), + title="βœ… Already Configured", + border_style="bright_green", + ) + console.print(already_exists_panel) + console.print() + + # Still try to set up secret if API key is available + if api_key: + logger.info( + f"[cmd_init.py:install_github_actions] Workflow exists, attempting secret setup for {owner}/{repo_name}" + ) + # Call setup API just for secret setup (it will handle the already_exists case) + secret_response = setup_github_actions( + owner=owner, + repo=repo_name, + base_branch=base_branch, + workflow_content=materialized_optimize_yml_content, + api_key=api_key, + ) + if secret_response.status_code == 200: + secret_data = secret_response.json() + secret_setup_success = secret_data.get("secret_setup_success", False) + secret_setup_error = secret_data.get("secret_setup_error") + + if secret_setup_success: + console.print( + Panel( + Text( + "βœ… Repository secret CODEFLASH_API_KEY configured", + style="green", + justify="center", + ), + title="βœ… Secret Configured", + border_style="bright_green", + ) + ) + console.print() + elif secret_setup_error: + warning_message = ( + "⚠️ Secret setup failed. You'll need to add CODEFLASH_API_KEY manually.\n\n" + ) + warning_message += f"Error: {secret_setup_error}\n\n" + warning_message += f"πŸ“ Add secret at: {get_github_secrets_page_url(repo)}" + + warning_panel = Panel( + Text(warning_message, style="yellow"), + title="⚠️ Manual Secret Setup Required", + border_style="yellow", + ) + console.print(warning_panel) + console.print() + + logger.info( + f"[cmd_init.py:install_github_actions] Workflow file already exists for {owner}/{repo_name}, skipping PR creation" + ) + return + + # Workflow file doesn't exist or content differs - proceed with PR creation console.print("Creating PR with GitHub Actions workflow...") logger.info( f"[cmd_init.py:install_github_actions] Calling setup_github_actions API for {owner}/{repo_name} on branch {base_branch}" From 9b6c0c18bcf1f4df14eec35a8425d4139bf08772 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Fri, 14 Nov 2025 03:42:17 +0200 Subject: [PATCH 07/19] skip user's prompt for github action installatin if already installed --- codeflash/cli_cmds/cmd_init.py | 248 +++++++++++++++++---------------- 1 file changed, 128 insertions(+), 120 deletions(-) diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index e9155f52b..b05c0ef2d 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -725,6 +725,132 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n workflows_path = git_root / ".github" / "workflows" optimize_yaml_path = workflows_path / "codeflash.yaml" + # Get repository information early for remote check + git_remote = config.get("git_remote", "origin") + base_branch = get_current_branch(repo) if repo.active_branch else "main" + + # Generate workflow content early (needed for comparison) + from importlib.resources import files + + benchmark_mode = False + benchmarks_root = config.get("benchmarks_root", "").strip() + if benchmarks_root and benchmarks_root != "": + benchmark_panel = Panel( + Text( + "πŸ“Š Benchmark Mode Available\n\n" + "I noticed you've configured a benchmarks_root in your config. " + "Benchmark mode will show the performance impact of Codeflash's optimizations on your benchmarks.", + style="cyan", + ), + title="πŸ“Š Benchmark Mode", + border_style="bright_cyan", + ) + console.print(benchmark_panel) + console.print() + + benchmark_questions = [ + inquirer.Confirm("benchmark_mode", message="Run GitHub Actions in benchmark mode?", default=True) + ] + + benchmark_answers = inquirer.prompt(benchmark_questions, theme=CodeflashTheme()) + benchmark_mode = benchmark_answers["benchmark_mode"] if benchmark_answers else False + + optimize_yml_content = ( + files("codeflash").joinpath("cli_cmds", "workflows", "codeflash-optimize.yaml").read_text(encoding="utf-8") + ) + materialized_optimize_yml_content = customize_codeflash_yaml_content( + optimize_yml_content, config, git_root, benchmark_mode + ) + + # Get API key for secret setup + try: + api_key = get_codeflash_api_key() + except OSError: + api_key = None + logger.info("[cmd_init.py:install_github_actions] No API key found, will skip secret setup") + + # Check if workflow file already exists on remote BEFORE showing prompt + try: + owner, repo_name = get_repo_owner_and_name(repo, git_remote) + logger.info( + f"[cmd_init.py:install_github_actions] Checking if workflow file exists for {owner}/{repo_name} on branch {base_branch}" + ) + check_response = check_workflow_file_exists(owner, repo_name, base_branch) + + if check_response.status_code == 200: + check_data = check_response.json() + if check_data.get("exists"): + existing_content = check_data.get("content", "") + if existing_content == materialized_optimize_yml_content: + # Workflow file already exists with same content - skip prompt and setup + pr_created_via_api = True + already_exists_message = "βœ… Workflow file already exists with the same content.\n\n" + already_exists_message += "No changes needed - your repository is already configured!" + + already_exists_panel = Panel( + Text(already_exists_message, style="green", justify="center"), + title="βœ… Already Configured", + border_style="bright_green", + ) + console.print(already_exists_panel) + console.print() + + # Still try to set up secret if API key is available + if api_key: + logger.info( + f"[cmd_init.py:install_github_actions] Workflow exists, attempting secret setup for {owner}/{repo_name}" + ) + secret_response = setup_github_actions( + owner=owner, + repo=repo_name, + base_branch=base_branch, + workflow_content=materialized_optimize_yml_content, + api_key=api_key, + ) + if secret_response.status_code == 200: + secret_data = secret_response.json() + secret_setup_success = secret_data.get("secret_setup_success", False) + secret_setup_error = secret_data.get("secret_setup_error") + + if secret_setup_success: + console.print( + Panel( + Text( + "βœ… Repository secret CODEFLASH_API_KEY configured", + style="green", + justify="center", + ), + title="βœ… Secret Configured", + border_style="bright_green", + ) + ) + console.print() + elif secret_setup_error: + warning_message = ( + "⚠️ Secret setup failed. You'll need to add CODEFLASH_API_KEY manually.\n\n" + ) + warning_message += f"Error: {secret_setup_error}\n\n" + warning_message += f"πŸ“ Add secret at: {get_github_secrets_page_url(repo)}" + + warning_panel = Panel( + Text(warning_message, style="yellow"), + title="⚠️ Manual Secret Setup Required", + border_style="yellow", + ) + console.print(warning_panel) + console.print() + + logger.info( + f"[cmd_init.py:install_github_actions] Workflow file already exists for {owner}/{repo_name}, skipping setup" + ) + return + except Exception as e: + logger.warning( + f"[cmd_init.py:install_github_actions] Could not check remote workflow file, will proceed with prompt: {e}" + ) + # Continue to show prompt if we can't check remote + + # Show prompt only if workflow doesn't exist on remote actions_panel = Panel( Text( "πŸ€– GitHub Actions Setup\n\n" @@ -738,7 +864,7 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n console.print(actions_panel) console.print() - # Check if the workflow file already exists + # Check if the workflow file already exists locally if optimize_yaml_path.exists(): overwrite_questions = [ inquirer.Confirm( @@ -782,52 +908,12 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n {"confirm_creation": creation_answers["confirm_creation"]}, ) workflows_path.mkdir(parents=True, exist_ok=True) - from importlib.resources import files - benchmark_mode = False - benchmarks_root = config.get("benchmarks_root", "").strip() - if benchmarks_root and benchmarks_root != "": - benchmark_panel = Panel( - Text( - "πŸ“Š Benchmark Mode Available\n\n" - "I noticed you've configured a benchmarks_root in your config. " - "Benchmark mode will show the performance impact of Codeflash's optimizations on your benchmarks.", - style="cyan", - ), - title="πŸ“Š Benchmark Mode", - border_style="bright_cyan", - ) - console.print(benchmark_panel) - console.print() - - benchmark_questions = [ - inquirer.Confirm("benchmark_mode", message="Run GitHub Actions in benchmark mode?", default=True) - ] - - benchmark_answers = inquirer.prompt(benchmark_questions, theme=CodeflashTheme()) - benchmark_mode = benchmark_answers["benchmark_mode"] if benchmark_answers else False - - optimize_yml_content = ( - files("codeflash").joinpath("cli_cmds", "workflows", "codeflash-optimize.yaml").read_text(encoding="utf-8") - ) - materialized_optimize_yml_content = customize_codeflash_yaml_content( - optimize_yml_content, config, git_root, benchmark_mode - ) - - # Get repository information for API call - git_remote = config.get("git_remote", "origin") pr_created_via_api = False pr_url = None secret_setup_success = False secret_setup_error = None - # Get API key for secret setup - try: - api_key = get_codeflash_api_key() - except OSError: - api_key = None - logger.info("[cmd_init.py:install_github_actions] No API key found, will skip secret setup") - try: owner, repo_name = get_repo_owner_and_name(repo, git_remote) except Exception as e: @@ -851,85 +937,7 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n else: # Try to create PR via API try: - base_branch = get_current_branch(repo) if repo.active_branch else "main" - - # Check if workflow file already exists on remote - logger.info( - f"[cmd_init.py:install_github_actions] Checking if workflow file exists for {owner}/{repo_name} on branch {base_branch}" - ) - check_response = check_workflow_file_exists(owner, repo_name, base_branch) - - if check_response.status_code == 200: - check_data = check_response.json() - if check_data.get("exists"): - # Workflow file already exists - check if content matches - existing_content = check_data.get("content", "") - if existing_content == materialized_optimize_yml_content: - # File exists with same content - skip PR creation - pr_created_via_api = True - already_exists_message = "βœ… Workflow file already exists with the same content.\n\n" - already_exists_message += "No changes needed - your repository is already configured!" - - already_exists_panel = Panel( - Text(already_exists_message, style="green", justify="center"), - title="βœ… Already Configured", - border_style="bright_green", - ) - console.print(already_exists_panel) - console.print() - - # Still try to set up secret if API key is available - if api_key: - logger.info( - f"[cmd_init.py:install_github_actions] Workflow exists, attempting secret setup for {owner}/{repo_name}" - ) - # Call setup API just for secret setup (it will handle the already_exists case) - secret_response = setup_github_actions( - owner=owner, - repo=repo_name, - base_branch=base_branch, - workflow_content=materialized_optimize_yml_content, - api_key=api_key, - ) - if secret_response.status_code == 200: - secret_data = secret_response.json() - secret_setup_success = secret_data.get("secret_setup_success", False) - secret_setup_error = secret_data.get("secret_setup_error") - - if secret_setup_success: - console.print( - Panel( - Text( - "βœ… Repository secret CODEFLASH_API_KEY configured", - style="green", - justify="center", - ), - title="βœ… Secret Configured", - border_style="bright_green", - ) - ) - console.print() - elif secret_setup_error: - warning_message = ( - "⚠️ Secret setup failed. You'll need to add CODEFLASH_API_KEY manually.\n\n" - ) - warning_message += f"Error: {secret_setup_error}\n\n" - warning_message += f"πŸ“ Add secret at: {get_github_secrets_page_url(repo)}" - - warning_panel = Panel( - Text(warning_message, style="yellow"), - title="⚠️ Manual Secret Setup Required", - border_style="yellow", - ) - console.print(warning_panel) - console.print() - - logger.info( - f"[cmd_init.py:install_github_actions] Workflow file already exists for {owner}/{repo_name}, skipping PR creation" - ) - return - - # Workflow file doesn't exist or content differs - proceed with PR creation + # Workflow file doesn't exist on remote or content differs - proceed with PR creation console.print("Creating PR with GitHub Actions workflow...") logger.info( f"[cmd_init.py:install_github_actions] Calling setup_github_actions API for {owner}/{repo_name} on branch {base_branch}" From 0c6065cd1a5e8a9351fbe1c752ca93e7831b0595 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Fri, 14 Nov 2025 04:37:09 +0200 Subject: [PATCH 08/19] local check for the codeflash.yaml instead of remote --- codeflash/cli_cmds/cmd_init.py | 198 +++++++++++++++------------------ 1 file changed, 90 insertions(+), 108 deletions(-) diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index b05c0ef2d..4a037b459 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -725,11 +725,97 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n workflows_path = git_root / ".github" / "workflows" optimize_yaml_path = workflows_path / "codeflash.yaml" - # Get repository information early for remote check + # Check if workflow file already exists locally BEFORE showing prompt + if optimize_yaml_path.exists(): + # Workflow file already exists locally - skip prompt and setup + already_exists_message = "βœ… GitHub Actions workflow file already exists.\n\n" + already_exists_message += "No changes needed - your repository is already configured!" + + already_exists_panel = Panel( + Text(already_exists_message, style="green", justify="center"), + title="βœ… Already Configured", + border_style="bright_green", + ) + console.print(already_exists_panel) + console.print() + + # Still try to set up secret if API key is available + try: + api_key = get_codeflash_api_key() + git_remote = config.get("git_remote", "origin") + owner, repo_name = get_repo_owner_and_name(repo, git_remote) + base_branch = get_current_branch(repo) if repo.active_branch else "main" + + # Generate workflow content for secret setup + from importlib.resources import files + + optimize_yml_content = ( + files("codeflash") + .joinpath("cli_cmds", "workflows", "codeflash-optimize.yaml") + .read_text(encoding="utf-8") + ) + materialized_optimize_yml_content = customize_codeflash_yaml_content( + optimize_yml_content, config, git_root, False + ) + + logger.info( + f"[cmd_init.py:install_github_actions] Workflow exists locally, attempting secret setup for {owner}/{repo_name}" + ) + secret_response = setup_github_actions( + owner=owner, + repo=repo_name, + base_branch=base_branch, + workflow_content=materialized_optimize_yml_content, + api_key=api_key, + ) + if secret_response.status_code == 200: + secret_data = secret_response.json() + secret_setup_success = secret_data.get("secret_setup_success", False) + secret_setup_error = secret_data.get("secret_setup_error") + + if secret_setup_success: + console.print( + Panel( + Text( + "βœ… Repository secret CODEFLASH_API_KEY configured", + style="green", + justify="center", + ), + title="βœ… Secret Configured", + border_style="bright_green", + ) + ) + console.print() + elif secret_setup_error: + warning_message = ( + "⚠️ Secret setup failed. You'll need to add CODEFLASH_API_KEY manually.\n\n" + ) + warning_message += f"Error: {secret_setup_error}\n\n" + warning_message += f"πŸ“ Add secret at: {get_github_secrets_page_url(repo)}" + + warning_panel = Panel( + Text(warning_message, style="yellow"), + title="⚠️ Manual Secret Setup Required", + border_style="yellow", + ) + console.print(warning_panel) + console.print() + except Exception as e: + logger.debug( + f"[cmd_init.py:install_github_actions] Could not set up secret (workflow exists locally): {e}" + ) + # Secret setup is optional, so we continue + + logger.info( + f"[cmd_init.py:install_github_actions] Workflow file already exists locally, skipping setup" + ) + return + + # Get repository information for API call git_remote = config.get("git_remote", "origin") base_branch = get_current_branch(repo) if repo.active_branch else "main" - # Generate workflow content early (needed for comparison) + # Generate workflow content from importlib.resources import files benchmark_mode = False @@ -769,88 +855,7 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n api_key = None logger.info("[cmd_init.py:install_github_actions] No API key found, will skip secret setup") - # Check if workflow file already exists on remote BEFORE showing prompt - try: - owner, repo_name = get_repo_owner_and_name(repo, git_remote) - logger.info( - f"[cmd_init.py:install_github_actions] Checking if workflow file exists for {owner}/{repo_name} on branch {base_branch}" - ) - check_response = check_workflow_file_exists(owner, repo_name, base_branch) - - if check_response.status_code == 200: - check_data = check_response.json() - if check_data.get("exists"): - existing_content = check_data.get("content", "") - if existing_content == materialized_optimize_yml_content: - # Workflow file already exists with same content - skip prompt and setup - pr_created_via_api = True - already_exists_message = "βœ… Workflow file already exists with the same content.\n\n" - already_exists_message += "No changes needed - your repository is already configured!" - - already_exists_panel = Panel( - Text(already_exists_message, style="green", justify="center"), - title="βœ… Already Configured", - border_style="bright_green", - ) - console.print(already_exists_panel) - console.print() - - # Still try to set up secret if API key is available - if api_key: - logger.info( - f"[cmd_init.py:install_github_actions] Workflow exists, attempting secret setup for {owner}/{repo_name}" - ) - secret_response = setup_github_actions( - owner=owner, - repo=repo_name, - base_branch=base_branch, - workflow_content=materialized_optimize_yml_content, - api_key=api_key, - ) - if secret_response.status_code == 200: - secret_data = secret_response.json() - secret_setup_success = secret_data.get("secret_setup_success", False) - secret_setup_error = secret_data.get("secret_setup_error") - - if secret_setup_success: - console.print( - Panel( - Text( - "βœ… Repository secret CODEFLASH_API_KEY configured", - style="green", - justify="center", - ), - title="βœ… Secret Configured", - border_style="bright_green", - ) - ) - console.print() - elif secret_setup_error: - warning_message = ( - "⚠️ Secret setup failed. You'll need to add CODEFLASH_API_KEY manually.\n\n" - ) - warning_message += f"Error: {secret_setup_error}\n\n" - warning_message += f"πŸ“ Add secret at: {get_github_secrets_page_url(repo)}" - - warning_panel = Panel( - Text(warning_message, style="yellow"), - title="⚠️ Manual Secret Setup Required", - border_style="yellow", - ) - console.print(warning_panel) - console.print() - - logger.info( - f"[cmd_init.py:install_github_actions] Workflow file already exists for {owner}/{repo_name}, skipping setup" - ) - return - except Exception as e: - logger.warning( - f"[cmd_init.py:install_github_actions] Could not check remote workflow file, will proceed with prompt: {e}" - ) - # Continue to show prompt if we can't check remote - - # Show prompt only if workflow doesn't exist on remote + # Show prompt only if workflow doesn't exist locally actions_panel = Panel( Text( "πŸ€– GitHub Actions Setup\n\n" @@ -864,33 +869,10 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n console.print(actions_panel) console.print() - # Check if the workflow file already exists locally - if optimize_yaml_path.exists(): - overwrite_questions = [ - inquirer.Confirm( - "confirm_overwrite", - message=f"GitHub Actions workflow already exists at {optimize_yaml_path}. Overwrite?", - default=False, - ) - ] - - overwrite_answers = inquirer.prompt(overwrite_questions, theme=CodeflashTheme()) - if not overwrite_answers or not overwrite_answers["confirm_overwrite"]: - skip_panel = Panel( - Text("⏩️ Skipping workflow creation.", style="yellow"), title="⏩️ Skipped", border_style="yellow" - ) - console.print(skip_panel) - ph("cli-github-workflow-skipped") - return - ph( - "cli-github-optimization-confirm-workflow-overwrite", - {"confirm_overwrite": overwrite_answers["confirm_overwrite"]}, - ) - creation_questions = [ inquirer.Confirm( "confirm_creation", - message="Set up GitHub Actions for continuous optimization? We'll create a pull request with the workflow file.", + message="Set up GitHub Actions for continuous optimization? We'll open a pull request with the workflow file.", default=True, ) ] From 03875b968fbabb7350ccc02896e886fd8ebf0fba Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Mon, 17 Nov 2025 23:34:45 +0200 Subject: [PATCH 09/19] adding collect_repo_files_for_workflow and generate_workflow_steps for github action --- codeflash/api/aiservice.py | 37 ++++++ codeflash/cli_cmds/cmd_init.py | 225 ++++++++++++++++++++++++++++++++- 2 files changed, 260 insertions(+), 2 deletions(-) diff --git a/codeflash/api/aiservice.py b/codeflash/api/aiservice.py index d206a00b6..348e6eb32 100644 --- a/codeflash/api/aiservice.py +++ b/codeflash/api/aiservice.py @@ -631,6 +631,43 @@ def get_optimization_review( console.rule() return "" + def generate_workflow_steps( + self, + repo_files: dict[str, str], + directory_structure: dict[str, Any], + codeflash_config: dict[str, Any] | None = None, + ) -> str | None: + """Generate GitHub Actions workflow steps based on repository analysis. + + :param repo_files: Dictionary mapping file paths to their contents + :param directory_structure: 2-level nested directory structure + :param codeflash_config: Optional codeflash configuration + :return: YAML string for workflow steps section, or None on error + """ + payload = { + "repo_files": repo_files, + "directory_structure": directory_structure, + "codeflash_config": codeflash_config, + } + + logger.info("Generating workflow steps via AI service...") + try: + response = self.make_ai_service_request("/workflow-gen", payload=payload, timeout=60) + except requests.exceptions.RequestException as e: + logger.warning(f"Error generating workflow steps: {e}") + return None + + if response.status_code == 200: + return cast("str", response.json().get("workflow_steps")) + else: + logger.warning(f"Failed to generate workflow steps: {response.status_code}") + try: + error = cast("str", response.json().get("error", "Unknown error")) + logger.debug(f"Error details: {error}") + except Exception: + pass + return None + class LocalAiServiceClient(AiServiceClient): """Client for interacting with the local AI service.""" diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index 4a037b459..269f05197 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -22,6 +22,7 @@ from rich.table import Table from rich.text import Text +from codeflash.api.aiservice import AiServiceClient from codeflash.api.cfapi import ( check_workflow_file_exists, is_github_app_installed_on_repo, @@ -754,7 +755,7 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n .joinpath("cli_cmds", "workflows", "codeflash-optimize.yaml") .read_text(encoding="utf-8") ) - materialized_optimize_yml_content = customize_codeflash_yaml_content( + materialized_optimize_yml_content = generate_dynamic_workflow_content( optimize_yml_content, config, git_root, False ) @@ -844,7 +845,7 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n optimize_yml_content = ( files("codeflash").joinpath("cli_cmds", "workflows", "codeflash-optimize.yaml").read_text(encoding="utf-8") ) - materialized_optimize_yml_content = customize_codeflash_yaml_content( + materialized_optimize_yml_content = generate_dynamic_workflow_content( optimize_yml_content, config, git_root, benchmark_mode ) @@ -1283,6 +1284,226 @@ def get_github_action_working_directory(toml_path: Path, git_root: Path) -> str: working-directory: ./{working_dir}""" +def collect_repo_files_for_workflow(git_root: Path) -> dict[str, Any]: + """Collect important repository files and directory structure for workflow generation. + + :param git_root: Root directory of the git repository + :return: Dictionary with 'files' (path -> content) and 'directory_structure' (nested dict) + """ + logger.info(f"[cmd_init.py:collect_repo_files_for_workflow] Collecting repo files from {git_root}") + + # Important files to collect with contents + important_files = [ + "pyproject.toml", + "requirements.txt", + "requirements-dev.txt", + "requirements/requirements.txt", + "requirements/dev.txt", + "Pipfile", + "Pipfile.lock", + "poetry.lock", + "uv.lock", + "setup.py", + "setup.cfg", + "Dockerfile", + "docker-compose.yml", + "docker-compose.yaml", + "Makefile", + "README.md", + "README.rst", + ] + + # Also collect GitHub workflows + workflows_path = git_root / ".github" / "workflows" + if workflows_path.exists(): + for workflow_file in workflows_path.glob("*.yml"): + important_files.append(str(workflow_file.relative_to(git_root))) + for workflow_file in workflows_path.glob("*.yaml"): + important_files.append(str(workflow_file.relative_to(git_root))) + + files_dict: dict[str, str] = {} + max_file_size = 8 * 1024 # 8KB limit per file + + for file_path_str in important_files: + file_path = git_root / file_path_str + if file_path.exists() and file_path.is_file(): + try: + content = file_path.read_text(encoding="utf-8", errors="ignore") + # Limit file size + if len(content) > max_file_size: + content = content[:max_file_size] + "\n... (truncated)" + files_dict[file_path_str] = content + logger.debug(f"[cmd_init.py:collect_repo_files_for_workflow] Collected {file_path_str} ({len(content)} chars)") + except Exception as e: + logger.warning(f"[cmd_init.py:collect_repo_files_for_workflow] Failed to read {file_path_str}: {e}") + + # Collect 2-level directory structure + directory_structure: dict[str, Any] = {} + try: + for item in sorted(git_root.iterdir()): + if item.name.startswith(".") and item.name not in [".github", ".git"]: + continue # Skip hidden files/folders except .github + + if item.is_dir(): + # Level 1: directory + dir_dict: dict[str, Any] = {"type": "directory", "contents": {}} + try: + # Level 2: contents of directory + for subitem in sorted(item.iterdir()): + if subitem.name.startswith("."): + continue + if subitem.is_dir(): + dir_dict["contents"][subitem.name] = {"type": "directory"} + else: + dir_dict["contents"][subitem.name] = {"type": "file"} + except PermissionError: + pass # Skip directories we can't read + directory_structure[item.name] = dir_dict + elif item.is_file(): + directory_structure[item.name] = {"type": "file"} + except Exception as e: + logger.warning(f"[cmd_init.py:collect_repo_files_for_workflow] Error collecting directory structure: {e}") + + logger.info( + f"[cmd_init.py:collect_repo_files_for_workflow] Collected {len(files_dict)} files and {len(directory_structure)} top-level items" + ) + + return {"files": files_dict, "directory_structure": directory_structure} + + +def generate_dynamic_workflow_content( + optimize_yml_content: str, + config: tuple[dict[str, Any], Path], + git_root: Path, + benchmark_mode: bool = False, # noqa: FBT001, FBT002 +) -> str: + """Generate workflow content with dynamic steps from AI service, falling back to static template. + + :param optimize_yml_content: Base workflow template content + :param config: Codeflash configuration tuple (dict, Path) + :param git_root: Root directory of the git repository + :param benchmark_mode: Whether to enable benchmark mode + :return: Complete workflow YAML content + """ + # First, do the basic replacements that are always needed + module_path = str(Path(config["module_root"]).relative_to(git_root) / "**") + optimize_yml_content = optimize_yml_content.replace("{{ codeflash_module_path }}", module_path) + + # Get working directory + toml_path = Path.cwd() / "pyproject.toml" + try: + with toml_path.open(encoding="utf8") as pyproject_file: + pyproject_data = tomlkit.parse(pyproject_file.read()) + except FileNotFoundError: + click.echo( + f"I couldn't find a pyproject.toml in the current directory.{LF}" + f"Please create a new empty pyproject.toml file here, OR if you use poetry then run `poetry init`, OR run `codeflash init` again from a directory with an existing pyproject.toml file." + ) + apologize_and_exit() + + working_dir = get_github_action_working_directory(toml_path, git_root) + optimize_yml_content = optimize_yml_content.replace("{{ working_directory }}", working_dir) + + # Try to generate dynamic steps using AI service + try: + logger.info("[cmd_init.py:generate_dynamic_workflow_content] Attempting to generate dynamic workflow steps") + repo_data = collect_repo_files_for_workflow(git_root) + + # Prepare codeflash config for AI + codeflash_config = { + "module_root": config["module_root"], + "tests_root": config.get("tests_root", ""), + "benchmark_mode": benchmark_mode, + } + + aiservice_client = AiServiceClient() + dynamic_steps = aiservice_client.generate_workflow_steps( + repo_files=repo_data["files"], + directory_structure=repo_data["directory_structure"], + codeflash_config=codeflash_config, + ) + + if dynamic_steps: + logger.info("[cmd_init.py:generate_dynamic_workflow_content] Successfully generated dynamic workflow steps") + # Replace the entire steps section with AI-generated steps + # Find the steps section in the template + steps_start = optimize_yml_content.find(" steps:") + if steps_start != -1: + # Find the end of the steps section (next line at same or less indentation) + lines = optimize_yml_content.split("\n") + steps_start_line = optimize_yml_content[:steps_start].count("\n") + steps_end_line = len(lines) + + # Find where steps section ends (next job or end of file) + for i in range(steps_start_line + 1, len(lines)): + line = lines[i] + # Stop if we hit a line that's not indented (new job or end of jobs) + if line and not line.startswith(" ") and not line.startswith("\t"): + steps_end_line = i + break + + # Extract steps content from AI response (remove "steps:" prefix if present) + steps_content = dynamic_steps + if steps_content.startswith("steps:"): + # Remove "steps:" and leading newline + steps_content = steps_content[6:].lstrip("\n") + + # Ensure proper indentation (8 spaces for steps section in YAML) + indented_steps = [] + for line in steps_content.split("\n"): + if line.strip(): + # If line doesn't start with enough spaces, add them + if not line.startswith(" "): + indented_steps.append(" " + line) + else: + # Preserve existing indentation but ensure minimum 8 spaces + current_indent = len(line) - len(line.lstrip()) + if current_indent < 8: + indented_steps.append(" " * 8 + line.lstrip()) + else: + indented_steps.append(line) + else: + indented_steps.append("") + + # Add codeflash command step at the end + dep_manager = determine_dependency_manager(pyproject_data) + codeflash_cmd = get_codeflash_github_action_command(dep_manager) + if benchmark_mode: + codeflash_cmd += " --benchmark" + + # Format codeflash command properly + if "|" in codeflash_cmd: + # Multi-line command + cmd_lines = codeflash_cmd.split("\n") + codeflash_step = f" - name: ⚑️Codeflash Optimization\n run: {cmd_lines[0].strip()}" + for cmd_line in cmd_lines[1:]: + codeflash_step += f"\n {cmd_line.strip()}" + else: + codeflash_step = f" - name: ⚑️Codeflash Optimization\n run: {codeflash_cmd}" + + indented_steps.append(codeflash_step) + + # Reconstruct the workflow + new_lines = lines[:steps_start_line] + [" steps:"] + indented_steps + lines[steps_end_line:] + optimize_yml_content = "\n".join(new_lines) + + logger.info("[cmd_init.py:generate_dynamic_workflow_content] Dynamic workflow generation successful") + return optimize_yml_content + else: + logger.warning("[cmd_init.py:generate_dynamic_workflow_content] Could not find steps section in template") + else: + logger.info("[cmd_init.py:generate_dynamic_workflow_content] AI service returned no steps, falling back to static") + + except Exception as e: + logger.warning( + f"[cmd_init.py:generate_dynamic_workflow_content] Error generating dynamic workflow, falling back to static: {e}" + ) + + # Fallback to static template + logger.info("[cmd_init.py:generate_dynamic_workflow_content] Using static workflow template") + return customize_codeflash_yaml_content(optimize_yml_content, config, git_root, benchmark_mode) + + def customize_codeflash_yaml_content( optimize_yml_content: str, config: tuple[dict[str, Any], Path], From cb20ff86f555ddc761128d39ef6922df04b2b299 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 18 Nov 2025 06:06:36 +0200 Subject: [PATCH 10/19] logs clean up & update --- codeflash/api/aiservice.py | 27 ++++++++++++++++++++------- codeflash/cli_cmds/cmd_init.py | 30 +++++++++++------------------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/codeflash/api/aiservice.py b/codeflash/api/aiservice.py index 348e6eb32..81c574745 100644 --- a/codeflash/api/aiservice.py +++ b/codeflash/api/aiservice.py @@ -650,22 +650,35 @@ def generate_workflow_steps( "codeflash_config": codeflash_config, } - logger.info("Generating workflow steps via AI service...") + logger.debug( + f"[aiservice.py:generate_workflow_steps] Sending request to AI service with {len(repo_files)} files, " + f"{len(directory_structure)} top-level directories" + ) + try: response = self.make_ai_service_request("/workflow-gen", payload=payload, timeout=60) except requests.exceptions.RequestException as e: - logger.warning(f"Error generating workflow steps: {e}") + logger.warning(f"[aiservice.py:generate_workflow_steps] Request exception: {e}") return None if response.status_code == 200: - return cast("str", response.json().get("workflow_steps")) + response_data = response.json() + workflow_steps = cast("str", response_data.get("workflow_steps")) + logger.debug( + f"[aiservice.py:generate_workflow_steps] Successfully received workflow steps " + f"({len(workflow_steps) if workflow_steps else 0} chars)" + ) + return workflow_steps else: - logger.warning(f"Failed to generate workflow steps: {response.status_code}") + logger.warning( + f"[aiservice.py:generate_workflow_steps] Failed with status {response.status_code}" + ) try: - error = cast("str", response.json().get("error", "Unknown error")) - logger.debug(f"Error details: {error}") + error_response = response.json() + error = cast("str", error_response.get("error", "Unknown error")) + logger.debug(f"[aiservice.py:generate_workflow_steps] Error: {error}") except Exception: - pass + logger.debug(f"[aiservice.py:generate_workflow_steps] Could not parse error response") return None diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index 269f05197..7ab73dd13 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -842,13 +842,6 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n benchmark_answers = inquirer.prompt(benchmark_questions, theme=CodeflashTheme()) benchmark_mode = benchmark_answers["benchmark_mode"] if benchmark_answers else False - optimize_yml_content = ( - files("codeflash").joinpath("cli_cmds", "workflows", "codeflash-optimize.yaml").read_text(encoding="utf-8") - ) - materialized_optimize_yml_content = generate_dynamic_workflow_content( - optimize_yml_content, config, git_root, benchmark_mode - ) - # Get API key for secret setup try: api_key = get_codeflash_api_key() @@ -890,6 +883,16 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n "cli-github-optimization-confirm-workflow-creation", {"confirm_creation": creation_answers["confirm_creation"]}, ) + + # Generate workflow content AFTER user confirmation + logger.info("[cmd_init.py:install_github_actions] User confirmed, generating workflow content...") + optimize_yml_content = ( + files("codeflash").joinpath("cli_cmds", "workflows", "codeflash-optimize.yaml").read_text(encoding="utf-8") + ) + materialized_optimize_yml_content = generate_dynamic_workflow_content( + optimize_yml_content, config, git_root, benchmark_mode + ) + workflows_path.mkdir(parents=True, exist_ok=True) pr_created_via_api = False @@ -1290,8 +1293,6 @@ def collect_repo_files_for_workflow(git_root: Path) -> dict[str, Any]: :param git_root: Root directory of the git repository :return: Dictionary with 'files' (path -> content) and 'directory_structure' (nested dict) """ - logger.info(f"[cmd_init.py:collect_repo_files_for_workflow] Collecting repo files from {git_root}") - # Important files to collect with contents important_files = [ "pyproject.toml", @@ -1333,7 +1334,6 @@ def collect_repo_files_for_workflow(git_root: Path) -> dict[str, Any]: if len(content) > max_file_size: content = content[:max_file_size] + "\n... (truncated)" files_dict[file_path_str] = content - logger.debug(f"[cmd_init.py:collect_repo_files_for_workflow] Collected {file_path_str} ({len(content)} chars)") except Exception as e: logger.warning(f"[cmd_init.py:collect_repo_files_for_workflow] Failed to read {file_path_str}: {e}") @@ -1364,10 +1364,6 @@ def collect_repo_files_for_workflow(git_root: Path) -> dict[str, Any]: except Exception as e: logger.warning(f"[cmd_init.py:collect_repo_files_for_workflow] Error collecting directory structure: {e}") - logger.info( - f"[cmd_init.py:collect_repo_files_for_workflow] Collected {len(files_dict)} files and {len(directory_structure)} top-level items" - ) - return {"files": files_dict, "directory_structure": directory_structure} @@ -1406,7 +1402,6 @@ def generate_dynamic_workflow_content( # Try to generate dynamic steps using AI service try: - logger.info("[cmd_init.py:generate_dynamic_workflow_content] Attempting to generate dynamic workflow steps") repo_data = collect_repo_files_for_workflow(git_root) # Prepare codeflash config for AI @@ -1424,7 +1419,6 @@ def generate_dynamic_workflow_content( ) if dynamic_steps: - logger.info("[cmd_init.py:generate_dynamic_workflow_content] Successfully generated dynamic workflow steps") # Replace the entire steps section with AI-generated steps # Find the steps section in the template steps_start = optimize_yml_content.find(" steps:") @@ -1487,12 +1481,11 @@ def generate_dynamic_workflow_content( new_lines = lines[:steps_start_line] + [" steps:"] + indented_steps + lines[steps_end_line:] optimize_yml_content = "\n".join(new_lines) - logger.info("[cmd_init.py:generate_dynamic_workflow_content] Dynamic workflow generation successful") return optimize_yml_content else: logger.warning("[cmd_init.py:generate_dynamic_workflow_content] Could not find steps section in template") else: - logger.info("[cmd_init.py:generate_dynamic_workflow_content] AI service returned no steps, falling back to static") + logger.debug("[cmd_init.py:generate_dynamic_workflow_content] AI service returned no steps, falling back to static") except Exception as e: logger.warning( @@ -1500,7 +1493,6 @@ def generate_dynamic_workflow_content( ) # Fallback to static template - logger.info("[cmd_init.py:generate_dynamic_workflow_content] Using static workflow template") return customize_codeflash_yaml_content(optimize_yml_content, config, git_root, benchmark_mode) From fc88658c5e0e7d94c46e57cd3c7921a171f5b687 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 26 Nov 2025 03:08:29 +0200 Subject: [PATCH 11/19] fix formatting --- codeflash/api/aiservice.py | 19 ++++++++----------- codeflash/cli_cmds/cmd_init.py | 25 ++++++++----------------- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/codeflash/api/aiservice.py b/codeflash/api/aiservice.py index 81c574745..54d07d980 100644 --- a/codeflash/api/aiservice.py +++ b/codeflash/api/aiservice.py @@ -669,17 +669,14 @@ def generate_workflow_steps( f"({len(workflow_steps) if workflow_steps else 0} chars)" ) return workflow_steps - else: - logger.warning( - f"[aiservice.py:generate_workflow_steps] Failed with status {response.status_code}" - ) - try: - error_response = response.json() - error = cast("str", error_response.get("error", "Unknown error")) - logger.debug(f"[aiservice.py:generate_workflow_steps] Error: {error}") - except Exception: - logger.debug(f"[aiservice.py:generate_workflow_steps] Could not parse error response") - return None + logger.warning(f"[aiservice.py:generate_workflow_steps] Failed with status {response.status_code}") + try: + error_response = response.json() + error = cast("str", error_response.get("error", "Unknown error")) + logger.debug(f"[aiservice.py:generate_workflow_steps] Error: {error}") + except Exception: + logger.debug("[aiservice.py:generate_workflow_steps] Could not parse error response") + return None class LocalAiServiceClient(AiServiceClient): diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index 7ab73dd13..f1647caec 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -23,11 +23,7 @@ from rich.text import Text from codeflash.api.aiservice import AiServiceClient -from codeflash.api.cfapi import ( - check_workflow_file_exists, - is_github_app_installed_on_repo, - setup_github_actions, -) +from codeflash.api.cfapi import is_github_app_installed_on_repo, setup_github_actions from codeflash.cli_cmds.cli_common import apologize_and_exit from codeflash.cli_cmds.console import console, logger from codeflash.cli_cmds.extension import install_vscode_extension @@ -778,9 +774,7 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n console.print( Panel( Text( - "βœ… Repository secret CODEFLASH_API_KEY configured", - style="green", - justify="center", + "βœ… Repository secret CODEFLASH_API_KEY configured", style="green", justify="center" ), title="βœ… Secret Configured", border_style="bright_green", @@ -788,9 +782,7 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n ) console.print() elif secret_setup_error: - warning_message = ( - "⚠️ Secret setup failed. You'll need to add CODEFLASH_API_KEY manually.\n\n" - ) + warning_message = "⚠️ Secret setup failed. You'll need to add CODEFLASH_API_KEY manually.\n\n" warning_message += f"Error: {secret_setup_error}\n\n" warning_message += f"πŸ“ Add secret at: {get_github_secrets_page_url(repo)}" @@ -807,9 +799,7 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n ) # Secret setup is optional, so we continue - logger.info( - f"[cmd_init.py:install_github_actions] Workflow file already exists locally, skipping setup" - ) + logger.info("[cmd_init.py:install_github_actions] Workflow file already exists locally, skipping setup") return # Get repository information for API call @@ -1482,10 +1472,11 @@ def generate_dynamic_workflow_content( optimize_yml_content = "\n".join(new_lines) return optimize_yml_content - else: - logger.warning("[cmd_init.py:generate_dynamic_workflow_content] Could not find steps section in template") + logger.warning("[cmd_init.py:generate_dynamic_workflow_content] Could not find steps section in template") else: - logger.debug("[cmd_init.py:generate_dynamic_workflow_content] AI service returned no steps, falling back to static") + logger.debug( + "[cmd_init.py:generate_dynamic_workflow_content] AI service returned no steps, falling back to static" + ) except Exception as e: logger.warning( From 32f29afd35521930151f18546def16e434a62ead Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 10 Dec 2025 15:45:27 +0200 Subject: [PATCH 12/19] fix linting for pre-commit --- codeflash/cli_cmds/cmd_init.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index 6c072b195..2dbea0dd1 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -22,8 +22,7 @@ from rich.text import Text from codeflash.api.aiservice import AiServiceClient -from codeflash.api.cfapi import is_github_app_installed_on_repo, setup_github_actions -from codeflash.api.cfapi import get_user_id, is_github_app_installed_on_repo +from codeflash.api.cfapi import get_user_id, is_github_app_installed_on_repo, setup_github_actions from codeflash.cli_cmds.cli_common import apologize_and_exit from codeflash.cli_cmds.console import console, logger from codeflash.cli_cmds.extension import install_vscode_extension @@ -686,7 +685,7 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n .read_text(encoding="utf-8") ) materialized_optimize_yml_content = generate_dynamic_workflow_content( - optimize_yml_content, config, git_root, False + optimize_yml_content, config, git_root, benchmark_mode=False ) logger.info( @@ -1241,10 +1240,12 @@ def collect_repo_files_for_workflow(git_root: Path) -> dict[str, Any]: # Also collect GitHub workflows workflows_path = git_root / ".github" / "workflows" if workflows_path.exists(): - for workflow_file in workflows_path.glob("*.yml"): - important_files.append(str(workflow_file.relative_to(git_root))) - for workflow_file in workflows_path.glob("*.yaml"): - important_files.append(str(workflow_file.relative_to(git_root))) + important_files.extend( + str(workflow_file.relative_to(git_root)) for workflow_file in workflows_path.glob("*.yml") + ) + important_files.extend( + str(workflow_file.relative_to(git_root)) for workflow_file in workflows_path.glob("*.yaml") + ) files_dict: dict[str, str] = {} max_file_size = 8 * 1024 # 8KB limit per file @@ -1402,10 +1403,7 @@ def generate_dynamic_workflow_content( indented_steps.append(codeflash_step) # Reconstruct the workflow - new_lines = lines[:steps_start_line] + [" steps:"] + indented_steps + lines[steps_end_line:] - optimize_yml_content = "\n".join(new_lines) - - return optimize_yml_content + return "\n".join([*lines[:steps_start_line], " steps:", *indented_steps, *lines[steps_end_line:]]) logger.warning("[cmd_init.py:generate_dynamic_workflow_content] Could not find steps section in template") else: logger.debug( From 1ce7cf7ab4c1f71a527f70b69349d7c0675f8174 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 10 Dec 2025 15:46:32 +0200 Subject: [PATCH 13/19] Updating the UV installation command to include install --upgrade --- codeflash/cli_cmds/cmd_init.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index 2dbea0dd1..31444d9dc 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -1179,7 +1179,9 @@ def get_dependency_installation_commands(dep_manager: DependencyManager) -> tupl pip install poetry poetry install --all-extras""" if dep_manager == DependencyManager.UV: - return "uv sync --all-extras" + return """| + uv sync --all-extras + uv pip install --upgrade codeflash""" # PIP or UNKNOWN return """| python -m pip install --upgrade pip From c9502b631b6671d8a469e19c61e899a65d7cca80 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 10 Dec 2025 15:59:34 +0200 Subject: [PATCH 14/19] remove unused function --- codeflash/api/cfapi.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/codeflash/api/cfapi.py b/codeflash/api/cfapi.py index 396011319..3e215453a 100644 --- a/codeflash/api/cfapi.py +++ b/codeflash/api/cfapi.py @@ -243,18 +243,6 @@ def create_pr( return make_cfapi_request(endpoint="/create-pr", method="POST", payload=payload) -def check_workflow_file_exists(owner: str, repo: str, base_branch: str) -> Response: - """Check if the GitHub Actions workflow file exists on the repository. - - :param owner: Repository owner (username or organization) - :param repo: Repository name - :param base_branch: Base branch to check (e.g., "main", "master") - :return: Response object with exists (bool) and content (str | None) fields - """ - params = {"owner": owner, "repo": repo, "baseBranch": base_branch} - return make_cfapi_request(endpoint="/check-workflow-file-exists", method="GET", params=params) - - def setup_github_actions( owner: str, repo: str, base_branch: str, workflow_content: str, api_key: str | None = None ) -> Response: From 98d2cefb6131fbd986fe11e789da8621e5eb2225 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 16 Dec 2025 11:24:49 +0200 Subject: [PATCH 15/19] revert the secret cf api key while setup of github workflow file --- codeflash/api/cfapi.py | 14 +--- codeflash/cli_cmds/cmd_init.py | 136 ++------------------------------- 2 files changed, 9 insertions(+), 141 deletions(-) diff --git a/codeflash/api/cfapi.py b/codeflash/api/cfapi.py index 3e215453a..688f35278 100644 --- a/codeflash/api/cfapi.py +++ b/codeflash/api/cfapi.py @@ -243,24 +243,18 @@ def create_pr( return make_cfapi_request(endpoint="/create-pr", method="POST", payload=payload) -def setup_github_actions( - owner: str, repo: str, base_branch: str, workflow_content: str, api_key: str | None = None -) -> Response: - """Set up GitHub Actions workflow by creating a PR with the workflow file and optionally setting up the repository secret. +def setup_github_actions(owner: str, repo: str, base_branch: str, workflow_content: str) -> Response: + """Set up GitHub Actions workflow by creating a PR with the workflow file. :param owner: Repository owner (username or organization) :param repo: Repository name :param base_branch: Base branch to create PR against (e.g., "main", "master") :param workflow_content: Content of the GitHub Actions workflow file (YAML) - :param api_key: API key to store as repository secret (if provided, will attempt to set up secret automatically) - :return: Response object with pr_url, pr_number, secret_setup_success, and secret_setup_error on success + :return: Response object with pr_url and pr_number on success """ payload = {"owner": owner, "repo": repo, "baseBranch": base_branch, "workflowContent": workflow_content} - # Include apiKey in payload if provided - this will be encrypted and stored as a repository secret - if api_key: - payload["apiKey"] = api_key - return make_cfapi_request(endpoint="/setup-github-actions", method="POST", payload=payload, api_key=api_key) + return make_cfapi_request(endpoint="/setup-github-actions", method="POST", payload=payload) def create_staging( diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index 31444d9dc..0b6e4f361 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -669,69 +669,6 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n console.print(already_exists_panel) console.print() - # Still try to set up secret if API key is available - try: - api_key = get_codeflash_api_key() - git_remote = config.get("git_remote", "origin") - owner, repo_name = get_repo_owner_and_name(repo, git_remote) - base_branch = get_current_branch(repo) if repo.active_branch else "main" - - # Generate workflow content for secret setup - from importlib.resources import files - - optimize_yml_content = ( - files("codeflash") - .joinpath("cli_cmds", "workflows", "codeflash-optimize.yaml") - .read_text(encoding="utf-8") - ) - materialized_optimize_yml_content = generate_dynamic_workflow_content( - optimize_yml_content, config, git_root, benchmark_mode=False - ) - - logger.info( - f"[cmd_init.py:install_github_actions] Workflow exists locally, attempting secret setup for {owner}/{repo_name}" - ) - secret_response = setup_github_actions( - owner=owner, - repo=repo_name, - base_branch=base_branch, - workflow_content=materialized_optimize_yml_content, - api_key=api_key, - ) - if secret_response.status_code == 200: - secret_data = secret_response.json() - secret_setup_success = secret_data.get("secret_setup_success", False) - secret_setup_error = secret_data.get("secret_setup_error") - - if secret_setup_success: - console.print( - Panel( - Text( - "βœ… Repository secret CODEFLASH_API_KEY configured", style="green", justify="center" - ), - title="βœ… Secret Configured", - border_style="bright_green", - ) - ) - console.print() - elif secret_setup_error: - warning_message = "⚠️ Secret setup failed. You'll need to add CODEFLASH_API_KEY manually.\n\n" - warning_message += f"Error: {secret_setup_error}\n\n" - warning_message += f"πŸ“ Add secret at: {get_github_secrets_page_url(repo)}" - - warning_panel = Panel( - Text(warning_message, style="yellow"), - title="⚠️ Manual Secret Setup Required", - border_style="yellow", - ) - console.print(warning_panel) - console.print() - except Exception as e: - logger.debug( - f"[cmd_init.py:install_github_actions] Could not set up secret (workflow exists locally): {e}" - ) - # Secret setup is optional, so we continue - logger.info("[cmd_init.py:install_github_actions] Workflow file already exists locally, skipping setup") return @@ -765,13 +702,6 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n benchmark_answers = inquirer.prompt(benchmark_questions, theme=CodeflashTheme()) benchmark_mode = benchmark_answers["benchmark_mode"] if benchmark_answers else False - # Get API key for secret setup - try: - api_key = get_codeflash_api_key() - except OSError: - api_key = None - logger.info("[cmd_init.py:install_github_actions] No API key found, will skip secret setup") - # Show prompt only if workflow doesn't exist locally actions_panel = Panel( Text( @@ -820,8 +750,6 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n pr_created_via_api = False pr_url = None - secret_setup_success = False - secret_setup_error = None try: owner, repo_name = get_repo_owner_and_name(repo, git_remote) @@ -857,22 +785,16 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n repo=repo_name, base_branch=base_branch, workflow_content=materialized_optimize_yml_content, - api_key=api_key, ) if response.status_code == 200: response_data = response.json() if response_data.get("success"): pr_url = response_data.get("pr_url") - secret_setup_success = response_data.get("secret_setup_success", False) - secret_setup_error = response_data.get("secret_setup_error") if pr_url: pr_created_via_api = True - # Build success message with secret status success_message = f"βœ… PR created: {pr_url}\n\n" - if secret_setup_success: - success_message += "βœ… Repository secret CODEFLASH_API_KEY configured\n\n" success_message += "Your repository is now configured for continuous optimization!" workflow_success_panel = Panel( @@ -883,32 +805,13 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n console.print(workflow_success_panel) console.print() - # Show warning if secret setup failed - if not secret_setup_success and api_key: - warning_message = ( - "⚠️ Secret setup failed. You'll need to add CODEFLASH_API_KEY manually.\n\n" - ) - if secret_setup_error: - warning_message += f"Error: {secret_setup_error}\n\n" - warning_message += f"πŸ“ Add secret at: {get_github_secrets_page_url(repo)}" - - warning_panel = Panel( - Text(warning_message, style="yellow"), - title="⚠️ Manual Secret Setup Required", - border_style="yellow", - ) - console.print(warning_panel) - console.print() - logger.info( - f"[cmd_init.py:install_github_actions] Successfully created PR #{response_data.get('pr_number')} for {owner}/{repo_name}, secret_setup_success={secret_setup_success}" + f"[cmd_init.py:install_github_actions] Successfully created PR #{response_data.get('pr_number')} for {owner}/{repo_name}" ) else: # File already exists with same content pr_created_via_api = True # Mark as handled (no PR needed) already_exists_message = "βœ… Workflow file already exists with the same content.\n\n" - if secret_setup_success: - already_exists_message += "βœ… Repository secret CODEFLASH_API_KEY configured\n\n" already_exists_message += "No changes needed - your repository is already configured!" already_exists_panel = Panel( @@ -918,23 +821,6 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n ) console.print(already_exists_panel) console.print() - - # Show warning if secret setup failed - if not secret_setup_success and api_key: - warning_message = ( - "⚠️ Secret setup failed. You'll need to add CODEFLASH_API_KEY manually.\n\n" - ) - if secret_setup_error: - warning_message += f"Error: {secret_setup_error}\n\n" - warning_message += f"πŸ“ Add secret at: {get_github_secrets_page_url(repo)}" - - warning_panel = Panel( - Text(warning_message, style="yellow"), - title="⚠️ Manual Secret Setup Required", - border_style="yellow", - ) - console.print(warning_panel) - console.print() else: # API returned success=false, extract error details error_data = response_data @@ -1054,30 +940,18 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n console.print(workflow_success_panel) console.print() - # Show appropriate message based on whether PR was created via API and secret setup status + # Show appropriate message based on whether PR was created via API if pr_created_via_api: if pr_url: - if secret_setup_success: - click.echo( - f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" - f"Once you merge the PR, the workflow will be active.{LF}" - ) - else: - # PR created but secret setup failed or skipped - click.echo( - f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" - f"Once you merge the PR and add the secret, the workflow will be active.{LF}" - ) - # File already exists - elif secret_setup_success: click.echo( f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" - f"The workflow is ready to use.{LF}" + f"Once you merge the PR, the workflow will be active.{LF}" ) else: + # File already exists click.echo( f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" - f"Just add the secret and the workflow will be active.{LF}" + f"The workflow is ready to use.{LF}" ) else: # Fell back to local file creation - show manual secret setup From 5861c83110d2fb7079825fe199ee97f671ef10c1 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 16 Dec 2025 13:28:26 +0200 Subject: [PATCH 16/19] Enhance UX by show a focused message and abort early when there is permission errors --- codeflash/api/aiservice.py | 11 ++++- codeflash/cli_cmds/cmd_init.py | 86 ++++++++++++++++++++++++++-------- 2 files changed, 76 insertions(+), 21 deletions(-) diff --git a/codeflash/api/aiservice.py b/codeflash/api/aiservice.py index 11b6d8864..f86ffe077 100644 --- a/codeflash/api/aiservice.py +++ b/codeflash/api/aiservice.py @@ -652,7 +652,10 @@ def generate_workflow_steps( try: response = self.make_ai_service_request("/workflow-gen", payload=payload, timeout=60) except requests.exceptions.RequestException as e: - logger.warning(f"[aiservice.py:generate_workflow_steps] Request exception: {e}") + # AI service unavailable - this is expected, will fall back to static workflow + logger.debug( + f"[aiservice.py:generate_workflow_steps] Request exception (falling back to static workflow): {e}" + ) return None if response.status_code == 200: @@ -663,7 +666,11 @@ def generate_workflow_steps( f"({len(workflow_steps) if workflow_steps else 0} chars)" ) return workflow_steps - logger.warning(f"[aiservice.py:generate_workflow_steps] Failed with status {response.status_code}") + # AI service unavailable or endpoint not found - this is expected, will fall back to static workflow + logger.debug( + f"[aiservice.py:generate_workflow_steps] AI service returned status {response.status_code}, " + f"falling back to static workflow generation" + ) try: error_response = response.json() error = cast("str", error_response.get("error", "Unknown error")) diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index 0b6e4f361..576dd97f3 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -638,7 +638,7 @@ def create_empty_pyproject_toml(pyproject_toml_path: Path) -> None: apologize_and_exit() -def install_github_actions(override_formatter_check: bool = False) -> None: # noqa: FBT001, FBT002, PLR0911 +def install_github_actions(override_formatter_check: bool = False) -> None: # noqa: FBT001, FBT002 try: config, _config_file_path = parse_config_file(override_formatter_check=override_formatter_check) @@ -829,7 +829,39 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n error_help = error_data.get("help", "") installation_url = error_data.get("installation_url") - # Show detailed error panel + # For permission errors, don't fall back - show a focused message and abort early + if response.status_code == 403: + logger.error( + f"[cmd_init.py:install_github_actions] Permission denied for {owner}/{repo_name}" + ) + # Extract installation_url if available, otherwise use default + installation_url_403 = error_data.get( + "installation_url", "https://github.com/apps/codeflash-ai/installations/select_target" + ) + + permission_error_panel = Panel( + Text( + "❌ Access Denied\n\n" + f"The GitHub App may not be installed on {owner}/{repo_name}, or it doesn't have the required permissions.\n\n" + "πŸ’‘ To fix this:\n" + "1. Install the CodeFlash GitHub App on your repository\n" + "2. Ensure the app has 'Contents: write', 'Workflows: write', and 'Pull requests: write' permissions\n" + "3. Make sure you have write access to the repository\n\n" + f"πŸ”— Install GitHub App: {installation_url_403}", + style="red", + ), + title="❌ Setup Failed", + border_style="red", + ) + console.print(permission_error_panel) + console.print() + click.echo( + f"Please install the CodeFlash GitHub App and ensure it has the required permissions.{LF}" + f"Visit: {installation_url_403}{LF}" + ) + apologize_and_exit() + + # Show detailed error panel for all other errors error_panel_text = f"❌ {error_msg}\n\n{error_message}\n" if error_help: error_panel_text += f"\nπŸ’‘ {error_help}\n" @@ -853,14 +885,6 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n ) return - # For permission errors, don't fall back - show clear instructions - if response.status_code == 403: - logger.error( - f"[cmd_init.py:install_github_actions] Permission denied for {owner}/{repo_name}" - ) - click.echo(f"Please ensure you have write access to {owner}/{repo_name} and try again.{LF}") - return - # For other errors, fall back to local file creation raise Exception(error_message) # noqa: TRY002, TRY301 else: @@ -872,7 +896,39 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n error_help = error_data.get("help", "") installation_url = error_data.get("installation_url") - # Show detailed error panel + # For permission errors, don't fall back - show a focused message and abort early + if response.status_code == 403: + logger.error( + f"[cmd_init.py:install_github_actions] Permission denied for {owner}/{repo_name}" + ) + # Extract installation_url if available, otherwise use default + installation_url_403 = error_data.get( + "installation_url", "https://github.com/apps/codeflash-ai/installations/select_target" + ) + + permission_error_panel = Panel( + Text( + "❌ Access Denied\n\n" + f"The GitHub App may not be installed on {owner}/{repo_name}, or it doesn't have the required permissions.\n\n" + "πŸ’‘ To fix this:\n" + "1. Install the CodeFlash GitHub App on your repository\n" + "2. Ensure the app has 'Contents: write', 'Workflows: write', and 'Pull requests: write' permissions\n" + "3. Make sure you have write access to the repository\n\n" + f"πŸ”— Install GitHub App: {installation_url_403}", + style="red", + ), + title="❌ Setup Failed", + border_style="red", + ) + console.print(permission_error_panel) + console.print() + click.echo( + f"Please install the CodeFlash GitHub App and ensure it has the required permissions.{LF}" + f"Visit: {installation_url_403}{LF}" + ) + apologize_and_exit() + + # Show detailed error panel for all other errors error_panel_text = f"❌ {error_msg}\n\n{error_message}\n" if error_help: error_panel_text += f"\nπŸ’‘ {error_help}\n" @@ -896,14 +952,6 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n ) return - # For permission errors, don't fall back - show clear instructions - if response.status_code == 403: - logger.error( - f"[cmd_init.py:install_github_actions] Permission denied for {owner}/{repo_name}" - ) - click.echo(f"Please ensure you have write access to {owner}/{repo_name} and try again.{LF}") - return - # For authentication errors, don't fall back if response.status_code == 401: logger.error( From 68491e02ebc2ccd84e3cf56289c1a27e68a0902e Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Thu, 18 Dec 2025 13:37:04 +0200 Subject: [PATCH 17/19] Improve branch detection in GitHub Actions setup by handling detached HEAD state and providing fallback options. Update user prompts for API key setup and enhance error logging for better user experience. --- codeflash/cli_cmds/cmd_init.py | 96 +++++++++++++++++-------------- codeflash/code_utils/git_utils.py | 51 ++++++++++++++-- 2 files changed, 100 insertions(+), 47 deletions(-) diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index b58d06cbf..b65a3a38f 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -699,7 +699,15 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n # Get repository information for API call git_remote = config.get("git_remote", "origin") - base_branch = get_current_branch(repo) if repo.active_branch else "main" + # get_current_branch handles detached HEAD and other edge cases internally + try: + base_branch = get_current_branch(repo) + except Exception as e: + logger.warning( + f"[cmd_init.py:install_github_actions] Could not determine current branch: {e}. " + "Falling back to 'main'." + ) + base_branch = "main" # Generate workflow content from importlib.resources import files @@ -1027,54 +1035,56 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n f"The workflow is ready to use.{LF}" ) else: - # Fell back to local file creation - show manual secret setup - try: - existing_api_key = get_codeflash_api_key() - except OSError: - existing_api_key = None - - # GitHub secrets setup panel (only shown when falling back to local file creation) - secrets_message = ( - "πŸ” Next Step: Add API Key as GitHub Secret\n\n" - "You'll need to add your CODEFLASH_API_KEY as a secret to your GitHub repository.\n\n" - "πŸ“‹ Steps:\n" - "1. Press Enter to open your repo's secrets page\n" - "2. Click 'New repository secret'\n" - "3. Add your API key with the variable name CODEFLASH_API_KEY" + # Fell back to local file creation + click.echo( + f"Please edit, commit and push this GitHub actions file to your repo, and you're all set!{LF}" + f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" ) - if existing_api_key: - secrets_message += f"\n\nπŸ”‘ Your API Key: {existing_api_key}" + # Show GitHub secrets setup panel (needed in both cases - PR created via API or local file) + try: + existing_api_key = get_codeflash_api_key() + except OSError: + existing_api_key = None + + # GitHub secrets setup panel - always shown since secrets are required for the workflow to work + secrets_message = ( + "πŸ” Next Step: Add API Key as GitHub Secret\n\n" + "You'll need to add your CODEFLASH_API_KEY as a secret to your GitHub repository.\n\n" + "πŸ“‹ Steps:\n" + "1. Press Enter to open your repo's secrets page\n" + "2. Click 'New repository secret'\n" + "3. Add your API key with the variable name CODEFLASH_API_KEY" + ) - secrets_panel = Panel( - Text(secrets_message, style="blue"), title="πŸ” GitHub Secrets Setup", border_style="bright_blue" - ) - console.print(secrets_panel) + if existing_api_key: + secrets_message += f"\n\nπŸ”‘ Your API Key: {existing_api_key}" - console.print(f"\nπŸ“ Press Enter to open: {get_github_secrets_page_url(repo)}") - console.input() + secrets_panel = Panel( + Text(secrets_message, style="blue"), title="πŸ” GitHub Secrets Setup", border_style="bright_blue" + ) + console.print(secrets_panel) - click.launch(get_github_secrets_page_url(repo)) + console.print(f"\nπŸ“ Press Enter to open: {get_github_secrets_page_url(repo)}") + console.input() - # Post-launch message panel - launch_panel = Panel( - Text( - "πŸ™ I opened your GitHub secrets page!\n\n" - "Note: If you see a 404, you probably don't have access to this repo's secrets. " - "Ask a repo admin to add it for you, or (not recommended) you can temporarily " - "hard-code your API key into the workflow file.", - style="cyan", - ), - title="🌐 Browser Opened", - border_style="bright_cyan", - ) - console.print(launch_panel) - click.pause() - click.echo() - click.echo( - f"Please edit, commit and push this GitHub actions file to your repo, and you're all set!{LF}" - f"πŸš€ Codeflash is now configured to automatically optimize new Github PRs!{LF}" - ) + click.launch(get_github_secrets_page_url(repo)) + + # Post-launch message panel + launch_panel = Panel( + Text( + "πŸ™ I opened your GitHub secrets page!\n\n" + "Note: If you see a 404, you probably don't have access to this repo's secrets. " + "Ask a repo admin to add it for you, or (not recommended) you can temporarily " + "hard-code your API key into the workflow file.", + style="cyan", + ), + title="🌐 Browser Opened", + border_style="bright_cyan", + ) + console.print(launch_panel) + click.pause() + console.print() ph("cli-github-workflow-created") except KeyboardInterrupt: apologize_and_exit() diff --git a/codeflash/code_utils/git_utils.py b/codeflash/code_utils/git_utils.py index 40a725692..65f09a022 100644 --- a/codeflash/code_utils/git_utils.py +++ b/codeflash/code_utils/git_utils.py @@ -70,12 +70,42 @@ def get_git_diff( def get_current_branch(repo: Repo | None = None) -> str: """Return the name of the current branch in the given repository. + Handles detached HEAD state and other edge cases by falling back to + the default branch (main or master) or "main" if no default branch exists. + :param repo: An optional Repo object. If not provided, the function will search for a repository in the current and parent directories. - :return: The name of the current branch. + :return: The name of the current branch, or "main" if HEAD is detached or + the branch cannot be determined. """ repository: Repo = repo if repo else git.Repo(search_parent_directories=True) - return repository.active_branch.name + + # Check if HEAD is detached (active_branch will be None) + if repository.head.is_detached: + logger.warning( + "HEAD is detached. Cannot determine current branch. Falling back to 'main'. " + "Consider checking out a branch before running Codeflash." + ) + # Try to find the default branch (main or master) + for default_branch in ["main", "master"]: + try: + if default_branch in repository.branches: + logger.info(f"Using '{default_branch}' as fallback branch.") + return default_branch + except Exception: + continue + # If no default branch found, return "main" as a safe default + return "main" + + # HEAD is not detached, safe to access active_branch + try: + return repository.active_branch.name + except (AttributeError, TypeError) as e: + logger.warning( + f"Could not determine active branch: {e}. Falling back to 'main'. " + "This may indicate the repository is in an unusual state." + ) + return "main" def get_remote_url(repo: Repo | None = None, git_remote: str | None = "origin") -> str: @@ -126,8 +156,21 @@ def confirm_proceeding_with_no_git_repo() -> str | bool: def check_and_push_branch(repo: git.Repo, git_remote: str | None = "origin", *, wait_for_push: bool = False) -> bool: - current_branch = repo.active_branch - current_branch_name = current_branch.name + # Check if HEAD is detached + if repo.head.is_detached: + logger.warning( + "⚠️ HEAD is detached. Cannot push branch. Please check out a branch before creating a PR." + ) + return False + + # Safe to access active_branch when HEAD is not detached + try: + current_branch = repo.active_branch + current_branch_name = current_branch.name + except (AttributeError, TypeError) as e: + logger.warning(f"⚠️ Could not determine active branch: {e}. Cannot push branch.") + return False + remote = repo.remote(name=git_remote) # Check if the branch is pushed From d268e5d0ffbb80c103f42d64b61a51920ed9ba6f Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Thu, 18 Dec 2025 14:05:04 +0200 Subject: [PATCH 18/19] Add tests for handling detached HEAD state in Git operations Enhance the test suite for Git utilities by adding scenarios to check behavior when the HEAD is detached. This includes mocking the repository state to ensure proper handling in both attached and detached scenarios during branch push operations. --- tests/test_git_utils.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_git_utils.py b/tests/test_git_utils.py index dd40521cc..b82f87ac3 100644 --- a/tests/test_git_utils.py +++ b/tests/test_git_utils.py @@ -67,6 +67,8 @@ def test_check_running_in_git_repo_not_in_git_repo_non_interactive(self, mock_is @patch("codeflash.code_utils.git_utils.Confirm.ask", return_value=True) def test_check_and_push_branch(self, mock_confirm, mock_isatty, mock_repo): mock_repo_instance = mock_repo.return_value + # Mock HEAD not being detached + mock_repo_instance.head.is_detached = False mock_repo_instance.active_branch.name = "test-branch" mock_repo_instance.refs = [] @@ -87,6 +89,8 @@ def test_check_and_push_branch(self, mock_confirm, mock_isatty, mock_repo): @patch("codeflash.code_utils.git_utils.sys.__stdin__.isatty", return_value=False) def test_check_and_push_branch_non_tty(self, mock_isatty, mock_repo): mock_repo_instance = mock_repo.return_value + # Mock HEAD not being detached + mock_repo_instance.head.is_detached = False mock_repo_instance.active_branch.name = "test-branch" mock_repo_instance.refs = [] @@ -97,6 +101,19 @@ def test_check_and_push_branch_non_tty(self, mock_isatty, mock_repo): mock_origin.push.assert_not_called() mock_origin.push.reset_mock() + @patch("codeflash.code_utils.git_utils.git.Repo") + def test_check_and_push_branch_detached_head(self, mock_repo): + mock_repo_instance = mock_repo.return_value + # Mock HEAD being detached + mock_repo_instance.head.is_detached = True + + mock_origin = mock_repo_instance.remote.return_value + mock_origin.push.return_value = None + + # Should return False when HEAD is detached + assert not check_and_push_branch(mock_repo_instance) + mock_origin.push.assert_not_called() + if __name__ == "__main__": unittest.main() From 352c15cb31629e5205342ff0a0a2638e7f1c2ed5 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Thu, 18 Dec 2025 14:06:46 +0200 Subject: [PATCH 19/19] fix linting --- codeflash/cli_cmds/cmd_init.py | 3 +-- codeflash/code_utils/git_utils.py | 15 +++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/codeflash/cli_cmds/cmd_init.py b/codeflash/cli_cmds/cmd_init.py index b65a3a38f..c1960a7cc 100644 --- a/codeflash/cli_cmds/cmd_init.py +++ b/codeflash/cli_cmds/cmd_init.py @@ -704,8 +704,7 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n base_branch = get_current_branch(repo) except Exception as e: logger.warning( - f"[cmd_init.py:install_github_actions] Could not determine current branch: {e}. " - "Falling back to 'main'." + f"[cmd_init.py:install_github_actions] Could not determine current branch: {e}. Falling back to 'main'." ) base_branch = "main" diff --git a/codeflash/code_utils/git_utils.py b/codeflash/code_utils/git_utils.py index 65f09a022..9abe86403 100644 --- a/codeflash/code_utils/git_utils.py +++ b/codeflash/code_utils/git_utils.py @@ -79,7 +79,7 @@ def get_current_branch(repo: Repo | None = None) -> str: the branch cannot be determined. """ repository: Repo = repo if repo else git.Repo(search_parent_directories=True) - + # Check if HEAD is detached (active_branch will be None) if repository.head.is_detached: logger.warning( @@ -92,11 +92,12 @@ def get_current_branch(repo: Repo | None = None) -> str: if default_branch in repository.branches: logger.info(f"Using '{default_branch}' as fallback branch.") return default_branch - except Exception: + except Exception as e: + logger.debug(f"Error checking for branch '{default_branch}': {e}") continue # If no default branch found, return "main" as a safe default return "main" - + # HEAD is not detached, safe to access active_branch try: return repository.active_branch.name @@ -158,11 +159,9 @@ def confirm_proceeding_with_no_git_repo() -> str | bool: def check_and_push_branch(repo: git.Repo, git_remote: str | None = "origin", *, wait_for_push: bool = False) -> bool: # Check if HEAD is detached if repo.head.is_detached: - logger.warning( - "⚠️ HEAD is detached. Cannot push branch. Please check out a branch before creating a PR." - ) + logger.warning("⚠️ HEAD is detached. Cannot push branch. Please check out a branch before creating a PR.") return False - + # Safe to access active_branch when HEAD is not detached try: current_branch = repo.active_branch @@ -170,7 +169,7 @@ def check_and_push_branch(repo: git.Repo, git_remote: str | None = "origin", *, except (AttributeError, TypeError) as e: logger.warning(f"⚠️ Could not determine active branch: {e}. Cannot push branch.") return False - + remote = repo.remote(name=git_remote) # Check if the branch is pushed