@lantharos/pult 0.12.0 → 0.13.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/cli.mjs +51 -7
- package/index.d.mts +44 -7
- package/index.mjs +8 -2
- package/package.json +1 -1
- package/{schema-D7N0bd_N.mjs → schema-DCS7N4sm.mjs} +1 -0
package/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as traceOrigins, L as findActionDef, M as valuesIn, S as kindOf, _ as normalizeFilter, a as collect, c as guards, d as Transport, h as hasPendingAttachments, i as validate, j as shownValue, k as eachBlock, r as toJsonSchema, x as humanize } from "./schema-
|
|
2
|
+
import { C as traceOrigins, L as findActionDef, M as valuesIn, S as kindOf, _ as normalizeFilter, a as collect, c as guards, d as Transport, h as hasPendingAttachments, i as validate, j as shownValue, k as eachBlock, r as toJsonSchema, x as humanize } from "./schema-DCS7N4sm.mjs";
|
|
3
3
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
import { parseArgs, styleText } from "node:util";
|
|
5
5
|
import { Cron } from "croner";
|
|
@@ -184,7 +184,7 @@ function actionChanges(prefix) {
|
|
|
184
184
|
}
|
|
185
185
|
//#endregion
|
|
186
186
|
//#region package.json
|
|
187
|
-
var version = "0.
|
|
187
|
+
var version = "0.13.0";
|
|
188
188
|
//#endregion
|
|
189
189
|
//#region ../protocol/src/checks.ts
|
|
190
190
|
const COLUMNS = [
|
|
@@ -274,6 +274,7 @@ function checkManifest(manifest) {
|
|
|
274
274
|
}
|
|
275
275
|
for (const [name, page] of Object.entries(manifest.pages)) if (page.blocks) checkBlocks(manifest, page.source, `page ${name}`, page.blocks, problems);
|
|
276
276
|
if (manifest.status) checkStatus(manifest, problems);
|
|
277
|
+
for (const [name, rule] of Object.entries(manifest.notify)) checkNotify(manifest, name, rule, problems);
|
|
277
278
|
for (const [name, flag] of Object.entries(manifest.flags)) {
|
|
278
279
|
if (flag.target && !manifest.resources[flag.target]) problems.push({
|
|
279
280
|
source: flag.source,
|
|
@@ -390,6 +391,32 @@ function checkSchedule(name, action, problems) {
|
|
|
390
391
|
message: `${name} runs on a schedule, so it can't need a second approver`
|
|
391
392
|
});
|
|
392
393
|
}
|
|
394
|
+
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
395
|
+
const MIN_ESCALATION_MS = 6e4;
|
|
396
|
+
function checkRecipients(manifest, recipients, problem) {
|
|
397
|
+
for (const recipient of recipients) {
|
|
398
|
+
if ("role" in recipient && !manifest.roles[recipient.role]) problem(`role "${recipient.role}", which doesn't exist`);
|
|
399
|
+
if ("person" in recipient && !EMAIL.test(recipient.person)) problem(`"${recipient.person}", which isn't an email address`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function checkNotify(manifest, name, rule, problems) {
|
|
403
|
+
const problem = (message) => problems.push({
|
|
404
|
+
source: rule.source,
|
|
405
|
+
message: `${name} ${message}`
|
|
406
|
+
});
|
|
407
|
+
if (!rule.on.length) problem("doesn't say when to notify");
|
|
408
|
+
if (!rule.to.length) problem("doesn't say who to notify");
|
|
409
|
+
for (const inbox of rule.inboxes) if (!manifest.inboxes[inbox]) problem(`watches unknown inbox "${inbox}"`);
|
|
410
|
+
checkRecipients(manifest, rule.to, (message) => problem(`notifies ${message}`));
|
|
411
|
+
let previous = 0;
|
|
412
|
+
for (const step of rule.escalate) {
|
|
413
|
+
if (step.after < MIN_ESCALATION_MS) problem("escalates sooner than a minute. Use 1m or more.");
|
|
414
|
+
else if (step.after <= previous) problem("escalates out of order. Each step should come later than the one before.");
|
|
415
|
+
if (!step.to.length) problem("has an escalation step that doesn't say who to notify");
|
|
416
|
+
checkRecipients(manifest, step.to, (message) => problem(`escalates to ${message}`));
|
|
417
|
+
previous = step.after;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
393
420
|
const COMPONENT = /^[a-z0-9][a-z0-9-]*$/;
|
|
394
421
|
const WEB_ADDRESS = /^https?:\/\/[^\s/$.?#].\S*$/;
|
|
395
422
|
const CSS_LIMIT = 5e4;
|
|
@@ -406,7 +433,7 @@ function checkStatus(manifest, problems) {
|
|
|
406
433
|
if (component.monitored && component.check) problem(`component ${name} can follow your app or a check, not both`);
|
|
407
434
|
if (component.check && !WEB_ADDRESS.test(component.check)) problem(`component ${name} checks "${component.check}", which isn't a web address`);
|
|
408
435
|
}
|
|
409
|
-
|
|
436
|
+
checkRecipients(manifest, status.alert, (message) => problem(`alerts ${message}`));
|
|
410
437
|
if (status.after < 6e4) problem("waits less than a minute before calling your app down. Use 1m or more.");
|
|
411
438
|
for (const [what, url] of [
|
|
412
439
|
["logo", status.logo?.light],
|
|
@@ -726,15 +753,32 @@ function byDefinition(a, b) {
|
|
|
726
753
|
return a.origin.file.localeCompare(b.origin.file) || a.origin.line - b.origin.line;
|
|
727
754
|
}
|
|
728
755
|
function notifyDef(locator, rule) {
|
|
756
|
+
const { options } = rule;
|
|
729
757
|
return {
|
|
730
758
|
source: locator.call("notify", rule.origin, rule.name),
|
|
731
|
-
inboxes:
|
|
732
|
-
on:
|
|
733
|
-
|
|
759
|
+
inboxes: options.inboxes.map((inbox) => inbox.name),
|
|
760
|
+
on: options.on,
|
|
761
|
+
where: options.where ? whereDef(options.where) : null,
|
|
762
|
+
to: recipients(options.to),
|
|
763
|
+
escalate: (options.escalate ?? []).map((step) => ({
|
|
764
|
+
after: milliseconds(step.after),
|
|
765
|
+
to: recipients(step.to)
|
|
766
|
+
}))
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function whereDef(where) {
|
|
770
|
+
return {
|
|
771
|
+
tags: where.tags ?? [],
|
|
772
|
+
data: Object.fromEntries(Object.entries(where.data ?? {}).map(([field, value]) => [field, Array.isArray(value) ? value : [value]]))
|
|
734
773
|
};
|
|
735
774
|
}
|
|
736
775
|
function recipients(list) {
|
|
737
|
-
return list.map((recipient) =>
|
|
776
|
+
return list.map((recipient) => {
|
|
777
|
+
if ("channel" in recipient) return { channel: recipient.channel };
|
|
778
|
+
if ("person" in recipient) return { person: recipient.person };
|
|
779
|
+
if ("assignee" in recipient) return { assignee: true };
|
|
780
|
+
return { role: recipient.name };
|
|
781
|
+
});
|
|
738
782
|
}
|
|
739
783
|
function logo(value) {
|
|
740
784
|
if (value === void 0) return null;
|
package/index.d.mts
CHANGED
|
@@ -92,6 +92,7 @@ interface ItemSummary {
|
|
|
92
92
|
triage: "pending" | "done" | null;
|
|
93
93
|
autoHandled: boolean;
|
|
94
94
|
dueAt: number | null;
|
|
95
|
+
urgent: boolean;
|
|
95
96
|
fields: Record<string, FieldValue>;
|
|
96
97
|
}
|
|
97
98
|
//#endregion
|
|
@@ -403,19 +404,34 @@ interface FlagGuard {
|
|
|
403
404
|
perHour: number;
|
|
404
405
|
then: "pause" | "revert";
|
|
405
406
|
}
|
|
406
|
-
type NotifyTrigger = "created" | "reopened" | "overdue" | {
|
|
407
|
+
type NotifyTrigger = "created" | "reopened" | "overdue" | "urgent" | {
|
|
407
408
|
spike: number;
|
|
408
409
|
};
|
|
409
|
-
type Recipient = {
|
|
410
|
+
type Recipient$1 = {
|
|
410
411
|
role: string;
|
|
411
412
|
} | {
|
|
412
413
|
channel: string;
|
|
414
|
+
} | {
|
|
415
|
+
person: string;
|
|
416
|
+
} | {
|
|
417
|
+
assignee: true;
|
|
413
418
|
};
|
|
419
|
+
type MatchValue = string | number | boolean;
|
|
420
|
+
interface NotifyWhere$1 {
|
|
421
|
+
tags: string[];
|
|
422
|
+
data: Record<string, MatchValue[]>;
|
|
423
|
+
}
|
|
424
|
+
interface Escalation {
|
|
425
|
+
after: number;
|
|
426
|
+
to: Recipient$1[];
|
|
427
|
+
}
|
|
414
428
|
interface NotifyDef {
|
|
415
429
|
source: string;
|
|
416
430
|
inboxes: string[];
|
|
417
431
|
on: NotifyTrigger[];
|
|
418
|
-
|
|
432
|
+
where: NotifyWhere$1 | null;
|
|
433
|
+
to: Recipient$1[];
|
|
434
|
+
escalate: Escalation[];
|
|
419
435
|
}
|
|
420
436
|
interface StatusComponentDef {
|
|
421
437
|
label: string;
|
|
@@ -434,7 +450,7 @@ interface StatusDef<Component extends string = string> {
|
|
|
434
450
|
website: string | null;
|
|
435
451
|
css: string | null;
|
|
436
452
|
components: Record<Component, StatusComponentDef>;
|
|
437
|
-
alert: Recipient[];
|
|
453
|
+
alert: Recipient$1[];
|
|
438
454
|
after: number;
|
|
439
455
|
}
|
|
440
456
|
type Delivery = {
|
|
@@ -902,11 +918,30 @@ export declare function space(name: string, options: SpaceOptions): Space;
|
|
|
902
918
|
interface Channel {
|
|
903
919
|
readonly channel: string;
|
|
904
920
|
}
|
|
921
|
+
interface Person {
|
|
922
|
+
readonly person: string;
|
|
923
|
+
}
|
|
924
|
+
interface Assignee {
|
|
925
|
+
readonly assignee: true;
|
|
926
|
+
}
|
|
927
|
+
type Recipient = Role | Channel | Person | Assignee;
|
|
905
928
|
export declare function slack(channel: string): Channel;
|
|
929
|
+
export declare function person(email: string): Person;
|
|
930
|
+
export declare const assignee: Assignee;
|
|
931
|
+
interface NotifyWhere {
|
|
932
|
+
tags?: string[];
|
|
933
|
+
data?: Record<string, MatchValue | MatchValue[]>;
|
|
934
|
+
}
|
|
935
|
+
interface EscalationStep {
|
|
936
|
+
after: Duration;
|
|
937
|
+
to: Recipient[];
|
|
938
|
+
}
|
|
906
939
|
interface NotifyOptions {
|
|
907
940
|
inboxes: Inbox[];
|
|
908
941
|
on: NotifyTrigger[];
|
|
909
|
-
|
|
942
|
+
where?: NotifyWhere;
|
|
943
|
+
to: Recipient[];
|
|
944
|
+
escalate?: EscalationStep[];
|
|
910
945
|
}
|
|
911
946
|
interface Notify extends Located {
|
|
912
947
|
readonly name: string;
|
|
@@ -930,7 +965,7 @@ interface StatusOptions {
|
|
|
930
965
|
website?: string;
|
|
931
966
|
css?: string;
|
|
932
967
|
components: Record<string, string | StatusComponent>;
|
|
933
|
-
alert?: (Role | Channel)[];
|
|
968
|
+
alert?: (Role | Channel | Person)[];
|
|
934
969
|
after?: Duration;
|
|
935
970
|
}
|
|
936
971
|
interface Status extends Located {
|
|
@@ -976,6 +1011,8 @@ interface SendOptions<S extends Schema | undefined, Suggested extends unknown[]
|
|
|
976
1011
|
data: InboxInput<S>;
|
|
977
1012
|
tags?: string[];
|
|
978
1013
|
suggest?: { [K in keyof Suggested]: Suggestion<Suggested[K]>; };
|
|
1014
|
+
urgent?: boolean;
|
|
1015
|
+
assign?: string;
|
|
979
1016
|
}
|
|
980
1017
|
interface ReceivedMessage extends Omit<IncomingMessage, "attachments"> {
|
|
981
1018
|
attachments?: BlockInput[];
|
|
@@ -1006,4 +1043,4 @@ export declare function createPult<Component extends string = string>({ key, pro
|
|
|
1006
1043
|
};
|
|
1007
1044
|
type Pult = ReturnType<typeof createPult>;
|
|
1008
1045
|
//#endregion
|
|
1009
|
-
export type { Action, ActionContext, ActionOptions, ActionRef, Actor, Block, BlockInput, ButtonInput, Channel, ComponentState, Config, ConfigOptions, Duration, FieldInput, Flag, FlagContext, FlagOptions, Guard, Inbox, InboxOptions, ItemView, ListOptions, ListQuery, ListenOptions, Manifest, Notify, NotifyOptions, OutgoingEmail, Page, PageContext, PageOptions, Project, Pult, PultOptions, Question, ReceivedMessage, Relation, Resource, ResourceOptions, Role, RoleOptions, Rule, SendOptions, SendResult, Space, SpaceOptions, StandaloneOptions, StateChange, Status, StatusOptions, Suggestion, Triage, View, ViewOptions };
|
|
1046
|
+
export type { Action, ActionContext, ActionOptions, ActionRef, Actor, Assignee, Block, BlockInput, ButtonInput, Channel, ComponentState, Config, ConfigOptions, Duration, EscalationStep, FieldInput, Flag, FlagContext, FlagOptions, Guard, Inbox, InboxOptions, ItemView, ListOptions, ListQuery, ListenOptions, Manifest, Notify, NotifyOptions, NotifyWhere, OutgoingEmail, Page, PageContext, PageOptions, Person, Project, Pult, PultOptions, Question, ReceivedMessage, Recipient, Relation, Resource, ResourceOptions, Role, RoleOptions, Rule, SendOptions, SendResult, Space, SpaceOptions, StandaloneOptions, StateChange, Status, StatusOptions, Suggestion, Triage, View, ViewOptions };
|
package/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as mapBlocks, D as sign, E as NonceWindow, F as HOME, I as actionId, L as findActionDef, M as valuesIn, N as DEFAULT_STATES, O as verify, S as kindOf, T as refKey, a as collect, b as captureOrigin, d as Transport, f as b, g as isPendingAttachment, h as hasPendingAttachments, i as validate, k as eachBlock, m as fieldValues, n as reviveDates, o as findAction, p as bindActions, s as flag, t as ValidationError, u as PultError, v as view, w as HEADER, y as KIND } from "./schema-
|
|
1
|
+
import { A as mapBlocks, D as sign, E as NonceWindow, F as HOME, I as actionId, L as findActionDef, M as valuesIn, N as DEFAULT_STATES, O as verify, S as kindOf, T as refKey, a as collect, b as captureOrigin, d as Transport, f as b, g as isPendingAttachment, h as hasPendingAttachments, i as validate, k as eachBlock, m as fieldValues, n as reviveDates, o as findAction, p as bindActions, s as flag, t as ValidationError, u as PultError, v as view, w as HEADER, y as KIND } from "./schema-DCS7N4sm.mjs";
|
|
2
2
|
//#region ../protocol/src/flags.ts
|
|
3
3
|
function rampAt(ramp, now = Date.now()) {
|
|
4
4
|
if (now >= ramp.end) return ramp.to;
|
|
@@ -843,6 +843,8 @@ function createPult({ key = env("PULT_KEY"), project, url = env("PULT_URL") ?? "
|
|
|
843
843
|
inbox: inbox.name,
|
|
844
844
|
fingerprint: item.fingerprint,
|
|
845
845
|
tags: item.tags,
|
|
846
|
+
urgent: item.urgent,
|
|
847
|
+
assign: item.assign,
|
|
846
848
|
suggest: item.suggest?.map(({ action, ...rest }) => ({
|
|
847
849
|
action: action.id,
|
|
848
850
|
...rest
|
|
@@ -1039,6 +1041,10 @@ function status(options) {
|
|
|
1039
1041
|
function slack(channel) {
|
|
1040
1042
|
return { channel: channel.replace(/^#/, "") };
|
|
1041
1043
|
}
|
|
1044
|
+
function person(email) {
|
|
1045
|
+
return { person: email.trim().toLowerCase() };
|
|
1046
|
+
}
|
|
1047
|
+
const assignee = { assignee: true };
|
|
1042
1048
|
function notify(name, options) {
|
|
1043
1049
|
return {
|
|
1044
1050
|
[KIND]: "notify",
|
|
@@ -1048,4 +1054,4 @@ function notify(name, options) {
|
|
|
1048
1054
|
};
|
|
1049
1055
|
}
|
|
1050
1056
|
//#endregion
|
|
1051
|
-
export { PultError, ValidationError, action, b, choice, createPult, defineConfig, flag, home, inbox, notify, noul, page, resource, role, run, score, setState, slack, space, status, tag, triage, view };
|
|
1057
|
+
export { PultError, ValidationError, action, assignee, b, choice, createPult, defineConfig, flag, home, inbox, notify, noul, page, person, resource, role, run, score, setState, slack, space, status, tag, triage, view };
|
package/package.json
CHANGED
|
@@ -590,6 +590,7 @@ function collect(modules) {
|
|
|
590
590
|
claim(found.notify, rule, "notifications");
|
|
591
591
|
for (const inbox of rule.options.inboxes) add(inbox);
|
|
592
592
|
for (const recipient of rule.options.to) add(recipient);
|
|
593
|
+
for (const step of rule.options.escalate ?? []) for (const recipient of step.to) add(recipient);
|
|
593
594
|
return;
|
|
594
595
|
}
|
|
595
596
|
case "status": {
|