- web/: Local chat UI (Express + WS → Codex bridge) - openwebui/: Preset, pipelines, knowledge manifest - Dockerfile + docker-compose.yml - Updated README with 3 frontend options - CLI-agnostic: works with Codex, Claude Code, Kiro, Gemini
104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Create an Aha epic in a target release using credentials from ~/.mcporter/mcporter.json.
|
|
|
|
Usage:
|
|
python3 skills/epics-standards/scripts/aha_create_epic.py \
|
|
--release MDM-R-889 \
|
|
--name "RDM PrivateLink on AWS" \
|
|
--description "Tracks Jira epic RP-176273 (...)" \
|
|
--jira-key RP-176273
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
|
|
MCPORTER_CONFIG = pathlib.Path.home() / ".mcporter" / "mcporter.json"
|
|
|
|
|
|
def load_aha_env() -> dict[str, str]:
|
|
cfg = json.loads(MCPORTER_CONFIG.read_text())
|
|
env = cfg["mcpServers"]["aha"]["env"]
|
|
return {"domain": env["AHA_DOMAIN"], "token": env["AHA_API_TOKEN"]}
|
|
|
|
|
|
def request(method: str, url: str, token: str, payload: dict | None = None) -> dict:
|
|
headers = {
|
|
"Authorization": f"Bearer {token}",
|
|
"Accept": "application/json",
|
|
"Content-Type": "application/json",
|
|
}
|
|
body = None if payload is None else json.dumps(payload).encode("utf-8")
|
|
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
return json.loads(resp.read().decode("utf-8"))
|
|
|
|
|
|
def release_ref_from_name(domain: str, token: str, release_name: str) -> str:
|
|
q = urllib.parse.quote(release_name)
|
|
url = f"https://{domain}.aha.io/api/v1/releases?q={q}&per_page=200"
|
|
data = request("GET", url, token)
|
|
for rel in data.get("releases", []):
|
|
if rel.get("name") == release_name:
|
|
return rel["reference_num"]
|
|
raise ValueError(f"Release not found by name: {release_name}")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--release", required=True, help="Release reference (e.g. MDM-R-889) or exact release name (e.g. 2026.2.0.0)")
|
|
parser.add_argument("--name", required=True, help="Aha epic name")
|
|
parser.add_argument("--description", required=True, help="Aha epic description/body")
|
|
parser.add_argument("--jira-key", required=False, help="Optional Jira key to print linking reminder")
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
aha = load_aha_env()
|
|
release = args.release
|
|
if not release.startswith("MDM-R-"):
|
|
release = release_ref_from_name(aha["domain"], aha["token"], release)
|
|
|
|
url = f"https://{aha['domain']}.aha.io/api/v1/releases/{release}/epics"
|
|
payload = {"epic": {"name": args.name, "description": args.description}}
|
|
data = request("POST", url, aha["token"], payload)
|
|
epic = data.get("epic", {})
|
|
|
|
print(json.dumps(
|
|
{
|
|
"aha_reference": epic.get("reference_num"),
|
|
"aha_url": epic.get("url"),
|
|
"release": release,
|
|
"jira_key": args.jira_key,
|
|
},
|
|
ensure_ascii=True,
|
|
))
|
|
|
|
if args.jira_key and epic.get("url"):
|
|
print(
|
|
f"Next: set Jira {args.jira_key} Aha! Reference = {epic['url']}",
|
|
file=sys.stderr,
|
|
)
|
|
return 0
|
|
except (KeyError, FileNotFoundError, ValueError) as exc:
|
|
print(f"Config/input error: {exc}", file=sys.stderr)
|
|
return 2
|
|
except urllib.error.HTTPError as exc:
|
|
detail = exc.read().decode("utf-8", errors="replace")
|
|
print(f"Aha API HTTP {exc.code}: {detail}", file=sys.stderr)
|
|
return 3
|
|
except Exception as exc: # noqa: BLE001
|
|
print(f"Unexpected error: {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|