@mikitasazan/notify 1.4.0 → 1.4.2
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/README.md +22 -16
- package/dist/cli-flags.js +3 -2
- package/dist/cli.js +70 -29
- package/dist/events.d.ts +132 -27
- package/dist/events.js +77 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/render.d.ts +10 -1
- package/dist/render.js +198 -80
- package/dist/routes.js +1 -1
- package/dist/send.js +21 -19
- package/dist/setup.js +9 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,36 +85,42 @@ notify report --project playhub --json < payload.json # весь объект
|
|
|
85
85
|
| `pr` | событие пул-реквеста | `project`, `action`, `number`, `title` |
|
|
86
86
|
| `issue` | событие задачи | `project`, `action`, `number`, `title` |
|
|
87
87
|
| `incident` | приложение сломалось прямо сейчас | `project`, `title` |
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
88
|
+
| `session` | рабочая сессия на маке в беде | `project`, `action` |
|
|
89
|
+
| `heartbeat_miss` | УСТАРЕЛ, молчание — это `job --status silent` | `project`, `job` |
|
|
90
|
+
|
|
91
|
+
Отдельного вида «файл» нет: `--path` применим к ЛЮБОМУ событию, и тогда
|
|
92
|
+
карточка едет подписью к вложению (подпись у Telegram ограничена 1024 знаками,
|
|
93
|
+
а не 4000). Слово `file` осталось псевдонимом и собирает `report` с вложением —
|
|
94
|
+
старый отправитель не замолкает.
|
|
90
95
|
|
|
91
96
|
Полные сигнатуры — `src/events.ts`.
|
|
92
97
|
|
|
93
|
-
**Ключ задачи.**
|
|
94
|
-
По
|
|
98
|
+
**Ключ задачи.** Первая строка каждой карточки — два тега: `#тип #ключ`.
|
|
99
|
+
По ним дневной разборщик сверяет «это 🔴 уже закрыто более поздней карточкой
|
|
95
100
|
той же задачи?» без сравнения человеческих формулировок. Явный `--key`
|
|
96
101
|
побеждает; без него ключ выводится из заголовка и меняется вместе с ним —
|
|
97
102
|
регулярный отправитель передаёт `--key` явно. В stdout CLI ключ не попадает.
|
|
98
103
|
|
|
99
104
|
**Единственная дверь для свободного HTML** — `sendReport()` (`report --json` у
|
|
100
105
|
аналитик): плотную строку дневного отчёта не разложить в `label=value` не
|
|
101
|
-
испортив. Стандартизирован транспорт, формат — нет, и только здесь.
|
|
106
|
+
испортив. Стандартизирован транспорт, формат тела — нет, и только здесь.
|
|
107
|
+
Строку тегов `#report #ключ` пакет ставит и здесь: тег — фильтр владельца, к
|
|
108
|
+
формату тела он отношения не имеет. Без явного ключа берётся `daily`.
|
|
102
109
|
|
|
103
110
|
**Неизвестный проект** не роняет вызвавший крон (код возврата 0), но больше и
|
|
104
111
|
не исчезает молча: в mac-config Ops уходит красная карточка «notify: событие
|
|
105
112
|
потеряно». До 18.08.2026 опечатка в `--project` терялась без следа неделями.
|
|
106
113
|
|
|
107
|
-
`--action` у `pr`: `opened`, `
|
|
108
|
-
`
|
|
109
|
-
|
|
110
|
-
`
|
|
111
|
-
подмена: иначе «запрошены правки» приехали бы как «открыт».
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
`
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
три карточки подряд. Правило владельца от 27.07.2026 — одна новость, одна
|
|
114
|
+
`--action` у `pr`: `opened`, `approved`, `changes_requested`, `merged`,
|
|
115
|
+
`closed`. У `issue`: `opened`, `assigned`, `closed`. Сырые имена GitHub, которые
|
|
116
|
+
означают то же самое, сводятся к ним: `reopened`, `ready_for_review` и
|
|
117
|
+
`review_requested` — это всё `opened`. Неизвестное действие — ошибка разбора, а
|
|
118
|
+
не молчаливая подмена: иначе «запрошены правки» приехали бы как «открыт».
|
|
119
|
+
|
|
120
|
+
**Из GitHub в «⚙️ Ops» уходит не всё, что там происходит.** Общий workflow
|
|
121
|
+
`.github/workflows/ops-notify.yml` отбрасывает `ready_for_review` и
|
|
122
|
+
`review_requested` ещё на входе: оба сообщают про PR, который уже объявлен
|
|
123
|
+
открытым, и открытие одного PR давало три карточки подряд. Правило владельца от 27.07.2026 — одна новость, одна
|
|
118
124
|
карточка: **открыт → вердикт ревью (👍 / 📝) → закрыт или смёржен**. Белый список
|
|
119
125
|
живёт в общем workflow, а не в подписках проектов, потому что подписка обязана
|
|
120
126
|
лежать в вызывающем репозитории и в четырёх проектах расходится сама собой;
|
package/dist/cli-flags.js
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export const KNOWN_FLAGS = new Set([
|
|
12
12
|
'action', 'actor', 'assignee', 'author', 'body', 'branch', 'commit',
|
|
13
|
-
'
|
|
14
|
-
'
|
|
13
|
+
'id', 'opened', 'reason', 'workdir',
|
|
14
|
+
'command', 'command-note', 'commit-body', 'commit-title', 'commit-url', 'detail',
|
|
15
|
+
'expected', 'filename', 'item', 'job', 'key', 'last-seen', 'line', 'logs', 'note',
|
|
15
16
|
'number', 'path', 'period', 'project', 'reviewer', 'stat', 'status',
|
|
16
17
|
'target', 'title', 'url', 'via', 'workflow-name', 'workflow-url',
|
|
17
18
|
// Флаги без значения. Живут здесь же, чтобы разбор и список не разошлись.
|
package/dist/cli.js
CHANGED
|
@@ -26,13 +26,25 @@ import { render } from "./render.js";
|
|
|
26
26
|
import { notify } from "./send.js";
|
|
27
27
|
import { setupTopic } from "./setup.js";
|
|
28
28
|
const log = (msg) => console.error(`[notify] ${msg}`);
|
|
29
|
+
/**
|
|
30
|
+
* Anything taken from the caller and echoed into a message goes through this.
|
|
31
|
+
*
|
|
32
|
+
* `sent`, `failed` and `skipped` are a CONTRACT: `notify-fail.sh` greps
|
|
33
|
+
* `^\[notify\] sent$`, the server-side silence watchdog matches `*sent*`, and
|
|
34
|
+
* `action.yml` warns on `*failed*|*skipped*`. An input value carrying one of
|
|
35
|
+
* those words puts it on stderr — `notify sent --project=x` printed
|
|
36
|
+
* `unknown event type: sent`, and the watchdog read a card that was never
|
|
37
|
+
* delivered as delivered, then stopped repeating the alarm. Found by Codex,
|
|
38
|
+
* 25.08.2026.
|
|
39
|
+
*/
|
|
40
|
+
const safe = (v) => String(v ?? '').replace(/\b(sent|failed|skipped)\b/gi, (w) => `${w[0]}·${w.slice(1)}`);
|
|
29
41
|
const args = process.argv.slice(2);
|
|
30
42
|
const command = args[0];
|
|
31
43
|
if (command === 'setup') {
|
|
32
44
|
const [, chatId, projectKey] = args;
|
|
33
45
|
if (!chatId || !projectKey) {
|
|
34
|
-
log('
|
|
35
|
-
log('
|
|
46
|
+
log('usage: notify setup <forum chat_id> <project key>');
|
|
47
|
+
log(' create the group first, turn Topics on in it, add the bot as admin');
|
|
36
48
|
process.exit(0);
|
|
37
49
|
}
|
|
38
50
|
await setupTopic(chatId, projectKey);
|
|
@@ -45,7 +57,7 @@ const BOOLEAN_FLAGS = new Set(['json', 'recovered', 'dry-run']);
|
|
|
45
57
|
for (let i = 1; i < args.length; i++) {
|
|
46
58
|
const arg = args[i];
|
|
47
59
|
if (!arg.startsWith('--')) {
|
|
48
|
-
parseErrors.push(`stray argument with no flag: "${arg}"`);
|
|
60
|
+
parseErrors.push(`stray argument with no flag: "${safe(arg)}"`);
|
|
49
61
|
continue;
|
|
50
62
|
}
|
|
51
63
|
// Форма `--key=value` обязательна для значений, начинающихся с `--`
|
|
@@ -67,7 +79,7 @@ for (let i = 1; i < args.length; i++) {
|
|
|
67
79
|
// ТЕРЯЛОСЬ ВСЁ сообщение, а `--status` без значения рисовал 🔴 на
|
|
68
80
|
// успешном деплое. Теперь это явная ошибка разбора.
|
|
69
81
|
if (next === undefined || next.startsWith('--')) {
|
|
70
|
-
parseErrors.push(`flag --${key} with no value`);
|
|
82
|
+
parseErrors.push(`flag --${safe(key)} with no value`);
|
|
71
83
|
continue;
|
|
72
84
|
}
|
|
73
85
|
i++;
|
|
@@ -76,7 +88,7 @@ for (let i = 1; i < args.length; i++) {
|
|
|
76
88
|
const one = (key) => flags.get(key)?.[0];
|
|
77
89
|
for (const key of flags.keys()) {
|
|
78
90
|
if (!KNOWN_FLAGS.has(key)) {
|
|
79
|
-
parseErrors.push(`unknown flag --${key}`);
|
|
91
|
+
parseErrors.push(`unknown flag --${safe(key)}`);
|
|
80
92
|
}
|
|
81
93
|
}
|
|
82
94
|
// Число с явной ошибкой разбора, иначе рендер рисовал «PR #NaN».
|
|
@@ -84,7 +96,7 @@ const num = (key) => {
|
|
|
84
96
|
const raw = one(key);
|
|
85
97
|
const n = Number(raw);
|
|
86
98
|
if (raw === undefined || Number.isNaN(n)) {
|
|
87
|
-
parseErrors.push(`--${key}:
|
|
99
|
+
parseErrors.push(`--${safe(key)}: expected a number, got "${safe(raw)}"`);
|
|
88
100
|
return 0;
|
|
89
101
|
}
|
|
90
102
|
return n;
|
|
@@ -102,8 +114,10 @@ const project = () => one('project');
|
|
|
102
114
|
const PR_ALIASES = {
|
|
103
115
|
opened: 'opened',
|
|
104
116
|
reopened: 'opened',
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
// A PR that is already announced is not announced again: both of these mean
|
|
118
|
+
// "this PR now wants eyes", which is what `opened` already says.
|
|
119
|
+
ready_for_review: 'opened',
|
|
120
|
+
review_requested: 'opened',
|
|
107
121
|
approved: 'approved',
|
|
108
122
|
changes_requested: 'changes_requested',
|
|
109
123
|
merged: 'merged',
|
|
@@ -121,7 +135,7 @@ const ISSUE_ALIASES = {
|
|
|
121
135
|
const prAction = (raw) => {
|
|
122
136
|
const hit = PR_ALIASES[(raw ?? '').toLowerCase()];
|
|
123
137
|
if (!hit) {
|
|
124
|
-
parseErrors.push(`--action: unknown PR action "${raw
|
|
138
|
+
parseErrors.push(`--action: unknown PR action "${safe(raw)}" (${Object.keys(PR_ALIASES).join(', ')})`);
|
|
125
139
|
return 'opened';
|
|
126
140
|
}
|
|
127
141
|
return hit;
|
|
@@ -153,6 +167,9 @@ const jobStatus = () => {
|
|
|
153
167
|
if (raw === 'disabled') {
|
|
154
168
|
return 'disabled';
|
|
155
169
|
}
|
|
170
|
+
if (raw === 'silent') {
|
|
171
|
+
return 'silent';
|
|
172
|
+
}
|
|
156
173
|
return raw === 'ok' || raw === 'success' || raw === 'passed' || raw === '0' ? 'ok' : 'fail';
|
|
157
174
|
};
|
|
158
175
|
let event;
|
|
@@ -166,7 +183,7 @@ if (flags.has('json')) {
|
|
|
166
183
|
catch (err) {
|
|
167
184
|
// Тоже в parseErrors: обе аналитики зовут CLI через `|| true`, и молчащий
|
|
168
185
|
// разбор JSON означал бы зелёный крон без дневного отчёта.
|
|
169
|
-
parseErrors.push(`--json from stdin did not parse: ${err instanceof Error ? err.message :
|
|
186
|
+
parseErrors.push(`--json from stdin did not parse: ${safe(err instanceof Error ? err.message : err)}`);
|
|
170
187
|
}
|
|
171
188
|
}
|
|
172
189
|
else {
|
|
@@ -192,11 +209,16 @@ else {
|
|
|
192
209
|
event = {
|
|
193
210
|
type: 'job',
|
|
194
211
|
project: project(),
|
|
195
|
-
job: one('job') ?? '(
|
|
212
|
+
job: one('job') ?? '(no name)',
|
|
196
213
|
status: jobStatus(),
|
|
214
|
+
expected: one('expected'),
|
|
215
|
+
lastSeen: one('last-seen'),
|
|
197
216
|
stats: pairs('stat'),
|
|
198
217
|
items: items(),
|
|
199
218
|
note: one('note'),
|
|
219
|
+
command: one('command'),
|
|
220
|
+
commandNote: one('command-note'),
|
|
221
|
+
logs: one('logs'),
|
|
200
222
|
workflowUrl: one('workflow-url'),
|
|
201
223
|
workflowName: one('workflow-name'),
|
|
202
224
|
url: one('url')
|
|
@@ -206,7 +228,7 @@ else {
|
|
|
206
228
|
event = {
|
|
207
229
|
type: 'report',
|
|
208
230
|
project: project(),
|
|
209
|
-
title: one('title') ?? '(
|
|
231
|
+
title: one('title') ?? '(no title)',
|
|
210
232
|
period: one('period'),
|
|
211
233
|
lines: pairs('line'),
|
|
212
234
|
items: items(),
|
|
@@ -236,7 +258,7 @@ else {
|
|
|
236
258
|
project: project(),
|
|
237
259
|
action: prAction(one('action')),
|
|
238
260
|
number: num('number'),
|
|
239
|
-
title: one('title') ?? '(
|
|
261
|
+
title: one('title') ?? '(no title)',
|
|
240
262
|
body: one('body'),
|
|
241
263
|
author: one('author'),
|
|
242
264
|
reviewer: one('reviewer'),
|
|
@@ -249,18 +271,33 @@ else {
|
|
|
249
271
|
project: project(),
|
|
250
272
|
action: issueAction(one('action')),
|
|
251
273
|
number: num('number'),
|
|
252
|
-
title: one('title') ?? '(
|
|
274
|
+
title: one('title') ?? '(no title)',
|
|
253
275
|
body: one('body'),
|
|
254
276
|
author: one('author'),
|
|
255
277
|
assignee: one('assignee'),
|
|
256
278
|
url: one('url')
|
|
257
279
|
};
|
|
258
280
|
break;
|
|
281
|
+
case 'session':
|
|
282
|
+
event = {
|
|
283
|
+
type: 'session',
|
|
284
|
+
project: project(),
|
|
285
|
+
action: one('action') ?? 'in trouble',
|
|
286
|
+
id: one('id'),
|
|
287
|
+
workdir: one('workdir'),
|
|
288
|
+
reason: one('reason'),
|
|
289
|
+
opened: one('opened'),
|
|
290
|
+
command: one('command'),
|
|
291
|
+
commandNote: one('command-note'),
|
|
292
|
+
// Only two states here, so `disabled` must not leak in from jobStatus.
|
|
293
|
+
status: jobStatus() === 'ok' ? 'ok' : 'fail'
|
|
294
|
+
};
|
|
295
|
+
break;
|
|
259
296
|
case 'incident':
|
|
260
297
|
event = {
|
|
261
298
|
type: 'incident',
|
|
262
299
|
project: project(),
|
|
263
|
-
title: one('title') ?? '(
|
|
300
|
+
title: one('title') ?? '(no title)',
|
|
264
301
|
detail: one('detail'),
|
|
265
302
|
logs: one('logs'),
|
|
266
303
|
url: one('url')
|
|
@@ -270,33 +307,31 @@ else {
|
|
|
270
307
|
event = {
|
|
271
308
|
type: 'heartbeat_miss',
|
|
272
309
|
project: project(),
|
|
273
|
-
job: one('job') ?? '(
|
|
310
|
+
job: one('job') ?? '(no name)',
|
|
274
311
|
lastSeen: one('last-seen'),
|
|
275
312
|
expected: one('expected'),
|
|
276
313
|
recovered: flags.has('recovered'),
|
|
277
314
|
note: one('note')
|
|
278
315
|
};
|
|
279
316
|
break;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
317
|
+
// `file` is no longer a kind of event — an attachment is a property any
|
|
318
|
+
// card may have. The word is kept as an alias so senders that still say
|
|
319
|
+
// `notify file` deliver a report card with the log attached, instead of
|
|
320
|
+
// falling into the unknown-type branch and going silent.
|
|
321
|
+
case 'file':
|
|
285
322
|
event = {
|
|
286
|
-
type: '
|
|
323
|
+
type: 'report',
|
|
287
324
|
project: project(),
|
|
288
|
-
title: one('title') ?? '(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
note: one('note')
|
|
325
|
+
title: one('title') ?? '(no title)',
|
|
326
|
+
period: one('note'),
|
|
327
|
+
lines: []
|
|
292
328
|
};
|
|
293
329
|
break;
|
|
294
|
-
}
|
|
295
330
|
default:
|
|
296
331
|
// В parseErrors, а не просто в лог: иначе неизвестный тип уходил в
|
|
297
332
|
// тишину — событие не собиралось, ошибок разбора не было, и CLI выходил
|
|
298
333
|
// нулём, ничего не отправив и ничего об этом не сказав.
|
|
299
|
-
parseErrors.push(`unknown event type: ${command ?? '(none given)'}`);
|
|
334
|
+
parseErrors.push(`unknown event type: ${safe(command ?? '(none given)')}`);
|
|
300
335
|
}
|
|
301
336
|
}
|
|
302
337
|
// --key применим к любому типу — одна точка вместо строки в каждом case
|
|
@@ -304,6 +339,12 @@ else {
|
|
|
304
339
|
// key в самом объекте, отсутствие флага не должно его затирать.
|
|
305
340
|
if (event) {
|
|
306
341
|
event.key = one('key') ?? event.key;
|
|
342
|
+
// --path is applicable to any type too, for the same reason --key is.
|
|
343
|
+
event.path = one('path') ?? event.path;
|
|
344
|
+
event.filename = one('filename') ?? event.filename;
|
|
345
|
+
}
|
|
346
|
+
if (event?.filename && !event.path) {
|
|
347
|
+
parseErrors.push('--filename: given without --path, so there is no file to name');
|
|
307
348
|
}
|
|
308
349
|
// Ошибки разбора — до отправки: лучше внятно сказать, что не так с командой,
|
|
309
350
|
// чем прислать сообщение с «true» вместо ссылки или 🔴 на успешном деплое.
|
|
@@ -343,7 +384,7 @@ if (event) {
|
|
|
343
384
|
catch (err) {
|
|
344
385
|
// Слово `failed` — контракт, тот же, что у ошибки разбора выше. Без него
|
|
345
386
|
// исключение при отправке читалось сторожами как «ничего не случилось».
|
|
346
|
-
log(`failed: ${err instanceof Error ? err.message :
|
|
387
|
+
log(`failed: ${safe(err instanceof Error ? err.message : err)}`);
|
|
347
388
|
}
|
|
348
389
|
}
|
|
349
390
|
process.exit(0);
|
package/dist/events.d.ts
CHANGED
|
@@ -22,8 +22,18 @@ export type Project = 'playhub' | 'one-q' | 'arvent' | 'game-publisher' | 'vault
|
|
|
22
22
|
* потоком по словам `sent|failed|skipped`) ключ не попадает никогда — новое
|
|
23
23
|
* слово там ослепило бы сторожа.
|
|
24
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* Общая часть любого события. `path` — локальный файл, который едет ВМЕСТЕ с
|
|
27
|
+
* карточкой: карточка становится подписью к вложению. Отдельного вида `file`
|
|
28
|
+
* нет с 25.08.2026 — прогон Arvent слал вердикт и лог двумя карточками про
|
|
29
|
+
* одну новость.
|
|
30
|
+
*/
|
|
25
31
|
type Keyed = {
|
|
26
32
|
key?: string;
|
|
33
|
+
/** Локальный файл; карточка уедет как подпись к нему (лимит подписи 1024). */
|
|
34
|
+
path?: string;
|
|
35
|
+
/** Имя файла в чате; по умолчанию — имя из `path`. */
|
|
36
|
+
filename?: string;
|
|
27
37
|
};
|
|
28
38
|
/**
|
|
29
39
|
* Позиция списка внутри сообщения: задача из дайджеста, упавшая проверка,
|
|
@@ -49,12 +59,12 @@ export type NotifyEvent = Keyed & (
|
|
|
49
59
|
commit?: string;
|
|
50
60
|
/** Ссылка на коммит — строка «коммит» становится кликабельной. */
|
|
51
61
|
commitUrl?: string;
|
|
52
|
-
/** Заголовок коммита — рендерится
|
|
62
|
+
/** Заголовок коммита — рендерится полем `Title:`, тело идёт цитатой ниже. */
|
|
53
63
|
commitTitle?: string;
|
|
54
64
|
/** Тело коммита, если есть — та же цитата, что и заголовок. */
|
|
55
65
|
commitBody?: string;
|
|
56
66
|
workflowUrl?: string;
|
|
57
|
-
/** Название прогона для видимого текста ссылки (по умолчанию —
|
|
67
|
+
/** Название прогона для видимого текста ссылки (по умолчанию — `open`). */
|
|
58
68
|
workflowName?: string;
|
|
59
69
|
url?: string;
|
|
60
70
|
/**
|
|
@@ -78,14 +88,48 @@ export type NotifyEvent = Keyed & (
|
|
|
78
88
|
type: 'job';
|
|
79
89
|
project: Project;
|
|
80
90
|
job: string;
|
|
81
|
-
/**
|
|
82
|
-
|
|
91
|
+
/**
|
|
92
|
+
* `disabled` — задача выключена извне (например GitHub Actions кончил
|
|
93
|
+
* бесплатные минуты), не провалилась сама.
|
|
94
|
+
*
|
|
95
|
+
* `silent` — задача не отчиталась в срок: она не упала, она вообще не
|
|
96
|
+
* подала признаков жизни. Это состояние ЗАДАЧИ, а не отдельный вид
|
|
97
|
+
* события — оно жило типом `heartbeat_miss`, и владелец справедливо
|
|
98
|
+
* спросил, почему задача по расписанию у него под двумя разными тегами.
|
|
99
|
+
* Хуже того: сторож молчания шлёт тот же машинный ключ, что и сама
|
|
100
|
+
* задача, так что красная карточка `#heartbeat #daily_import` не
|
|
101
|
+
* закрывалась зелёной `#job #daily_import` — разборщик ищет пару по
|
|
102
|
+
* ПОЛНОМУ тегу. Один поток на задачу это чинит.
|
|
103
|
+
*/
|
|
104
|
+
status: 'ok' | 'fail' | 'disabled' | 'silent';
|
|
105
|
+
/** Как часто задача обязана отмечаться — для `silent` и для возврата из него. */
|
|
106
|
+
expected?: string;
|
|
107
|
+
/** Когда её видели в последний раз. */
|
|
108
|
+
lastSeen?: string;
|
|
83
109
|
stats?: Array<[label: string, value: string | number]>;
|
|
84
110
|
/** Детали: что именно упало, замечания прогона; у `disabled` — список выключенных процессов (каждый со своей ссылкой). */
|
|
85
111
|
items?: Item[];
|
|
86
112
|
note?: string;
|
|
113
|
+
/**
|
|
114
|
+
* A command for him to run, rendered monospaced so Telegram makes it
|
|
115
|
+
* tap-to-copy. For the case where the card names something on this Mac
|
|
116
|
+
* that no URL can reach — a stopped local session, a latch file.
|
|
117
|
+
*/
|
|
118
|
+
command?: string;
|
|
119
|
+
/**
|
|
120
|
+
* WHAT that command does. The owner, on a bare `rm` in a card: "я сейчас
|
|
121
|
+
* введу её и сделаю хуй пойми что, я ж не знаю, что делаю". A command he
|
|
122
|
+
* cannot read is one he cannot run, so it never travels alone.
|
|
123
|
+
*/
|
|
124
|
+
commandNote?: string;
|
|
125
|
+
/**
|
|
126
|
+
* A local log path — monospaced, not a link, same as on an incident.
|
|
127
|
+
* It used to be glued onto the end of the reason sentence behind a
|
|
128
|
+
* colon, which is what made a red card read as one long run-on line.
|
|
129
|
+
*/
|
|
130
|
+
logs?: string;
|
|
87
131
|
workflowUrl?: string;
|
|
88
|
-
/** Название прогона для видимого текста ссылки (по умолчанию —
|
|
132
|
+
/** Название прогона для видимого текста ссылки (по умолчанию — `open`). */
|
|
89
133
|
workflowName?: string;
|
|
90
134
|
/**
|
|
91
135
|
* Запасное имя для ссылки на прогон: половина отправителей шлёт её как
|
|
@@ -139,9 +183,9 @@ export type NotifyEvent = Keyed & (
|
|
|
139
183
|
* schedule, a manual press. Renders as `Reason:`, same as on deploy.
|
|
140
184
|
*/
|
|
141
185
|
note?: string;
|
|
142
|
-
/** Ссылка на прогон (workflow run) — отдельно от `url
|
|
186
|
+
/** Ссылка на прогон (workflow run) — отдельно от `url` — запасной для `workflowUrl`. */
|
|
143
187
|
workflowUrl?: string;
|
|
144
|
-
/** Название прогона для видимого текста ссылки (по умолчанию —
|
|
188
|
+
/** Название прогона для видимого текста ссылки (по умолчанию — `open`). */
|
|
145
189
|
workflowName?: string;
|
|
146
190
|
url?: string;
|
|
147
191
|
}
|
|
@@ -153,7 +197,7 @@ export type NotifyEvent = Keyed & (
|
|
|
153
197
|
| {
|
|
154
198
|
type: 'pr';
|
|
155
199
|
project: Project;
|
|
156
|
-
action: 'opened' | '
|
|
200
|
+
action: 'opened' | 'approved' | 'changes_requested' | 'merged' | 'closed';
|
|
157
201
|
number: number;
|
|
158
202
|
title: string;
|
|
159
203
|
/** PR description — quoted on its own; the title is the `Title:` field above it. */
|
|
@@ -185,7 +229,47 @@ export type NotifyEvent = Keyed & (
|
|
|
185
229
|
logs?: string;
|
|
186
230
|
url?: string;
|
|
187
231
|
}
|
|
188
|
-
/**
|
|
232
|
+
/**
|
|
233
|
+
* A working session on this Mac is in trouble — not a job, not a workflow.
|
|
234
|
+
* It went out as `job` at first and read wrong: `#job` promises something
|
|
235
|
+
* scheduled that ran and failed, and the owner rightly asked what a burning
|
|
236
|
+
* session was doing under that heading.
|
|
237
|
+
*
|
|
238
|
+
* What makes it its own type rather than an `incident`: a session has an
|
|
239
|
+
* identity nothing else here has — an id, a working directory, and the line
|
|
240
|
+
* he typed to start it, which is the ONLY thing that tells two of his open
|
|
241
|
+
* sessions apart.
|
|
242
|
+
*/
|
|
243
|
+
| {
|
|
244
|
+
type: 'session';
|
|
245
|
+
project: Project;
|
|
246
|
+
/** What happened, as the second line reads it: `Session: burning the limit`. */
|
|
247
|
+
action: string;
|
|
248
|
+
/** The session's own id — the identifier field, first, as everywhere else. */
|
|
249
|
+
id?: string;
|
|
250
|
+
/** Working directory name, when several sessions opened with a similar line. */
|
|
251
|
+
workdir?: string;
|
|
252
|
+
/** One line of measurement: what the guard saw. */
|
|
253
|
+
reason?: string;
|
|
254
|
+
/**
|
|
255
|
+
* The line he opened the session with. Quoted, never a field: it is his
|
|
256
|
+
* own writing, it runs long, and a field would clip it to one short line
|
|
257
|
+
* — which is exactly how the first version of this card lost it.
|
|
258
|
+
*/
|
|
259
|
+
opened?: string;
|
|
260
|
+
/** A command for him to run, monospaced so Telegram makes it copyable. */
|
|
261
|
+
command?: string;
|
|
262
|
+
/** WHAT that command does — see the note on `job.commandNote`. */
|
|
263
|
+
commandNote?: string;
|
|
264
|
+
/** `fail` red, `ok` green — a session that recovered is not an alarm. */
|
|
265
|
+
status?: 'fail' | 'ok';
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* УСТАРЕЛО с 1.4.2: используйте `job` со статусом `silent`. Тип остаётся,
|
|
269
|
+
* потому что сторож молчания живёт на сервере и до выкатки шлёт именно его —
|
|
270
|
+
* убрать значит потерять карточку молчания ровно тогда, когда она нужна.
|
|
271
|
+
* Новых вызовов не добавлять.
|
|
272
|
+
*/
|
|
189
273
|
| {
|
|
190
274
|
type: 'heartbeat_miss';
|
|
191
275
|
project: Project;
|
|
@@ -196,26 +280,47 @@ export type NotifyEvent = Keyed & (
|
|
|
196
280
|
recovered?: boolean;
|
|
197
281
|
/** Готовое предложение-причина; без него собирается из lastSeen/expected. */
|
|
198
282
|
note?: string;
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Файл-вложение (sendDocument) с подписью-карточкой. Появился, когда
|
|
202
|
-
* eval-отчёт Arvent слал файл голым curl мимо пакета: без ретраев файл
|
|
203
|
-
* терялся в любую сетевую икоту, а chat_id и номер вкладки жили копией,
|
|
204
|
-
* которая устаревает молча. Маршрут — тот же `ROUTES`, транспорт — с теми
|
|
205
|
-
* же повторами. Подпись у Telegram ограничена 1024 символами — режется
|
|
206
|
-
* тем же безопасным клампом.
|
|
207
|
-
*/
|
|
208
|
-
| {
|
|
209
|
-
type: 'file';
|
|
210
|
-
project: Project;
|
|
211
|
-
title: string;
|
|
212
|
-
/** Путь к локальному файлу. */
|
|
213
|
-
path: string;
|
|
214
|
-
/** Имя файла в чате; по умолчанию — имя из `path`. */
|
|
215
|
-
filename?: string;
|
|
216
|
-
note?: string;
|
|
217
283
|
});
|
|
218
284
|
export type EventType = NotifyEvent['type'];
|
|
219
285
|
/** Красное = со звуком. Всё остальное — тихо. (Отдельной темы «инциденты» больше нет — авария видна в ленте проекта.) */
|
|
286
|
+
/**
|
|
287
|
+
* Словарь значков. Два закона, и оба поставлены владельцем 25.08.2026:
|
|
288
|
+
*
|
|
289
|
+
* 1. ВНУТРИ одного тега у каждого слова свой значок. Раньше значков было
|
|
290
|
+
* ровно четыре на весь пакет, и `Issue: opened` с `Issue: assigned`
|
|
291
|
+
* выглядели одинаково, а у задачи три разных беды — fail, disabled,
|
|
292
|
+
* silent — были одним и тем же красным кругом.
|
|
293
|
+
* 2. МЕЖДУ тегами одинаковый смысл выглядит одинаково. `fail` — это 🔴 и в
|
|
294
|
+
* выкатке, и в CI, и в задаче; «появилось новое» — 🆕 и у задачи на доске,
|
|
295
|
+
* и у PR, и у файла.
|
|
296
|
+
*
|
|
297
|
+
* И третий, который держит первые два честными: у значка фиксированный звук.
|
|
298
|
+
* Не у события, не у статуса — у значка. Пока звук выводился отдельным
|
|
299
|
+
* правилом, `🔴 PR: changes_requested` приходила беззвучно.
|
|
300
|
+
*/
|
|
301
|
+
export declare const ICON: {
|
|
302
|
+
readonly ok: "✅";
|
|
303
|
+
readonly red: "🔴";
|
|
304
|
+
readonly alarm: "🚨";
|
|
305
|
+
readonly off: "🚫";
|
|
306
|
+
readonly unknown: "❓";
|
|
307
|
+
readonly fresh: "🆕";
|
|
308
|
+
readonly taken: "🙋";
|
|
309
|
+
readonly landed: "🎉";
|
|
310
|
+
readonly discarded: "🗑️";
|
|
311
|
+
readonly approved: "👍";
|
|
312
|
+
readonly changes: "📝";
|
|
313
|
+
readonly info: "ℹ️";
|
|
314
|
+
};
|
|
315
|
+
/** The sound is a property of the icon, and of nothing else. */
|
|
316
|
+
export declare const LOUD: ReadonlySet<string>;
|
|
317
|
+
export declare const PR_ICON: Record<Extract<NotifyEvent, {
|
|
318
|
+
type: 'pr';
|
|
319
|
+
}>['action'], string>;
|
|
320
|
+
export declare const ISSUE_ICON: Record<Extract<NotifyEvent, {
|
|
321
|
+
type: 'issue';
|
|
322
|
+
}>['action'], string>;
|
|
323
|
+
/** Одно место, где решается значок карточки, — и рендер, и звук берут его отсюда. */
|
|
324
|
+
export declare const iconFor: (e: NotifyEvent) => string;
|
|
220
325
|
export declare const severity: (e: NotifyEvent) => "info" | "error";
|
|
221
326
|
export {};
|
package/dist/events.js
CHANGED
|
@@ -10,19 +10,82 @@
|
|
|
10
10
|
* Тогда старый вызывающий код и новый пакет совместимы в обе стороны.
|
|
11
11
|
*/
|
|
12
12
|
/** Красное = со звуком. Всё остальное — тихо. (Отдельной темы «инциденты» больше нет — авария видна в ленте проекта.) */
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Словарь значков. Два закона, и оба поставлены владельцем 25.08.2026:
|
|
15
|
+
*
|
|
16
|
+
* 1. ВНУТРИ одного тега у каждого слова свой значок. Раньше значков было
|
|
17
|
+
* ровно четыре на весь пакет, и `Issue: opened` с `Issue: assigned`
|
|
18
|
+
* выглядели одинаково, а у задачи три разных беды — fail, disabled,
|
|
19
|
+
* silent — были одним и тем же красным кругом.
|
|
20
|
+
* 2. МЕЖДУ тегами одинаковый смысл выглядит одинаково. `fail` — это 🔴 и в
|
|
21
|
+
* выкатке, и в CI, и в задаче; «появилось новое» — 🆕 и у задачи на доске,
|
|
22
|
+
* и у PR, и у файла.
|
|
23
|
+
*
|
|
24
|
+
* И третий, который держит первые два честными: у значка фиксированный звук.
|
|
25
|
+
* Не у события, не у статуса — у значка. Пока звук выводился отдельным
|
|
26
|
+
* правилом, `🔴 PR: changes_requested` приходила беззвучно.
|
|
27
|
+
*/
|
|
28
|
+
export const ICON = {
|
|
29
|
+
ok: '✅', // passed, closed, done
|
|
30
|
+
red: '🔴', // broken
|
|
31
|
+
alarm: '🚨', // burning right now
|
|
32
|
+
off: '🚫', // switched off — it will not run until someone turns it back on
|
|
33
|
+
unknown: '❓', // did not report: alive or dead is unknown
|
|
34
|
+
fresh: '🆕', // something new appeared
|
|
35
|
+
taken: '🙋', // someone took it
|
|
36
|
+
landed: '🎉', // merged — the work is in
|
|
37
|
+
discarded: '🗑️', // closed without reaching the result
|
|
38
|
+
approved: '👍', // a human approved it
|
|
39
|
+
changes: '📝', // a human wants edits — not a failure, and not loud
|
|
40
|
+
info: 'ℹ️' // a summary, for information
|
|
41
|
+
};
|
|
42
|
+
/** The sound is a property of the icon, and of nothing else. */
|
|
43
|
+
export const LOUD = new Set([ICON.red, ICON.alarm, ICON.off, ICON.unknown]);
|
|
44
|
+
export const PR_ICON = {
|
|
45
|
+
opened: ICON.fresh,
|
|
46
|
+
approved: ICON.approved,
|
|
47
|
+
changes_requested: ICON.changes,
|
|
48
|
+
merged: ICON.landed,
|
|
49
|
+
closed: ICON.discarded
|
|
50
|
+
};
|
|
51
|
+
export const ISSUE_ICON = {
|
|
52
|
+
opened: ICON.fresh,
|
|
53
|
+
assigned: ICON.taken,
|
|
54
|
+
closed: ICON.ok
|
|
55
|
+
};
|
|
56
|
+
const JOB_ICON = {
|
|
57
|
+
ok: ICON.ok,
|
|
58
|
+
fail: ICON.red,
|
|
59
|
+
disabled: ICON.off,
|
|
60
|
+
silent: ICON.unknown
|
|
61
|
+
};
|
|
62
|
+
/** Одно место, где решается значок карточки, — и рендер, и звук берут его отсюда. */
|
|
63
|
+
export const iconFor = (e) => {
|
|
64
|
+
switch (e.type) {
|
|
65
|
+
case 'deploy':
|
|
66
|
+
case 'ci':
|
|
67
|
+
return e.status === 'ok' ? ICON.ok : ICON.red;
|
|
68
|
+
case 'job':
|
|
69
|
+
return JOB_ICON[e.status];
|
|
70
|
+
case 'session':
|
|
71
|
+
return e.status === 'ok' ? ICON.ok : ICON.alarm;
|
|
72
|
+
case 'incident':
|
|
73
|
+
return ICON.alarm;
|
|
74
|
+
case 'heartbeat_miss':
|
|
75
|
+
return e.recovered ? ICON.ok : ICON.unknown;
|
|
76
|
+
case 'pr':
|
|
77
|
+
return PR_ICON[e.action];
|
|
78
|
+
case 'issue':
|
|
79
|
+
return ISSUE_ICON[e.action];
|
|
80
|
+
case 'report':
|
|
81
|
+
return ICON.info;
|
|
26
82
|
}
|
|
27
|
-
|
|
83
|
+
};
|
|
84
|
+
export const severity = (e) => {
|
|
85
|
+
// ONE law: the icon decides the sound. There is no second list of "which
|
|
86
|
+
// events are bad" to keep in sync with the icons — keeping two lists is how
|
|
87
|
+
// `🔴 PR: changes_requested` ended up arriving MUTED, the only red card in
|
|
88
|
+
// the package that did not ring, because severity() looked at `status` and a
|
|
89
|
+
// pull request has an `action`.
|
|
90
|
+
return LOUD.has(iconFor(e)) ? 'error' : 'info';
|
|
28
91
|
};
|
package/dist/index.d.ts
CHANGED