@misterhuydo/sentinel 1.4.67 → 1.4.68

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.4.67",
3
+ "version": "1.4.68",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -985,6 +985,11 @@ _TOOLS = [
985
985
  "description": "Optional 8-char fingerprint to target a specific fix PR. "
986
986
  "If omitted, merges the most recent open PR for the repo.",
987
987
  },
988
+ "pr_number": {
989
+ "type": "integer",
990
+ "description": "Merge a specific PR by number (e.g. a Renovate PR). "
991
+ "When set, repo_name is still required but fingerprint is ignored.",
992
+ },
988
993
  },
989
994
  "required": ["repo_name"],
990
995
  },
@@ -1008,6 +1013,30 @@ _TOOLS = [
1008
1013
  "required": ["tool_name"],
1009
1014
  },
1010
1015
  },
1016
+ {
1017
+ "name": "list_renovate_prs",
1018
+ "description": (
1019
+ "List all open Renovate bot pull requests across all managed repos. "
1020
+ "Shows package name, version change, Renovate confidence, CI status, age, and merge-readiness. "
1021
+ "Use before deciding which Renovate PRs to merge. "
1022
+ "e.g. 'show renovate PRs', 'what dependency upgrades are pending?', "
1023
+ "'list open renovate pull requests', 'any renovate PRs ready to merge?'"
1024
+ ),
1025
+ "input_schema": {
1026
+ "type": "object",
1027
+ "properties": {
1028
+ "repo_name": {
1029
+ "type": "string",
1030
+ "description": "Filter to a specific repo. Omit to scan all managed repos.",
1031
+ },
1032
+ "ready_only": {
1033
+ "type": "boolean",
1034
+ "description": "If true, only show PRs where CI passes and there are no conflicts.",
1035
+ "default": False,
1036
+ },
1037
+ },
1038
+ },
1039
+ },
1011
1040
  {
1012
1041
  "name": "manage_release",
1013
1042
  "description": (
@@ -2597,6 +2626,120 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2597
2626
  return json.dumps({"error": f"Install timed out for '{tool_name}'"})
2598
2627
 
2599
2628
 
2629
+
2630
+ if name == "list_renovate_prs":
2631
+ import requests as _req
2632
+ from datetime import datetime, timezone
2633
+
2634
+ github_token = cfg_loader.sentinel.github_token
2635
+ if not github_token:
2636
+ return json.dumps({"error": "GITHUB_TOKEN not configured — cannot query GitHub API"})
2637
+
2638
+ filter_repo = inputs.get("repo_name", "").strip()
2639
+ ready_only = bool(inputs.get("ready_only", False))
2640
+ headers = {
2641
+ "Authorization": f"Bearer {github_token}",
2642
+ "Accept": "application/vnd.github+json",
2643
+ }
2644
+
2645
+ def _owner_repo_from_url(url):
2646
+ if url.startswith("git@"):
2647
+ return url.split(":")[-1].removesuffix(".git")
2648
+ return "/".join(url.rstrip("/").split("/")[-2:]).removesuffix(".git")
2649
+
2650
+ def _ci_status(owner_repo, sha):
2651
+ r = _req.get(
2652
+ f"https://api.github.com/repos/{owner_repo}/commits/{sha}/check-runs",
2653
+ headers=headers, params={"per_page": 20}, timeout=10,
2654
+ )
2655
+ if r.status_code != 200:
2656
+ return "unknown"
2657
+ runs = r.json().get("check_runs", [])
2658
+ if not runs:
2659
+ return "no checks"
2660
+ statuses = {run["conclusion"] for run in runs if run["status"] == "completed"}
2661
+ if "failure" in statuses or "cancelled" in statuses:
2662
+ return "failing"
2663
+ if all(s == "success" for s in statuses) and len(statuses) > 0:
2664
+ return "passing"
2665
+ return "pending"
2666
+
2667
+ def _parse_renovate_body(body):
2668
+ """Extract package change table and confidence from Renovate PR body."""
2669
+ if not body:
2670
+ return [], "unknown"
2671
+ # Confidence line: "| ... | Confidence |" table header, data follows
2672
+ conf = "unknown"
2673
+ conf_m = __import__("re").search(r"\| *(low|moderate|high|neutral|very high) *\|", body, __import__("re").IGNORECASE)
2674
+ if conf_m:
2675
+ conf = conf_m.group(1).lower()
2676
+ # Package table rows: | package | X.Y → A.B |
2677
+ changes = __import__("re").findall(r"\[([^\]]+)\].*?(\d+[\.\d]*)\s*[→\-]+\s*(\d+[\.\d]*)", body)
2678
+ pkgs = [{"package": p, "from": f, "to": t} for p, f, t in changes[:5]]
2679
+ return pkgs, conf
2680
+
2681
+ all_prs = []
2682
+ repos_to_scan = {
2683
+ name: repo for name, repo in cfg_loader.repos.items()
2684
+ if (not filter_repo) or (filter_repo.lower() in name.lower())
2685
+ }
2686
+
2687
+ for repo_name_k, repo in repos_to_scan.items():
2688
+ if not repo.repo_url:
2689
+ continue
2690
+ owner_repo = _owner_repo_from_url(repo.repo_url)
2691
+ try:
2692
+ r = _req.get(
2693
+ f"https://api.github.com/repos/{owner_repo}/pulls",
2694
+ headers=headers,
2695
+ params={"state": "open", "per_page": 50},
2696
+ timeout=15,
2697
+ )
2698
+ if r.status_code != 200:
2699
+ continue
2700
+ for pr in r.json():
2701
+ labels = [l["name"] for l in pr.get("labels", [])]
2702
+ if "renovate" not in labels:
2703
+ continue
2704
+ sha = pr["head"]["sha"]
2705
+ ci = _ci_status(owner_repo, sha)
2706
+ mergeable = pr.get("mergeable")
2707
+ conflict = (mergeable is False)
2708
+ pkgs, conf = _parse_renovate_body(pr.get("body", ""))
2709
+ created = pr.get("created_at", "")
2710
+ age_days = 0
2711
+ if created:
2712
+ try:
2713
+ dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
2714
+ age_days = (datetime.now(timezone.utc) - dt).days
2715
+ except Exception:
2716
+ pass
2717
+ ready = (ci == "passing" and not conflict)
2718
+ if ready_only and not ready:
2719
+ continue
2720
+ all_prs.append({
2721
+ "repo": repo_name_k,
2722
+ "pr_number": pr["number"],
2723
+ "title": pr["title"],
2724
+ "url": pr["html_url"],
2725
+ "packages": pkgs,
2726
+ "confidence": conf,
2727
+ "ci": ci,
2728
+ "conflict": conflict,
2729
+ "age_days": age_days,
2730
+ "ready_to_merge": ready,
2731
+ })
2732
+ except Exception as exc:
2733
+ logger.warning("list_renovate_prs: error scanning %s: %s", repo_name_k, exc)
2734
+
2735
+ all_prs.sort(key=lambda p: (0 if p["ready_to_merge"] else 1, p["repo"]))
2736
+ return json.dumps({
2737
+ "total": len(all_prs),
2738
+ "ready_count": sum(1 for p in all_prs if p["ready_to_merge"]),
2739
+ "prs": all_prs,
2740
+ "note": "Use merge_pr with repo_name + pr_number to merge a specific PR." if all_prs else "No open Renovate PRs found.",
2741
+ })
2742
+
2600
2743
  if name == "manage_release":
2601
2744
  from .dependency_manager import plan_cascade, execute_cascade, get_artifact_id, get_release_version
2602
2745
  from .cicd_trigger import trigger as cicd_trigger, _trigger_jenkins, _trigger_jenkins_release