feat: 添加g3fo-commit-jira skill
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
#!/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 or script 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 base64
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("ERROR: 'requests' is required. Run: pip install requests", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
CONFIG_FILENAME = "jira_upload.env"
|
||||
|
||||
|
||||
def _find_config_file(explicit_path: Optional[str], script_dir: Path) -> Optional[Path]:
|
||||
if explicit_path:
|
||||
p = Path(explicit_path)
|
||||
return p if p.exists() and p.is_file() else None
|
||||
cwd_file = Path.cwd() / CONFIG_FILENAME
|
||||
if cwd_file.exists():
|
||||
return cwd_file
|
||||
script_dir_file = script_dir / CONFIG_FILENAME
|
||||
if script_dir_file.exists():
|
||||
return script_dir_file
|
||||
return None
|
||||
|
||||
|
||||
def load_config(config_path: Optional[str]) -> Dict[str, str]:
|
||||
"""Load KEY=VALUE from jira_upload.env. Env vars override file values."""
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
path = _find_config_file(config_path, script_dir)
|
||||
out = {}
|
||||
if path:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if "=" in line:
|
||||
k, _, v = line.partition("=")
|
||||
out[k.strip()] = v.strip().strip('"').strip("'")
|
||||
for key in ("JIRA_BASE_URL", "JIRA_EMAIL", "JIRA_API_TOKEN"):
|
||||
if key in os.environ:
|
||||
out[key] = os.environ[key]
|
||||
return out
|
||||
|
||||
|
||||
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 or script dir)")
|
||||
args = parser.parse_args()
|
||||
|
||||
cfg = load_config(args.config)
|
||||
base_url = (cfg.get("JIRA_BASE_URL") or "").rstrip("/")
|
||||
email = cfg.get("JIRA_EMAIL")
|
||||
token = cfg.get("JIRA_API_TOKEN")
|
||||
|
||||
if not base_url or not email or not token:
|
||||
print("ERROR: Set JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN in config file or environment.", file=sys.stderr)
|
||||
print(f" Config file: {CONFIG_FILENAME} (in current dir or script dir), or use --config PATH", 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"
|
||||
auth_str = base64.b64encode(f"{email}:{token}".encode()).decode()
|
||||
headers = {
|
||||
"Authorization": f"Basic {auth_str}",
|
||||
"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())
|
||||
Reference in New Issue
Block a user