@misterhuydo/sentinel 1.4.70 → 1.4.72
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/add.js +84 -41
- package/package.json +1 -1
- package/python/sentinel/config_loader.py +13 -2
- package/templates/sentinel.properties +10 -0
package/.cairn/.hint-lock
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-03-26T18:
|
|
1
|
+
2026-03-26T18:52:08.498Z
|
package/.cairn/session.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"message": "Auto-checkpoint at 2026-03-26T18:
|
|
3
|
-
"checkpoint_at": "2026-03-26T18:
|
|
2
|
+
"message": "Auto-checkpoint at 2026-03-26T18:49:49.152Z",
|
|
3
|
+
"checkpoint_at": "2026-03-26T18:49:49.153Z",
|
|
4
4
|
"active_files": [],
|
|
5
5
|
"notes": [],
|
|
6
6
|
"mtime_snapshot": {}
|
package/lib/add.js
CHANGED
|
@@ -313,13 +313,20 @@ async function addFromGit(gitUrl, workspace) {
|
|
|
313
313
|
const projectDir = path.join(workspace, name);
|
|
314
314
|
step(`[2/3] Scanning repo-configs in ${repoSlug}…`);
|
|
315
315
|
|
|
316
|
+
const sshEnv = keyFile
|
|
317
|
+
? gitEnv({ GIT_SSH_COMMAND: `ssh -i ${keyFile} -o StrictHostKeyChecking=no -o BatchMode=yes` })
|
|
318
|
+
: gitEnv({});
|
|
316
319
|
if (!fs.existsSync(projectDir)) {
|
|
317
|
-
const cloneEnv = keyFile
|
|
318
|
-
? gitEnv({ GIT_SSH_COMMAND: `ssh -i ${keyFile} -o StrictHostKeyChecking=no -o BatchMode=yes` })
|
|
319
|
-
: gitEnv({});
|
|
320
320
|
spawnSync(gitBin(), ['clone', '--depth', '1', gitUrl, projectDir], {
|
|
321
321
|
stdio: 'inherit',
|
|
322
|
-
env:
|
|
322
|
+
env: sshEnv,
|
|
323
|
+
});
|
|
324
|
+
} else {
|
|
325
|
+
// Pull latest so we get up-to-date sentinel.properties (e.g. GIT_ACCESS setting)
|
|
326
|
+
spawnSync(gitBin(), ['pull', '--rebase'], {
|
|
327
|
+
cwd: projectDir,
|
|
328
|
+
stdio: 'inherit',
|
|
329
|
+
env: sshEnv,
|
|
323
330
|
});
|
|
324
331
|
}
|
|
325
332
|
|
|
@@ -337,6 +344,23 @@ async function addFromGit(gitUrl, workspace) {
|
|
|
337
344
|
}
|
|
338
345
|
|
|
339
346
|
|
|
347
|
+
// Read project-level GIT_ACCESS from the cloned sentinel.properties
|
|
348
|
+
const sentinelPropsPath = path.join(projectDir, 'config', 'sentinel.properties');
|
|
349
|
+
let projectGitAccess = '';
|
|
350
|
+
let projectGitSshKey = '';
|
|
351
|
+
if (fs.existsSync(sentinelPropsPath)) {
|
|
352
|
+
const lines = fs.readFileSync(sentinelPropsPath, 'utf8').split('\n');
|
|
353
|
+
for (const line of lines) {
|
|
354
|
+
const trimmed = line.trim();
|
|
355
|
+
if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
|
|
356
|
+
const [k, ...rest] = trimmed.split('=');
|
|
357
|
+
const v = rest.join('=').split('#')[0].trim();
|
|
358
|
+
if (k.trim() === 'GIT_ACCESS') projectGitAccess = v;
|
|
359
|
+
if (k.trim() === 'GIT_SSH_KEY') projectGitSshKey = v.replace(/^~/, require('os').homedir());
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const useSshUserKey = projectGitAccess === 'ssh_user_key';
|
|
363
|
+
|
|
340
364
|
// Classify each discovered repo
|
|
341
365
|
const privateRepos = [];
|
|
342
366
|
const publicRepos = [];
|
|
@@ -355,50 +379,69 @@ async function addFromGit(gitUrl, workspace) {
|
|
|
355
379
|
}
|
|
356
380
|
}
|
|
357
381
|
|
|
358
|
-
// ── 3.
|
|
382
|
+
// ── 3. Set up access for all private repos ─────────────────────────────────
|
|
359
383
|
if (privateRepos.length > 0) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
r
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
384
|
+
if (useSshUserKey) {
|
|
385
|
+
// Project uses a personal SSH key — validate access, no deploy keys needed
|
|
386
|
+
step(`[3/3] Validating access to ${privateRepos.length} private repo(s) via SSH user key…`);
|
|
387
|
+
const sshKeyForValidation = projectGitSshKey || null;
|
|
388
|
+
for (const r of privateRepos) {
|
|
389
|
+
const v = validateAccess(r.url, sshKeyForValidation);
|
|
390
|
+
if (!v.ok) {
|
|
391
|
+
console.error(chalk.red(` ✖ ${r.slug}: cannot reach ${r.url}`));
|
|
392
|
+
if (v.stderr) console.error(chalk.red(' ' + v.stderr));
|
|
393
|
+
console.error(chalk.yellow(` Check that GIT_SSH_KEY (${projectGitSshKey || 'SSH agent'}) has access, then re-run.`));
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
ok(`${r.slug}: reachable`);
|
|
397
|
+
}
|
|
371
398
|
for (const r of publicRepos) {
|
|
372
|
-
|
|
399
|
+
ok(`${r.slug}: public, no key needed`);
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
// Default: generate a deploy key per private repo
|
|
403
|
+
step(`[3/3] Deploy keys needed for ${privateRepos.length} private repo(s)`);
|
|
404
|
+
|
|
405
|
+
for (const r of privateRepos) {
|
|
406
|
+
const { keyFile: rKey } = generateDeployKey(r.slug, projectDir);
|
|
407
|
+
r.keyFile = rKey;
|
|
408
|
+
const rOrgRepo = gitUrlToOrgRepo(r.url);
|
|
409
|
+
printDeployKeyInstructions(rOrgRepo, rKey);
|
|
373
410
|
}
|
|
374
|
-
console.log('');
|
|
375
|
-
}
|
|
376
411
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
412
|
+
if (publicRepos.length > 0) {
|
|
413
|
+
console.log(chalk.green(' ✔ Public repos (no deploy key needed):'));
|
|
414
|
+
for (const r of publicRepos) {
|
|
415
|
+
console.log(chalk.green(` ${r.slug}`));
|
|
416
|
+
}
|
|
417
|
+
console.log('');
|
|
418
|
+
}
|
|
381
419
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
420
|
+
await prompts({
|
|
421
|
+
type: 'text', name: '_', format: () => '',
|
|
422
|
+
message: chalk.bold(`Press Enter once you've added all ${privateRepos.length} deploy key(s) to GitHub…`),
|
|
423
|
+
}, { onCancel: () => process.exit(0) });
|
|
424
|
+
|
|
425
|
+
// Validate each private repo
|
|
426
|
+
step('Validating repository access…');
|
|
427
|
+
for (const r of privateRepos) {
|
|
428
|
+
const v = validateAccess(r.url, r.keyFile);
|
|
429
|
+
if (!v.ok) {
|
|
430
|
+
console.error(chalk.red(` ✖ ${r.slug}: cannot reach ${r.url}`));
|
|
431
|
+
if (v.stderr) console.error(chalk.red(' ' + v.stderr));
|
|
432
|
+
console.error(chalk.yellow(' Fix access then re-run sentinel add.'));
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
ok(`${r.slug}: reachable`);
|
|
436
|
+
}
|
|
437
|
+
for (const r of publicRepos) {
|
|
438
|
+
ok(`${r.slug}: public, no key needed`);
|
|
391
439
|
}
|
|
392
|
-
ok(`${r.slug}: reachable`);
|
|
393
|
-
}
|
|
394
|
-
for (const r of publicRepos) {
|
|
395
|
-
ok(`${r.slug}: public, no key needed`);
|
|
396
|
-
}
|
|
397
440
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
441
|
+
// Keys stored at <projectDir>/<slug>.key — config_loader.py auto-discovers them.
|
|
442
|
+
for (const r of privateRepos) {
|
|
443
|
+
info(`Key stored at ${r.keyFile} (auto-discovered, not committed to git)`);
|
|
444
|
+
}
|
|
402
445
|
}
|
|
403
446
|
} else if (discovered.length > 0) {
|
|
404
447
|
step('[3/3] All repos are public — no deploy keys needed');
|
package/package.json
CHANGED
|
@@ -109,6 +109,7 @@ class RepoConfig:
|
|
|
109
109
|
cicd_user: str = "" # Jenkins username for Basic auth (defaults to "sentinel")
|
|
110
110
|
health_url: str = "" # optional: HTTP endpoint returning {"Status": "true"}
|
|
111
111
|
cicd_token: str = ""
|
|
112
|
+
git_access: str = "" # ssh_user_key | ssh_deploy_key | https_token | https_pat
|
|
112
113
|
ssh_key_file: str = "" # path to SSH private key; sets GIT_SSH_COMMAND when present
|
|
113
114
|
|
|
114
115
|
|
|
@@ -226,6 +227,13 @@ class ConfigLoader:
|
|
|
226
227
|
repos_dir = self.config_dir / "repo-configs"
|
|
227
228
|
if not repos_dir.exists():
|
|
228
229
|
return
|
|
230
|
+
|
|
231
|
+
# Project-level defaults from sentinel.properties
|
|
232
|
+
sentinel_path = self.config_dir / "sentinel.properties"
|
|
233
|
+
proj_d: dict[str, str] = _parse_properties(str(sentinel_path)) if sentinel_path.exists() else {}
|
|
234
|
+
default_git_access = proj_d.get("GIT_ACCESS", "")
|
|
235
|
+
default_git_ssh_key = os.path.expanduser(proj_d.get("GIT_SSH_KEY", ""))
|
|
236
|
+
|
|
229
237
|
self.repos = {}
|
|
230
238
|
for path in sorted(repos_dir.glob("*.properties")):
|
|
231
239
|
if path.name.startswith("_"):
|
|
@@ -242,8 +250,11 @@ class ConfigLoader:
|
|
|
242
250
|
r.cicd_user = d.get("CICD_USER", "")
|
|
243
251
|
r.cicd_token = d.get("CICD_TOKEN", "")
|
|
244
252
|
r.health_url = d.get("HEALTH_URL", "")
|
|
245
|
-
r.
|
|
246
|
-
#
|
|
253
|
+
r.git_access = d.get("GIT_ACCESS", default_git_access)
|
|
254
|
+
# GIT_SSH_KEY (preferred) or legacy SSH_KEY_FILE, then project default
|
|
255
|
+
raw_key = d.get("GIT_SSH_KEY", "") or d.get("SSH_KEY_FILE", "")
|
|
256
|
+
r.ssh_key_file = os.path.expanduser(raw_key) if raw_key else default_git_ssh_key
|
|
257
|
+
# Auto-discover deploy key in project dir if no key configured
|
|
247
258
|
if not r.ssh_key_file:
|
|
248
259
|
auto_key = Path(self.config_dir).parent / f"{r.repo_name}.key"
|
|
249
260
|
if auto_key.exists():
|
|
@@ -18,6 +18,16 @@ MAILS=you@yourdomain.com
|
|
|
18
18
|
# Uncomment here only if this project needs a different token.
|
|
19
19
|
# GITHUB_TOKEN=<github-pat>
|
|
20
20
|
|
|
21
|
+
# Default SSH access method for all repos in this project.
|
|
22
|
+
# Per-repo configs (config/repo-configs/*.properties) can override these.
|
|
23
|
+
#
|
|
24
|
+
# Options:
|
|
25
|
+
# ssh_user_key — personal key at GIT_SSH_KEY path (full account access, recommended)
|
|
26
|
+
# ssh_deploy_key — per-repo deploy key generated by sentinel add (default if not set)
|
|
27
|
+
#
|
|
28
|
+
# GIT_ACCESS=ssh_user_key
|
|
29
|
+
# GIT_SSH_KEY=~/.ssh/your-key
|
|
30
|
+
|
|
21
31
|
# Fix confirmation: hours of silence after a fix marker appears in production logs before
|
|
22
32
|
# the fix is declared confirmed. Increase for services that deploy infrequently.
|
|
23
33
|
# MARKER_CONFIRM_HOURS=24
|