feat(g3fo-commit-jira): 新增无 MCP 的 Jira CLI 并增强 AI 执行流程约束

- 新增 `jira_cli.py` 脚本,支持在不依赖 Atlassian MCP 的环境下通过同一 Jira Token 进行搜索、创建 Issue 和评论
- 新增共享配置模块 `jira_env.py`,统一加载 `jira_upload.env` 并支持在技能根目录查找配置文件
- 更新 `SKILL.md` 和参考文档,增加「AI 执行契约」和详细的前置条件说明,强制 Step 1→5 线性执行顺序
- 新增 `agent_execution_checklist.md` 检查清单,防止 AI 跳步、误用工具或未等用户确认就绑定 Issue
- 改进 `jira_cli.py` 的评论和描述生成,将常见 Markdown 语法转换为 Jira ADF 格式以获得更好的渲染效果
This commit is contained in:
2026-03-23 13:38:41 +08:00
parent cd6c8a0450
commit 57fca4e468
9 changed files with 1018 additions and 78 deletions
@@ -2,18 +2,15 @@
"""
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.
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 base64
import os
import sys
from pathlib import Path
from typing import Dict, Optional
try:
import requests
@@ -21,57 +18,34 @@ except ImportError:
print("ERROR: 'requests' is required. Run: pip install requests", file=sys.stderr)
sys.exit(1)
CONFIG_FILENAME = "jira_upload.env"
from jira_env import CONFIG_FILENAME, auth_header, load_config, require_credentials
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
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 or script dir)")
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()
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)
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()
@@ -87,9 +61,8 @@ def main() -> int:
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}",
"Authorization": f"Basic {auth_header(email, token)}",
"X-Atlassian-Token": "no-check",
}