@mikitasazan/notify 1.3.0 → 1.4.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/dist/cli-flags.d.ts +11 -0
- package/dist/cli-flags.js +19 -0
- package/dist/cli.js +44 -9
- package/dist/events.d.ts +14 -2
- package/dist/render.js +120 -38
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Каждое имя флага, которое CLI умеет читать. Существует ради одной вещи:
|
|
3
|
+
* опечатка в имени флага раньше просто игнорировалась. `--noto=...` вместо
|
|
4
|
+
* `--note=...` рисовал карточку без причины и выходил нулём — владелец получал
|
|
5
|
+
* обрезанное сообщение, а вызывающая задача считала, что всё хорошо.
|
|
6
|
+
*
|
|
7
|
+
* Список закрытый и проверяется тестом: тест вытаскивает из этого же файла все
|
|
8
|
+
* имена, которые читает `one`/`num`/`pairs`, и требует, чтобы каждое было
|
|
9
|
+
* здесь. Разойтись молча он не может.
|
|
10
|
+
*/
|
|
11
|
+
export declare const KNOWN_FLAGS: ReadonlySet<string>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Каждое имя флага, которое CLI умеет читать. Существует ради одной вещи:
|
|
3
|
+
* опечатка в имени флага раньше просто игнорировалась. `--noto=...` вместо
|
|
4
|
+
* `--note=...` рисовал карточку без причины и выходил нулём — владелец получал
|
|
5
|
+
* обрезанное сообщение, а вызывающая задача считала, что всё хорошо.
|
|
6
|
+
*
|
|
7
|
+
* Список закрытый и проверяется тестом: тест вытаскивает из этого же файла все
|
|
8
|
+
* имена, которые читает `one`/`num`/`pairs`, и требует, чтобы каждое было
|
|
9
|
+
* здесь. Разойтись молча он не может.
|
|
10
|
+
*/
|
|
11
|
+
export const KNOWN_FLAGS = new Set([
|
|
12
|
+
'action', 'actor', 'assignee', 'author', 'body', 'branch', 'commit',
|
|
13
|
+
'commit-body', 'commit-title', 'commit-url', 'detail', 'expected',
|
|
14
|
+
'filename', 'item', 'job', 'key', 'last-seen', 'line', 'logs', 'note',
|
|
15
|
+
'number', 'path', 'period', 'project', 'reviewer', 'stat', 'status',
|
|
16
|
+
'target', 'title', 'url', 'via', 'workflow-name', 'workflow-url',
|
|
17
|
+
// Флаги без значения. Живут здесь же, чтобы разбор и список не разошлись.
|
|
18
|
+
'json', 'recovered', 'dry-run'
|
|
19
|
+
]);
|
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
* notify setup <chat_id форума> <ключ-проекта> # создать вкладки, см. setup.ts
|
|
22
22
|
*/
|
|
23
23
|
import { readFileSync } from 'node:fs';
|
|
24
|
+
import { KNOWN_FLAGS } from "./cli-flags.js";
|
|
25
|
+
import { render } from "./render.js";
|
|
24
26
|
import { notify } from "./send.js";
|
|
25
27
|
import { setupTopic } from "./setup.js";
|
|
26
28
|
const log = (msg) => console.error(`[notify] ${msg}`);
|
|
@@ -39,11 +41,11 @@ if (command === 'setup') {
|
|
|
39
41
|
const flags = new Map();
|
|
40
42
|
const parseErrors = [];
|
|
41
43
|
/** Флаги без значения. Всё остальное обязано его иметь. */
|
|
42
|
-
const BOOLEAN_FLAGS = new Set(['json', 'recovered']);
|
|
44
|
+
const BOOLEAN_FLAGS = new Set(['json', 'recovered', 'dry-run']);
|
|
43
45
|
for (let i = 1; i < args.length; i++) {
|
|
44
46
|
const arg = args[i];
|
|
45
47
|
if (!arg.startsWith('--')) {
|
|
46
|
-
parseErrors.push(
|
|
48
|
+
parseErrors.push(`stray argument with no flag: "${arg}"`);
|
|
47
49
|
continue;
|
|
48
50
|
}
|
|
49
51
|
// Форма `--key=value` обязательна для значений, начинающихся с `--`
|
|
@@ -65,13 +67,18 @@ for (let i = 1; i < args.length; i++) {
|
|
|
65
67
|
// ТЕРЯЛОСЬ ВСЁ сообщение, а `--status` без значения рисовал 🔴 на
|
|
66
68
|
// успешном деплое. Теперь это явная ошибка разбора.
|
|
67
69
|
if (next === undefined || next.startsWith('--')) {
|
|
68
|
-
parseErrors.push(
|
|
70
|
+
parseErrors.push(`flag --${key} with no value`);
|
|
69
71
|
continue;
|
|
70
72
|
}
|
|
71
73
|
i++;
|
|
72
74
|
flags.set(key, [...(flags.get(key) ?? []), next]);
|
|
73
75
|
}
|
|
74
76
|
const one = (key) => flags.get(key)?.[0];
|
|
77
|
+
for (const key of flags.keys()) {
|
|
78
|
+
if (!KNOWN_FLAGS.has(key)) {
|
|
79
|
+
parseErrors.push(`unknown flag --${key}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
75
82
|
// Число с явной ошибкой разбора, иначе рендер рисовал «PR #NaN».
|
|
76
83
|
const num = (key) => {
|
|
77
84
|
const raw = one(key);
|
|
@@ -114,7 +121,7 @@ const ISSUE_ALIASES = {
|
|
|
114
121
|
const prAction = (raw) => {
|
|
115
122
|
const hit = PR_ALIASES[(raw ?? '').toLowerCase()];
|
|
116
123
|
if (!hit) {
|
|
117
|
-
parseErrors.push(`--action:
|
|
124
|
+
parseErrors.push(`--action: unknown PR action "${raw ?? ''}" (${Object.keys(PR_ALIASES).join(', ')})`);
|
|
118
125
|
return 'opened';
|
|
119
126
|
}
|
|
120
127
|
return hit;
|
|
@@ -122,7 +129,7 @@ const prAction = (raw) => {
|
|
|
122
129
|
const issueAction = (raw) => {
|
|
123
130
|
const hit = ISSUE_ALIASES[(raw ?? '').toLowerCase()];
|
|
124
131
|
if (!hit) {
|
|
125
|
-
parseErrors.push(`--action:
|
|
132
|
+
parseErrors.push(`--action: unknown issue action "${raw ?? ''}" (${Object.keys(ISSUE_ALIASES).join(', ')})`);
|
|
126
133
|
return 'opened';
|
|
127
134
|
}
|
|
128
135
|
return hit;
|
|
@@ -157,7 +164,9 @@ if (flags.has('json')) {
|
|
|
157
164
|
event = { ...payload, type: command };
|
|
158
165
|
}
|
|
159
166
|
catch (err) {
|
|
160
|
-
|
|
167
|
+
// Тоже в parseErrors: обе аналитики зовут CLI через `|| true`, и молчащий
|
|
168
|
+
// разбор JSON означал бы зелёный крон без дневного отчёта.
|
|
169
|
+
parseErrors.push(`--json from stdin did not parse: ${err instanceof Error ? err.message : String(err)}`);
|
|
161
170
|
}
|
|
162
171
|
}
|
|
163
172
|
else {
|
|
@@ -215,6 +224,7 @@ else {
|
|
|
215
224
|
commitTitle: one('commit-title'),
|
|
216
225
|
commitBody: one('commit-body'),
|
|
217
226
|
actor: one('actor'),
|
|
227
|
+
note: one('note'),
|
|
218
228
|
workflowUrl: one('workflow-url'),
|
|
219
229
|
workflowName: one('workflow-name'),
|
|
220
230
|
url: one('url')
|
|
@@ -227,6 +237,7 @@ else {
|
|
|
227
237
|
action: prAction(one('action')),
|
|
228
238
|
number: num('number'),
|
|
229
239
|
title: one('title') ?? '(без заголовка)',
|
|
240
|
+
body: one('body'),
|
|
230
241
|
author: one('author'),
|
|
231
242
|
reviewer: one('reviewer'),
|
|
232
243
|
url: one('url')
|
|
@@ -282,7 +293,10 @@ else {
|
|
|
282
293
|
break;
|
|
283
294
|
}
|
|
284
295
|
default:
|
|
285
|
-
|
|
296
|
+
// В parseErrors, а не просто в лог: иначе неизвестный тип уходил в
|
|
297
|
+
// тишину — событие не собиралось, ошибок разбора не было, и CLI выходил
|
|
298
|
+
// нулём, ничего не отправив и ничего об этом не сказав.
|
|
299
|
+
parseErrors.push(`unknown event type: ${command ?? '(none given)'}`);
|
|
286
300
|
}
|
|
287
301
|
}
|
|
288
302
|
// --key применим к любому типу — одна точка вместо строки в каждом case
|
|
@@ -297,7 +311,26 @@ if (parseErrors.length > 0) {
|
|
|
297
311
|
for (const err of parseErrors) {
|
|
298
312
|
log(err);
|
|
299
313
|
}
|
|
300
|
-
|
|
314
|
+
// The word `failed` is a CONTRACT, not prose. Two readers match on it: the
|
|
315
|
+
// GitHub Action turns it into a yellow annotation, and the VPS watchdog reads
|
|
316
|
+
// the combined stream for `sent|failed|skipped`. Before this, a parse error
|
|
317
|
+
// printed neither word — the run stayed green, the watchdog stayed quiet, and
|
|
318
|
+
// the card simply never existed.
|
|
319
|
+
// It must NOT contain the substring `sent`: heartbeat-check.sh matches `*sent*`
|
|
320
|
+
// and would read a failure as a success.
|
|
321
|
+
log('failed: bad command, nothing delivered');
|
|
322
|
+
process.exit(0);
|
|
323
|
+
}
|
|
324
|
+
if (event && flags.has('dry-run')) {
|
|
325
|
+
// The rendered card on STDOUT, nothing sent and no token needed. This is how
|
|
326
|
+
// a change to the format is shown to the owner before it reaches a forum, and
|
|
327
|
+
// how ~25 edited call sites are checked one by one — the package always exits
|
|
328
|
+
// 0, so a typo in a flag is otherwise silent.
|
|
329
|
+
//
|
|
330
|
+
// stdout, not stderr: every other line this CLI prints goes to stderr, and
|
|
331
|
+
// watchdogs read that stream for the words `sent|failed|skipped`. A card
|
|
332
|
+
// printed there would be read as a verdict.
|
|
333
|
+
process.stdout.write(`${render(event)}\n`);
|
|
301
334
|
process.exit(0);
|
|
302
335
|
}
|
|
303
336
|
if (event) {
|
|
@@ -308,7 +341,9 @@ if (event) {
|
|
|
308
341
|
log(await notify(event));
|
|
309
342
|
}
|
|
310
343
|
catch (err) {
|
|
311
|
-
|
|
344
|
+
// Слово `failed` — контракт, тот же, что у ошибки разбора выше. Без него
|
|
345
|
+
// исключение при отправке читалось сторожами как «ничего не случилось».
|
|
346
|
+
log(`failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
312
347
|
}
|
|
313
348
|
}
|
|
314
349
|
process.exit(0);
|
package/dist/events.d.ts
CHANGED
|
@@ -87,6 +87,11 @@ export type NotifyEvent = Keyed & (
|
|
|
87
87
|
workflowUrl?: string;
|
|
88
88
|
/** Название прогона для видимого текста ссылки (по умолчанию — просто "run"). */
|
|
89
89
|
workflowName?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Запасное имя для ссылки на прогон: половина отправителей шлёт её как
|
|
92
|
+
* `--url`. Рендер берёт `workflowUrl ?? url`, так что оба имени работают.
|
|
93
|
+
* В новых вызовах предпочитай `workflowUrl` — оно говорит, куда ведёт.
|
|
94
|
+
*/
|
|
90
95
|
url?: string;
|
|
91
96
|
}
|
|
92
97
|
/** Сводка с цифрами: дневной отчёт, дайджест аналитики. */
|
|
@@ -124,11 +129,16 @@ export type NotifyEvent = Keyed & (
|
|
|
124
129
|
commit?: string;
|
|
125
130
|
/** Ссылка на коммит — хэш становится кликабельным. */
|
|
126
131
|
commitUrl?: string;
|
|
127
|
-
/** Заголовок коммита (subject) —
|
|
132
|
+
/** Заголовок коммита (subject) — отдельное поле `Title:`, не цитата. */
|
|
128
133
|
commitTitle?: string;
|
|
129
134
|
/** Тело коммита (после subject) — та же цитата, что и заголовок. */
|
|
130
135
|
commitBody?: string;
|
|
131
136
|
actor?: string;
|
|
137
|
+
/**
|
|
138
|
+
* Why this run happened, when there is no commit to point at: a nightly
|
|
139
|
+
* schedule, a manual press. Renders as `Reason:`, same as on deploy.
|
|
140
|
+
*/
|
|
141
|
+
note?: string;
|
|
132
142
|
/** Ссылка на прогон (workflow run) — отдельно от `url`, который у CI не используется. */
|
|
133
143
|
workflowUrl?: string;
|
|
134
144
|
/** Название прогона для видимого текста ссылки (по умолчанию — просто "run"). */
|
|
@@ -146,6 +156,8 @@ export type NotifyEvent = Keyed & (
|
|
|
146
156
|
action: 'opened' | 'ready_for_review' | 'review_requested' | 'approved' | 'changes_requested' | 'merged' | 'closed';
|
|
147
157
|
number: number;
|
|
148
158
|
title: string;
|
|
159
|
+
/** PR description — quoted on its own; the title is the `Title:` field above it. */
|
|
160
|
+
body?: string;
|
|
149
161
|
author?: string;
|
|
150
162
|
reviewer?: string;
|
|
151
163
|
url?: string;
|
|
@@ -157,7 +169,7 @@ export type NotifyEvent = Keyed & (
|
|
|
157
169
|
action: 'opened' | 'assigned' | 'closed';
|
|
158
170
|
number: number;
|
|
159
171
|
title: string;
|
|
160
|
-
/** Тело задачи —
|
|
172
|
+
/** Тело задачи — цитата под полем `Title:`, отдельно от заголовка. */
|
|
161
173
|
body?: string;
|
|
162
174
|
author?: string;
|
|
163
175
|
assignee?: string;
|
package/dist/render.js
CHANGED
|
@@ -41,7 +41,12 @@ export const clampMessage = (text, limit = 4000) => {
|
|
|
41
41
|
// blockquote — с приходом цитаты для примечаний/деталей длинный detail режется
|
|
42
42
|
// прямо посередине неё, и без этого тега Telegram отвечал бы 400 на незакрытую
|
|
43
43
|
// цитату (regex `<blockquote[ >]` ловит и вариант с атрибутом `expandable`).
|
|
44
|
-
|
|
44
|
+
// `u` в списке с 2026-08-25: заголовок группы рисуется как `<i><u>…</u></i>`,
|
|
45
|
+
// и обрезанный посередине длинный заголовок оставлял `<u>` незакрытым.
|
|
46
|
+
// Telegram отвечает на такое 400 — то есть карточка пропадала целиком, а
|
|
47
|
+
// отправитель с `|| true` этого не замечал. Нашёл Codex; воспроизводится
|
|
48
|
+
// отчётом с именем группы в 5000 знаков.
|
|
49
|
+
const tail = ['b', 'a', 'i', 'u', 'code', 'blockquote']
|
|
45
50
|
.filter((t) => {
|
|
46
51
|
const opened = (body.match(new RegExp(`<${t}[ >]`, 'g')) ?? []).length;
|
|
47
52
|
const closed = (body.match(new RegExp(`</${t}>`, 'g')) ?? []).length;
|
|
@@ -79,14 +84,23 @@ const fieldLink = (label, url, text) => {
|
|
|
79
84
|
if (text === undefined || text === null || text === '') {
|
|
80
85
|
return null;
|
|
81
86
|
}
|
|
82
|
-
|
|
87
|
+
// `firstLine` here for the same reason `field` has it: the linked case used to
|
|
88
|
+
// skip it, so a multi-line value (arvent's two-line `commit`) became two-line
|
|
89
|
+
// LINK TEXT instead of one identifier.
|
|
90
|
+
return url
|
|
91
|
+
? `<b>${esc(cap(label))}:</b> <a href="${esc(url)}">${esc(firstLine(text))}</a>`
|
|
92
|
+
: field(label, text);
|
|
83
93
|
};
|
|
84
94
|
/**
|
|
85
95
|
* Поле-действие (`workflow:`): в отличие от `fieldLink`, без URL это НЕ
|
|
86
96
|
* поле — прогону просто некуда вести, показывать голое слово «run» без
|
|
87
97
|
* ссылки бессмысленнее, чем не показывать строку вовсе.
|
|
88
98
|
*/
|
|
89
|
-
|
|
99
|
+
// Текст ссылки — имя того, куда она ведёт (имя workflow, имя прогона). Запасное
|
|
100
|
+
// слово было «run»: существительное, которое ничего не называет — владелец читал
|
|
101
|
+
// «Workflow: run» и не понимал, что это. «open» — глагол, он хотя бы честно
|
|
102
|
+
// говорит, что это ссылка, а не название.
|
|
103
|
+
const fieldAction = (label, url, text) => url ? `<b>${esc(cap(label))}:</b> <a href="${esc(url)}">${esc(text ?? 'open')}</a>` : null;
|
|
90
104
|
/** Моноширинное поле — путь/команда для копирования, не ссылка. */
|
|
91
105
|
const fieldCode = (label, value) => value ? `<b>${esc(cap(label))}:</b> <code>${esc(value)}</code>` : null;
|
|
92
106
|
/** Заголовок группы: курсив + подчёркивание, без жирности, без двоеточия. */
|
|
@@ -120,56 +134,90 @@ const renderGroup = (g) => [
|
|
|
120
134
|
...g.items.map((it, i) => groupItem(it, i, false))
|
|
121
135
|
];
|
|
122
136
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
137
|
+
* ОДНО правило на все карточки, где есть и заголовок, и тело: заголовок —
|
|
138
|
+
* обычное поле `Title:`, тело — цитата, и в цитате больше ничего нет.
|
|
139
|
+
*
|
|
140
|
+
* Раньше заголовок клался В ЦИТАТУ вместе с телом, разделённые пустой
|
|
141
|
+
* строкой. Владелец нашёл, чем это плохо: заголовок — главное в карточке, то,
|
|
142
|
+
* ЧТО это, а лежал он серым текстом того же веса, что и описание, и отличить
|
|
143
|
+
* одно от другого можно было только по пустой строке. У PR без тела карточка
|
|
144
|
+
* вырождалась в одинокую серую цитату из одной строки.
|
|
145
|
+
*
|
|
146
|
+
* Заголовок режется до первой строки: многострочный subject коммита не должен
|
|
147
|
+
* затягивать в поле собственное тело.
|
|
126
148
|
*/
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
const text = [title ? firstLine(title) : undefined, body].filter(Boolean).join('\n\n');
|
|
132
|
-
return note(text);
|
|
133
|
-
};
|
|
149
|
+
const titleField = (title) => field('Title', title);
|
|
150
|
+
const bodyQuote = (body) => body ? note(body) : null;
|
|
134
151
|
// Значок = статус сообщения, не тип события. Ровно четыре на весь пакет —
|
|
135
152
|
// закреплённая легенда в форумах обещает это владельцу как факт, не как
|
|
136
153
|
// приближение. 🔴 сломалось, 🚨 инцидент, ✅ прошло, ℹ️ к сведению.
|
|
137
154
|
const ICON = { red: '🔴', alarm: '🚨', ok: '✅', info: 'ℹ️' };
|
|
138
155
|
/** Строка 2: значок вне жирного, `<b>Тип:</b> действие` — то же поле, не особый случай. */
|
|
139
|
-
|
|
156
|
+
// `action` объявлен строкой, но приходит и из `--json`, и из прямых вызовов на
|
|
157
|
+
// JS, где типов нет. Пустое или отсутствующее значение давало строку `ℹ️ null`
|
|
158
|
+
// прямо во второй строке карточки. Пустая строка честнее: поле просто исчезает.
|
|
159
|
+
const typeLine = (icon, type, action) => {
|
|
160
|
+
// `field` возвращает null на пустом значении, а интерполяция null в шаблон
|
|
161
|
+
// печатает слово «null». Так вторая строка карточки становилась `ℹ️ null` —
|
|
162
|
+
// достижимо через `--json` и прямой вызов на JS, где типов нет.
|
|
163
|
+
const line = field(type, action);
|
|
164
|
+
return line === null ? `${icon} <b>${esc(cap(type))}</b>` : `${icon} ${line}`;
|
|
165
|
+
};
|
|
166
|
+
// `workflowUrl ?? url`: половина отправителей шлёт ссылку на прогон под именем
|
|
167
|
+
// `--url` — это имя было в пакете раньше и осталось в вызовах. Рендер читал
|
|
168
|
+
// только `workflowUrl`, поэтому красная карточка приходила БЕЗ ЕДИНОЙ ССЫЛКИ
|
|
169
|
+
// на логи. Отвергать `--url` было бы честнее по имени и хуже по делу: намерение
|
|
170
|
+
// однозначно, а карточка без ссылки бесполезна ровно тогда, когда нужна.
|
|
140
171
|
const renderDeploy = (e) => {
|
|
141
172
|
const icon = e.status === 'ok' ? ICON.ok : ICON.red;
|
|
142
173
|
return join([
|
|
143
174
|
typeLine(icon, 'Deploy', e.status),
|
|
144
175
|
'',
|
|
145
176
|
fieldLink('Commit', e.commitUrl, e.commit),
|
|
146
|
-
|
|
177
|
+
titleField(e.commitTitle),
|
|
178
|
+
bodyQuote(e.commitBody),
|
|
147
179
|
field('Via', e.via),
|
|
148
180
|
field('Target', e.target),
|
|
149
181
|
field('Reason', e.note),
|
|
150
|
-
e.workflowUrl ? '' : null,
|
|
151
|
-
fieldAction('Workflow', e.workflowUrl, e.workflowName)
|
|
182
|
+
e.workflowUrl ?? e.url ? '' : null,
|
|
183
|
+
fieldAction('Workflow', e.workflowUrl ?? e.url, e.workflowName)
|
|
152
184
|
]);
|
|
153
185
|
};
|
|
154
186
|
const renderJob = (e) => {
|
|
155
187
|
const icon = e.status === 'fail' || e.status === 'disabled' ? ICON.red : ICON.ok;
|
|
156
188
|
const hasItems = (e.items ?? []).length > 0;
|
|
189
|
+
const disabledList = hasItems && e.status === 'disabled';
|
|
157
190
|
return join([
|
|
158
191
|
typeLine(icon, 'Job', e.status),
|
|
159
192
|
'',
|
|
193
|
+
// The name is NOT the type line: line 2 is `Job: fail` by the format's own
|
|
194
|
+
// rule, so the name is its own field. Called `Task:` and not `Job:` because
|
|
195
|
+
// repeating the label of the line right above it reads as a mistake.
|
|
196
|
+
// Until now the name was dropped entirely — every caller passed it and the
|
|
197
|
+
// owner only ever saw it as the small grey instance tag.
|
|
198
|
+
field('Task', e.job),
|
|
160
199
|
field('Reason', e.note),
|
|
161
200
|
...(e.stats ?? []).map(([label, value]) => field(label, value)),
|
|
162
201
|
hasItems ? '' : null,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
202
|
+
// Heading ONLY for `disabled`. It used to print for any job carrying a
|
|
203
|
+
// list, so playhub's daily card of newly published games was headed
|
|
204
|
+
// "Disabled workflows".
|
|
205
|
+
disabledList ? group('Disabled workflows') : null,
|
|
206
|
+
...(hasItems ? bullets(e.items, disabledList) : []),
|
|
207
|
+
e.workflowUrl ?? e.url ? '' : null,
|
|
208
|
+
fieldAction('Workflow', e.workflowUrl ?? e.url, e.workflowName)
|
|
167
209
|
]);
|
|
168
210
|
};
|
|
169
211
|
const renderReport = (e) => {
|
|
170
212
|
if (e.groups && e.groups.length > 0) {
|
|
171
213
|
const body = e.groups.flatMap((g, i) => (i === 0 ? renderGroup(g) : ['', ...renderGroup(g)]));
|
|
172
|
-
return join([
|
|
214
|
+
return join([
|
|
215
|
+
typeLine(ICON.info, 'Report', e.period ? `${e.title} · ${e.period}` : e.title),
|
|
216
|
+
'',
|
|
217
|
+
...body,
|
|
218
|
+
e.url ? '' : null,
|
|
219
|
+
fieldAction('Details', e.url, undefined)
|
|
220
|
+
]);
|
|
173
221
|
}
|
|
174
222
|
const items = bullets(e.items, false);
|
|
175
223
|
return join([
|
|
@@ -177,7 +225,11 @@ const renderReport = (e) => {
|
|
|
177
225
|
'',
|
|
178
226
|
...(e.lines ?? []).map(([label, value]) => field(label, value)),
|
|
179
227
|
items.length > 0 ? '' : null,
|
|
180
|
-
...items
|
|
228
|
+
...items,
|
|
229
|
+
// Обе аналитики шлют сюда ссылку на снимок дня в docs/. Рендер её не читал,
|
|
230
|
+
// и дневной отчёт приходил без единственного способа посмотреть подробности.
|
|
231
|
+
e.url ? '' : null,
|
|
232
|
+
fieldAction('Details', e.url, undefined)
|
|
181
233
|
]);
|
|
182
234
|
};
|
|
183
235
|
const renderCi = (e) => {
|
|
@@ -186,10 +238,12 @@ const renderCi = (e) => {
|
|
|
186
238
|
typeLine(icon, 'CI', e.status),
|
|
187
239
|
'',
|
|
188
240
|
fieldLink('Commit', e.commitUrl, e.commit),
|
|
189
|
-
|
|
241
|
+
titleField(e.commitTitle),
|
|
242
|
+
bodyQuote(e.commitBody),
|
|
190
243
|
field('Actor', e.actor),
|
|
191
|
-
|
|
192
|
-
|
|
244
|
+
field('Reason', e.note),
|
|
245
|
+
e.workflowUrl ?? e.url ? '' : null,
|
|
246
|
+
fieldAction('Workflow', e.workflowUrl ?? e.url, e.workflowName)
|
|
193
247
|
]);
|
|
194
248
|
};
|
|
195
249
|
// PR/Issue: значок теперь по статусу (четыре на пакет), не по действию —
|
|
@@ -213,39 +267,67 @@ const ISSUE_ICON = {
|
|
|
213
267
|
const renderPr = (e) => join([
|
|
214
268
|
typeLine(PR_ICON[e.action], 'PR', e.action),
|
|
215
269
|
'',
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
'',
|
|
270
|
+
// Идентификатор первым, заголовок под ним: так вещь читается «#118, вот
|
|
271
|
+
// такая», а не «вот такая, кстати #118» — и так её пишет сам GitHub.
|
|
272
|
+
fieldLink('Number', e.url, `#${e.number}`),
|
|
273
|
+
titleField(e.title),
|
|
274
|
+
bodyQuote(e.body),
|
|
275
|
+
// Без пустой строки перед автором: у задачи её нет, и одно и то же поле
|
|
276
|
+
// не должно стоять по-разному в двух соседних карточках. Пустая строка в
|
|
277
|
+
// этом формате означает «дальше указатель, куда пойти» — автор не он.
|
|
219
278
|
field('Author', e.author),
|
|
220
279
|
field('Reviewer', e.reviewer)
|
|
221
280
|
]);
|
|
222
281
|
const renderIssue = (e) => join([
|
|
223
282
|
typeLine(ISSUE_ICON[e.action], 'Issue', e.action),
|
|
224
283
|
'',
|
|
225
|
-
fieldLink('
|
|
226
|
-
|
|
284
|
+
fieldLink('Number', e.url, `#${e.number}`),
|
|
285
|
+
titleField(e.title),
|
|
286
|
+
bodyQuote(e.body),
|
|
227
287
|
field('Author', e.author),
|
|
228
288
|
field('Assignee', e.assignee)
|
|
229
289
|
]);
|
|
230
290
|
const renderIncident = (e) => join([
|
|
231
291
|
typeLine(ICON.alarm, 'Incident', 'open'),
|
|
232
292
|
'',
|
|
233
|
-
|
|
293
|
+
// `detail` is a diagnosis of several lines (vault greps three of them plus a
|
|
294
|
+
// log path). It used to go through `field`, which keeps only the first line,
|
|
295
|
+
// so every alarm this package ever sent arrived gutted. Same shape as a
|
|
296
|
+
// commit now: short label, full text quoted under it.
|
|
297
|
+
// Ярлык `Title`, а не `Reason`: у аварии заголовок — такой же заголовок,
|
|
298
|
+
// как у коммита и задачи, и называться в одной карточке он должен так же.
|
|
299
|
+
titleField(e.title),
|
|
300
|
+
e.detail && e.detail !== e.title ? note(e.detail) : null,
|
|
234
301
|
e.logs || e.url ? '' : null,
|
|
235
302
|
fieldCode('Logs', e.logs),
|
|
236
303
|
fieldAction('Workflow', e.url, undefined)
|
|
237
304
|
]);
|
|
305
|
+
// Раньше всё это склеивалось в одну строку `Reason:` через тире: «имя — no
|
|
306
|
+
// reports — expected X, last seen Y». Каждая другая карточка кладёт факт на
|
|
307
|
+
// свою строку с ярлыком, и владелец справедливо спросил, зачем тут отдельный
|
|
308
|
+
// формат. Отдельного формата больше нет.
|
|
238
309
|
const renderHeartbeatMiss = (e) => {
|
|
239
310
|
const icon = e.recovered ? ICON.ok : ICON.red;
|
|
240
311
|
const action = e.recovered ? 'ok' : 'miss';
|
|
241
|
-
|
|
242
|
-
(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
312
|
+
return join([
|
|
313
|
+
typeLine(icon, 'Heartbeat', action),
|
|
314
|
+
'',
|
|
315
|
+
field('Task', e.job),
|
|
316
|
+
field('Reason', e.note),
|
|
317
|
+
field('Expected', e.expected),
|
|
318
|
+
field(e.recovered ? 'Last run' : 'Last seen', e.lastSeen)
|
|
319
|
+
]);
|
|
246
320
|
};
|
|
247
321
|
// Подпись файла — та же карточка, но лимит Telegram у caption свой: 1024.
|
|
248
|
-
const renderFile = (e) => join([
|
|
322
|
+
const renderFile = (e) => join([
|
|
323
|
+
typeLine(ICON.info, 'File', 'new'),
|
|
324
|
+
'',
|
|
325
|
+
// Раньше здесь стояло `field('Title', e.note ?? e.title)`: подпись файла
|
|
326
|
+
// приходила под ярлыком заголовка, а сам заголовок из карточки исчезал.
|
|
327
|
+
// Один ярлык — один смысл: Title это title, Reason это note.
|
|
328
|
+
field('Title', e.title),
|
|
329
|
+
field('Reason', e.note)
|
|
330
|
+
]);
|
|
249
331
|
const RENDERERS = {
|
|
250
332
|
deploy: renderDeploy,
|
|
251
333
|
job: renderJob,
|
package/package.json
CHANGED