diff --git a/modules/pastebin_helpers.py b/modules/pastebin_helpers.py new file mode 100644 index 0000000..397d648 --- /dev/null +++ b/modules/pastebin_helpers.py @@ -0,0 +1,43 @@ +import requests + +def create_paste( + *, + developer_key: str, + content: str, + timeout_seconds: float = 15.0, +) -> str: + # Make sure the API key is not empty + if not developer_key.strip(): + raise ValueError("Pastebin developer key cannot be empty") + + # Make sure there is actually something to upload + if not content.strip(): + raise ValueError("Paste content cannot be empty") + + # Data Pastebin expects + payload = { + "api_dev_key": developer_key.strip(), + "api_option": "paste", + "api_paste_code": content, + } + + # Send the POST request to Pastebin + response = requests.post( + "https://pastebin.com/api/api_post.php", + data=payload, + timeout=timeout_seconds, + ) + + response.raise_for_status() + + # Pastebin returns either: + # - a Pastebin URL on success + # - text starting with "Bad API request" on failure + response_body = response.text.strip() + + if response_body.startswith("Bad API request"): + raise RuntimeError( + f"Pastebin rejected the request: {response_body}" + ) + + return response_body \ No newline at end of file diff --git a/server.py b/server.py index 83aeba6..47ae6e0 100644 --- a/server.py +++ b/server.py @@ -16,6 +16,7 @@ from fastapi import BackgroundTasks, FastAPI, Request, Response from fastapi.middleware.cors import CORSMiddleware from prometheus_client import generate_latest +from dotenv import load_dotenv import requests import uvicorn import websocket @@ -24,7 +25,10 @@ from metrics import MetricsHandler from modules.args import get_args from modules.commit_status_helpers import CommitStatus, push_commit_status +from modules.pastebin_helpers import create_paste +load_dotenv() +PASTEBIN_DEV_API_KEY = os.getenv("PASTEBIN_DEV_API_KEY") args = get_args() logging.basicConfig( @@ -57,7 +61,6 @@ CICD_DISCORD_WEBHOOK_URL = None GITHUB_TOKEN = None - @dataclasses.dataclass class RepoConfig: name: str @@ -141,6 +144,20 @@ def run_command(command_args: list, cwd: str) -> ExecutionResult: logger.exception(f"Failed to execute {cmd_str}") return ExecutionResult(command=cmd_str) +def build_execution_log( + step_title: str, + execution_result: ExecutionResult, +) -> str: + return ( + f"Step: {step_title}\n" + f"Command: {execution_result.command}\n" + f"Exit Code: {execution_result.exit_code}\n" + f"Success: {execution_result.success}\n" + f"\nSTDOUT:\n" + f"{execution_result.stdout or '(empty)'}\n" + f"\nSTDERR:\n" + f"{execution_result.stderr or '(empty)'}\n" + ) def push_github_commit_status(status: DeploymentStatus): if status.is_dev: @@ -165,8 +182,29 @@ def push_github_commit_status(status: DeploymentStatus): for step_title, status_field_name in execution_results: execution_result = getattr(status, status_field_name, None) - deployment_failed = execution_result is not None and not execution_result.success - + if execution_result is None: + continue + + deployment_failed = not execution_result.success + + paste_url = None + + if not PASTEBIN_DEV_API_KEY: + logger.warning("Pastebin API key missing") + else: + try: + paste_url = create_paste( + developer_key = PASTEBIN_DEV_API_KEY, + content = build_execution_log( + step_title, + execution_result, + ), + ) + logger.info(f"{step_title} logs uploaded to Pastebin") + except Exception: + logger.exception( + f"Failed to upload {step_title} logs to Pastebin" + ) commit_status = CommitStatus( state = "failure" if deployment_failed else "success", @@ -176,6 +214,7 @@ def push_github_commit_status(status: DeploymentStatus): else "Deployment successful" ), context = f"[sce-cicd] {step_title}", + target_url = paste_url, ) try: @@ -461,7 +500,7 @@ def should_skip_deployment(files_changed: List[str], ignore_patterns: List[str]) SMEE2_URL = data.get("smee2_url") SMEE2_API_KEY = data.get("smee2_api_key") CICD_DISCORD_WEBHOOK_URL = data.get("cicd_discord_webhook_url") - GITHUB_TOKEN = data.get("github_token") + GITHUB_TOKEN = data.get("github_token") or os.getenv("GITHUB_TOKEN") for r in raw_repos: # make a new entry into the result dictionary # the key is a tuple of the repo name and branch @@ -697,6 +736,5 @@ def smee_listen(): get_docker_images_disk_usage_bytes() smee_listen() - if __name__ == "__main__": uvicorn.run("server:app", port=args.port, reload=True)