gabriel / muse public
webhooks.py python
370 lines 14.5 KB
Raw
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