webhooks.py
python
sha256:e6465e8a9b7fa8e6223ed4a3576e96c568c913ae2caeb9c31f15e7a81b250b40
docs: add | jq convention to --json section of agent-guide
Sonnet 4.6
1 day ago
| 1 | import argparse |
| 2 | from ._core import * |
| 3 | |
| 4 | def run_webhook_create(args: argparse.Namespace) -> None: |
| 5 | """Register a new webhook subscription for a repository. |
| 6 | |
| 7 | Requires write/admin access or repo ownership. |
| 8 | |
| 9 | JSON output is the raw API response merged with the standard envelope. |
| 10 | Human-readable text goes to stderr. |
| 11 | |
| 12 | Agent quickstart |
| 13 | ---------------- |
| 14 | :: |
| 15 | |
| 16 | muse hub webhook create --url https://ci.example.com/hook --events push --json |
| 17 | # → {"muse_version": "...", ..., "webhookId": "...", "url": "...", "events": [...]} |
| 18 | |
| 19 | Exit codes |
| 20 | ---------- |
| 21 | 0 Webhook registered. |
| 22 | 1 Validation or auth error. |
| 23 | 2 Not inside a Muse repository. |
| 24 | 3 API error. |
| 25 | """ |
| 26 | url: str = args.url |
| 27 | events: list[str] = args.events |
| 28 | secret: str = args.secret |
| 29 | json_output: bool = args.json_output |
| 30 | |
| 31 | if not url.strip(): |
| 32 | print("❌ --url must not be empty.", file=sys.stderr) |
| 33 | raise SystemExit(ExitCode.USER_ERROR) |
| 34 | if not events: |
| 35 | print("❌ --events must include at least one event type.", file=sys.stderr) |
| 36 | raise SystemExit(ExitCode.USER_ERROR) |
| 37 | |
| 38 | elapsed = start_timer() |
| 39 | hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args)) |
| 40 | repo_id = _resolve_repo_id(hub_url, identity) |
| 41 | |
| 42 | data = _hub_api( |
| 43 | hub_url, identity, "POST", |
| 44 | f"/api/repos/{repo_id}/webhooks", |
| 45 | body={"url": url, "events": events, "secret": secret}, |
| 46 | ) |
| 47 | |
| 48 | if json_output: |
| 49 | print(json.dumps({**make_envelope(elapsed), **data})) |
| 50 | return |
| 51 | |
| 52 | webhook_id = data.get("webhookId", data.get("webhook_id", "")) |
| 53 | print(f"✅ Webhook registered: {webhook_id}", file=sys.stderr) |
| 54 | print(f" URL: {url}", file=sys.stderr) |
| 55 | print(f" Events: {', '.join(events)}", file=sys.stderr) |
| 56 | |
| 57 | def run_webhook_list(args: argparse.Namespace) -> None: |
| 58 | """List all webhook subscriptions for a repository. |
| 59 | |
| 60 | Authentication required. |
| 61 | |
| 62 | JSON output is the raw API response merged with the standard envelope. |
| 63 | Human-readable text goes to stdout for easy piping. |
| 64 | |
| 65 | Agent quickstart |
| 66 | ---------------- |
| 67 | :: |
| 68 | |
| 69 | muse hub webhook list --json |
| 70 | muse hub webhook list --json | jq '.webhooks[].webhookId' |
| 71 | |
| 72 | Exit codes |
| 73 | ---------- |
| 74 | 0 Success. |
| 75 | 1 Auth error. |
| 76 | 2 Not inside a Muse repository. |
| 77 | 3 API error. |
| 78 | """ |
| 79 | json_output: bool = args.json_output |
| 80 | elapsed = start_timer() |
| 81 | hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args)) |
| 82 | repo_id = _resolve_repo_id(hub_url, identity) |
| 83 | |
| 84 | data = _hub_api(hub_url, identity, "GET", f"/api/repos/{repo_id}/webhooks") |
| 85 | |
| 86 | if json_output: |
| 87 | print(json.dumps({**make_envelope(elapsed), **data})) |
| 88 | return |
| 89 | |
| 90 | webhooks = data.get("webhooks", []) |
| 91 | if not webhooks: |
| 92 | print("No webhooks registered.", file=sys.stderr) |
| 93 | return |
| 94 | for wh in webhooks: |
| 95 | wid = wh.get("webhookId", wh.get("webhook_id", "")) |
| 96 | wurl = wh.get("url", "") |
| 97 | evts = ", ".join(wh.get("events", [])) |
| 98 | print(f" {wid} {wurl} [{evts}]") |
| 99 | |
| 100 | def run_webhook_delete(args: argparse.Namespace) -> None: |
| 101 | """Delete a webhook subscription and all its delivery history. |
| 102 | |
| 103 | Requires write/admin access or repo ownership. |
| 104 | |
| 105 | JSON output includes envelope fields plus ``deleted`` (bool) and |
| 106 | ``webhook_id`` (str). |
| 107 | |
| 108 | Agent quickstart |
| 109 | ---------------- |
| 110 | :: |
| 111 | |
| 112 | muse hub webhook delete <webhook-id> --json |
| 113 | # → {"muse_version": "...", ..., "deleted": true, "webhook_id": "..."} |
| 114 | |
| 115 | Exit codes |
| 116 | ---------- |
| 117 | 0 Webhook deleted. |
| 118 | 1 Auth or not-found error. |
| 119 | 2 Not inside a Muse repository. |
| 120 | 3 API error. |
| 121 | """ |
| 122 | webhook_id: str = args.webhook_id |
| 123 | json_output: bool = args.json_output |
| 124 | |
| 125 | elapsed = start_timer() |
| 126 | hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args)) |
| 127 | repo_id = _resolve_repo_id(hub_url, identity) |
| 128 | |
| 129 | _hub_api(hub_url, identity, "DELETE", f"/api/repos/{repo_id}/webhooks/{webhook_id}") |
| 130 | |
| 131 | if json_output: |
| 132 | print(json.dumps({**make_envelope(elapsed), **{"deleted": True, "webhook_id": webhook_id}})) |
| 133 | return |
| 134 | |
| 135 | print(f"✅ Webhook {webhook_id} deleted.", file=sys.stderr) |
| 136 | |
| 137 | def run_webhook_delivery_list(args: argparse.Namespace) -> None: |
| 138 | """List recent deliveries for a webhook on MuseHub. |
| 139 | |
| 140 | JSON output is the raw API response merged with the standard envelope. |
| 141 | Human-readable text goes to stderr. |
| 142 | |
| 143 | Agent quickstart |
| 144 | ---------------- |
| 145 | :: |
| 146 | |
| 147 | muse hub webhook delivery-list --webhook-id <id> --json |
| 148 | muse hub webhook delivery-list --webhook-id <id> --limit 20 --json | jq '.deliveries[].status' |
| 149 | |
| 150 | JSON output keys (from hub): ``deliveries`` (list), ``nextCursor``, ``total``. |
| 151 | Each delivery: ``deliveryId``, ``event``, ``status``, ``statusCode``, |
| 152 | ``duration``, ``createdAt``. |
| 153 | |
| 154 | Exit codes |
| 155 | ---------- |
| 156 | 0 Success. |
| 157 | 1 Auth error. |
| 158 | 2 Not inside a Muse repository. |
| 159 | 3 API error. |
| 160 | """ |
| 161 | elapsed = start_timer() |
| 162 | hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args)) |
| 163 | repo_id = _resolve_repo_id(hub_url, identity) |
| 164 | |
| 165 | params: dict[str, str | int] = {"limit": args.limit} |
| 166 | data = _hub_api(hub_url, identity, "GET", |
| 167 | f"/api/repos/{repo_id}/webhooks/{args.webhook_id}/deliveries", |
| 168 | params=params) |
| 169 | |
| 170 | if args.json_output: |
| 171 | print(json.dumps({**make_envelope(elapsed), **data})) |
| 172 | return |
| 173 | |
| 174 | deliveries = data.get("deliveries", []) |
| 175 | if not deliveries: |
| 176 | print(" No deliveries found.", file=sys.stderr) |
| 177 | return |
| 178 | |
| 179 | print(f"\n Deliveries for webhook {args.webhook_id} ({len(deliveries)} shown)", file=sys.stderr) |
| 180 | print(f" {'─' * 60}", file=sys.stderr) |
| 181 | for d in deliveries: |
| 182 | status = d.get("status", "unknown") |
| 183 | code = d.get("statusCode", "") |
| 184 | event = d.get("event", "") |
| 185 | created = str(d.get("createdAt", ""))[:10] |
| 186 | print(f" [{created}] {event} {status} ({code})", file=sys.stderr) |
| 187 | |
| 188 | def run_webhook_redeliver(args: argparse.Namespace) -> None: |
| 189 | """Re-send a previously attempted webhook delivery. |
| 190 | |
| 191 | JSON output is the raw API response merged with the standard envelope. |
| 192 | Human-readable text goes to stderr. |
| 193 | |
| 194 | Agent quickstart |
| 195 | ---------------- |
| 196 | :: |
| 197 | |
| 198 | muse hub webhook redeliver --webhook-id <id> --delivery-id <id> --json |
| 199 | # → {"muse_version": "...", ..., "deliveryId": "...", "redelivered": true} |
| 200 | |
| 201 | JSON output keys (from hub): ``deliveryId``, ``redelivered``, ``status``. |
| 202 | |
| 203 | Exit codes |
| 204 | ---------- |
| 205 | 0 Redelivery triggered. |
| 206 | 1 Auth error. |
| 207 | 2 Not inside a Muse repository. |
| 208 | 3 API error. |
| 209 | """ |
| 210 | elapsed = start_timer() |
| 211 | hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args)) |
| 212 | repo_id = _resolve_repo_id(hub_url, identity) |
| 213 | |
| 214 | data = _hub_api(hub_url, identity, "POST", |
| 215 | f"/api/repos/{repo_id}/webhooks/{args.webhook_id}/deliveries/{args.delivery_id}/redeliver") |
| 216 | |
| 217 | if args.json_output: |
| 218 | print(json.dumps({**make_envelope(elapsed), **data})) |
| 219 | return |
| 220 | |
| 221 | print(f"✅ Delivery {args.delivery_id} redelivered.", file=sys.stderr) |
| 222 | |
| 223 | def register(subs: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None: |
| 224 | """Register webhooks subcommands.""" |
| 225 | # ── webhook ─────────────────────────────────────────────────────────────── |
| 226 | webhook_p = subs.add_parser( |
| 227 | "webhook", |
| 228 | help="Manage webhook subscriptions on MuseHub.", |
| 229 | formatter_class=argparse.RawDescriptionHelpFormatter, |
| 230 | ) |
| 231 | webhook_subs = webhook_p.add_subparsers(dest="webhook_subcommand", metavar="WEBHOOK_COMMAND") |
| 232 | webhook_subs.required = True |
| 233 | |
| 234 | webhook_create_p = webhook_subs.add_parser( |
| 235 | "create", |
| 236 | help="Register a new webhook subscription.", |
| 237 | description=( |
| 238 | "Register a webhook that receives HTTP POST payloads for repository events.\n" |
| 239 | "Valid event types: push, proposal, issue, release, branch, tag, session, analysis.\n" |
| 240 | "Requires write/admin access or repo ownership.\n\n" |
| 241 | "Agent quickstart:\n" |
| 242 | " muse hub webhook create --url https://example.com/hook --events push release --json\n\n" |
| 243 | "Exit codes: 0 success, 1 validation/auth error, 2 not in repo, 3 API error." |
| 244 | ), |
| 245 | formatter_class=argparse.RawDescriptionHelpFormatter, |
| 246 | ) |
| 247 | webhook_create_p.add_argument( |
| 248 | "--url", required=True, help="HTTPS URL to receive event payloads.", |
| 249 | ) |
| 250 | webhook_create_p.add_argument( |
| 251 | "--events", nargs="+", required=True, metavar="EVENT", |
| 252 | help="Event types to subscribe to (e.g. push release issue).", |
| 253 | ) |
| 254 | webhook_create_p.add_argument( |
| 255 | "--secret", default="", help="HMAC-SHA256 signing secret (optional, plaintext).", |
| 256 | ) |
| 257 | webhook_create_p.add_argument( |
| 258 | "--hub", dest="hub", default=None, metavar="URL", |
| 259 | help="Override the hub URL from config.", |
| 260 | ) |
| 261 | webhook_create_p.add_argument( |
| 262 | "--repo", dest="repo", default=None, metavar="OWNER/REPO", |
| 263 | help="Specify repo as owner/repo.", |
| 264 | ) |
| 265 | webhook_create_p.add_argument( |
| 266 | "--json", "-j", action="store_true", dest="json_output", |
| 267 | help="Emit JSON webhook record on success.", |
| 268 | ) |
| 269 | webhook_create_p.set_defaults(func=run_webhook_create) |
| 270 | |
| 271 | webhook_list_p = webhook_subs.add_parser( |
| 272 | "list", |
| 273 | help="List webhook subscriptions for a repo.", |
| 274 | description=( |
| 275 | "List all registered webhook subscriptions for a repository.\n\n" |
| 276 | "Agent quickstart:\n" |
| 277 | " muse hub webhook list --json\n\n" |
| 278 | "Exit codes: 0 success, 1 auth error, 2 not in repo, 3 API error." |
| 279 | ), |
| 280 | formatter_class=argparse.RawDescriptionHelpFormatter, |
| 281 | ) |
| 282 | webhook_list_p.add_argument( |
| 283 | "--hub", dest="hub", default=None, metavar="URL", |
| 284 | help="Override the hub URL from config.", |
| 285 | ) |
| 286 | webhook_list_p.add_argument( |
| 287 | "--repo", dest="repo", default=None, metavar="OWNER/REPO", |
| 288 | help="Specify repo as owner/repo.", |
| 289 | ) |
| 290 | webhook_list_p.add_argument( |
| 291 | "--json", "-j", action="store_true", dest="json_output", |
| 292 | help="Emit JSON list of webhooks.", |
| 293 | ) |
| 294 | webhook_list_p.set_defaults(func=run_webhook_list) |
| 295 | |
| 296 | webhook_delete_p = webhook_subs.add_parser( |
| 297 | "delete", |
| 298 | help="Delete a webhook subscription.", |
| 299 | description=( |
| 300 | "Delete a webhook subscription and all its delivery history.\n" |
| 301 | "Requires write/admin access or repo ownership.\n\n" |
| 302 | "Agent quickstart:\n" |
| 303 | " muse hub webhook delete <webhook-id> --json\n\n" |
| 304 | "Exit codes: 0 success, 1 auth/not-found error, 2 not in repo, 3 API error." |
| 305 | ), |
| 306 | formatter_class=argparse.RawDescriptionHelpFormatter, |
| 307 | ) |
| 308 | webhook_delete_p.add_argument("webhook_id", help="ID of the webhook to delete.") |
| 309 | webhook_delete_p.add_argument( |
| 310 | "--hub", dest="hub", default=None, metavar="URL", |
| 311 | help="Override the hub URL from config.", |
| 312 | ) |
| 313 | webhook_delete_p.add_argument( |
| 314 | "--repo", dest="repo", default=None, metavar="OWNER/REPO", |
| 315 | help="Specify repo as owner/repo.", |
| 316 | ) |
| 317 | webhook_delete_p.add_argument( |
| 318 | "--json", "-j", action="store_true", dest="json_output", |
| 319 | help="Emit JSON confirmation on success.", |
| 320 | ) |
| 321 | webhook_delete_p.set_defaults(func=run_webhook_delete) |
| 322 | |
| 323 | webhook_p.set_defaults(func=lambda a: webhook_p.print_help()) |
| 324 | |
| 325 | # ── webhook deliveries ──────────────────────────────────────────────────── |
| 326 | # Extend existing webhook subparser with delivery commands |
| 327 | webhook_delivery_list_p = webhook_subs.add_parser( |
| 328 | "delivery-list", help="List webhook deliveries.", |
| 329 | description=( |
| 330 | "List recent deliveries for a webhook.\n\n" |
| 331 | " muse hub webhook delivery-list --webhook-id <id>\n" |
| 332 | " muse hub webhook delivery-list --webhook-id <id> --limit 20 --json\n\n" |
| 333 | "Exit codes: 0 success, 1 auth error, 2 not in repo, 3 API error." |
| 334 | ), |
| 335 | formatter_class=argparse.RawDescriptionHelpFormatter, |
| 336 | ) |
| 337 | webhook_delivery_list_p.add_argument("--hub", dest="hub", default=None, metavar="URL", |
| 338 | help="Override the hub URL from config.") |
| 339 | webhook_delivery_list_p.add_argument("--repo", dest="repo", default=None, metavar="OWNER/REPO", |
| 340 | help="Specify repo as owner/repo.") |
| 341 | webhook_delivery_list_p.add_argument("--webhook-id", dest="webhook_id", required=True, |
| 342 | help="ID of the webhook.") |
| 343 | webhook_delivery_list_p.add_argument("--limit", type=int, default=20, metavar="N", |
| 344 | help="Max deliveries to return (default 20).") |
| 345 | webhook_delivery_list_p.add_argument("--json", "-j", action="store_true", dest="json_output", |
| 346 | help="Emit JSON output.") |
| 347 | webhook_delivery_list_p.set_defaults(func=run_webhook_delivery_list) |
| 348 | |
| 349 | webhook_redeliver_p = webhook_subs.add_parser( |
| 350 | "redeliver", help="Redeliver a webhook delivery.", |
| 351 | description=( |
| 352 | "Re-send a previously attempted webhook delivery.\n\n" |
| 353 | " muse hub webhook redeliver --webhook-id <id> --delivery-id <id>\n" |
| 354 | " muse hub webhook redeliver --webhook-id <id> --delivery-id <id> --json\n\n" |
| 355 | "Exit codes: 0 success, 1 auth error, 2 not in repo, 3 API error." |
| 356 | ), |
| 357 | formatter_class=argparse.RawDescriptionHelpFormatter, |
| 358 | ) |
| 359 | webhook_redeliver_p.add_argument("--hub", dest="hub", default=None, metavar="URL", |
| 360 | help="Override the hub URL from config.") |
| 361 | webhook_redeliver_p.add_argument("--repo", dest="repo", default=None, metavar="OWNER/REPO", |
| 362 | help="Specify repo as owner/repo.") |
| 363 | webhook_redeliver_p.add_argument("--webhook-id", dest="webhook_id", required=True, |
| 364 | help="ID of the webhook.") |
| 365 | webhook_redeliver_p.add_argument("--delivery-id", dest="delivery_id", required=True, |
| 366 | help="ID of the delivery to redeliver.") |
| 367 | webhook_redeliver_p.add_argument("--json", "-j", action="store_true", dest="json_output", |
| 368 | help="Emit JSON output.") |
| 369 | webhook_redeliver_p.set_defaults(func=run_webhook_redeliver) |
| 370 |
File History
1 commit
sha256:e6465e8a9b7fa8e6223ed4a3576e96c568c913ae2caeb9c31f15e7a81b250b40
docs: add | jq convention to --json section of agent-guide
Sonnet 4.6
1 day ago