@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.2.8",
3
+ "version": "1.3.0",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -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
@@ -155,6 +155,7 @@ def generate_fix(
155
155
  repo: RepoConfig,
156
156
  cfg: SentinelConfig,
157
157
  patches_dir: Path,
158
+ store=None,
158
159
  ) -> tuple[str, Path | None, str]:
159
160
  """
160
161
  Generate a fix for the given error event.
@@ -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
- submitter_uid = getattr(event, "submitter_user_id", "")
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=submitter_uid)
97
+ submitter_user_id="")
99
98
  else:
100
- notify_fix_blocked(sentinel, event.source, event.message,
101
- reason=f"Claude Code returned {status.upper()}",
102
- repo_name=repo.repo_name,
103
- submitter_user_id=submitter_uid)
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
- submitter_uid = getattr(event, "submitter_user_id", "")
110
- notify_fix_blocked(sentinel, event.source, event.message,
111
- reason="Patch was generated but commit/tests failed",
112
- repo_name=repo.repo_name,
113
- submitter_user_id=submitter_uid)
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
- notify_fix_blocked(sentinel, event.source, event.message,
190
- reason=marker, repo_name=repo.repo_name,
191
- submitter_user_id=submitter_uid)
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
- health_results = evaluate_repos(
311
- cfg_loader.repos, cfg_loader.log_sources, cfg_loader.sentinel.workspace_dir,
312
- store=store,
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
- synth = ErrorEvent(
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
- severity="ERROR",
322
- message=f"App startup failure: {hr['message']}",
323
- raw_lines=[hr["startup_failure_line"]],
324
- timestamp=None,
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)