@misterhuydo/sentinel 1.6.7 → 1.6.9

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/.cairn/.hint-lock CHANGED
@@ -1 +1 @@
1
- 2026-04-24T13:18:30.501Z
1
+ 2026-04-24T14:02:31.712Z
@@ -10,11 +10,5 @@
10
10
  "state": "edit-ready",
11
11
  "minifiedAt": 1774252437350.0059,
12
12
  "readCount": 1
13
- },
14
- "J:\\Projects\\Sentinel\\cli\\python\\sentinel\\cicd_trigger.py": {
15
- "tempPath": "J:\\Projects\\Sentinel\\cli\\.cairn\\views\\7802b9_cicd_trigger.py",
16
- "state": "compressed",
17
- "minifiedAt": 1774523631399.9514,
18
- "readCount": 1
19
13
  }
20
14
  }
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-04-24T13:27:50.750Z",
3
- "checkpoint_at": "2026-04-24T13:27:50.751Z",
2
+ "message": "Auto-checkpoint at 2026-04-24T14:25:27.008Z",
3
+ "checkpoint_at": "2026-04-24T14:25:27.009Z",
4
4
  "active_files": [
5
5
  "J:\\Projects\\Sentinel\\cli\\bin\\sentinel.js",
6
6
  "J:\\Projects\\Sentinel\\cli\\lib\\test.js",
@@ -187,6 +187,6 @@
187
187
  "mtime_snapshot": {
188
188
  "J:\\Projects\\Sentinel\\cli\\bin\\sentinel.js": 1774252515044.4768,
189
189
  "J:\\Projects\\Sentinel\\cli\\lib\\test.js": 1774252437350.0059,
190
- "J:\\Projects\\Sentinel\\cli\\python\\sentinel\\cicd_trigger.py": 1774523631399.9514
190
+ "J:\\Projects\\Sentinel\\cli\\python\\sentinel\\cicd_trigger.py": 1777039448643.5408
191
191
  }
192
192
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.6.7",
3
+ "version": "1.6.9",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -1 +1 @@
1
- __version__ = "1.6.7"
1
+ __version__ = "1.6.9"
@@ -40,7 +40,12 @@ def trigger(repo: RepoConfig, store: StateStore, fingerprint: str, wait: bool =
40
40
 
41
41
  def _trigger_jenkins(repo: RepoConfig) -> bool:
42
42
  url = f"{repo.cicd_job_url.rstrip('/')}/build"
43
- resp = requests.post(url, auth=("sentinel", repo.cicd_token), timeout=15)
43
+ # Use repo's CICD_USER if set; fall back to literal "sentinel". Hardcoding
44
+ # the user (as this previously did) caused 401s for every repo whose Jenkins
45
+ # API token was issued under a non-"sentinel" username.
46
+ resp = requests.post(
47
+ url, auth=(repo.cicd_user or "sentinel", repo.cicd_token), timeout=15,
48
+ )
44
49
  success = resp.status_code in (200, 201, 204)
45
50
  if success:
46
51
  logger.info("Jenkins build triggered: %s", repo.cicd_job_url)
@@ -605,15 +605,18 @@ def generate_fix(
605
605
  slack_alert(cfg.slack_bot_token, cfg.slack_channel, msg)
606
606
  return "error", None, ""
607
607
 
608
+ # Parse the JSON envelope: get session_id, cost, and the actual result text.
609
+ parsed = _parse_claude_json(raw_output)
610
+
611
+ # Pass empty when not is_error — the auth-signal regex would otherwise
612
+ # match trigger words ("authentication", "login", "billing", ...) inside
613
+ # successful diff content.
608
614
  alert_if_rate_limited(
609
615
  cfg.slack_bot_token,
610
616
  cfg.slack_channel,
611
617
  source=f"fix_engine/{event.fingerprint}",
612
- output=raw_output,
618
+ output=raw_output if parsed["is_error"] else "",
613
619
  )
614
-
615
- # Parse the JSON envelope: get session_id, cost, and the actual result text.
616
- parsed = _parse_claude_json(raw_output)
617
620
  if parsed["is_error"] and not parsed["result"]:
618
621
  # Fall back to legacy text parsing if JSON decode failed completely.
619
622
  # (Older Claude CLI versions may not emit JSON; --output-format flag is ignored.)
@@ -479,7 +479,7 @@ def _run_tests(repo: RepoConfig, local_path: str) -> bool:
479
479
 
480
480
  logger.info("Running tests for %s: %s", repo.repo_name, " ".join(cmd))
481
481
  try:
482
- r = subprocess.run(cmd, cwd=local_path, capture_output=True, text=True, timeout=300)
482
+ r = subprocess.run(cmd, cwd=local_path, capture_output=True, text=True, timeout=900)
483
483
  except FileNotFoundError:
484
484
  raise MissingToolError(cmd[0])
485
485
  if r.returncode != 0:
@@ -606,6 +606,15 @@ async def _handle_issue(event: IssueEvent, cfg_loader: ConfigLoader, store: Stat
606
606
  mark_done(event.issue_file) # archive so it doesn't re-prompt every poll
607
607
  return
608
608
 
609
+ # Per-project lock — serialise issue processing within a project so two
610
+ # concurrent claude sessions never race on the working tree of any repo.
611
+ async with _project_lock(sentinel.project_name or "_default"):
612
+ return await _handle_issue_locked(event, repo, cfg_loader, store)
613
+
614
+
615
+ async def _handle_issue_locked(event, repo, cfg_loader, store):
616
+ """Heavy work portion of _handle_issue — serialised per project via _project_lock."""
617
+ sentinel = cfg_loader.sentinel
609
618
  auto_commit = resolve_auto_commit(repo, sentinel)
610
619
  auto_release = resolve_auto_release(repo, sentinel)
611
620
 
@@ -1,171 +0,0 @@
1
- import logging
2
- import re
3
- import time
4
- from pathlib import Path
5
- import requests
6
- from .config_loader import RepoConfig
7
- from .state_store import StateStore
8
- logger = logging.getLogger(__name__)
9
- JENKINS_RELEASE_TIMEOUT = 900
10
- JENKINS_POLL_INTERVAL = 20
11
- def trigger(repo: RepoConfig, store: StateStore, fingerprint: str, wait: bool = False) -> bool:
12
- if not repo.cicd_type or not repo.cicd_job_url:
13
- return True
14
- cicd_type = repo.cicd_type.lower()
15
- if cicd_type == "jenkins":
16
- return _trigger_jenkins(repo)
17
- elif cicd_type in ("jenkins_release", "jenkins-release"):
18
- return _trigger_jenkins_release(repo, wait=wait)
19
- elif cicd_type in ("github_actions", "github-actions"):
20
- return _trigger_github_actions(repo, fingerprint)
21
- else:
22
- logger.warning("Unknown CICD_TYPE '%s' for %s", repo.cicd_type, repo.repo_name)
23
- return False
24
- def _trigger_jenkins(repo: RepoConfig) -> bool:
25
- url = f"{repo.cicd_job_url.rstrip('/')}/build"
26
- resp = requests.post(url, auth=("sentinel", repo.cicd_token), timeout=15)
27
- success = resp.status_code in (200, 201, 204)
28
- if success:
29
- logger.info("Jenkins build triggered: %s", repo.cicd_job_url)
30
- else:
31
- logger.error("Jenkins trigger failed (%s): %s", resp.status_code, resp.text[:200])
32
- return success
33
- def _get_last_build_number(session: requests.Session, job_url: str) -> int:
34
- try:
35
- r = session.get(f"{job_url.rstrip('/')}/api/json", timeout=10)
36
- data = r.json()
37
- lb = data.get("lastBuild")
38
- return lb["number"] if lb else 0
39
- except Exception as exc:
40
- logger.warning("Could not get last build number for %s: %s", job_url, exc)
41
- return 0
42
- def _wait_for_jenkins_build(
43
- session: requests.Session,
44
- job_url: str,
45
- prev_build_num: int,
46
- timeout: int = JENKINS_RELEASE_TIMEOUT,
47
- ) -> bool:
48
- deadline = time.time() + timeout
49
- build_num = None
50
- while time.time() < deadline:
51
- current = _get_last_build_number(session, job_url)
52
- if current > prev_build_num:
53
- build_num = current
54
- logger.info("Jenkins build
55
- break
56
- logger.debug("Waiting for Jenkins build to start (last=%d)...", current)
57
- time.sleep(JENKINS_POLL_INTERVAL)
58
- else:
59
- logger.error("Timed out waiting for Jenkins build to start: %s", job_url)
60
- return False
61
- build_api = f"{job_url.rstrip('/')}/{build_num}/api/json"
62
- while time.time() < deadline:
63
- try:
64
- r = session.get(build_api, timeout=10)
65
- data = r.json()
66
- if not data.get("building", True):
67
- result = data.get("result", "UNKNOWN")
68
- if result == "SUCCESS":
69
- logger.info("Jenkins build
70
- return True
71
- else:
72
- logger.error("Jenkins build
73
- return False
74
- elapsed = int(time.time() - (deadline - timeout))
75
- logger.info("Jenkins build
76
- except Exception as exc:
77
- logger.warning("Jenkins poll error for %s: %s", build_api, exc)
78
- time.sleep(JENKINS_POLL_INTERVAL)
79
- logger.error("Timed out waiting for Jenkins build
80
- return False
81
- def _trigger_jenkins_release(repo: RepoConfig, wait: bool = False) -> bool:
82
- release_ver, dev_ver = _maven_release_versions(repo.local_path)
83
- if not release_ver:
84
- logger.error("Jenkins release: could not determine release version from pom.xml")
85
- return False
86
- url = f"{repo.cicd_job_url.rstrip('/')}/m2release/submit"
87
- auth = (repo.cicd_user or "sentinel", repo.cicd_token)
88
- session = requests.Session()
89
- session.auth = auth
90
- from urllib.parse import urlparse
91
- parsed = urlparse(repo.cicd_job_url)
92
- jenkins_root = f"{parsed.scheme}://{parsed.netloc}"
93
- crumb_data = session.get(f"{jenkins_root}/crumbIssuer/api/json", timeout=10).json()
94
- crumb_header = {crumb_data["crumbRequestField"]: crumb_data["crumb"]}
95
- prev_build_num = _get_last_build_number(session, repo.cicd_job_url) if wait else 0
96
- scm_tag = f"{repo.repo_name}-{release_ver}"
97
- import json as _json
98
- stapler_json = _json.dumps({
99
- "releaseVersion": release_ver,
100
- "developmentVersion": dev_ver,
101
- "isDryRun": False,
102
- "specifyScmCredentials": False,
103
- "scmUsername": "",
104
- "scmCommentPrefix": "[maven-release-plugin] ",
105
- "appendHudsonUserName": False,
106
- "specifyScmTag": False,
107
- "scmTag": scm_tag,
108
- })
109
- resp = session.post(
110
- url,
111
- headers={
112
- **crumb_header,
113
- "Referer": f"{repo.cicd_job_url.rstrip('/')}/m2release/",
114
- },
115
- data={
116
- "releaseVersion": release_ver,
117
- "developmentVersion": dev_ver,
118
- "scmCommentPrefix": "[maven-release-plugin] ",
119
- "scmTag": scm_tag,
120
- "json": stapler_json,
121
- },
122
- timeout=15,
123
- allow_redirects=False,
124
- )
125
- triggered = resp.status_code in (200, 201, 204, 302)
126
- if not triggered:
127
- logger.error("Jenkins release trigger failed (%s): %s", resp.status_code, resp.text[:200])
128
- return False
129
- logger.info(
130
- "Jenkins Maven Release triggered: %s → %s (next: %s)%s",
131
- repo.cicd_job_url, release_ver, dev_ver,
132
- " — waiting for completion..." if wait else "",
133
- )
134
- if wait:
135
- return _wait_for_jenkins_build(session, repo.cicd_job_url, prev_build_num)
136
- return True
137
- def _maven_release_versions(local_path: str) -> tuple[str, str]:
138
- pom = Path(local_path) / "pom.xml"
139
- if not pom.exists():
140
- return ("", "")
141
- text = pom.read_text(encoding="utf-8", errors="ignore")
142
- m = re.search(r"<version>(\d+\.\d+\.\d+)-SNAPSHOT</version>", text)
143
- if not m:
144
- return ("", "")
145
- parts = m.group(1).split(".")
146
- release_ver = m.group(1)
147
- next_patch = int(parts[2]) + 1
148
- dev_ver = f"{parts[0]}.{parts[1]}.{next_patch}-SNAPSHOT"
149
- return release_ver, dev_ver
150
- def _trigger_github_actions(repo: RepoConfig, fingerprint: str) -> bool:
151
- owner_repo = _owner_repo(repo.repo_url)
152
- url = f"https://api.github.com/repos/{owner_repo}/dispatches"
153
- resp = requests.post(
154
- url,
155
- json={"event_type": "sentinel-deploy", "client_payload": {"fingerprint": fingerprint}},
156
- headers={
157
- "Authorization": f"Bearer {repo.cicd_token}",
158
- "Accept": "application/vnd.github+json",
159
- },
160
- timeout=15,
161
- )
162
- success = resp.status_code == 204
163
- if success:
164
- logger.info("GitHub Actions dispatch sent for %s", owner_repo)
165
- else:
166
- logger.error("GH Actions dispatch failed (%s): %s", resp.status_code, resp.text[:200])
167
- return success
168
- def _owner_repo(repo_url: str) -> str:
169
- if repo_url.startswith("git@"):
170
- return repo_url.split(":")[-1].removesuffix(".git")
171
- return "/".join(repo_url.rstrip("/").split("/")[-2:]).removesuffix(".git")