@misterhuydo/sentinel 1.2.8 → 1.3.0
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/minify-map.json +8 -1
- package/.cairn/session.json +2 -2
- package/.cairn/views/a348d8_sentinel.js +79 -0
- package/lib/add.js +470 -415
- package/package.json +1 -1
- package/python/scripts/gen_deploy_keys.sh +94 -0
- package/python/scripts/setup_deploy_keys.sh +63 -0
- package/python/sentinel/fix_engine.py +1 -0
- package/python/sentinel/main.py +39 -29
package/package.json
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# gen_deploy_keys.sh — Generate one ed25519 deploy key per GitHub repo.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./gen_deploy_keys.sh org/repo1 org/repo2 ...
|
|
6
|
+
#
|
|
7
|
+
# What it does:
|
|
8
|
+
# 1. Generates ~/.ssh/<repo>.key and ~/.ssh/<repo>.key.pub for each repo
|
|
9
|
+
# 2. Adds a Host block to ~/.ssh/config so git uses the right key per repo
|
|
10
|
+
# 3. Adds github.com to known_hosts (if not already there)
|
|
11
|
+
# 4. Prints each public key for pasting into GitHub → Settings → Deploy keys
|
|
12
|
+
#
|
|
13
|
+
# After running:
|
|
14
|
+
# - Add each printed public key to its repo on GitHub (allow write access)
|
|
15
|
+
# - Use the SSH alias in git URLs: git@github-<repo>:org/repo.git
|
|
16
|
+
# - Test with: ssh -T github-<repo>
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
if [[ $# -eq 0 ]]; then
|
|
21
|
+
echo "Usage: $0 org/repo1 org/repo2 ..."
|
|
22
|
+
echo " e.g. $0 Opplysningen1881/sentinel-1881 Opplysningen1881/1881-SSOLoginWebApp"
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
SSH_DIR="$HOME/.ssh"
|
|
27
|
+
mkdir -p "$SSH_DIR"
|
|
28
|
+
chmod 700 "$SSH_DIR"
|
|
29
|
+
|
|
30
|
+
# Add GitHub host key once
|
|
31
|
+
if ! grep -q "github.com" "$SSH_DIR/known_hosts" 2>/dev/null; then
|
|
32
|
+
echo "Adding GitHub to known_hosts..."
|
|
33
|
+
ssh-keyscan github.com >> "$SSH_DIR/known_hosts" 2>/dev/null
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
touch "$SSH_DIR/config"
|
|
37
|
+
chmod 600 "$SSH_DIR/config"
|
|
38
|
+
|
|
39
|
+
for repo_path in "$@"; do
|
|
40
|
+
repo="${repo_path##*/}" # strip org/ prefix → just the repo name
|
|
41
|
+
keyfile="$SSH_DIR/${repo}.key"
|
|
42
|
+
|
|
43
|
+
echo ""
|
|
44
|
+
echo "══════════════════════════════════════════════"
|
|
45
|
+
echo " Repo: $repo_path"
|
|
46
|
+
echo " Keyfile: $keyfile"
|
|
47
|
+
echo "══════════════════════════════════════════════"
|
|
48
|
+
|
|
49
|
+
# Generate (skip if key already exists)
|
|
50
|
+
if [[ -f "$keyfile" ]]; then
|
|
51
|
+
echo " Key already exists — skipping generation (delete $keyfile to regenerate)"
|
|
52
|
+
else
|
|
53
|
+
ssh-keygen -t ed25519 -C "sentinel@${repo}" -f "$keyfile" -N "" -q
|
|
54
|
+
echo " Key generated."
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Add SSH config block (skip if Host already configured)
|
|
58
|
+
if ! grep -q "Host github-${repo}" "$SSH_DIR/config" 2>/dev/null; then
|
|
59
|
+
cat >> "$SSH_DIR/config" << EOF
|
|
60
|
+
|
|
61
|
+
Host github-${repo}
|
|
62
|
+
HostName github.com
|
|
63
|
+
User git
|
|
64
|
+
IdentityFile ${keyfile}
|
|
65
|
+
IdentitiesOnly yes
|
|
66
|
+
EOF
|
|
67
|
+
echo " SSH config block added."
|
|
68
|
+
else
|
|
69
|
+
echo " SSH config block already exists — skipping."
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
echo ""
|
|
73
|
+
echo " ┌─ Add this deploy key to GitHub ─────────────────────────────────────┐"
|
|
74
|
+
echo " │ $repo_path → Settings → Deploy keys → Add deploy key"
|
|
75
|
+
echo " │ Title: sentinel@$(hostname)"
|
|
76
|
+
echo " │ Allow write access: ✓"
|
|
77
|
+
echo " └──────────────────────────────────────────────────────────────────────┘"
|
|
78
|
+
echo ""
|
|
79
|
+
cat "$keyfile.pub"
|
|
80
|
+
done
|
|
81
|
+
|
|
82
|
+
echo ""
|
|
83
|
+
echo "══════════════════════════════════════════════"
|
|
84
|
+
echo "Done. After adding keys on GitHub, test each:"
|
|
85
|
+
for repo_path in "$@"; do
|
|
86
|
+
repo="${repo_path##*/}"
|
|
87
|
+
echo " ssh -T github-${repo}"
|
|
88
|
+
done
|
|
89
|
+
echo ""
|
|
90
|
+
echo "Use SSH aliases in sentinel add:"
|
|
91
|
+
for repo_path in "$@"; do
|
|
92
|
+
repo="${repo_path##*/}"
|
|
93
|
+
echo " sentinel add git@github-${repo}:${repo_path}.git"
|
|
94
|
+
done
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Usage: ./setup_deploy_keys.sh repo1 repo2 ...
|
|
3
|
+
# Generates one deploy key per repo, prints each public key for GitHub.
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
if [[ $# -eq 0 ]]; then
|
|
8
|
+
echo "Usage: $0 repo1 repo2 ..."
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
SSH_DIR="$HOME/.ssh"
|
|
13
|
+
|
|
14
|
+
# Wipe known state
|
|
15
|
+
echo "Cleaning ~/.ssh ..."
|
|
16
|
+
rm -f "$SSH_DIR"/known_hosts "$SSH_DIR"/known_hosts.old
|
|
17
|
+
for f in "$SSH_DIR"/*.pub "$SSH_DIR"/config; do
|
|
18
|
+
[[ -f "$f" ]] && rm -f "$f" "${f%.pub}"
|
|
19
|
+
done
|
|
20
|
+
rm -f "$SSH_DIR"/config
|
|
21
|
+
|
|
22
|
+
# Re-fetch GitHub host key once
|
|
23
|
+
ssh-keyscan github.com >> "$SSH_DIR/known_hosts" 2>/dev/null
|
|
24
|
+
echo "GitHub host key added."
|
|
25
|
+
echo ""
|
|
26
|
+
|
|
27
|
+
# Fresh SSH config
|
|
28
|
+
CONFIG="$SSH_DIR/config"
|
|
29
|
+
|
|
30
|
+
for repo in "$@"; do
|
|
31
|
+
keyfile="$SSH_DIR/$repo"
|
|
32
|
+
|
|
33
|
+
echo "──────────────────────────────────────────"
|
|
34
|
+
echo "Repo: $repo"
|
|
35
|
+
|
|
36
|
+
# Generate key
|
|
37
|
+
ssh-keygen -t ed25519 -C "sentinel@$repo" -f "$keyfile" -N "" -q
|
|
38
|
+
|
|
39
|
+
# Append SSH config block
|
|
40
|
+
cat >> "$CONFIG" << EOF
|
|
41
|
+
|
|
42
|
+
Host github-$repo
|
|
43
|
+
HostName github.com
|
|
44
|
+
User git
|
|
45
|
+
IdentityFile $keyfile
|
|
46
|
+
IdentitiesOnly yes
|
|
47
|
+
EOF
|
|
48
|
+
|
|
49
|
+
echo ""
|
|
50
|
+
echo "Deploy key for: github.com/Opplysningen1881/$repo"
|
|
51
|
+
echo "→ GitHub: Settings → Deploy keys → Add deploy key (allow write access)"
|
|
52
|
+
echo ""
|
|
53
|
+
cat "$keyfile.pub"
|
|
54
|
+
echo ""
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
chmod 600 "$CONFIG"
|
|
58
|
+
|
|
59
|
+
echo "──────────────────────────────────────────"
|
|
60
|
+
echo "Done. After adding keys on GitHub, test with:"
|
|
61
|
+
for repo in "$@"; do
|
|
62
|
+
echo " ssh -T github-$repo"
|
|
63
|
+
done
|
package/python/sentinel/main.py
CHANGED
|
@@ -90,27 +90,31 @@ async def _handle_error(event: ErrorEvent, cfg_loader: ConfigLoader, store: Stat
|
|
|
90
90
|
if status != "patch" or patch_path is None:
|
|
91
91
|
outcome = "skipped" if status in ("skip", "needs_human") else "failed"
|
|
92
92
|
store.record_fix(event.fingerprint, outcome, repo_name=repo.repo_name)
|
|
93
|
-
|
|
93
|
+
# For log-detected errors: NEEDS_HUMAN -> DM/channel; SKIP -> email only (not spam)
|
|
94
94
|
if status == "needs_human":
|
|
95
|
-
# marker holds the reason string for needs_human
|
|
96
95
|
notify_fix_blocked(sentinel, event.source, event.message,
|
|
97
96
|
reason=marker, repo_name=repo.repo_name,
|
|
98
|
-
submitter_user_id=
|
|
97
|
+
submitter_user_id="")
|
|
99
98
|
else:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
send_failure_notification(sentinel, {
|
|
100
|
+
"source": event.source,
|
|
101
|
+
"message": event.message,
|
|
102
|
+
"repo_name": repo.repo_name,
|
|
103
|
+
"reason": f"Claude Code returned {status.upper()}",
|
|
104
|
+
"body": event.full_text()[:500],
|
|
105
|
+
})
|
|
104
106
|
return
|
|
105
107
|
|
|
106
108
|
commit_status, commit_hash = apply_and_commit(event, patch_path, repo, sentinel)
|
|
107
109
|
if commit_status != "committed":
|
|
108
110
|
store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
send_failure_notification(sentinel, {
|
|
112
|
+
"source": event.source,
|
|
113
|
+
"message": event.message,
|
|
114
|
+
"repo_name": repo.repo_name,
|
|
115
|
+
"reason": "Patch was generated but commit/tests failed",
|
|
116
|
+
"body": event.full_text()[:500],
|
|
117
|
+
})
|
|
114
118
|
return
|
|
115
119
|
|
|
116
120
|
branch, pr_url = publish(event, repo, sentinel, commit_hash)
|
|
@@ -184,16 +188,12 @@ async def _handle_issue(event: IssueEvent, cfg_loader: ConfigLoader, store: Stat
|
|
|
184
188
|
if status != "patch" or patch_path is None:
|
|
185
189
|
store.record_fix(event.fingerprint, "skipped" if status in ("skip", "needs_human") else "failed",
|
|
186
190
|
repo_name=repo.repo_name)
|
|
191
|
+
# For user-submitted issues: always notify (person is waiting)
|
|
187
192
|
submitter_uid = getattr(event, "submitter_user_id", "")
|
|
188
|
-
if status == "needs_human"
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
else:
|
|
193
|
-
notify_fix_blocked(sentinel, event.source, event.message,
|
|
194
|
-
reason=f"Claude Code returned {status.upper()}",
|
|
195
|
-
repo_name=repo.repo_name,
|
|
196
|
-
submitter_user_id=submitter_uid)
|
|
193
|
+
reason_text = marker if status == "needs_human" else f"Claude Code returned {status.upper()}"
|
|
194
|
+
notify_fix_blocked(sentinel, event.source, event.message,
|
|
195
|
+
reason=reason_text, repo_name=repo.repo_name,
|
|
196
|
+
submitter_user_id=submitter_uid)
|
|
197
197
|
mark_done(event.issue_file)
|
|
198
198
|
return
|
|
199
199
|
|
|
@@ -307,21 +307,31 @@ async def poll_cycle(cfg_loader: ConfigLoader, store: StateStore):
|
|
|
307
307
|
|
|
308
308
|
# -- Health URL checks -------------------------------------------------------
|
|
309
309
|
if cfg_loader.repos:
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
310
|
+
import asyncio as _asyncio
|
|
311
|
+
_loop = _asyncio.get_event_loop()
|
|
312
|
+
health_results = await _loop.run_in_executor(
|
|
313
|
+
None,
|
|
314
|
+
lambda: evaluate_repos(
|
|
315
|
+
cfg_loader.repos, cfg_loader.log_sources,
|
|
316
|
+
cfg_loader.sentinel.workspace_dir, store=store,
|
|
317
|
+
)
|
|
313
318
|
)
|
|
314
319
|
for hr in health_results:
|
|
315
320
|
if hr["action"] == "fix":
|
|
316
321
|
fp = f"health-{hr['repo_name']}"
|
|
317
322
|
store.record_error(fp, f"health_checker/{hr['repo_name']}", hr["message"])
|
|
318
323
|
if not store.fix_attempted_recently(fp, hours=6):
|
|
319
|
-
|
|
324
|
+
from .log_parser import ErrorEvent as _EE
|
|
325
|
+
from datetime import datetime, timezone as _tz
|
|
326
|
+
synth = _EE(
|
|
320
327
|
source=f"health_checker/{hr['repo_name']}",
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
328
|
+
log_file="",
|
|
329
|
+
timestamp=datetime.now(_tz.utc).isoformat(),
|
|
330
|
+
level="ERROR",
|
|
331
|
+
thread="health_checker",
|
|
332
|
+
logger_name="health_checker",
|
|
333
|
+
message=f"App startup failure detected: {hr['message']}",
|
|
334
|
+
stack_trace=[hr["startup_failure_line"]] if hr["startup_failure_line"] else [],
|
|
325
335
|
)
|
|
326
336
|
synth.fingerprint = fp
|
|
327
337
|
await _handle_error(synth, cfg_loader, store)
|