@misterhuydo/sentinel 1.4.68 → 1.4.70
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/session.json +2 -2
- package/lib/.cairn/minify-map.json +3 -3
- package/lib/.cairn/views/fb78ac_upgrade.js +16 -1
- package/lib/add.js +30 -16
- package/lib/generate.js +6 -1
- package/package.json +1 -1
- package/python/scripts/fix_ask_codebase_context.py +249 -0
- package/python/scripts/fix_ask_codebase_stdin.py +49 -0
- package/python/scripts/fix_chain_slack.py +67 -0
- package/python/scripts/fix_fstring.py +51 -0
- package/python/scripts/fix_knowledge_cache.py +323 -0
- package/python/scripts/fix_knowledge_cache_staleness.py +294 -0
- package/python/scripts/fix_merge_confirm.py +295 -0
- package/python/scripts/fix_permission_messages.py +78 -0
- package/python/scripts/fix_pr_check_head_detect.py +84 -0
- package/python/scripts/fix_pr_msg_newlines.py +57 -0
- package/python/scripts/fix_pr_tracking_boss.py +265 -0
- package/python/scripts/fix_pr_tracking_db.py +212 -0
- package/python/scripts/fix_pr_tracking_main.py +174 -0
- package/python/scripts/fix_project_isolation.py +197 -0
- package/python/scripts/fix_system_prompt.py +444 -0
- package/python/scripts/fix_two_bugs.py +220 -0
- package/python/scripts/patch_chain_release.py +236 -0
- package/python/sentinel/cicd_trigger.py +125 -16
- package/python/sentinel/dependency_manager.py +129 -18
- package/python/sentinel/git_manager.py +46 -12
- package/python/sentinel/notify.py +34 -0
- package/python/sentinel/sentinel_boss.py +4139 -3326
|
@@ -33,12 +33,14 @@ class CascadeResult:
|
|
|
33
33
|
# ── pom.xml helpers ────────────────────────────────────────────────────────────
|
|
34
34
|
|
|
35
35
|
def get_artifact_id(local_path: str) -> str:
|
|
36
|
-
"""Return the root <artifactId> from pom.xml."""
|
|
36
|
+
"""Return the root <artifactId> from pom.xml (skips <parent> block)."""
|
|
37
37
|
pom = Path(local_path) / "pom.xml"
|
|
38
38
|
if not pom.exists():
|
|
39
39
|
return ""
|
|
40
40
|
text = pom.read_text(encoding="utf-8", errors="ignore")
|
|
41
|
-
|
|
41
|
+
# Strip <parent>...</parent> so we don't pick up the parent artifactId
|
|
42
|
+
text_no_parent = re.sub(r"<parent>.*?</parent>", "", text, flags=re.DOTALL)
|
|
43
|
+
m = re.search(r"<artifactId>([^<]+)</artifactId>", text_no_parent)
|
|
42
44
|
return m.group(1).strip() if m else ""
|
|
43
45
|
|
|
44
46
|
|
|
@@ -57,6 +59,17 @@ def get_release_version(local_path: str) -> str:
|
|
|
57
59
|
return get_pom_version(local_path).replace("-SNAPSHOT", "")
|
|
58
60
|
|
|
59
61
|
|
|
62
|
+
def _resolve_property(prop_ref: str, text: str) -> str:
|
|
63
|
+
"""Resolve a Maven ${property.name} reference from the <properties> section."""
|
|
64
|
+
if not prop_ref.startswith("${"):
|
|
65
|
+
return prop_ref
|
|
66
|
+
prop_name = prop_ref[2:-1] # strip ${ and }
|
|
67
|
+
m = re.search(
|
|
68
|
+
rf"<{re.escape(prop_name)}>([^<]+)</{re.escape(prop_name)}>", text
|
|
69
|
+
)
|
|
70
|
+
return m.group(1).strip() if m else prop_ref
|
|
71
|
+
|
|
72
|
+
|
|
60
73
|
def find_dependents(
|
|
61
74
|
artifact_id: str,
|
|
62
75
|
repos: dict,
|
|
@@ -75,18 +88,28 @@ def find_dependents(
|
|
|
75
88
|
if not pom.exists():
|
|
76
89
|
continue
|
|
77
90
|
text = pom.read_text(encoding="utf-8", errors="ignore")
|
|
78
|
-
|
|
79
|
-
block_m
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
# Iterate individual blocks to avoid cross-boundary matching
|
|
92
|
+
for block_m in re.finditer(r"<dependency>(.*?)</dependency>", text, re.DOTALL):
|
|
93
|
+
inner = block_m.group(1)
|
|
94
|
+
if not re.search(
|
|
95
|
+
rf"<artifactId>\s*{re.escape(artifact_id)}\s*</artifactId>", inner
|
|
96
|
+
):
|
|
97
|
+
continue
|
|
98
|
+
ver_m = re.search(r"<version>([^<]+)</version>", inner)
|
|
99
|
+
if ver_m:
|
|
100
|
+
raw = ver_m.group(1).strip()
|
|
101
|
+
current_ver = _resolve_property(raw, text)
|
|
102
|
+
else:
|
|
103
|
+
current_ver = "?"
|
|
83
104
|
results.append((repo, current_ver))
|
|
105
|
+
break
|
|
84
106
|
return sorted(results, key=lambda t: t[0].repo_name)
|
|
85
107
|
|
|
86
108
|
|
|
87
109
|
def update_dependency(local_path: str, artifact_id: str, new_version: str) -> bool:
|
|
88
110
|
"""
|
|
89
111
|
Update the <version> inside the <dependency> block for artifact_id in pom.xml.
|
|
112
|
+
Handles both direct versions and ${property} references (updates <properties> section).
|
|
90
113
|
Returns True if the file was changed.
|
|
91
114
|
"""
|
|
92
115
|
pom = Path(local_path) / "pom.xml"
|
|
@@ -94,13 +117,58 @@ def update_dependency(local_path: str, artifact_id: str, new_version: str) -> bo
|
|
|
94
117
|
return False
|
|
95
118
|
text = pom.read_text(encoding="utf-8", errors="ignore")
|
|
96
119
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
120
|
+
# First pass: find the target block and check for property reference
|
|
121
|
+
prop_name: str | None = None
|
|
122
|
+
found = False
|
|
123
|
+
for block_m in re.finditer(r"<dependency>(.*?)</dependency>", text, re.DOTALL):
|
|
124
|
+
inner = block_m.group(1)
|
|
125
|
+
if not re.search(
|
|
126
|
+
rf"<artifactId>\s*{re.escape(artifact_id)}\s*</artifactId>", inner
|
|
127
|
+
):
|
|
128
|
+
continue
|
|
129
|
+
found = True
|
|
130
|
+
ver_m = re.search(r"<version>\$\{([^}]+)\}</version>", inner)
|
|
131
|
+
if ver_m:
|
|
132
|
+
prop_name = ver_m.group(1)
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
if not found:
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
if prop_name:
|
|
139
|
+
# Update the property in <properties> section instead of the inline version
|
|
140
|
+
new_text, n = re.subn(
|
|
141
|
+
rf"(<{re.escape(prop_name)}>)[^<]+(</\s*{re.escape(prop_name)}>)",
|
|
142
|
+
rf"\g<1>{new_version}\2",
|
|
143
|
+
text,
|
|
144
|
+
)
|
|
145
|
+
if n == 0 or new_text == text:
|
|
146
|
+
logger.warning(
|
|
147
|
+
"update_dependency: property '%s' not found in <properties> for %s",
|
|
148
|
+
prop_name, artifact_id,
|
|
149
|
+
)
|
|
150
|
+
return False
|
|
151
|
+
else:
|
|
152
|
+
# Direct version — replace only inside the matching block, block by block
|
|
153
|
+
def _replace_block(m: re.Match) -> str:
|
|
154
|
+
inner = m.group(1)
|
|
155
|
+
if not re.search(
|
|
156
|
+
rf"<artifactId>\s*{re.escape(artifact_id)}\s*</artifactId>", inner
|
|
157
|
+
):
|
|
158
|
+
return m.group(0)
|
|
159
|
+
new_inner = re.sub(
|
|
160
|
+
r"<version>[^<]+</version>",
|
|
161
|
+
f"<version>{new_version}</version>",
|
|
162
|
+
inner,
|
|
163
|
+
count=1,
|
|
164
|
+
)
|
|
165
|
+
return f"<dependency>{new_inner}</dependency>"
|
|
166
|
+
|
|
167
|
+
new_text = re.sub(
|
|
168
|
+
r"<dependency>(.*?)</dependency>", _replace_block, text, flags=re.DOTALL
|
|
169
|
+
)
|
|
100
170
|
|
|
101
|
-
|
|
102
|
-
new_text, n = re.subn(pattern, _replace_version, text, flags=re.DOTALL)
|
|
103
|
-
if n == 0 or new_text == text:
|
|
171
|
+
if new_text == text:
|
|
104
172
|
return False
|
|
105
173
|
pom.write_text(new_text, encoding="utf-8")
|
|
106
174
|
return True
|
|
@@ -164,7 +232,8 @@ def execute_cascade(
|
|
|
164
232
|
Returns one CascadeResult per dependent repo attempted.
|
|
165
233
|
"""
|
|
166
234
|
# Import here to avoid circular imports
|
|
167
|
-
from .git_manager import commit_file_change, push_dep_update, open_dep_pr, _git_env
|
|
235
|
+
from .git_manager import commit_file_change, push_dep_update, open_dep_pr, _git_env, _git, maven_compile_check, MissingToolError # noqa: E501
|
|
236
|
+
from .notify import notify_cascade_build_failed
|
|
168
237
|
|
|
169
238
|
all_dependents = find_dependents(artifact_id, repos, skip_repo=source_repo_name)
|
|
170
239
|
if target_repo_names:
|
|
@@ -175,27 +244,69 @@ def execute_cascade(
|
|
|
175
244
|
for repo, old_version in all_dependents:
|
|
176
245
|
result = CascadeResult(repo_name=repo.repo_name, success=False, old_version=old_version, new_version=new_version)
|
|
177
246
|
try:
|
|
247
|
+
# Ensure clean working tree before pulling — a previous failed attempt
|
|
248
|
+
# may have left pom.xml dirty, which causes git pull --rebase to fail.
|
|
249
|
+
_git(["checkout", "pom.xml"], cwd=repo.local_path, env=_git_env(repo))
|
|
250
|
+
|
|
251
|
+
# Pull BEFORE modifying pom.xml — git pull --rebase fails on dirty working tree
|
|
252
|
+
pull_r = _git(["pull", "--rebase", "origin", repo.branch], cwd=repo.local_path, env=_git_env(repo))
|
|
253
|
+
if pull_r.returncode != 0:
|
|
254
|
+
result.error = f"git pull failed: {pull_r.stderr.strip()[:200]}"
|
|
255
|
+
results.append(result)
|
|
256
|
+
continue
|
|
257
|
+
|
|
178
258
|
changed = update_dependency(repo.local_path, artifact_id, new_version)
|
|
179
259
|
if not changed:
|
|
180
260
|
result.error = "pom.xml not changed (already up to date?)"
|
|
181
261
|
results.append(result)
|
|
182
262
|
continue
|
|
183
263
|
|
|
264
|
+
# Dry-run: compile before committing — catch broken deps early
|
|
265
|
+
try:
|
|
266
|
+
compile_ok, compile_output = maven_compile_check(repo.local_path)
|
|
267
|
+
except MissingToolError:
|
|
268
|
+
compile_ok, compile_output = False, "mvn not installed on this server"
|
|
269
|
+
|
|
270
|
+
if not compile_ok:
|
|
271
|
+
# Revert pom.xml — never commit a broken dep update
|
|
272
|
+
_git(["checkout", "pom.xml"], cwd=repo.local_path, env=_git_env(repo))
|
|
273
|
+
result.error = f"Maven compile failed — pom.xml reverted: {compile_output[-200:]}"
|
|
274
|
+
logger.error(
|
|
275
|
+
"Cascade build check failed for %s after bumping %s=%s:\n%s",
|
|
276
|
+
repo.repo_name, artifact_id, new_version, compile_output[-500:],
|
|
277
|
+
)
|
|
278
|
+
notify_cascade_build_failed(
|
|
279
|
+
cfg, repo.repo_name, artifact_id, new_version, compile_output,
|
|
280
|
+
)
|
|
281
|
+
results.append(result)
|
|
282
|
+
continue
|
|
283
|
+
|
|
184
284
|
commit_msg = (
|
|
185
285
|
f"chore(deps): update {artifact_id} to {new_version} [sentinel]\n\n"
|
|
186
286
|
f"Automated dependency bump by Sentinel after {source_repo_name} release."
|
|
187
287
|
)
|
|
188
|
-
status, commit_hash = commit_file_change(repo, ["pom.xml"], commit_msg)
|
|
288
|
+
status, commit_hash = commit_file_change(repo, ["pom.xml"], commit_msg, skip_pull=True)
|
|
189
289
|
if status != "committed":
|
|
190
290
|
result.error = "git commit failed"
|
|
191
291
|
results.append(result)
|
|
192
292
|
continue
|
|
193
293
|
|
|
194
|
-
branch, pr_url = push_dep_update(repo, cfg, artifact_id, new_version, commit_hash)
|
|
195
|
-
result.success = True
|
|
294
|
+
branch, pr_url, push_ok = push_dep_update(repo, cfg, artifact_id, new_version, commit_hash)
|
|
196
295
|
result.branch = branch
|
|
197
296
|
result.pr_url = pr_url
|
|
198
|
-
|
|
297
|
+
if push_ok:
|
|
298
|
+
result.success = True
|
|
299
|
+
logger.info(
|
|
300
|
+
"Cascade updated %s → %s=%s (branch: %s)",
|
|
301
|
+
repo.repo_name, artifact_id, new_version, branch,
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
result.success = False
|
|
305
|
+
result.error = "commit is local only — no push access; Jenkins will handle its own release push"
|
|
306
|
+
logger.warning(
|
|
307
|
+
"Cascade committed %s → %s=%s locally but push skipped (no write access)",
|
|
308
|
+
repo.repo_name, artifact_id, new_version,
|
|
309
|
+
)
|
|
199
310
|
|
|
200
311
|
except Exception as exc:
|
|
201
312
|
result.error = str(exc)
|
|
@@ -51,6 +51,27 @@ def _git_env(repo: RepoConfig) -> dict:
|
|
|
51
51
|
return env
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def maven_compile_check(local_path: str, timeout: int = 300) -> tuple[bool, str]:
|
|
55
|
+
"""
|
|
56
|
+
Run `mvn compile -DskipTests -q --batch-mode` to verify the pom.xml is valid
|
|
57
|
+
and all dependencies resolve before committing.
|
|
58
|
+
Returns (success, output). Raises MissingToolError if mvn is not installed.
|
|
59
|
+
"""
|
|
60
|
+
import shutil
|
|
61
|
+
mvn = shutil.which("mvn")
|
|
62
|
+
if not mvn:
|
|
63
|
+
raise MissingToolError("mvn")
|
|
64
|
+
r = subprocess.run(
|
|
65
|
+
[mvn, "compile", "-DskipTests", "-q", "--batch-mode"],
|
|
66
|
+
cwd=local_path,
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
timeout=timeout,
|
|
70
|
+
)
|
|
71
|
+
output = (r.stdout + r.stderr).strip()
|
|
72
|
+
return r.returncode == 0, output
|
|
73
|
+
|
|
74
|
+
|
|
54
75
|
def _check_protected_paths(patch_path: Path) -> bool:
|
|
55
76
|
text = patch_path.read_text(encoding="utf-8", errors="replace")
|
|
56
77
|
for line in text.splitlines():
|
|
@@ -162,19 +183,24 @@ def commit_file_change(
|
|
|
162
183
|
repo: RepoConfig,
|
|
163
184
|
files: list[str],
|
|
164
185
|
commit_msg: str,
|
|
186
|
+
skip_pull: bool = False,
|
|
165
187
|
) -> tuple[str, str]:
|
|
166
188
|
"""
|
|
167
|
-
|
|
189
|
+
Stage the given files and commit, optionally pulling first.
|
|
168
190
|
Returns (status, commit_hash) — status: "committed" | "failed".
|
|
169
191
|
Does NOT run tests (dependency bumps don't need them).
|
|
192
|
+
|
|
193
|
+
skip_pull=True when the caller already pulled before modifying files
|
|
194
|
+
(git pull --rebase fails on a dirty working tree).
|
|
170
195
|
"""
|
|
171
196
|
env = _git_env(repo)
|
|
172
197
|
local_path = repo.local_path
|
|
173
198
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
199
|
+
if not skip_pull:
|
|
200
|
+
r = _git(["pull", "--rebase", "origin", repo.branch], cwd=local_path, env=env)
|
|
201
|
+
if r.returncode != 0:
|
|
202
|
+
logger.error("git pull failed for %s:\n%s", repo.repo_name, r.stderr)
|
|
203
|
+
return "failed", ""
|
|
178
204
|
|
|
179
205
|
_git(["add"] + files, cwd=local_path, env=env)
|
|
180
206
|
r = _git(["commit", "-m", commit_msg], cwd=local_path, env=env)
|
|
@@ -194,11 +220,12 @@ def push_dep_update(
|
|
|
194
220
|
artifact_id: str,
|
|
195
221
|
new_version: str,
|
|
196
222
|
commit_hash: str,
|
|
197
|
-
) -> tuple[str, str]:
|
|
223
|
+
) -> tuple[str, str, bool]:
|
|
198
224
|
"""
|
|
199
225
|
Push a dependency update commit. AUTO_PUBLISH=true → main branch.
|
|
200
226
|
AUTO_PUBLISH=false → sentinel/dep-<artifact>-<version> branch + open PR.
|
|
201
|
-
Returns (branch, pr_url).
|
|
227
|
+
Returns (branch, pr_url, push_ok).
|
|
228
|
+
push_ok=False means the commit is local only (no write access) — chain release continues.
|
|
202
229
|
"""
|
|
203
230
|
env = _git_env(repo)
|
|
204
231
|
local_path = repo.local_path
|
|
@@ -206,8 +233,12 @@ def push_dep_update(
|
|
|
206
233
|
if repo.auto_publish:
|
|
207
234
|
r = _git(["push", "origin", repo.branch], cwd=local_path, env=env)
|
|
208
235
|
if r.returncode != 0:
|
|
209
|
-
logger.
|
|
210
|
-
|
|
236
|
+
logger.warning(
|
|
237
|
+
"git push failed for %s (commit is local only): %s",
|
|
238
|
+
repo.repo_name, r.stderr.strip().splitlines()[0] if r.stderr else "",
|
|
239
|
+
)
|
|
240
|
+
return repo.branch, "", False
|
|
241
|
+
return repo.branch, "", True
|
|
211
242
|
else:
|
|
212
243
|
safe_artifact = re.sub(r"[^a-zA-Z0-9_-]", "-", artifact_id)
|
|
213
244
|
safe_version = re.sub(r"[^a-zA-Z0-9._-]", "-", new_version)
|
|
@@ -215,12 +246,15 @@ def push_dep_update(
|
|
|
215
246
|
_git(["checkout", "-B", branch], cwd=local_path, env=env)
|
|
216
247
|
r = _git(["push", "-u", "origin", branch], cwd=local_path, env=env)
|
|
217
248
|
if r.returncode != 0:
|
|
218
|
-
logger.
|
|
249
|
+
logger.warning(
|
|
250
|
+
"git push branch failed for %s (commit is local only): %s",
|
|
251
|
+
repo.repo_name, r.stderr.strip().splitlines()[0] if r.stderr else "",
|
|
252
|
+
)
|
|
219
253
|
_git(["checkout", repo.branch], cwd=local_path, env=env)
|
|
220
|
-
return branch, ""
|
|
254
|
+
return branch, "", False
|
|
221
255
|
_git(["checkout", repo.branch], cwd=local_path, env=env)
|
|
222
256
|
pr_url = open_dep_pr(repo, cfg, branch, artifact_id, new_version, commit_hash)
|
|
223
|
-
return branch, pr_url
|
|
257
|
+
return branch, pr_url, True
|
|
224
258
|
|
|
225
259
|
|
|
226
260
|
def open_dep_pr(
|
|
@@ -355,6 +355,40 @@ def notify_cascade_result(
|
|
|
355
355
|
slack_alert(cfg.slack_bot_token, cfg.slack_channel, text)
|
|
356
356
|
|
|
357
357
|
|
|
358
|
+
def notify_cascade_build_failed(
|
|
359
|
+
cfg,
|
|
360
|
+
repo_name: str,
|
|
361
|
+
artifact_id: str,
|
|
362
|
+
new_version: str,
|
|
363
|
+
build_output: str,
|
|
364
|
+
user_id: str = "",
|
|
365
|
+
) -> None:
|
|
366
|
+
"""Alert admin when a Maven compile check fails after a dep bump — pom.xml was reverted."""
|
|
367
|
+
# Maven errors accumulate at the bottom — show the last 600 chars
|
|
368
|
+
snippet = build_output.strip()[-600:]
|
|
369
|
+
text = (
|
|
370
|
+
f":x: *Cascade build check failed — `{repo_name}`*\n"
|
|
371
|
+
f"Bumping `{artifact_id}` \u2192 `{new_version}` caused a compile error.\n"
|
|
372
|
+
f"`pom.xml` has been reverted. Manual intervention required.\n"
|
|
373
|
+
f"```{snippet}```"
|
|
374
|
+
)
|
|
375
|
+
if user_id:
|
|
376
|
+
slack_alert(cfg.slack_bot_token, cfg.slack_channel, f"<@{user_id}> {text}")
|
|
377
|
+
else:
|
|
378
|
+
slack_alert(cfg.slack_bot_token, cfg.slack_channel, f"<!channel> {text}")
|
|
379
|
+
try:
|
|
380
|
+
from .reporter import send_failure_notification
|
|
381
|
+
send_failure_notification(cfg, {
|
|
382
|
+
"source": repo_name,
|
|
383
|
+
"message": f"Cascade dep bump {artifact_id}={new_version} failed Maven compile",
|
|
384
|
+
"repo_name": repo_name,
|
|
385
|
+
"reason": f"Maven compile error after dep bump: {build_output[:300]}",
|
|
386
|
+
"body": build_output,
|
|
387
|
+
})
|
|
388
|
+
except Exception as exc:
|
|
389
|
+
logger.warning("notify_cascade_build_failed: email notification failed: %s", exc)
|
|
390
|
+
|
|
391
|
+
|
|
358
392
|
def alert_if_rate_limited(
|
|
359
393
|
bot_token: str,
|
|
360
394
|
channel: str,
|