@misterhuydo/sentinel 1.4.66 → 1.4.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1008,6 +1008,48 @@ _TOOLS = [
1008
1008
  "required": ["tool_name"],
1009
1009
  },
1010
1010
  },
1011
+ {
1012
+ "name": "manage_release",
1013
+ "description": (
1014
+ "Plan and execute repository operations: build, Maven release, dependency updates, "
1015
+ "or a full release-and-cascade (release one repo then bump its version in all dependents). "
1016
+ "Always presents a confirmation plan before acting. "
1017
+ "Examples: 'build Whydah-TypeLib', 'release Whydah-TypeLib', "
1018
+ "'update Whydah-Java-SDK to use the latest Whydah-TypeLib', "
1019
+ "'trigger a release for Whydah-TypeLib and update all dependents', "
1020
+ "'update all repos to use the latest whydah-typelib release'"
1021
+ ),
1022
+ "input_schema": {
1023
+ "type": "object",
1024
+ "properties": {
1025
+ "operation": {
1026
+ "type": "string",
1027
+ "enum": ["build", "release", "update_deps", "release_and_cascade"],
1028
+ "description": (
1029
+ "build: trigger Jenkins build only. "
1030
+ "release: trigger Maven Release; auto-cascades if AUTO_PUBLISH=true. "
1031
+ "update_deps: update dependency version in target repos (source already released). "
1032
+ "release_and_cascade: release source_repo then cascade to all dependents."
1033
+ ),
1034
+ },
1035
+ "source_repo": {
1036
+ "type": "string",
1037
+ "description": "Repo to build/release, or the repo whose artifact is being updated.",
1038
+ },
1039
+ "target_repos": {
1040
+ "type": "array",
1041
+ "items": {"type": "string"},
1042
+ "description": "Repos to update. Empty = auto-detect all dependents.",
1043
+ },
1044
+ "confirmed": {
1045
+ "type": "boolean",
1046
+ "description": "false = show plan and ask for confirmation. true = execute.",
1047
+ "default": False,
1048
+ },
1049
+ },
1050
+ "required": ["operation", "source_repo"],
1051
+ },
1052
+ },
1011
1053
  {
1012
1054
  "name": "set_maintenance",
1013
1055
  "description": (
@@ -2329,7 +2371,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2329
2371
  return json.dumps({"error": "cannot determine user — not clearing"})
2330
2372
 
2331
2373
  # ── Admin-only tools ──────────────────────────────────────────────────────
2332
- _ADMIN_TOOLS = {"list_all_users", "clear_user_history", "reset_fingerprint", "list_all_errors", "export_db", "merge_pr", "install_tool"}
2374
+ _ADMIN_TOOLS = {"list_all_users", "clear_user_history", "reset_fingerprint", "list_all_errors", "export_db", "merge_pr", "install_tool", "manage_release"}
2333
2375
  if name in _ADMIN_TOOLS:
2334
2376
  if not is_admin:
2335
2377
  return json.dumps({"error": "Admin access required. You are not in SLACK_ADMIN_USERS."})
@@ -2554,6 +2596,126 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2554
2596
  except _sp.TimeoutExpired:
2555
2597
  return json.dumps({"error": f"Install timed out for '{tool_name}'"})
2556
2598
 
2599
+
2600
+ if name == "manage_release":
2601
+ from .dependency_manager import plan_cascade, execute_cascade, get_artifact_id, get_release_version
2602
+ from .cicd_trigger import trigger as cicd_trigger, _trigger_jenkins, _trigger_jenkins_release
2603
+ from .notify import notify_cascade_started, notify_cascade_result
2604
+
2605
+ operation = inputs.get("operation", "").strip()
2606
+ source_repo = inputs.get("source_repo", "").strip()
2607
+ target_repos = inputs.get("target_repos") or []
2608
+ confirmed = bool(inputs.get("confirmed", False))
2609
+
2610
+ repo = cfg_loader.repos.get(source_repo)
2611
+ if not repo:
2612
+ # fuzzy match
2613
+ for rname in cfg_loader.repos:
2614
+ if source_repo.lower() in rname.lower():
2615
+ repo = cfg_loader.repos[rname]
2616
+ source_repo = rname
2617
+ break
2618
+ if not repo:
2619
+ return json.dumps({"error": f"Repo not found: {source_repo}. Known repos: {list(cfg_loader.repos.keys())}"})
2620
+
2621
+ # ── Plan phase (confirmed=false) ──────────────────────────────────────
2622
+ if not confirmed:
2623
+ if operation == "build":
2624
+ return json.dumps({
2625
+ "plan": f"Trigger Jenkins build for {source_repo}",
2626
+ "job_url": repo.cicd_job_url,
2627
+ "note": "This triggers a regular build, not a release.",
2628
+ "confirm_prompt": "Reply with confirmed=true to proceed.",
2629
+ })
2630
+
2631
+ if operation in ("release", "release_and_cascade"):
2632
+ cascade_plan = plan_cascade(source_repo, cfg_loader.repos, target_repos or None)
2633
+ if "error" in cascade_plan:
2634
+ return json.dumps(cascade_plan)
2635
+ cascade_note = (
2636
+ f"After release, will update {len(cascade_plan['dependents'])} dependent repo(s)."
2637
+ if cascade_plan["dependents"] else "No dependent repos found in config."
2638
+ )
2639
+ return json.dumps({
2640
+ "plan": f"Trigger Maven Release for {source_repo}",
2641
+ "release_version": cascade_plan["new_version"],
2642
+ "dev_version_after": cascade_plan.get("new_version", ""),
2643
+ "job_url": repo.cicd_job_url,
2644
+ "cascade": cascade_plan["dependents"],
2645
+ "cascade_note": cascade_note,
2646
+ "auto_publish_source": repo.auto_publish,
2647
+ "confirm_prompt": "Reply with confirmed=true to proceed.",
2648
+ })
2649
+
2650
+ if operation == "update_deps":
2651
+ cascade_plan = plan_cascade(source_repo, cfg_loader.repos, target_repos or None)
2652
+ if "error" in cascade_plan:
2653
+ return json.dumps(cascade_plan)
2654
+ if not cascade_plan["dependents"]:
2655
+ return json.dumps({"note": f"No repos depend on {cascade_plan['artifact_id']} — nothing to update."})
2656
+ return json.dumps({
2657
+ "plan": f"Update {cascade_plan['artifact_id']} to {cascade_plan['new_version']} in dependent repos",
2658
+ "artifact_id": cascade_plan["artifact_id"],
2659
+ "new_version": cascade_plan["new_version"],
2660
+ "targets": cascade_plan["dependents"],
2661
+ "confirm_prompt": "Reply with confirmed=true to proceed.",
2662
+ })
2663
+
2664
+ # ── Execute phase (confirmed=true) ────────────────────────────────────
2665
+ if operation == "build":
2666
+ success = _trigger_jenkins(repo)
2667
+ logger.info("Boss manage_release: build triggered for %s by %s", source_repo, user_id)
2668
+ return json.dumps({"status": "triggered" if success else "failed", "repo": source_repo, "job_url": repo.cicd_job_url})
2669
+
2670
+ if operation in ("release", "release_and_cascade"):
2671
+ success = _trigger_jenkins_release(repo)
2672
+ logger.info("Boss manage_release: release triggered for %s by %s (success=%s)", source_repo, user_id, success)
2673
+ if not success:
2674
+ return json.dumps({"status": "failed", "repo": source_repo, "error": "Jenkins release trigger failed — check logs"})
2675
+
2676
+ # Cascade immediately if release_and_cascade, or if AUTO_PUBLISH=true
2677
+ do_cascade = (operation == "release_and_cascade") or repo.auto_publish
2678
+ if do_cascade:
2679
+ artifact_id = get_artifact_id(repo.local_path)
2680
+ new_version = get_release_version(repo.local_path)
2681
+ if artifact_id and new_version:
2682
+ target_names = target_repos or None
2683
+ cascade_plan = plan_cascade(source_repo, cfg_loader.repos, target_names)
2684
+ target_repo_names = [d["repo"] for d in cascade_plan.get("dependents", [])]
2685
+ if target_repo_names:
2686
+ notify_cascade_started(cfg_loader.sentinel, artifact_id, new_version, target_repo_names, user_id)
2687
+ results = execute_cascade(source_repo, new_version, artifact_id, cfg_loader.repos, cfg_loader.sentinel, target_names)
2688
+ notify_cascade_result(cfg_loader.sentinel, artifact_id, new_version, results, user_id)
2689
+ return json.dumps({
2690
+ "status": "released_and_cascaded",
2691
+ "repo": source_repo,
2692
+ "version": new_version,
2693
+ "cascade": [{"repo": r.repo_name, "success": r.success, "pr_url": r.pr_url, "error": r.error} for r in results],
2694
+ })
2695
+ return json.dumps({"status": "released", "repo": source_repo, "note": "Cascade will run automatically on next poll if AUTO_PUBLISH=true."})
2696
+
2697
+ if operation == "update_deps":
2698
+ artifact_id = get_artifact_id(repo.local_path)
2699
+ new_version = get_release_version(repo.local_path)
2700
+ if not artifact_id or not new_version:
2701
+ return json.dumps({"error": f"Could not read artifact/version from {source_repo}/pom.xml"})
2702
+ target_names = target_repos or None
2703
+ cascade_plan = plan_cascade(source_repo, cfg_loader.repos, target_names)
2704
+ target_repo_names = [d["repo"] for d in cascade_plan.get("dependents", [])]
2705
+ if not target_repo_names:
2706
+ return json.dumps({"note": f"No repos depend on {artifact_id} — nothing to update."})
2707
+ notify_cascade_started(cfg_loader.sentinel, artifact_id, new_version, target_repo_names, user_id)
2708
+ results = execute_cascade(source_repo, new_version, artifact_id, cfg_loader.repos, cfg_loader.sentinel, target_names)
2709
+ notify_cascade_result(cfg_loader.sentinel, artifact_id, new_version, results, user_id)
2710
+ return json.dumps({
2711
+ "status": "updated",
2712
+ "artifact_id": artifact_id,
2713
+ "version": new_version,
2714
+ "results": [{"repo": r.repo_name, "success": r.success, "pr_url": r.pr_url, "error": r.error} for r in results],
2715
+ })
2716
+
2717
+ return json.dumps({"error": f"Unknown operation: {operation}"})
2718
+
2557
2719
  return json.dumps({"error": f"unknown tool: {name}"})
2558
2720
 
2559
2721