@theholocron/cli 5.0.0-alpha.186 → 5.0.0-alpha.187
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/dist/cli.mjs +361 -30
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/plugin/capabilities.d.mts +31 -1
- package/package.json +3 -3
package/dist/cli.mjs
CHANGED
|
@@ -409,6 +409,7 @@ const COMMAND_CONTEXTS = {
|
|
|
409
409
|
"secrets sync": "workspace",
|
|
410
410
|
"notifications channel add": "workspace",
|
|
411
411
|
"notifications setup": "workspace",
|
|
412
|
+
"notifications verify": "workspace",
|
|
412
413
|
deploy: "workspace",
|
|
413
414
|
"deploy cleanup-preview": "workspace",
|
|
414
415
|
"monitoring check": "workspace",
|
|
@@ -2402,8 +2403,12 @@ async function runDeployFromFiles(input) {
|
|
|
2402
2403
|
* (newlines and the C1 range, whose 0x9b is an 8-bit CSI) so a crafted body
|
|
2403
2404
|
* can't forge extra report lines or inject terminal escape sequences. */
|
|
2404
2405
|
const MAX_DETAIL_LENGTH = 200;
|
|
2406
|
+
/** Control characters (newlines and the C1 range included) turned into spaces, whitespace collapsed, no length cap. */
|
|
2407
|
+
function stripControl(text) {
|
|
2408
|
+
return text.replace(/[\x00-\x1f\x7f-\x9f]/g, " ").replace(/\s+/g, " ").trim();
|
|
2409
|
+
}
|
|
2405
2410
|
function sanitizeDetail(text) {
|
|
2406
|
-
const stripped = text
|
|
2411
|
+
const stripped = stripControl(text);
|
|
2407
2412
|
return stripped.length > MAX_DETAIL_LENGTH ? `${stripped.slice(0, MAX_DETAIL_LENGTH)}…` : stripped;
|
|
2408
2413
|
}
|
|
2409
2414
|
/**
|
|
@@ -2416,11 +2421,12 @@ function sanitizeDetail(text) {
|
|
|
2416
2421
|
* able to overwhelm or manipulate the terminal.
|
|
2417
2422
|
*/
|
|
2418
2423
|
function describeProviderError(err) {
|
|
2424
|
+
const base = stripControl(errorMessage(err));
|
|
2419
2425
|
if (err instanceof ProviderApiError$1 && err.details) {
|
|
2420
2426
|
const detail = sanitizeDetail(String(err.details));
|
|
2421
|
-
if (detail) return `${
|
|
2427
|
+
if (detail) return `${base}: ${detail}`;
|
|
2422
2428
|
}
|
|
2423
|
-
return
|
|
2429
|
+
return base;
|
|
2424
2430
|
}
|
|
2425
2431
|
/**
|
|
2426
2432
|
* What to put in structured logs: the plain message of the underlying error,
|
|
@@ -3279,21 +3285,66 @@ async function assertSecretsReachable(secrets, scope) {
|
|
|
3279
3285
|
/** Why a capability is not loaded: the plugin's own load error when there is one (e.g. a missing token). */
|
|
3280
3286
|
function whyMissing(loader, key, fallback) {
|
|
3281
3287
|
const failure = loader.loadFailures().find((f) => f.key === key);
|
|
3282
|
-
return failure ? `${fallback} (\`${failure.provider}\` failed to load: ${failure.error
|
|
3288
|
+
return failure ? `${fallback} (\`${failure.provider}\` failed to load: ${describeProviderError(failure.error)})` : fallback;
|
|
3283
3289
|
}
|
|
3284
|
-
/**
|
|
3285
|
-
function
|
|
3286
|
-
const
|
|
3287
|
-
if (
|
|
3290
|
+
/** The configured `notifications` providers, or an error that says why there are none (including a plugin that failed to load). */
|
|
3291
|
+
function notificationProviders(loader) {
|
|
3292
|
+
const providers = loader.has("notifications") ? loader.get("notifications") : [];
|
|
3293
|
+
if (providers.length === 0) throw new Error(whyMissing(loader, "notifications", "no `notifications` provider is available — add one to holocron.config.ts"));
|
|
3294
|
+
return providers;
|
|
3295
|
+
}
|
|
3296
|
+
/**
|
|
3297
|
+
* Pick the provider that can do what the command needs; with several, `--provider`
|
|
3298
|
+
* decides. `can` says what "able" means (a command that only reads needs `inspect`, not
|
|
3299
|
+
* provisioning), and `verb` words the errors.
|
|
3300
|
+
*/
|
|
3301
|
+
function pickProvider(providers, wanted, can, verb) {
|
|
3302
|
+
const capable = providers.filter(can);
|
|
3303
|
+
if (capable.length === 0) throw new Error(`no \`notifications\` provider can ${verb.infinitive} — add one that implements it (for example discord) to holocron.config.ts`);
|
|
3288
3304
|
if (wanted) {
|
|
3289
3305
|
const match = capable.find((p) => p.providerName === wanted);
|
|
3290
|
-
if (!match) throw new Error(`no
|
|
3306
|
+
if (!match) throw new Error(`no ${verb.gerund} \`notifications\` provider named \`${wanted}\` (have: ${names(capable)})`);
|
|
3291
3307
|
return match;
|
|
3292
3308
|
}
|
|
3293
|
-
if (capable.length > 1) throw new Error(`several \`notifications\` providers can
|
|
3309
|
+
if (capable.length > 1) throw new Error(`several \`notifications\` providers can ${verb.infinitive} (${names(capable)}) — pass --provider <name>`);
|
|
3294
3310
|
return capable[0];
|
|
3295
3311
|
}
|
|
3312
|
+
/** Pick the provider that can provision. Any ensure* method counts: a role-only layout needs no channel support. */
|
|
3313
|
+
function pickProvisioner(providers, wanted) {
|
|
3314
|
+
return pickProvider(providers, wanted, (p) => typeof p.ensureChannel === "function" || typeof p.ensureCategory === "function" || typeof p.ensureRole === "function", {
|
|
3315
|
+
infinitive: "provision",
|
|
3316
|
+
gerund: "provisioning"
|
|
3317
|
+
});
|
|
3318
|
+
}
|
|
3296
3319
|
const names = (ps) => ps.map((p) => p.providerName).join(", ");
|
|
3320
|
+
/**
|
|
3321
|
+
* The first reason the layout's secret names cannot work, or undefined. Two things
|
|
3322
|
+
* cannot share one secret: the second write would overwrite the first and callers
|
|
3323
|
+
* would read the wrong value. Punctuation normalising (release-notes vs
|
|
3324
|
+
* release_notes) makes that easy to hit by accident, GitHub secret names are
|
|
3325
|
+
* case-insensitive, and the preflight's own prefix is reserved. Shared by `setup`
|
|
3326
|
+
* (which refuses) and `verify` (which reports it).
|
|
3327
|
+
*/
|
|
3328
|
+
function layoutSecretProblem(layout) {
|
|
3329
|
+
const taken = /* @__PURE__ */ new Map();
|
|
3330
|
+
const claim = (secret, owner, hint) => {
|
|
3331
|
+
if (isReservedSecret(secret)) return `${owner} would store into ${secret}, a name reserved for the setup preflight — choose another`;
|
|
3332
|
+
const key = secret.toUpperCase();
|
|
3333
|
+
const other = taken.get(key);
|
|
3334
|
+
if (other) return `${other} and ${owner} would both store into ${secret} — ${hint}`;
|
|
3335
|
+
taken.set(key, owner);
|
|
3336
|
+
};
|
|
3337
|
+
for (const category of layout.categories ?? []) for (const channel of category.channels ?? []) {
|
|
3338
|
+
if (!channel.webhook) continue;
|
|
3339
|
+
const clash = claim(channel.webhook !== true && channel.webhook.secret || defaultSecretName(channel.name), `channel \`${channel.name}\``, "give one a distinct `webhook.secret`");
|
|
3340
|
+
if (clash) return clash;
|
|
3341
|
+
}
|
|
3342
|
+
for (const role of layout.roles ?? []) {
|
|
3343
|
+
if (!role.secret) continue;
|
|
3344
|
+
const clash = claim(role.secret, `role \`${role.name}\``, "give one a distinct secret");
|
|
3345
|
+
if (clash) return clash;
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3297
3348
|
/** Ensure one channel and, if asked, its webhook + secret. Shared with `notifications setup`. */
|
|
3298
3349
|
async function provisionChannel(deps, spec) {
|
|
3299
3350
|
const { notifications, secrets, scope, dryRun, print } = deps;
|
|
@@ -3367,7 +3418,7 @@ async function runNotificationsChannelAdd(input) {
|
|
|
3367
3418
|
const dryRun = input.context.dryRun ?? false;
|
|
3368
3419
|
print(`Holocron notifications channel add — ${input.name}${dryRun ? " (dry-run)" : ""}`);
|
|
3369
3420
|
try {
|
|
3370
|
-
const notifications = pickProvisioner(
|
|
3421
|
+
const notifications = pickProvisioner(notificationProviders(loader), input.provider);
|
|
3371
3422
|
let secrets;
|
|
3372
3423
|
if (input.withWebhook && !dryRun) {
|
|
3373
3424
|
if (!loader.has("secrets")) throw new Error(whyMissing(loader, "secrets", "`--with-webhook` needs a `secrets` capability — add a `secrets` provider to holocron.config.ts"));
|
|
@@ -3467,7 +3518,7 @@ async function runNotificationsSetup(input) {
|
|
|
3467
3518
|
};
|
|
3468
3519
|
let notifications;
|
|
3469
3520
|
try {
|
|
3470
|
-
notifications = pickProvisioner(
|
|
3521
|
+
notifications = pickProvisioner(notificationProviders(loader), input.provider);
|
|
3471
3522
|
} catch (err) {
|
|
3472
3523
|
return refuse(errorMessage(err));
|
|
3473
3524
|
}
|
|
@@ -3490,24 +3541,8 @@ async function runNotificationsSetup(input) {
|
|
|
3490
3541
|
} catch (err) {
|
|
3491
3542
|
return refuse(errorMessage(err), logReason(err));
|
|
3492
3543
|
}
|
|
3493
|
-
const
|
|
3494
|
-
|
|
3495
|
-
if (isReservedSecret(secret)) return `${owner} would store into ${secret}, a name reserved for the setup preflight — choose another`;
|
|
3496
|
-
const key = secret.toUpperCase();
|
|
3497
|
-
const other = taken.get(key);
|
|
3498
|
-
if (other) return `${other} and ${owner} would both store into ${secret} — ${hint}`;
|
|
3499
|
-
taken.set(key, owner);
|
|
3500
|
-
};
|
|
3501
|
-
for (const category of layout.categories ?? []) for (const channel of category.channels ?? []) {
|
|
3502
|
-
if (!channel.webhook) continue;
|
|
3503
|
-
const clash = claim(channel.webhook !== true && channel.webhook.secret || defaultSecretName(channel.name), `channel \`${channel.name}\``, "give one a distinct `webhook.secret`");
|
|
3504
|
-
if (clash) return refuse(clash);
|
|
3505
|
-
}
|
|
3506
|
-
for (const role of layout.roles ?? []) {
|
|
3507
|
-
if (!role.secret) continue;
|
|
3508
|
-
const clash = claim(role.secret, `role \`${role.name}\``, "give one a distinct secret");
|
|
3509
|
-
if (clash) return refuse(clash);
|
|
3510
|
-
}
|
|
3544
|
+
const secretProblem = layoutSecretProblem(layout);
|
|
3545
|
+
if (secretProblem) return refuse(secretProblem);
|
|
3511
3546
|
print(`Holocron notifications setup — ${notifications.providerName}${dryRun ? " (dry-run)" : ""}`);
|
|
3512
3547
|
const attempt = async (step, fn) => {
|
|
3513
3548
|
try {
|
|
@@ -3597,6 +3632,270 @@ async function runNotificationsSetup(input) {
|
|
|
3597
3632
|
return report;
|
|
3598
3633
|
}
|
|
3599
3634
|
//#endregion
|
|
3635
|
+
//#region src/commands/notifications-verify/notifications-verify.ts
|
|
3636
|
+
/**
|
|
3637
|
+
* `holocron notifications verify` -- read-only: compare the layout a provider
|
|
3638
|
+
* declares in `holocron.config.ts` with what is actually on the server and in the
|
|
3639
|
+
* secrets store, step by step.
|
|
3640
|
+
*
|
|
3641
|
+
* It is the end-to-end check for `notifications setup`, and stays useful after
|
|
3642
|
+
* as a drift check. Everything the layout and `setup` cannot do yet is listed
|
|
3643
|
+
* too, as a failing "not built" step, so the command is red until the last
|
|
3644
|
+
* feature lands and each feature built turns exactly one step green. The list
|
|
3645
|
+
* is {@link PLANNED_CHECKS}; the numbers match the table in holocron#1098.
|
|
3646
|
+
*/
|
|
3647
|
+
/** Features not built yet, in the order of the table in holocron#1098. Move an entry into a real check when it lands. */
|
|
3648
|
+
const PLANNED_CHECKS = [
|
|
3649
|
+
{
|
|
3650
|
+
step: 8,
|
|
3651
|
+
id: "permissions",
|
|
3652
|
+
title: "channel and category permissions"
|
|
3653
|
+
},
|
|
3654
|
+
{
|
|
3655
|
+
step: 9,
|
|
3656
|
+
id: "role-appearance",
|
|
3657
|
+
title: "role colour, hoist and permissions"
|
|
3658
|
+
},
|
|
3659
|
+
{
|
|
3660
|
+
step: 10,
|
|
3661
|
+
id: "server-settings",
|
|
3662
|
+
title: "server settings (verification, notifications, system channels)"
|
|
3663
|
+
},
|
|
3664
|
+
{
|
|
3665
|
+
step: 11,
|
|
3666
|
+
id: "welcome",
|
|
3667
|
+
title: "welcome and onboarding content"
|
|
3668
|
+
},
|
|
3669
|
+
{
|
|
3670
|
+
step: 12,
|
|
3671
|
+
id: "webhook-appearance",
|
|
3672
|
+
title: "webhook names and avatars"
|
|
3673
|
+
},
|
|
3674
|
+
{
|
|
3675
|
+
step: 13,
|
|
3676
|
+
id: "app-settings",
|
|
3677
|
+
title: "bot application settings (description, tags, install, links)"
|
|
3678
|
+
},
|
|
3679
|
+
{
|
|
3680
|
+
step: 14,
|
|
3681
|
+
id: "role-assignment",
|
|
3682
|
+
title: "role assigned to team members"
|
|
3683
|
+
},
|
|
3684
|
+
{
|
|
3685
|
+
step: 15,
|
|
3686
|
+
id: "post-works",
|
|
3687
|
+
title: "a test post reached every channel (platform.notifyCheck)"
|
|
3688
|
+
},
|
|
3689
|
+
{
|
|
3690
|
+
step: 16,
|
|
3691
|
+
id: "server-creation",
|
|
3692
|
+
title: "the server itself created from config"
|
|
3693
|
+
}
|
|
3694
|
+
];
|
|
3695
|
+
const same = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
3696
|
+
const shown = (s) => sanitizeDetail(s);
|
|
3697
|
+
async function runNotificationsVerify(input) {
|
|
3698
|
+
const print = input.print ?? ((line) => console.log(line));
|
|
3699
|
+
const logger = input.logger ?? getLogger();
|
|
3700
|
+
/* v8 ignore next -- the real loader is only built outside tests, which always inject one */
|
|
3701
|
+
const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
|
|
3702
|
+
await loader.load();
|
|
3703
|
+
assertPluginsResolvable(loader, "notifications verify");
|
|
3704
|
+
const checks = [];
|
|
3705
|
+
const add = (id, title, problems, pass) => {
|
|
3706
|
+
checks.push({
|
|
3707
|
+
id,
|
|
3708
|
+
title,
|
|
3709
|
+
status: problems.length === 0 ? "pass" : "fail",
|
|
3710
|
+
detail: problems.length === 0 ? pass : `${problems.length} problem(s)`,
|
|
3711
|
+
problems
|
|
3712
|
+
});
|
|
3713
|
+
};
|
|
3714
|
+
const byId = (id) => checks.find((c) => c.id === id);
|
|
3715
|
+
const failed = (id, title, reason) => {
|
|
3716
|
+
checks.push({
|
|
3717
|
+
id,
|
|
3718
|
+
title,
|
|
3719
|
+
status: "fail",
|
|
3720
|
+
detail: reason,
|
|
3721
|
+
problems: []
|
|
3722
|
+
});
|
|
3723
|
+
};
|
|
3724
|
+
const summarise = () => ({
|
|
3725
|
+
pass: checks.filter((c) => c.status === "pass").length,
|
|
3726
|
+
fail: checks.filter((c) => c.status === "fail").length,
|
|
3727
|
+
planned: checks.filter((c) => c.status === "planned").length
|
|
3728
|
+
});
|
|
3729
|
+
const finish = () => {
|
|
3730
|
+
const summary = summarise();
|
|
3731
|
+
const report = {
|
|
3732
|
+
status: summary.fail + summary.planned > 0 ? "fail" : "ok",
|
|
3733
|
+
checks,
|
|
3734
|
+
summary
|
|
3735
|
+
};
|
|
3736
|
+
logger.info({
|
|
3737
|
+
...summary,
|
|
3738
|
+
status: report.status,
|
|
3739
|
+
checks: checks.map((c) => `${c.id}:${c.status}`)
|
|
3740
|
+
}, "notifications verify");
|
|
3741
|
+
return report;
|
|
3742
|
+
};
|
|
3743
|
+
const refuse = (message, reason = message) => {
|
|
3744
|
+
print(` ✗ ${message}`);
|
|
3745
|
+
logger.warn({ reason }, "notifications verify");
|
|
3746
|
+
return {
|
|
3747
|
+
status: "fail",
|
|
3748
|
+
checks,
|
|
3749
|
+
summary: summarise(),
|
|
3750
|
+
message
|
|
3751
|
+
};
|
|
3752
|
+
};
|
|
3753
|
+
let notifications;
|
|
3754
|
+
try {
|
|
3755
|
+
notifications = pickProvider(notificationProviders(loader), input.provider, (p) => typeof p.inspect === "function", {
|
|
3756
|
+
infinitive: "inspect a server",
|
|
3757
|
+
gerund: "inspecting"
|
|
3758
|
+
});
|
|
3759
|
+
} catch (err) {
|
|
3760
|
+
return refuse(describeProviderError(err));
|
|
3761
|
+
}
|
|
3762
|
+
const layout = notifications.layout;
|
|
3763
|
+
if (!layout) return refuse(`the \`${notifications.providerName}\` provider declares no layout — add \`layout\` to its options in holocron.config.ts`);
|
|
3764
|
+
print(`Holocron notifications verify — ${notifications.providerName}\n`);
|
|
3765
|
+
let found;
|
|
3766
|
+
let readError = "";
|
|
3767
|
+
try {
|
|
3768
|
+
found = await notifications.inspect(layout);
|
|
3769
|
+
} catch (err) {
|
|
3770
|
+
readError = describeProviderError(err);
|
|
3771
|
+
}
|
|
3772
|
+
if (notifications.whoami) try {
|
|
3773
|
+
const who = await notifications.whoami();
|
|
3774
|
+
if (who) checks.push({
|
|
3775
|
+
id: "server",
|
|
3776
|
+
title: "server",
|
|
3777
|
+
status: "pass",
|
|
3778
|
+
detail: shown(who.target),
|
|
3779
|
+
problems: []
|
|
3780
|
+
});
|
|
3781
|
+
else failed("server", "server", "no server configured (the provider's whoami has nothing to check)");
|
|
3782
|
+
} catch (err) {
|
|
3783
|
+
failed("server", "server", describeProviderError(err));
|
|
3784
|
+
}
|
|
3785
|
+
else if (found) checks.push({
|
|
3786
|
+
id: "server",
|
|
3787
|
+
title: "server",
|
|
3788
|
+
status: "pass",
|
|
3789
|
+
detail: "readable (the provider has no whoami)",
|
|
3790
|
+
problems: []
|
|
3791
|
+
});
|
|
3792
|
+
else failed("server", "server", readError);
|
|
3793
|
+
const declaredChannels = (layout.categories ?? []).flatMap((cat) => (cat.channels ?? []).map((channel) => ({
|
|
3794
|
+
cat: cat.name,
|
|
3795
|
+
channel
|
|
3796
|
+
})));
|
|
3797
|
+
const pick = (name, category) => {
|
|
3798
|
+
const named = found.channels.filter((c) => same(c.name, name));
|
|
3799
|
+
return {
|
|
3800
|
+
named,
|
|
3801
|
+
match: named.find((c) => c.category !== void 0 && same(c.category, category))
|
|
3802
|
+
};
|
|
3803
|
+
};
|
|
3804
|
+
if (!found) {
|
|
3805
|
+
if (byId("server")?.status !== "fail") failed("read", "read server", readError);
|
|
3806
|
+
for (const [id, title] of [
|
|
3807
|
+
["categories", "categories"],
|
|
3808
|
+
["channels", "channels"],
|
|
3809
|
+
["webhooks", "webhooks"],
|
|
3810
|
+
["roles", "roles"]
|
|
3811
|
+
]) failed(id, title, "not checked: could not read the server");
|
|
3812
|
+
} else {
|
|
3813
|
+
const inspected = found;
|
|
3814
|
+
const categories = layout.categories ?? [];
|
|
3815
|
+
add("categories", "categories", categories.filter((cat) => !inspected.categories.some((name) => same(name, cat.name))).map((cat) => `${shown(cat.name)}: missing`), categories.length === 0 ? "none declared" : `${categories.length} of ${categories.length} exist`);
|
|
3816
|
+
const channelProblems = [];
|
|
3817
|
+
for (const { cat, channel } of declaredChannels) {
|
|
3818
|
+
const { named, match } = pick(channel.name, cat);
|
|
3819
|
+
if (named.length === 0) {
|
|
3820
|
+
channelProblems.push(`#${shown(channel.name)}: missing`);
|
|
3821
|
+
continue;
|
|
3822
|
+
}
|
|
3823
|
+
if (!match) {
|
|
3824
|
+
const where = named.map((c) => c.category ?? "no category").join(", ");
|
|
3825
|
+
channelProblems.push(`#${shown(channel.name)}: exists, but under ${shown(where)} and not ${shown(cat)}`);
|
|
3826
|
+
continue;
|
|
3827
|
+
}
|
|
3828
|
+
if (channel.topic && (match.topic ?? "") !== channel.topic) channelProblems.push(`#${shown(channel.name)}: topic is "${shown(match.topic ?? "")}" but the layout says "${shown(channel.topic)}"`);
|
|
3829
|
+
}
|
|
3830
|
+
add("channels", "channels", channelProblems, declaredChannels.length === 0 ? "none declared" : `${declaredChannels.length} of ${declaredChannels.length} as declared`);
|
|
3831
|
+
const withWebhook = declaredChannels.filter(({ channel }) => channel.webhook);
|
|
3832
|
+
const webhookProblems = [];
|
|
3833
|
+
for (const { cat, channel } of withWebhook) {
|
|
3834
|
+
const name = (channel.webhook === true ? {} : channel.webhook).name ?? channel.name;
|
|
3835
|
+
const { match } = pick(channel.name, cat);
|
|
3836
|
+
if (!match) webhookProblems.push(`#${shown(channel.name)}: no channel to hold the webhook`);
|
|
3837
|
+
else if (match.webhooksError) webhookProblems.push(`#${shown(channel.name)}: could not list its webhooks (${shown(match.webhooksError)})`);
|
|
3838
|
+
else if (!match.webhooks.some((w) => same(w, name))) webhookProblems.push(`#${shown(channel.name)}: no webhook named ${shown(name)}`);
|
|
3839
|
+
}
|
|
3840
|
+
add("webhooks", "webhooks", webhookProblems, withWebhook.length === 0 ? "none declared" : `${withWebhook.length} of ${withWebhook.length} exist`);
|
|
3841
|
+
const roles = layout.roles ?? [];
|
|
3842
|
+
const roleProblems = [];
|
|
3843
|
+
if (inspected.rolesError) roleProblems.push(`could not read the roles (${shown(inspected.rolesError)})`);
|
|
3844
|
+
for (const role of inspected.rolesError ? [] : roles) {
|
|
3845
|
+
const actual = inspected.roles.find((r) => same(r.name, role.name));
|
|
3846
|
+
if (!actual) roleProblems.push(`${shown(role.name)}: missing`);
|
|
3847
|
+
else if (role.mentionable !== void 0 && actual.mentionable !== role.mentionable) roleProblems.push(`${shown(role.name)}: mentionable is ${actual.mentionable} but the layout says ${role.mentionable}`);
|
|
3848
|
+
}
|
|
3849
|
+
add("roles", "roles", roleProblems, roles.length === 0 ? "none declared" : `${roles.length} of ${roles.length} as declared`);
|
|
3850
|
+
}
|
|
3851
|
+
const wanted = [...declaredChannels.filter(({ channel }) => channel.webhook).map(({ channel }) => channel.webhook !== true && channel.webhook && channel.webhook.secret ? channel.webhook.secret : defaultSecretName(channel.name)), ...(layout.roles ?? []).flatMap((r) => r.secret ? [r.secret] : [])];
|
|
3852
|
+
if (wanted.length === 0) checks.push({
|
|
3853
|
+
id: "secrets",
|
|
3854
|
+
title: "secrets",
|
|
3855
|
+
status: "pass",
|
|
3856
|
+
detail: "none declared",
|
|
3857
|
+
problems: []
|
|
3858
|
+
});
|
|
3859
|
+
else if (!loader.has("secrets")) failed("secrets", "secrets", whyMissing(loader, "secrets", "no `secrets` provider to check them with"));
|
|
3860
|
+
else {
|
|
3861
|
+
const org = input.context.org;
|
|
3862
|
+
const scope = input.scope ?? (org ? {
|
|
3863
|
+
kind: "organization",
|
|
3864
|
+
name: org
|
|
3865
|
+
} : void 0);
|
|
3866
|
+
if (!scope) failed("secrets", "secrets", "no org to look under — pass --scope or set `org` in holocron.config.ts");
|
|
3867
|
+
else try {
|
|
3868
|
+
const have = new Set((await loader.get("secrets").listSecrets(scope)).map((n) => n.toUpperCase()));
|
|
3869
|
+
const clash = layoutSecretProblem(layout);
|
|
3870
|
+
add("secrets", "secrets", [...clash ? [shown(clash)] : [], ...wanted.filter((n) => !have.has(n.toUpperCase())).map((n) => `${shown(n)}: missing`)], `${wanted.length} of ${wanted.length} exist`);
|
|
3871
|
+
} catch (err) {
|
|
3872
|
+
failed("secrets", "secrets", describeProviderError(err));
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
if (!input.builtOnly) for (const planned of PLANNED_CHECKS) checks.push({
|
|
3876
|
+
id: planned.id,
|
|
3877
|
+
title: planned.title,
|
|
3878
|
+
status: "planned",
|
|
3879
|
+
detail: `not built yet (#1098, step ${planned.step})`,
|
|
3880
|
+
problems: [],
|
|
3881
|
+
step: planned.step
|
|
3882
|
+
});
|
|
3883
|
+
const built = checks.filter((c) => c.status !== "planned");
|
|
3884
|
+
const width = Math.max(...built.map((c) => c.title.length));
|
|
3885
|
+
for (const check of built) {
|
|
3886
|
+
print(` ${check.status === "pass" ? "✓" : "✗"} ${check.title.padEnd(width)} ${check.detail}`);
|
|
3887
|
+
for (const problem of check.problems) print(` - ${problem}`);
|
|
3888
|
+
}
|
|
3889
|
+
const planned = checks.filter((c) => c.status === "planned");
|
|
3890
|
+
if (planned.length > 0) {
|
|
3891
|
+
print("\nNot built yet (each fails until it lands, see #1098):");
|
|
3892
|
+
for (const check of planned) print(` ✗ ${String(check.step).padStart(2)} ${check.title}`);
|
|
3893
|
+
}
|
|
3894
|
+
const summary = summarise();
|
|
3895
|
+
print(`\n${summary.pass} passed, ${summary.fail} failed${input.builtOnly ? "" : `, ${summary.planned} not built`}`);
|
|
3896
|
+
return finish();
|
|
3897
|
+
}
|
|
3898
|
+
//#endregion
|
|
3600
3899
|
//#region src/commands/npm-bump-versions/npm-bump-versions.ts
|
|
3601
3900
|
async function runNpmBumpVersions(input) {
|
|
3602
3901
|
const print = input.print ?? ((line) => console.log(line));
|
|
@@ -9733,6 +10032,12 @@ const COMMAND_REGISTRY = [
|
|
|
9733
10032
|
}],
|
|
9734
10033
|
supportsDryRun: true
|
|
9735
10034
|
},
|
|
10035
|
+
{
|
|
10036
|
+
name: "notifications verify",
|
|
10037
|
+
description: "Check the layout against the server and the secrets store, step by step (read-only)",
|
|
10038
|
+
positionals: [],
|
|
10039
|
+
supportsDryRun: false
|
|
10040
|
+
},
|
|
9736
10041
|
{
|
|
9737
10042
|
name: "notifications setup",
|
|
9738
10043
|
description: "Apply the server layout a notifications provider declares in holocron.config.ts",
|
|
@@ -11291,6 +11596,32 @@ try {
|
|
|
11291
11596
|
...argv.scope ? { scope: parseScope(argv.scope) } : {},
|
|
11292
11597
|
...argv.provider ? { provider: argv.provider } : {}
|
|
11293
11598
|
})).status === "fail") process.exitCode = 1;
|
|
11599
|
+
}).command("verify", "Check the layout against the server and the secrets store, step by step (read-only)", (y) => y.option("scope", {
|
|
11600
|
+
type: "string",
|
|
11601
|
+
describe: "Where the layout's secrets live: \"repo\", \"env=<name>\" or \"org=<name>\" (default: the org)"
|
|
11602
|
+
}).option("provider", {
|
|
11603
|
+
type: "string",
|
|
11604
|
+
describe: "Which notifications provider, when several can inspect"
|
|
11605
|
+
}).option("built-only", {
|
|
11606
|
+
type: "boolean",
|
|
11607
|
+
default: false,
|
|
11608
|
+
describe: "Leave out the features that are not built yet (a routine drift check)"
|
|
11609
|
+
}), async (argv) => {
|
|
11610
|
+
const tokens = tokenContext(argv.token);
|
|
11611
|
+
if (!tokens) return;
|
|
11612
|
+
const loaded = await loadConfig(argv.cwd);
|
|
11613
|
+
applyResolvedConfig(argv, loaded.resolved);
|
|
11614
|
+
if ((await runNotificationsVerify({
|
|
11615
|
+
loaded,
|
|
11616
|
+
context: {
|
|
11617
|
+
repoRoot: argv.cwd,
|
|
11618
|
+
...tokens,
|
|
11619
|
+
org: resolveOrg(argv, loaded.resolved)
|
|
11620
|
+
},
|
|
11621
|
+
...argv.scope ? { scope: parseScope(argv.scope) } : {},
|
|
11622
|
+
...argv.provider ? { provider: argv.provider } : {},
|
|
11623
|
+
...argv.builtOnly ? { builtOnly: true } : {}
|
|
11624
|
+
})).status === "fail") process.exitCode = 1;
|
|
11294
11625
|
}).command("$0", false, () => {}, async (argv) => {
|
|
11295
11626
|
await launchGroupMenu("notifications", argv);
|
|
11296
11627
|
}), () => {}).command("sync", "Sync generated files to other repos", (g) => g.command("github", "Sync workflow templates and composite actions to theholocron/.github", (y) => y.option("repo", {
|