Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sast/before.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":"1.128.1","results":[],"errors":[{"code":2,"level":"error","type":"SemgrepError","message":"Invalid scanning root: repo"}],"paths":{"scanned":[]},"skipped_rules":[]}
149 changes: 102 additions & 47 deletions src/autofic_core/app.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import os
import sys
from pathlib import Path
import time
import traceback

from pathlib import Path
from rich.console import Console

from autofic_core.errors import *
from autofic_core.utils.ui_utils import print_help_message
from autofic_core.pipeline import AutoFiCPipeline
from autofic_core.utils.ui_utils import print_help_message, print_divider
from autofic_core.pipeline import AutoFiCPipeline
from autofic_core.log.log_writer import LogManager
from autofic_core.log.log_generator import LogGenerator

Expand All @@ -31,7 +32,7 @@ def __init__(self, explain, repo, save_dir, sast, llm, llm_retry, patch, pr):

def run(self):
try:
self._validate_options()
self.validate_options()
if self.explain:
print_help_message()
return
Expand All @@ -50,7 +51,7 @@ def run(self):
pipeline.run()

if self.pr:
self._run_pr(pipeline)
self.run_pr()

except AutoficError as e:
console.print(str(e), style="red")
Expand All @@ -61,7 +62,7 @@ def run(self):
console.print(traceback.format_exc(), style="red")
sys.exit(1)

def _validate_options(self):
def validate_options(self):
if not self.repo:
raise NoRepositoryError()
if not self.save_dir:
Expand All @@ -75,55 +76,109 @@ def _validate_options(self):
if not self.patch and self.pr:
raise PRWithoutPatchError()

def _run_pr(self, pipeline):
# PR automation
branch_num = 1
def run_pr(self):
try:
print_divider("PR Automation Stage")

pr_procedure = self.initialize_pr_procedure()

console.print("[1] Initializing PR process & checking branches...\n", style="bold cyan")
time.sleep(0.5)
pr_procedure.post_init()
pr_procedure.mv_workdir()
pr_procedure.check_branch_exists()

console.print("\n[2] Notifying webhooks...\n", style="bold cyan")
time.sleep(0.5)
self.notify_webhooks(pr_procedure)

console.print("\n[3] Creating and pushing PR workflow YAML...\n", style="bold cyan")
time.sleep(0.5)
self.handle_pr_yml(pr_procedure)

console.print("\n[4] Changing files for the pull request...\n", style="bold cyan")
time.sleep(0.5)
pr_procedure.change_files()

console.print("\n[5] Updating branch and creating Pull Request...\n", style="bold cyan")
time.sleep(0.5)
pr_procedure.current_main_branch()
pr_procedure.generate_pr()
pr_number = pr_procedure.create_pr()

console.print(f"\n[ SUCCESS ] Pull Request created successfully!\n", style="bold green")
time.sleep(0.5)

self.finalize_logging(pr_procedure, pr_number)

except Exception as e:
console.print(f"[ PR ERROR ] {e}", style="bold red")
console.print(traceback.format_exc(), style="red")
raise

def initialize_pr_procedure(self):
base_branch = 'main'
branch_name = "UNKNOWN"
repo_name = "UNKOWN"
upstream_owner = "UNKOWN"
save_dir = Path(self.save_dir).joinpath('repo')
repo_url = self.repo.rstrip('/').replace('.git', '')
secret_discord = os.getenv('DISCORD_WEBHOOK_URL')
secret_slack = os.getenv('SLACK_WEBHOOK_URL')
json_path = str(Path(self.save_dir).joinpath("sast") / "before.json")
token = os.getenv('GITHUB_TOKEN')
user_name = os.getenv('USER_NAME')

# Define PRProcedure class
json_path = str(Path(self.save_dir).joinpath("sast") / "before.json")
tool = self.sast.lower() if self.sast else None
pr_procedure = PRProcedure(
base_branch, repo_name, upstream_owner,
save_dir, repo_url, token, user_name, json_path, tool

return PRProcedure(
base_branch=base_branch,
repo_name="UNKNOWN",
upstream_owner="UNKNOWN",
save_dir=save_dir,
repo_url=repo_url,
token=token,
user_name=user_name,
json_path=json_path,
tool=tool
)
# Chapter 1
pr_procedure.post_init()

def notify_webhooks(self, pr_procedure):
secret_discord = os.getenv('DISCORD_WEBHOOK_URL')
secret_slack = os.getenv('SLACK_WEBHOOK_URL')

user_name = pr_procedure.user_name
repo_name = pr_procedure.repo_name
upstream_owner = pr_procedure.upstream_owner
# Chaper 2
pr_procedure.mv_workdir()
# Chapter 3
pr_procedure.check_branch_exists()
branch_name = pr_procedure.branch_name
# Chapter 4
token = pr_procedure.token

EnvEncrypy(user_name, repo_name, token).webhook_secret_notifier('DISCORD_WEBHOOK_URL', secret_discord)
EnvEncrypy(user_name, repo_name, token).webhook_secret_notifier('SLACK_WEBHOOK_URL', secret_slack)
# Chapter 5
AboutYml().create_pr_yml()
AboutYml().push_pr_yml(user_name, repo_name, token, branch_name)
# Chapter 6
pr_procedure.change_files()
# Chapter 7
pr_procedure.current_main_branch()
# Chapter 8,9
pr_procedure.generate_pr()
pr_number = pr_procedure.create_pr()

# for log
repo_data = self.log_gen.generate_repo_log(save_dir=Path(self.save_dir), name=repo_name, owner=upstream_owner,
repo_url=repo_url, sastTool=tool, rerun=self.llm_retry)
pr_log_data = self.log_gen.generate_pr_log(owner=upstream_owner, repo=repo_name, user_name=user_name,
repo_url=repo_url, repo_hash=repo_data["repo_hash"],
pr_number=pr_number)

def handle_pr_yml(self, pr_procedure):
user_name = pr_procedure.user_name
repo_name = pr_procedure.repo_name
token = pr_procedure.token
branch_name = pr_procedure.branch_name

yml_handler = AboutYml()
yml_handler.create_pr_yml()
yml_handler.push_pr_yml(user_name, repo_name, token, branch_name)

def finalize_logging(self, pr_procedure, pr_number):
tool = self.sast.lower() if self.sast else None
repo_url = self.repo.rstrip('/').replace('.git', '')

repo_data = self.log_gen.generate_repo_log(
save_dir=Path(self.save_dir),
name=pr_procedure.repo_name,
owner=pr_procedure.upstream_owner,
repo_url=repo_url,
sastTool=tool,
rerun=self.llm_retry
)

pr_log_data = self.log_gen.generate_pr_log(
owner=pr_procedure.upstream_owner,
repo=pr_procedure.repo_name,
user_name=pr_procedure.user_name,
repo_url=repo_url,
repo_hash=repo_data["repo_hash"],
pr_number=pr_number
)

self.log_manager.add_pr_log(pr_log_data)
self.log_manager.add_repo_status(repo_data)
6 changes: 3 additions & 3 deletions src/autofic_core/patch/apply_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def apply_all(self) -> bool:
failed_patches.append(patch_file)

if failed_patches:
self.console.print(f"\n[cyan][ INFO ] {len(failed_patches)} patches failed → trying overwrite from parsed (see logs)[/cyan]\n")
self.console.print(f"[cyan][ INFO ] {len(failed_patches)} patches failed → trying overwrite from parsed (see logs)[/cyan]\n")
for patch_file in failed_patches:
self.overwrite_with_parsed(patch_file)

Expand All @@ -66,7 +66,7 @@ def apply_single(self, patch_file: Path) -> bool:
)

if result.returncode == 0:
self.console.print(f"[white][✓] Patch applied: {patch_file.name}[/white]")
self.console.print(f"[white][✓] Patch applied: {patch_file.name}[/white]\n")
return True
else:
self.console.print(PatchFailMessages.PATCH_FAILED.format(patch_file.name), style="yellow")
Expand Down Expand Up @@ -161,7 +161,7 @@ def overwrite_with_parsed(self, patch_file: Path) -> bool:

try:
shutil.copyfile(matched_file, repo_file)
self.console.print(f"[white][✓] Overwrote repo file: {repo_file}[/white]")
self.console.print(f"[white][✓] Overwrote repo file: {repo_file}[/white]\n")
return True
except Exception as e:
self.console.print(PatchErrorMessages.OVERWRITE_FAILED.format(e), style="red")
Expand Down
31 changes: 18 additions & 13 deletions src/autofic_core/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ def clone(self):

try:
if self.handler.needs_fork:
console.print("\nAttempting to fork the repository...\n", style="cyan")
console.print("Attempting to fork the repository...\n", style="cyan")
self.handler.fork()
time.sleep(1)
console.print("\n[ SUCCESS ] Fork completed\n", style="green")
console.print("[ SUCCESS ] Fork completed\n", style="green")

self.clone_path = Path(
self.handler.clone_repo(save_dir=str(self.save_dir), use_forked=self.handler.needs_fork))
console.print(f"\n[ SUCCESS ] Repository cloned successfully: {self.clone_path}\n", style="green")
console.print(f"[ SUCCESS ] Repository cloned successfully: {self.clone_path}", style="green")

except ForkFailedError as e:
sys.exit(1)
Expand All @@ -77,8 +77,10 @@ def __init__(self, repo_path: Path, save_dir: Path):
self.save_dir = save_dir

def run(self):
description = "Running Semgrep...".ljust(28)
with create_progress() as progress:
task = progress.add_task("[cyan]Running Semgrep...", total=100)
task = progress.add_task(description, total=100)

start = time.time()
runner = SemgrepRunner(repo_path=str(self.repo_path), rule="p/javascript")
result = runner.run_semgrep()
Expand Down Expand Up @@ -123,8 +125,10 @@ def __init__(self, repo_path: Path, save_dir: Path):
self.save_dir = save_dir

def run(self):
description = "Running CodeQL...".ljust(28)
with create_progress() as progress:
task = progress.add_task("[cyan]Running CodeQL...", total=100)
task = progress.add_task(description, total=100)

start = time.time()
runner = CodeQLRunner(repo_path=str(self.repo_path))
result_path = runner.run_codeql()
Expand Down Expand Up @@ -166,10 +170,12 @@ class SnykCodeHandler:
def __init__(self, repo_path: Path, save_dir: Path):
self.repo_path = repo_path
self.save_dir = save_dir

def run(self):
description = "Running SnykCode...".ljust(28)
with create_progress() as progress:
task = progress.add_task("[cyan]Running SnykCode...", total=100)
task = progress.add_task(description, total=100)

start = time.time()
runner = SnykCodeRunner(repo_path=str(self.repo_path))
result = runner.run_snykcode()
Expand Down Expand Up @@ -267,7 +273,6 @@ def __init__(self, sast_result_path: Path, repo_path: Path, save_dir: Path, tool
self.patch_dir = save_dir / "patch"

def run(self):
console.print()
print_divider("LLM Response Generation Stage")

prompt_generator = PromptGenerator()
Expand All @@ -284,10 +289,10 @@ def run(self):

llm = LLMRunner()
self.llm_output_dir.mkdir(parents=True, exist_ok=True)

console.print("Starting GPT response generation\n")
description = "Generating LLM responses... \n".ljust(28)
with create_progress() as progress:
task = progress.add_task("[magenta]Generating LLM responses...", total=len(prompts))
task = progress.add_task(description, total=len(prompts))

for p in prompts:
response = llm.run(p.prompt)
Expand All @@ -298,7 +303,7 @@ def run(self):
time.sleep(0.01)
progress.update(task, completed=100)

console.print(f"\n[ SUCCESS ] LLM responses saved → {self.llm_output_dir}\n", style="green")
console.print(f"[ SUCCESS ] LLM responses saved → {self.llm_output_dir}", style="green")
return prompts, file_snippets

def retry(self):
Expand Down Expand Up @@ -370,7 +375,7 @@ def run(self):
success = patch_applier.apply_all()

if success:
console.print(f"\n[ SUCCESS ] All patches successfully applied\n", style="green")
console.print(f"[ SUCCESS ] All patches successfully applied", style="green")
else:
console.print(f"\n[ WARN ] Some patches failed to apply → {self.repo_dir}\n", style="yellow")

Expand Down
10 changes: 6 additions & 4 deletions src/autofic_core/pr_auto/create_yml.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"""
import os
import subprocess
import click
from rich.console import Console

console = Console()

# Handles creation and git operations for GitHub Actions workflow YAML files
class AboutYml:
Expand Down Expand Up @@ -82,7 +84,7 @@ def push_pr_yml(self, user_name, repo_name, token, branch_name):
"""
repo_url = f'https://x-access-token:{token}@github.com/{user_name}/{repo_name}.git'
subprocess.run(['git', 'remote', 'set-url', 'origin', repo_url], check=True)
click.secho("[ INFO ] Pushing the generated .github/workflows/pr_notify.yml.", fg="yellow")
console.print("[ INFO ] Created GitHub Actions workflow file: pr_notify.yml\n", style="white")
subprocess.run(['git', 'add', '.github/workflows/pr_notify.yml'], check=True)
subprocess.run(['git', 'commit', '-m', "[Autofic] Create package.json and CI workflow"], check=True)
subprocess.run(['git', 'push', 'origin', branch_name], check=True)
subprocess.run(['git', 'commit', '-m', "[ AutoFiC ] Create package.json and CI workflow"], check=True)
subprocess.run(['git', 'push', 'origin', branch_name], check=True)
Loading