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:
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Shared Jira config loading for g3fo-commit-jira scripts.
|
||||
Uses the same jira_upload.env / env vars as upload_attachment.py.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
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
|
||||
parent_skill = script_dir.parent / CONFIG_FILENAME
|
||||
if parent_skill.exists():
|
||||
return parent_skill
|
||||
return None
|
||||
|
||||
|
||||
def load_config(config_path: Optional[str] = None, script_dir: Optional[Path] = None) -> Dict[str, str]:
|
||||
"""Load KEY=VALUE from jira_upload.env. Env vars override file values."""
|
||||
if script_dir is None:
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
path = find_config_file(config_path, script_dir)
|
||||
out: Dict[str, str] = {}
|
||||
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 require_credentials(cfg: Dict[str, str]) -> tuple:
|
||||
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:
|
||||
raise ValueError(
|
||||
"Missing JIRA_BASE_URL, JIRA_EMAIL, or JIRA_API_TOKEN. "
|
||||
f"Set in {CONFIG_FILENAME} or environment (see references/env_config.md)."
|
||||
)
|
||||
return base_url, email, token
|
||||
|
||||
|
||||
def auth_header(email: str, token: str) -> str:
|
||||
return base64.b64encode(f"{email}:{token}".encode()).decode()
|
||||
|
||||
|
||||
def session_headers(email: str, token: str, json_body: bool = True) -> Dict[str, str]:
|
||||
h = {
|
||||
"Authorization": f"Basic {auth_header(email, token)}",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if json_body:
|
||||
h["Content-Type"] = "application/json"
|
||||
return h
|
||||
Reference in New Issue
Block a user