@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 +1 -1
- package/.cairn/minify-map.json +0 -6
- package/.cairn/session.json +3 -3
- package/package.json +1 -1
- package/python/sentinel/__init__.py +1 -1
- package/python/sentinel/cicd_trigger.py +6 -1
- package/python/sentinel/fix_engine.py +7 -4
- package/python/sentinel/git_manager.py +1 -1
- package/python/sentinel/main.py +9 -0
- package/.cairn/views/7802b9_cicd_trigger.py +0 -171
package/.cairn/.hint-lock
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-04-
|
|
1
|
+
2026-04-24T14:02:31.712Z
|
package/.cairn/minify-map.json
CHANGED
|
@@ -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
|
}
|
package/.cairn/session.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"message": "Auto-checkpoint at 2026-04-
|
|
3
|
-
"checkpoint_at": "2026-04-
|
|
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":
|
|
190
|
+
"J:\\Projects\\Sentinel\\cli\\python\\sentinel\\cicd_trigger.py": 1777039448643.5408
|
|
191
191
|
}
|
|
192
192
|
}
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.6.
|
|
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
|
-
|
|
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=
|
|
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:
|
package/python/sentinel/main.py
CHANGED
|
@@ -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")
|