#!/usr/bin/env python3 """ Upload one or more files as attachments to a Jira Cloud issue via REST API v3. Config: from jira_upload.env (current dir, script dir, or skill parent dir) or env vars. Usage: python upload_attachment.py --issue ISSUE_KEY --file path1 [--file path2 ...] python upload_attachment.py --config /path/to/jira_upload.env --issue KEY --file path1 """ import argparse import sys from pathlib import Path try: import requests except ImportError: print("ERROR: 'requests' is required. Run: pip install requests", file=sys.stderr) sys.exit(1) from jira_env import CONFIG_FILENAME, auth_header, load_config, require_credentials SCRIPT_DIR = Path(__file__).resolve().parent def main() -> int: parser = argparse.ArgumentParser(description="Upload files as attachments to a Jira issue") parser.add_argument("--issue", required=True, help="Jira issue key (e.g. G3SF-123)") parser.add_argument( "--file", action="append", required=True, dest="files", help="Path to file to upload (can be repeated)", ) parser.add_argument( "--config", default=None, help=f"Path to config file (default: {CONFIG_FILENAME} in cwd, script dir, or skill dir)", ) args = parser.parse_args() try: cfg = load_config(args.config, SCRIPT_DIR) base_url, email, token = require_credentials(cfg) except ValueError as e: print(f"ERROR: {e}", file=sys.stderr) print(f" Use --config PATH or create {CONFIG_FILENAME}", file=sys.stderr) return 1 issue_key = args.issue.strip() files_to_upload = [] for p in args.files: path = Path(p) if not path.exists(): print(f"ERROR: File not found: {path}", file=sys.stderr) return 1 if not path.is_file(): print(f"ERROR: Not a file: {path}", file=sys.stderr) return 1 files_to_upload.append(path) url = f"{base_url}/rest/api/3/issue/{issue_key}/attachments" headers = { "Authorization": f"Basic {auth_header(email, token)}", "X-Atlassian-Token": "no-check", } for path in files_to_upload: with open(path, "rb") as f: files = {"file": (path.name, f)} try: resp = requests.post(url, headers=headers, files=files, timeout=60) except requests.RequestException as e: print(f"ERROR: Request failed for {path.name}: {e}", file=sys.stderr) return 1 if resp.status_code not in (200, 201): print(f"ERROR: Upload failed for {path.name}: HTTP {resp.status_code}", file=sys.stderr) if resp.text: print(resp.text[:500], file=sys.stderr) return 1 data = resp.json() if isinstance(data, list) and data: for att in data: print(f"OK: {att.get('filename', path.name)} (id={att.get('id')}, size={att.get('size', '?')})") else: print(f"OK: {path.name}") return 0 if __name__ == "__main__": sys.exit(main())